chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
"""isort:skip_file"""
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.dataclass.utils import merge_with_parent, populate_dataclass
|
||||
from hydra.core.config_store import ConfigStore
|
||||
|
||||
from .fairseq_task import FairseqTask, LegacyFairseqTask # noqa
|
||||
|
||||
|
||||
# register dataclass
|
||||
TASK_DATACLASS_REGISTRY = {}
|
||||
TASK_REGISTRY = {}
|
||||
TASK_CLASS_NAMES = set()
|
||||
|
||||
|
||||
def setup_task(cfg: FairseqDataclass, **kwargs):
|
||||
task = None
|
||||
task_name = getattr(cfg, "task", None)
|
||||
|
||||
if isinstance(task_name, str):
|
||||
# legacy tasks
|
||||
task = TASK_REGISTRY[task_name]
|
||||
if task_name in TASK_DATACLASS_REGISTRY:
|
||||
dc = TASK_DATACLASS_REGISTRY[task_name]
|
||||
cfg = populate_dataclass(dc(), cfg)
|
||||
else:
|
||||
task_name = getattr(cfg, "_name", None)
|
||||
|
||||
if task_name and task_name in TASK_DATACLASS_REGISTRY:
|
||||
dc = TASK_DATACLASS_REGISTRY[task_name]
|
||||
cfg = merge_with_parent(dc(), cfg)
|
||||
task = TASK_REGISTRY[task_name]
|
||||
|
||||
assert task is not None, f"Could not infer task type from {cfg}. Available tasks: {TASK_REGISTRY.keys()}"
|
||||
|
||||
return task.setup_task(cfg, **kwargs)
|
||||
|
||||
|
||||
def register_task(name, dataclass=None):
|
||||
"""
|
||||
New tasks can be added to fairseq with the
|
||||
:func:`~fairseq.tasks.register_task` function decorator.
|
||||
|
||||
For example::
|
||||
|
||||
@register_task('classification')
|
||||
class ClassificationTask(FairseqTask):
|
||||
(...)
|
||||
|
||||
.. note::
|
||||
|
||||
All Tasks must implement the :class:`~fairseq.tasks.FairseqTask`
|
||||
interface.
|
||||
|
||||
Args:
|
||||
name (str): the name of the task
|
||||
"""
|
||||
|
||||
def register_task_cls(cls):
|
||||
if name in TASK_REGISTRY:
|
||||
raise ValueError("Cannot register duplicate task ({})".format(name))
|
||||
if not issubclass(cls, FairseqTask):
|
||||
raise ValueError(
|
||||
"Task ({}: {}) must extend FairseqTask".format(name, cls.__name__)
|
||||
)
|
||||
if cls.__name__ in TASK_CLASS_NAMES:
|
||||
raise ValueError(
|
||||
"Cannot register task with duplicate class name ({})".format(
|
||||
cls.__name__
|
||||
)
|
||||
)
|
||||
TASK_REGISTRY[name] = cls
|
||||
TASK_CLASS_NAMES.add(cls.__name__)
|
||||
|
||||
if dataclass is not None and not issubclass(dataclass, FairseqDataclass):
|
||||
raise ValueError(
|
||||
"Dataclass {} must extend FairseqDataclass".format(dataclass)
|
||||
)
|
||||
|
||||
cls.__dataclass = dataclass
|
||||
if dataclass is not None:
|
||||
TASK_DATACLASS_REGISTRY[name] = dataclass
|
||||
|
||||
cs = ConfigStore.instance()
|
||||
node = dataclass()
|
||||
node._name = name
|
||||
cs.store(name=name, group="task", node=node, provider="fairseq")
|
||||
|
||||
return cls
|
||||
|
||||
return register_task_cls
|
||||
|
||||
|
||||
def get_task(name):
|
||||
return TASK_REGISTRY[name]
|
||||
|
||||
|
||||
# automatically import any Python files in the tasks/ directory
|
||||
tasks_dir = os.path.dirname(__file__)
|
||||
for file in os.listdir(tasks_dir):
|
||||
path = os.path.join(tasks_dir, file)
|
||||
if (
|
||||
not file.startswith("_")
|
||||
and not file.startswith(".")
|
||||
and (file.endswith(".py") or os.path.isdir(path))
|
||||
):
|
||||
task_name = file[: file.find(".py")] if file.endswith(".py") else file
|
||||
module = importlib.import_module("fairseq.tasks." + task_name)
|
||||
|
||||
# expose `task_parser` for sphinx
|
||||
if task_name in TASK_REGISTRY:
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
group_task = parser.add_argument_group("Task name")
|
||||
# fmt: off
|
||||
group_task.add_argument('--task', metavar=task_name,
|
||||
help='Enable this task with: ``--task=' + task_name + '``')
|
||||
# fmt: on
|
||||
group_args = parser.add_argument_group("Additional command-line arguments")
|
||||
TASK_REGISTRY[task_name].add_args(group_args)
|
||||
globals()[task_name + "_parser"] = parser
|
||||
@@ -0,0 +1,282 @@
|
||||
# Copyright (c) 2017-present, Facebook, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the LICENSE file in
|
||||
# the root directory of this source tree. An additional grant of patent rights
|
||||
# can be found in the PATENTS file in the same directory.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import torch
|
||||
|
||||
from argparse import Namespace
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Any
|
||||
from omegaconf import MISSING
|
||||
|
||||
from fairseq.data import AddTargetDataset, Dictionary, FileAudioDataset, encoders
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.dataclass.configs import GenerationConfig
|
||||
|
||||
from . import FairseqTask, register_task
|
||||
from .. import utils
|
||||
from ..logging import metrics
|
||||
|
||||
|
||||
class LabelEncoder(object):
|
||||
def __init__(self, dictionary):
|
||||
self.dictionary = dictionary
|
||||
|
||||
def __call__(self, label):
|
||||
return self.dictionary.encode_line(
|
||||
label, append_eos=False, add_if_not_exist=False
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioPretrainingConfig(FairseqDataclass):
|
||||
data: str = field(default=MISSING, metadata={"help": "path to data directory"})
|
||||
labels: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={"help": "extension of the label file to load, used for fine-tuning"},
|
||||
)
|
||||
sample_rate: int = field(
|
||||
default=16_000,
|
||||
metadata={
|
||||
"help": "target sample rate. audio files will be up/down sampled to this rate"
|
||||
},
|
||||
)
|
||||
normalize: bool = field(
|
||||
default=False,
|
||||
metadata={"help": "if set, normalizes input to have 0 mean and unit variance"},
|
||||
)
|
||||
enable_padding: bool = field(
|
||||
default=False, metadata={"help": "pad shorter samples instead of cropping"}
|
||||
)
|
||||
max_sample_size: Optional[int] = field(
|
||||
default=None, metadata={"help": "max sample size to crop to for batching"}
|
||||
)
|
||||
min_sample_size: Optional[int] = field(
|
||||
default=None, metadata={"help": "min sample size to skip small examples"}
|
||||
)
|
||||
|
||||
# Options for reporting WER metrics during validation. Only applicable to
|
||||
# Seq2Seq models during fine-tuning
|
||||
eval_wer: bool = field(
|
||||
default=False, metadata={"help": "compute WER for Seq2Seq models"}
|
||||
)
|
||||
eval_wer_config: GenerationConfig = field(
|
||||
default_factory=lambda: GenerationConfig(),
|
||||
metadata={"help": "beam search config for evaluating wer during training"},
|
||||
)
|
||||
eval_wer_tokenizer: Any = field(
|
||||
default=None,
|
||||
metadata={"help": "tokenizer config for evaluating wer during training"},
|
||||
)
|
||||
eval_wer_post_process: str = field(
|
||||
default="letter",
|
||||
metadata={
|
||||
"help": "remove BPE tokens before scoring (can be sentencepiece, letter, and more)"
|
||||
},
|
||||
)
|
||||
autoregressive: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"help": "required for autoregressive decoders (like seq2seq models); "
|
||||
"adds 'prev_output_tokens' to input and appends eos to target"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@register_task("audio_pretraining", dataclass=AudioPretrainingConfig)
|
||||
class AudioPretrainingTask(FairseqTask):
|
||||
""""""
|
||||
|
||||
cfg: AudioPretrainingConfig
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cfg: AudioPretrainingConfig,
|
||||
):
|
||||
super().__init__(cfg)
|
||||
if cfg.eval_wer:
|
||||
assert cfg.labels is not None, "eval_wer can only be set during fine-tuning"
|
||||
self.blank_symbol = "<s>"
|
||||
|
||||
self.state.add_factory("target_dictionary", self.load_target_dictionary)
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, cfg: AudioPretrainingConfig, **kwargs):
|
||||
"""Setup the task (e.g., load dictionaries).
|
||||
|
||||
Args:
|
||||
cfg (AudioPretrainingConfig): configuration of this task
|
||||
"""
|
||||
|
||||
return cls(cfg)
|
||||
|
||||
def load_target_dictionary(self):
|
||||
if self.cfg.labels:
|
||||
dict_path = os.path.join(self.cfg.data, f"dict.{self.cfg.labels}.txt")
|
||||
return Dictionary.load(dict_path)
|
||||
return None
|
||||
|
||||
def load_dataset(self, split: str, task_cfg: FairseqDataclass = None, **kwargs):
|
||||
data_path = self.cfg.data
|
||||
task_cfg = task_cfg or self.cfg
|
||||
|
||||
# upgrade old task
|
||||
if isinstance(task_cfg, Namespace):
|
||||
if not hasattr(task_cfg, "autoregressive"):
|
||||
task_cfg.autoregressive = not task_cfg.criterion == 'ctc'
|
||||
|
||||
manifest = os.path.join(data_path, "{}.tsv".format(split))
|
||||
self.datasets[split] = FileAudioDataset(
|
||||
manifest,
|
||||
sample_rate=task_cfg.get('sample_rate', self.cfg.sample_rate),
|
||||
max_sample_size=self.cfg.max_sample_size,
|
||||
min_sample_size=self.cfg.min_sample_size,
|
||||
pad=task_cfg.labels is not None or task_cfg.enable_padding,
|
||||
normalize=task_cfg.normalize,
|
||||
)
|
||||
|
||||
if task_cfg.labels:
|
||||
label_path = os.path.join(data_path, f"{split}.{task_cfg.labels}")
|
||||
with open(label_path, "r") as f:
|
||||
labels = [
|
||||
line for i, line in enumerate(f)
|
||||
if i in self.datasets[split].line_inds
|
||||
]
|
||||
|
||||
assert len(labels) == len(self.datasets[split]), (
|
||||
f"labels length ({len(labels)}) and dataset length "
|
||||
f"({len(self.datasets[split])}) do not match")
|
||||
|
||||
process_label = LabelEncoder(self.target_dictionary)
|
||||
|
||||
self.datasets[split] = AddTargetDataset(
|
||||
self.datasets[split],
|
||||
labels,
|
||||
pad=self.target_dictionary.pad(),
|
||||
eos=self.target_dictionary.eos(),
|
||||
batch_targets=True,
|
||||
process_label=process_label,
|
||||
add_to_input=task_cfg.get('autoregressive', False),
|
||||
)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
"""Return the :class:`~fairseq.data.Dictionary` for the language
|
||||
model."""
|
||||
return self.state.target_dictionary
|
||||
|
||||
def max_positions(self):
|
||||
"""Maximum input length supported by the encoder."""
|
||||
return (sys.maxsize, sys.maxsize)
|
||||
|
||||
def filter_indices_by_size(
|
||||
self,
|
||||
indices,
|
||||
dataset,
|
||||
max_positions=None,
|
||||
ignore_invalid_inputs=False,
|
||||
):
|
||||
# we do not need to filter by size in this task as dataloaders take care of this
|
||||
return indices
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
loss, sample_size, logging_output = super().valid_step(sample, model, criterion)
|
||||
if self.cfg.eval_wer and self.cfg.autoregressive:
|
||||
metrics = self._inference_with_wer(self.sequence_generator, sample, model)
|
||||
logging_output["_num_char_errors"] = metrics["num_char_errors"]
|
||||
logging_output["_num_chars"] = metrics["num_chars"]
|
||||
logging_output["_num_word_errors"] = metrics["num_word_errors"]
|
||||
logging_output["_num_words"] = metrics["num_words"]
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def build_model(self, model_cfg: FairseqDataclass):
|
||||
model = super().build_model(model_cfg)
|
||||
|
||||
if self.cfg.eval_wer and self.cfg.autoregressive:
|
||||
self.sequence_generator = self.build_generator(
|
||||
[model],
|
||||
self.cfg.eval_wer_config,
|
||||
)
|
||||
if self.cfg.eval_wer_tokenizer:
|
||||
self.tokenizer = encoders.build_tokenizer(self.cfg.eval_wer_tokenizer)
|
||||
else:
|
||||
self.tokenizer = None
|
||||
return model
|
||||
|
||||
def _inference_with_wer(self, generator, sample, model):
|
||||
import editdistance
|
||||
|
||||
def decode(toks):
|
||||
s = self.target_dictionary.string(
|
||||
toks.int().cpu(),
|
||||
self.cfg.eval_wer_post_process,
|
||||
escape_unk=True,
|
||||
)
|
||||
if self.tokenizer:
|
||||
s = self.tokenizer.decode(s)
|
||||
return s
|
||||
|
||||
num_word_errors, num_char_errors = 0, 0
|
||||
num_chars, num_words = 0, 0
|
||||
gen_out = self.inference_step(generator, [model], sample, None)
|
||||
for i in range(len(gen_out)):
|
||||
hyp = decode(gen_out[i][0]["tokens"])
|
||||
ref = decode(
|
||||
utils.strip_pad(sample["target"][i], self.target_dictionary.pad()),
|
||||
)
|
||||
num_char_errors += editdistance.eval(hyp, ref)
|
||||
num_chars += len(ref)
|
||||
hyp_words = hyp.split()
|
||||
ref_words = ref.split()
|
||||
num_word_errors += editdistance.eval(hyp_words, ref_words)
|
||||
num_words += len(ref_words)
|
||||
|
||||
return {
|
||||
"num_char_errors": num_char_errors,
|
||||
"num_chars": num_chars,
|
||||
"num_word_errors": num_word_errors,
|
||||
"num_words": num_words,
|
||||
}
|
||||
|
||||
def reduce_metrics(self, logging_outputs, criterion):
|
||||
super().reduce_metrics(logging_outputs, criterion)
|
||||
|
||||
zero = torch.scalar_tensor(0.0)
|
||||
num_char_errors = sum(
|
||||
log.get("_num_char_errors", zero) for log in logging_outputs
|
||||
)
|
||||
num_chars = sum(log.get("_num_chars", zero) for log in logging_outputs)
|
||||
num_word_errors = sum(
|
||||
log.get("_num_word_errors", zero) for log in logging_outputs
|
||||
)
|
||||
num_words = sum(log.get("_num_words", zero) for log in logging_outputs)
|
||||
metrics.log_scalar("_num_char_errors", num_char_errors)
|
||||
metrics.log_scalar("_num_chars", num_chars)
|
||||
metrics.log_scalar("_num_word_errors", num_word_errors)
|
||||
metrics.log_scalar("_num_words", num_words)
|
||||
if num_words > 0:
|
||||
metrics.log_derived(
|
||||
"uer",
|
||||
lambda meters: meters["_num_char_errors"].sum
|
||||
* 100.0
|
||||
/ meters["_num_chars"].sum
|
||||
if meters["_num_chars"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
metrics.log_derived(
|
||||
"wer",
|
||||
lambda meters: meters["_num_word_errors"].sum
|
||||
* 100.0
|
||||
/ meters["_num_words"].sum
|
||||
if meters["_num_words"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import numpy as np
|
||||
from fairseq import tokenizer, utils
|
||||
from fairseq.data import ConcatDataset, Dictionary, TokenBlockDataset, data_utils
|
||||
from fairseq.data.legacy.masked_lm_dataset import MaskedLMDataset
|
||||
from fairseq.data.legacy.masked_lm_dictionary import MaskedLMDictionary
|
||||
from fairseq.data.multi_corpus_sampled_dataset import MultiCorpusSampledDataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("cross_lingual_lm")
|
||||
class CrossLingualLMTask(LegacyFairseqTask):
|
||||
"""
|
||||
Task for training cross-lingual language models.
|
||||
|
||||
For more details look at: https://arxiv.org/pdf/1901.07291.pdf
|
||||
|
||||
Args:
|
||||
dictionary (Dictionary): the dictionary for the input of the task
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data",
|
||||
help="colon separated path to data directories list, \
|
||||
will be iterated upon during epochs in round-robin manner",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments" " per sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--monolingual-langs",
|
||||
default="en",
|
||||
type=str,
|
||||
help="comma separated list of languages for which we"
|
||||
" want to train XLM on",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shuffle",
|
||||
action="store_true",
|
||||
help="shuffle each monolingual dataset while" " training",
|
||||
)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
self.distributed_world_size = args.distributed_world_size
|
||||
self.langs2id = self._lang_to_id(args.monolingual_langs)
|
||||
|
||||
def _lang_to_id(self, languages: str):
|
||||
"""
|
||||
Build a map from languages to ids. These ids are used as segment labels
|
||||
for cross-lingual LM training.
|
||||
"""
|
||||
lang2id = {}
|
||||
langs = [l.strip() for l in languages.split(",")]
|
||||
for id, lang in enumerate(langs):
|
||||
lang2id[lang] = id
|
||||
return lang2id
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
return MaskedLMDictionary.load(filename)
|
||||
|
||||
@classmethod
|
||||
def build_dictionary(
|
||||
cls, filenames, workers=1, threshold=-1, nwords=-1, padding_factor=8
|
||||
):
|
||||
d = MaskedLMDictionary()
|
||||
for filename in filenames:
|
||||
Dictionary.add_file_to_dictionary(
|
||||
filename, d, tokenizer.tokenize_line, workers
|
||||
)
|
||||
d.finalize(threshold=threshold, nwords=nwords, padding_factor=padding_factor)
|
||||
return d
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task."""
|
||||
dictionary = MaskedLMDictionary.load(os.path.join(args.data, "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
return cls(args, dictionary)
|
||||
|
||||
def _load_single_lang_dataset(self, split, epoch):
|
||||
loaded_datasets = []
|
||||
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
for k in itertools.count():
|
||||
split_k = split + (str(k) if k > 0 else "")
|
||||
path = os.path.join(data_path, split_k)
|
||||
|
||||
ds = data_utils.load_indexed_dataset(
|
||||
path, self.dictionary, self.args.dataset_impl
|
||||
)
|
||||
if ds is None:
|
||||
if k > 0:
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, data_path)
|
||||
)
|
||||
|
||||
# Since we append each block with the classification_token,
|
||||
# we need to effectively create blocks of length
|
||||
# tokens_per_sample-1
|
||||
loaded_datasets.append(
|
||||
TokenBlockDataset(
|
||||
ds,
|
||||
ds.sizes,
|
||||
self.args.tokens_per_sample - 1,
|
||||
pad=self.dictionary.pad(),
|
||||
eos=self.dictionary.eos(),
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"{} {} {} examples".format(data_path, split_k, len(loaded_datasets[-1]))
|
||||
)
|
||||
|
||||
if len(loaded_datasets) == 1:
|
||||
dataset = loaded_datasets[0]
|
||||
sizes = dataset.sizes
|
||||
else:
|
||||
dataset = ConcatDataset(loaded_datasets)
|
||||
sizes = np.concatenate([ds.sizes for ds in loaded_datasets])
|
||||
|
||||
return dataset, sizes
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
dataset_map = OrderedDict()
|
||||
|
||||
for lang in self.langs2id.keys():
|
||||
# Datasets are expected to be in "split.lang" format (Eg: train.en)
|
||||
language_split = "{}.{}".format(split, lang)
|
||||
|
||||
block_dataset, sizes = self._load_single_lang_dataset(
|
||||
split=language_split, epoch=epoch
|
||||
)
|
||||
|
||||
dataset_map[lang] = MaskedLMDataset(
|
||||
dataset=block_dataset,
|
||||
sizes=sizes,
|
||||
vocab=self.dictionary,
|
||||
pad_idx=self.dictionary.pad(),
|
||||
mask_idx=self.dictionary.mask(),
|
||||
classif_token_idx=self.dictionary.eos(),
|
||||
sep_token_idx=self.dictionary.eos(),
|
||||
shuffle=getattr(self.args, "shuffle", False),
|
||||
has_pairs=False,
|
||||
segment_id=self.langs2id[lang],
|
||||
seed=self.seed,
|
||||
)
|
||||
|
||||
self.datasets[split] = MultiCorpusSampledDataset(dataset_map)
|
||||
logger.info(
|
||||
"{} {} {} examples".format(
|
||||
utils.split_paths(self.args.data)[epoch - 1],
|
||||
split,
|
||||
len(self.datasets[split]),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
AppendTokenDataset,
|
||||
DenoisingDataset,
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
PadDataset,
|
||||
PrependTokenDataset,
|
||||
StripTokenDataset,
|
||||
TokenBlockDataset,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.encoders.utils import get_whole_word_mask
|
||||
from fairseq.data.shorten_dataset import maybe_shorten_dataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
import numpy as np
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("denoising")
|
||||
class DenoisingTask(LegacyFairseqTask):
|
||||
"""
|
||||
Denoising task for applying sequence to sequence denoising. (ie. BART)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument("data", help="path to data directory")
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments"
|
||||
" per sample for dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-break-mode",
|
||||
default="complete_doc",
|
||||
type=str,
|
||||
help="mode for breaking sentence",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask",
|
||||
default=0.0,
|
||||
type=float,
|
||||
help="fraction of words/subwords that will be masked",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-random",
|
||||
default=0.0,
|
||||
type=float,
|
||||
help="instead of using [MASK], use random token this often",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insert",
|
||||
default=0.0,
|
||||
type=float,
|
||||
help="insert this percentage of additional random tokens",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--permute",
|
||||
default=0.0,
|
||||
type=float,
|
||||
help="take this proportion of subwords and permute them",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rotate",
|
||||
default=0.5,
|
||||
type=float,
|
||||
help="rotate this proportion of inputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--poisson-lambda",
|
||||
default=3.0,
|
||||
type=float,
|
||||
help="randomly shuffle sentences for this proportion of inputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--permute-sentences",
|
||||
default=0.0,
|
||||
type=float,
|
||||
help="shuffle this proportion of sentences in all inputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-length",
|
||||
default="subword",
|
||||
type=str,
|
||||
choices=["subword", "word", "span-poisson"],
|
||||
help="mask length to choose",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--replace-length",
|
||||
default=-1,
|
||||
type=int,
|
||||
help="when masking N tokens, replace with 0, 1, or N tokens (use -1 for N)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-source-positions",
|
||||
default=1024,
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="max number of tokens in the source sequence",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-target-positions",
|
||||
default=1024,
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="max number of tokens in the target sequence",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--shorten-method",
|
||||
default="none",
|
||||
choices=["none", "truncate", "random_crop"],
|
||||
help="if not none, shorten sequences that exceed --tokens-per-sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shorten-data-split-list",
|
||||
default="",
|
||||
help="comma-separated list of dataset splits to apply shortening to, "
|
||||
'e.g., "train,valid" (default: all dataset splits)',
|
||||
)
|
||||
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
# add mask token
|
||||
self.mask_idx = self.dictionary.add_symbol("<mask>")
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task."""
|
||||
dictionary = Dictionary.load(os.path.join(args.data, "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
if not hasattr(args, "shuffle_instance"):
|
||||
args.shuffle_instance = False
|
||||
return cls(args, dictionary)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
split_path = os.path.join(data_path, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
self.dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
if dataset is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
dataset = StripTokenDataset(dataset, self.dictionary.eos())
|
||||
|
||||
dataset = maybe_shorten_dataset(
|
||||
dataset,
|
||||
split,
|
||||
self.args.shorten_data_split_list,
|
||||
self.args.shorten_method,
|
||||
self.args.tokens_per_sample,
|
||||
self.args.seed,
|
||||
)
|
||||
|
||||
# create continuous blocks of tokens
|
||||
dataset = TokenBlockDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.args.tokens_per_sample - 2, # one less for <s> and one for </s>
|
||||
pad=self.dictionary.pad(),
|
||||
eos=self.dictionary.eos(),
|
||||
break_mode=self.args.sample_break_mode,
|
||||
document_sep_len=0,
|
||||
)
|
||||
|
||||
# prepend beginning-of-sentence token (<s>, equiv. to [CLS] in BERT)
|
||||
dataset = PrependTokenDataset(dataset, self.source_dictionary.bos())
|
||||
dataset = AppendTokenDataset(dataset, self.source_dictionary.eos())
|
||||
|
||||
mask_whole_words = (
|
||||
get_whole_word_mask(self.args, self.source_dictionary)
|
||||
if self.args.mask_length != "subword"
|
||||
else None
|
||||
)
|
||||
|
||||
self.datasets[split] = DenoisingDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.dictionary,
|
||||
self.mask_idx,
|
||||
mask_whole_words,
|
||||
shuffle=self.args.shuffle_instance,
|
||||
seed=self.seed,
|
||||
args=self.args,
|
||||
)
|
||||
logger.info(
|
||||
"Split: {0}, Loaded {1} samples of denoising_dataset".format(
|
||||
split,
|
||||
len(self.datasets[split]),
|
||||
)
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, **kwargs):
|
||||
"""
|
||||
Generate batches for inference. We assume that the input begins with a
|
||||
bos symbol (`<s>`) and ends with an eos symbol (`</s>`).
|
||||
"""
|
||||
pad = self.source_dictionary.pad()
|
||||
eos = self.source_dictionary.eos()
|
||||
src_dataset = TokenBlockDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
block_size=self.args.tokens_per_sample - 2, # for <s> and </s>
|
||||
pad=pad,
|
||||
eos=eos,
|
||||
break_mode=self.args.sample_break_mode,
|
||||
document_sep_len=0,
|
||||
)
|
||||
prev_output_tokens = PrependTokenDataset(
|
||||
StripTokenDataset(src_dataset, eos), eos
|
||||
)
|
||||
src_dataset = PadDataset(src_dataset, pad_idx=pad, left_pad=False)
|
||||
return NestedDictionaryDataset(
|
||||
{
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": src_dataset,
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
"prev_output_tokens": PadDataset(
|
||||
prev_output_tokens, pad_idx=pad, left_pad=False
|
||||
),
|
||||
},
|
||||
"target": src_dataset,
|
||||
},
|
||||
sizes=[np.array(src_lengths)],
|
||||
)
|
||||
|
||||
def max_positions(self):
|
||||
"""Return the max sentence length allowed by the task."""
|
||||
return (self.args.max_source_positions, self.args.max_target_positions)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
"""Return the source :class:`~fairseq.data.Dictionary`."""
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
"""Return the target :class:`~fairseq.data.Dictionary`."""
|
||||
return self.dictionary
|
||||
@@ -0,0 +1,650 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from argparse import Namespace
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
import torch
|
||||
from fairseq import metrics, search, tokenizer, utils
|
||||
from fairseq.data import Dictionary, FairseqDataset, data_utils, encoders, iterators
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.dataclass.utils import gen_parser_from_dataclass
|
||||
from omegaconf import DictConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StatefulContainer(object):
|
||||
|
||||
_state: Dict[str, Any] = dict()
|
||||
_factories: Dict[str, Callable[[], Any]] = dict()
|
||||
|
||||
def add_factory(self, name, factory: Callable[[], Any]):
|
||||
self._factories[name] = factory
|
||||
|
||||
def merge_state_dict(self, state_dict: Dict[str, Any]):
|
||||
self._state.update(state_dict)
|
||||
|
||||
@property
|
||||
def state_dict(self) -> Dict[str, Any]:
|
||||
return self._state
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self._state and name in self._factories:
|
||||
self._state[name] = self._factories[name]()
|
||||
|
||||
if name in self._state:
|
||||
return self._state[name]
|
||||
|
||||
raise AttributeError(f"Task state has no factory for attribute {name}")
|
||||
|
||||
|
||||
class FairseqTask(object):
|
||||
"""
|
||||
Tasks store dictionaries and provide helpers for loading/iterating over
|
||||
Datasets, initializing the Model/Criterion and calculating the loss.
|
||||
|
||||
Tasks have limited statefulness. In particular, state that needs to be
|
||||
saved to/loaded from checkpoints needs to be stored in the `self.state`
|
||||
:class:`StatefulContainer` object. For example::
|
||||
|
||||
self.state.add_factory("dictionary", self.load_dictionary)
|
||||
print(self.state.dictionary) # calls self.load_dictionary()
|
||||
|
||||
This is necessary so that when loading checkpoints, we can properly
|
||||
recreate the task state after initializing the task instance.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def add_args(cls, parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
dc = getattr(cls, "__dataclass", None)
|
||||
if dc is not None:
|
||||
gen_parser_from_dataclass(parser, dc())
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed(criterion) -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `train_step` and `valid_step` can
|
||||
be summed across workers prior to calling `aggregate_logging_outputs`.
|
||||
Setting this to True will improves distributed training speed.
|
||||
"""
|
||||
return criterion.logging_outputs_can_be_summed()
|
||||
|
||||
cfg: FairseqDataclass
|
||||
datasets: Dict[str, FairseqDataset]
|
||||
dataset_to_epoch_iter: Dict[FairseqDataset, Any]
|
||||
state: StatefulContainer = None
|
||||
|
||||
def __init__(self, cfg: FairseqDataclass, **kwargs):
|
||||
self.cfg = cfg
|
||||
self.datasets = dict()
|
||||
self.dataset_to_epoch_iter = dict()
|
||||
self.state = StatefulContainer()
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
"""Load the dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
return Dictionary.load(filename)
|
||||
|
||||
@classmethod
|
||||
def build_dictionary(
|
||||
cls, filenames, workers=1, threshold=-1, nwords=-1, padding_factor=8
|
||||
):
|
||||
"""Build the dictionary
|
||||
|
||||
Args:
|
||||
filenames (list): list of filenames
|
||||
workers (int): number of concurrent workers
|
||||
threshold (int): defines the minimum word count
|
||||
nwords (int): defines the total number of words in the final dictionary,
|
||||
including special symbols
|
||||
padding_factor (int): can be used to pad the dictionary size to be a
|
||||
multiple of 8, which is important on some hardware (e.g., Nvidia
|
||||
Tensor Cores).
|
||||
"""
|
||||
d = Dictionary()
|
||||
for filename in filenames:
|
||||
Dictionary.add_file_to_dictionary(
|
||||
filename, d, tokenizer.tokenize_line, workers
|
||||
)
|
||||
d.finalize(threshold=threshold, nwords=nwords, padding_factor=padding_factor)
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, cfg: DictConfig, **kwargs):
|
||||
"""Setup the task (e.g., load dictionaries).
|
||||
|
||||
Args:
|
||||
cfg (omegaconf.DictConfig): parsed command-line arguments
|
||||
"""
|
||||
return cls(cfg, **kwargs)
|
||||
|
||||
def has_sharded_data(self, split):
|
||||
return os.pathsep in getattr(self.cfg, "data", "")
|
||||
|
||||
def load_dataset(
|
||||
self,
|
||||
split: str,
|
||||
combine: bool = False,
|
||||
task_cfg: FairseqDataclass = None,
|
||||
**kwargs
|
||||
):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
combine (bool): combines a split segmented into pieces into one dataset
|
||||
task_cfg (FairseqDataclass): optional task configuration stored in the checkpoint that can be used
|
||||
to load datasets
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def dataset(self, split):
|
||||
"""
|
||||
Return a loaded dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
|
||||
Returns:
|
||||
a :class:`~fairseq.data.FairseqDataset` corresponding to *split*
|
||||
"""
|
||||
from fairseq.data import FairseqDataset
|
||||
|
||||
if split not in self.datasets:
|
||||
raise KeyError("Dataset not loaded: " + split)
|
||||
if not isinstance(self.datasets[split], FairseqDataset):
|
||||
raise TypeError("Datasets are expected to be of type FairseqDataset")
|
||||
return self.datasets[split]
|
||||
|
||||
def filter_indices_by_size(
|
||||
self, indices, dataset, max_positions=None, ignore_invalid_inputs=False
|
||||
):
|
||||
"""
|
||||
Filter examples that are too large
|
||||
|
||||
Args:
|
||||
indices (np.array): original array of sample indices
|
||||
dataset (~fairseq.data.FairseqDataset): dataset to batch
|
||||
max_positions (optional): max sentence length supported by the
|
||||
model (default: None).
|
||||
ignore_invalid_inputs (bool, optional): don't raise Exception for
|
||||
sentences that are too long (default: False).
|
||||
Returns:
|
||||
np.array: array of filtered sample indices
|
||||
"""
|
||||
indices, ignored = dataset.filter_indices_by_size(indices, max_positions)
|
||||
if len(ignored) > 0:
|
||||
if not ignore_invalid_inputs:
|
||||
raise Exception(
|
||||
(
|
||||
"Size of sample #{} is invalid (={}) since max_positions={}, "
|
||||
"skip this example with --skip-invalid-size-inputs-valid-test"
|
||||
).format(ignored[0], dataset.size(ignored[0]), max_positions)
|
||||
)
|
||||
logger.warning(
|
||||
(
|
||||
"{:,} samples have invalid sizes and will be skipped, "
|
||||
"max_positions={}, first few sample ids={}"
|
||||
).format(len(ignored), max_positions, ignored[:10])
|
||||
)
|
||||
return indices
|
||||
|
||||
def can_reuse_epoch_itr(self, dataset):
|
||||
# We can reuse the epoch iterator across epochs as long as the dataset
|
||||
# hasn't disabled it. We default to ``False`` here, although in practice
|
||||
# this will be ``True`` for most datasets that inherit from
|
||||
# ``FairseqDataset`` due to the base implementation there.
|
||||
return getattr(dataset, "can_reuse_epoch_itr_across_epochs", False)
|
||||
|
||||
def get_batch_iterator(
|
||||
self,
|
||||
dataset,
|
||||
max_tokens=None,
|
||||
max_sentences=None,
|
||||
max_positions=None,
|
||||
ignore_invalid_inputs=False,
|
||||
required_batch_size_multiple=1,
|
||||
seed=1,
|
||||
num_shards=1,
|
||||
shard_id=0,
|
||||
num_workers=0,
|
||||
epoch=1,
|
||||
data_buffer_size=0,
|
||||
disable_iterator_cache=False,
|
||||
):
|
||||
"""
|
||||
Get an iterator that yields batches of data from the given dataset.
|
||||
|
||||
Args:
|
||||
dataset (~fairseq.data.FairseqDataset): dataset to batch
|
||||
max_tokens (int, optional): max number of tokens in each batch
|
||||
(default: None).
|
||||
max_sentences (int, optional): max number of sentences in each
|
||||
batch (default: None).
|
||||
max_positions (optional): max sentence length supported by the
|
||||
model (default: None).
|
||||
ignore_invalid_inputs (bool, optional): don't raise Exception for
|
||||
sentences that are too long (default: False).
|
||||
required_batch_size_multiple (int, optional): require batch size to
|
||||
be a multiple of N (default: 1).
|
||||
seed (int, optional): seed for random number generator for
|
||||
reproducibility (default: 1).
|
||||
num_shards (int, optional): shard the data iterator into N
|
||||
shards (default: 1).
|
||||
shard_id (int, optional): which shard of the data iterator to
|
||||
return (default: 0).
|
||||
num_workers (int, optional): how many subprocesses to use for data
|
||||
loading. 0 means the data will be loaded in the main process
|
||||
(default: 0).
|
||||
epoch (int, optional): the epoch to start the iterator from
|
||||
(default: 1).
|
||||
data_buffer_size (int, optional): number of batches to
|
||||
preload (default: 0).
|
||||
disable_iterator_cache (bool, optional): don't cache the
|
||||
EpochBatchIterator (ignores `FairseqTask::can_reuse_epoch_itr`)
|
||||
(default: False).
|
||||
Returns:
|
||||
~fairseq.iterators.EpochBatchIterator: a batched iterator over the
|
||||
given dataset split
|
||||
"""
|
||||
can_reuse_epoch_itr = not disable_iterator_cache and self.can_reuse_epoch_itr(
|
||||
dataset
|
||||
)
|
||||
if can_reuse_epoch_itr and dataset in self.dataset_to_epoch_iter:
|
||||
logger.debug("reusing EpochBatchIterator for epoch {}".format(epoch))
|
||||
return self.dataset_to_epoch_iter[dataset]
|
||||
|
||||
assert isinstance(dataset, FairseqDataset)
|
||||
|
||||
# initialize the dataset with the correct starting epoch
|
||||
dataset.set_epoch(epoch)
|
||||
|
||||
# get indices ordered by example size
|
||||
with data_utils.numpy_seed(seed):
|
||||
indices = dataset.ordered_indices()
|
||||
|
||||
# filter examples that are too large
|
||||
if max_positions is not None:
|
||||
indices = self.filter_indices_by_size(
|
||||
indices, dataset, max_positions, ignore_invalid_inputs
|
||||
)
|
||||
|
||||
# create mini-batches with given size constraints
|
||||
batch_sampler = dataset.batch_by_size(
|
||||
indices,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=max_sentences,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
)
|
||||
|
||||
# return a reusable, sharded iterator
|
||||
epoch_iter = iterators.EpochBatchIterator(
|
||||
dataset=dataset,
|
||||
collate_fn=dataset.collater,
|
||||
batch_sampler=batch_sampler,
|
||||
seed=seed,
|
||||
num_shards=num_shards,
|
||||
shard_id=shard_id,
|
||||
num_workers=num_workers,
|
||||
epoch=epoch,
|
||||
buffer_size=data_buffer_size,
|
||||
)
|
||||
|
||||
if can_reuse_epoch_itr:
|
||||
self.dataset_to_epoch_iter[dataset] = epoch_iter
|
||||
|
||||
return epoch_iter
|
||||
|
||||
def build_model(self, cfg: FairseqDataclass):
|
||||
"""
|
||||
Build the :class:`~fairseq.models.BaseFairseqModel` instance for this
|
||||
task.
|
||||
|
||||
Args:
|
||||
cfg (FairseqDataclass): configuration object
|
||||
|
||||
Returns:
|
||||
a :class:`~fairseq.models.BaseFairseqModel` instance
|
||||
"""
|
||||
from fairseq import models, quantization_utils
|
||||
|
||||
model = models.build_model(cfg, self)
|
||||
model = quantization_utils.quantize_model_scalar(model, cfg)
|
||||
return model
|
||||
|
||||
def build_criterion(self, cfg: DictConfig):
|
||||
"""
|
||||
Build the :class:`~fairseq.criterions.FairseqCriterion` instance for
|
||||
this task.
|
||||
|
||||
Args:
|
||||
cfg (omegaconf.DictConfig): configration object
|
||||
|
||||
Returns:
|
||||
a :class:`~fairseq.criterions.FairseqCriterion` instance
|
||||
"""
|
||||
from fairseq import criterions
|
||||
|
||||
return criterions.build_criterion(cfg, self)
|
||||
|
||||
def build_generator(
|
||||
self, models, args, seq_gen_cls=None, extra_gen_cls_kwargs=None
|
||||
):
|
||||
if getattr(args, "score_reference", False):
|
||||
from fairseq.sequence_scorer import SequenceScorer
|
||||
|
||||
return SequenceScorer(
|
||||
self.target_dictionary,
|
||||
compute_alignment=getattr(args, "print_alignment", False),
|
||||
)
|
||||
|
||||
from fairseq.sequence_generator import (
|
||||
SequenceGenerator,
|
||||
SequenceGeneratorWithAlignment,
|
||||
)
|
||||
try:
|
||||
from fairseq.fb_sequence_generator import FBSequenceGenerator
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
# Choose search strategy. Defaults to Beam Search.
|
||||
sampling = getattr(args, "sampling", False)
|
||||
sampling_topk = getattr(args, "sampling_topk", -1)
|
||||
sampling_topp = getattr(args, "sampling_topp", -1.0)
|
||||
diverse_beam_groups = getattr(args, "diverse_beam_groups", -1)
|
||||
diverse_beam_strength = getattr(args, "diverse_beam_strength", 0.5)
|
||||
match_source_len = getattr(args, "match_source_len", False)
|
||||
diversity_rate = getattr(args, "diversity_rate", -1)
|
||||
constrained = getattr(args, "constraints", False)
|
||||
prefix_allowed_tokens_fn = getattr(args, "prefix_allowed_tokens_fn", None)
|
||||
if (
|
||||
sum(
|
||||
int(cond)
|
||||
for cond in [
|
||||
sampling,
|
||||
diverse_beam_groups > 0,
|
||||
match_source_len,
|
||||
diversity_rate > 0,
|
||||
]
|
||||
)
|
||||
> 1
|
||||
):
|
||||
raise ValueError("Provided Search parameters are mutually exclusive.")
|
||||
assert sampling_topk < 0 or sampling, "--sampling-topk requires --sampling"
|
||||
assert sampling_topp < 0 or sampling, "--sampling-topp requires --sampling"
|
||||
|
||||
if sampling:
|
||||
search_strategy = search.Sampling(
|
||||
self.target_dictionary, sampling_topk, sampling_topp
|
||||
)
|
||||
elif diverse_beam_groups > 0:
|
||||
search_strategy = search.DiverseBeamSearch(
|
||||
self.target_dictionary, diverse_beam_groups, diverse_beam_strength
|
||||
)
|
||||
elif match_source_len:
|
||||
# this is useful for tagging applications where the output
|
||||
# length should match the input length, so we hardcode the
|
||||
# length constraints for simplicity
|
||||
search_strategy = search.LengthConstrainedBeamSearch(
|
||||
self.target_dictionary,
|
||||
min_len_a=1,
|
||||
min_len_b=0,
|
||||
max_len_a=1,
|
||||
max_len_b=0,
|
||||
)
|
||||
elif diversity_rate > -1:
|
||||
search_strategy = search.DiverseSiblingsSearch(
|
||||
self.target_dictionary, diversity_rate
|
||||
)
|
||||
elif constrained:
|
||||
search_strategy = search.LexicallyConstrainedBeamSearch(
|
||||
self.target_dictionary, args.constraints
|
||||
)
|
||||
elif prefix_allowed_tokens_fn:
|
||||
search_strategy = search.PrefixConstrainedBeamSearch(
|
||||
self.target_dictionary, prefix_allowed_tokens_fn
|
||||
)
|
||||
else:
|
||||
search_strategy = search.BeamSearch(self.target_dictionary)
|
||||
|
||||
extra_gen_cls_kwargs = extra_gen_cls_kwargs or {}
|
||||
if seq_gen_cls is None:
|
||||
if getattr(args, "print_alignment", False):
|
||||
seq_gen_cls = SequenceGeneratorWithAlignment
|
||||
extra_gen_cls_kwargs["print_alignment"] = args.print_alignment
|
||||
elif getattr(args, "fb_seq_gen", False):
|
||||
seq_gen_cls = FBSequenceGenerator
|
||||
else:
|
||||
seq_gen_cls = SequenceGenerator
|
||||
|
||||
return seq_gen_cls(
|
||||
models,
|
||||
self.target_dictionary,
|
||||
beam_size=getattr(args, "beam", 5),
|
||||
max_len_a=getattr(args, "max_len_a", 0),
|
||||
max_len_b=getattr(args, "max_len_b", 200),
|
||||
min_len=getattr(args, "min_len", 1),
|
||||
normalize_scores=(not getattr(args, "unnormalized", False)),
|
||||
len_penalty=getattr(args, "lenpen", 1),
|
||||
unk_penalty=getattr(args, "unkpen", 0),
|
||||
temperature=getattr(args, "temperature", 1.0),
|
||||
match_source_len=getattr(args, "match_source_len", False),
|
||||
no_repeat_ngram_size=getattr(args, "no_repeat_ngram_size", 0),
|
||||
search_strategy=search_strategy,
|
||||
**extra_gen_cls_kwargs,
|
||||
)
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
"""
|
||||
Do forward and backward, and return the loss as computed by *criterion*
|
||||
for the given *model* and *sample*.
|
||||
|
||||
Args:
|
||||
sample (dict): the mini-batch. The format is defined by the
|
||||
:class:`~fairseq.data.FairseqDataset`.
|
||||
model (~fairseq.models.BaseFairseqModel): the model
|
||||
criterion (~fairseq.criterions.FairseqCriterion): the criterion
|
||||
optimizer (~fairseq.optim.FairseqOptimizer): the optimizer
|
||||
update_num (int): the current update
|
||||
ignore_grad (bool): multiply loss by 0 if this is set to True
|
||||
|
||||
Returns:
|
||||
tuple:
|
||||
- the loss
|
||||
- the sample size, which is used as the denominator for the
|
||||
gradient
|
||||
- logging outputs to display while training
|
||||
"""
|
||||
model.train()
|
||||
model.set_num_updates(update_num)
|
||||
with torch.autograd.profiler.record_function("forward"):
|
||||
loss, sample_size, logging_output = criterion(model, sample)
|
||||
if ignore_grad:
|
||||
loss *= 0
|
||||
with torch.autograd.profiler.record_function("backward"):
|
||||
optimizer.backward(loss)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
loss, sample_size, logging_output = criterion(model, sample)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def optimizer_step(self, optimizer, model, update_num):
|
||||
optimizer.step()
|
||||
|
||||
def build_dataset_for_inference(
|
||||
self, src_tokens: List[torch.Tensor], src_lengths: List[int], **kwargs
|
||||
) -> torch.utils.data.Dataset:
|
||||
raise NotImplementedError
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
with torch.no_grad():
|
||||
return generator.generate(
|
||||
models, sample, prefix_tokens=prefix_tokens, constraints=constraints
|
||||
)
|
||||
|
||||
def begin_epoch(self, epoch, model):
|
||||
"""Hook function called before the start of each epoch."""
|
||||
pass
|
||||
|
||||
def begin_valid_epoch(self, epoch, model):
|
||||
"""Hook function called before the start of each validation epoch."""
|
||||
pass
|
||||
|
||||
def aggregate_logging_outputs(self, logging_outputs, criterion):
|
||||
"""[deprecated] Aggregate logging outputs from data parallel training."""
|
||||
utils.deprecation_warning(
|
||||
"The aggregate_logging_outputs API is deprecated. "
|
||||
"Please use the reduce_metrics API instead."
|
||||
)
|
||||
with metrics.aggregate() as agg:
|
||||
self.reduce_metrics(logging_outputs, criterion)
|
||||
return agg.get_smoothed_values()
|
||||
|
||||
def reduce_metrics(self, logging_outputs, criterion):
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
# backward compatibility for tasks that override aggregate_logging_outputs
|
||||
base_func = FairseqTask.aggregate_logging_outputs
|
||||
self_func = getattr(self, "aggregate_logging_outputs").__func__
|
||||
if self_func is not base_func:
|
||||
utils.deprecation_warning(
|
||||
"Tasks should implement the reduce_metrics API. "
|
||||
"Falling back to deprecated aggregate_logging_outputs API."
|
||||
)
|
||||
agg_logging_outputs = self.aggregate_logging_outputs(
|
||||
logging_outputs, criterion
|
||||
)
|
||||
for k, v in agg_logging_outputs.items():
|
||||
metrics.log_scalar(k, v)
|
||||
return
|
||||
|
||||
if not any("ntokens" in log for log in logging_outputs):
|
||||
warnings.warn(
|
||||
"ntokens not found in Criterion logging outputs, cannot log wpb or wps"
|
||||
)
|
||||
else:
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("wpb", ntokens, priority=180, round=1)
|
||||
metrics.log_speed("wps", ntokens, priority=90, round=1)
|
||||
|
||||
if not any("nsentences" in log for log in logging_outputs):
|
||||
warnings.warn(
|
||||
"nsentences not found in Criterion logging outputs, cannot log bsz"
|
||||
)
|
||||
else:
|
||||
nsentences = sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("bsz", nsentences, priority=190, round=1)
|
||||
|
||||
criterion.__class__.reduce_metrics(logging_outputs)
|
||||
|
||||
def state_dict(self):
|
||||
if self.state is not None:
|
||||
return self.state.state_dict
|
||||
return {}
|
||||
|
||||
def load_state_dict(self, state_dict: Dict[str, Any]):
|
||||
if self.state is not None:
|
||||
self.state.merge_state_dict(state_dict)
|
||||
|
||||
def max_positions(self):
|
||||
"""Return the max input length allowed by the task."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
"""Return the source :class:`~fairseq.data.Dictionary` (if applicable
|
||||
for this task)."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
"""Return the target :class:`~fairseq.data.Dictionary` (if applicable
|
||||
for this task)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def build_tokenizer(self, args):
|
||||
"""Build the pre-tokenizer for this task."""
|
||||
return encoders.build_tokenizer(args)
|
||||
|
||||
def build_bpe(self, args):
|
||||
"""Build the tokenizer for this task."""
|
||||
return encoders.build_bpe(args)
|
||||
|
||||
def get_interactive_tokens_and_lengths(self, lines, encode_fn):
|
||||
tokens = [
|
||||
self.source_dictionary.encode_line(
|
||||
encode_fn(src_str), add_if_not_exist=False
|
||||
).long()
|
||||
for src_str in lines
|
||||
]
|
||||
lengths = [t.numel() for t in tokens]
|
||||
return tokens, lengths
|
||||
|
||||
|
||||
class LegacyFairseqTask(FairseqTask):
|
||||
def __init__(self, args: Namespace):
|
||||
self.args = args
|
||||
self.datasets = {}
|
||||
self.dataset_to_epoch_iter = {}
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args: Namespace, **kwargs):
|
||||
"""Setup the task (e.g., load dictionaries).
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
"""
|
||||
return cls(args, **kwargs)
|
||||
|
||||
def has_sharded_data(self, split):
|
||||
return os.pathsep in getattr(self.args, "data", "")
|
||||
|
||||
def build_model(self, args: Namespace):
|
||||
"""
|
||||
Build the :class:`~fairseq.models.BaseFairseqModel` instance for this
|
||||
task.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
a :class:`~fairseq.models.BaseFairseqModel` instance
|
||||
"""
|
||||
from fairseq import models, quantization_utils
|
||||
|
||||
model = models.build_model(args, self)
|
||||
model = quantization_utils.quantize_model_scalar(model, args)
|
||||
return model
|
||||
|
||||
def build_criterion(self, args: Namespace):
|
||||
"""
|
||||
Build the :class:`~fairseq.criterions.FairseqCriterion` instance for
|
||||
this task.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
|
||||
Returns:
|
||||
a :class:`~fairseq.criterions.FairseqCriterion` instance
|
||||
"""
|
||||
from fairseq import criterions
|
||||
|
||||
return criterions.build_criterion(args, self)
|
||||
@@ -0,0 +1,358 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
AppendTokenDataset,
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
LMContextWindowDataset,
|
||||
MonolingualDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
PadDataset,
|
||||
PrependTokenDataset,
|
||||
StripTokenDataset,
|
||||
TokenBlockDataset,
|
||||
TruncatedDictionary,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.indexed_dataset import get_available_dataset_impl
|
||||
from fairseq.data.shorten_dataset import maybe_shorten_dataset
|
||||
from fairseq.dataclass import ChoiceEnum, FairseqDataclass
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
SAMPLE_BREAK_MODE_CHOICES = ChoiceEnum(["none", "complete", "complete_doc", "eos"])
|
||||
SHORTEN_METHOD_CHOICES = ChoiceEnum(["none", "truncate", "random_crop"])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LanguageModelingConfig(FairseqDataclass):
|
||||
data: Optional[str] = field(
|
||||
default=None, metadata={"help": "path to data directory"}
|
||||
)
|
||||
sample_break_mode: SAMPLE_BREAK_MODE_CHOICES = field(
|
||||
default="none",
|
||||
metadata={
|
||||
"help": 'If omitted or "none", fills each sample with tokens-per-sample '
|
||||
'tokens. If set to "complete", splits samples only at the end '
|
||||
"of sentence, but may include multiple sentences per sample. "
|
||||
'"complete_doc" is similar but respects doc boundaries. '
|
||||
'If set to "eos", includes only one sentence per sample.'
|
||||
},
|
||||
)
|
||||
tokens_per_sample: int = field(
|
||||
default=1024,
|
||||
metadata={"help": "max number of tokens per sample for LM dataset"},
|
||||
)
|
||||
output_dictionary_size: int = field(
|
||||
default=-1, metadata={"help": "limit the size of output dictionary"}
|
||||
)
|
||||
self_target: bool = field(default=False, metadata={"help": "include self target"})
|
||||
future_target: bool = field(
|
||||
default=False, metadata={"help": "include future target"}
|
||||
)
|
||||
past_target: bool = field(default=False, metadata={"help": "include past target"})
|
||||
add_bos_token: bool = field(
|
||||
default=False, metadata={"help": "prepend beginning of sentence token (<s>)"}
|
||||
)
|
||||
max_target_positions: Optional[int] = field(
|
||||
default=None, metadata={"help": "max number of tokens in the target sequence"}
|
||||
)
|
||||
shorten_method: SHORTEN_METHOD_CHOICES = field(
|
||||
default="none",
|
||||
metadata={
|
||||
"help": "if not none, shorten sequences that exceed --tokens-per-sample"
|
||||
},
|
||||
)
|
||||
shorten_data_split_list: str = field(
|
||||
default="",
|
||||
metadata={
|
||||
"help": "comma-separated list of dataset splits to apply shortening to, "
|
||||
'e.g., "train,valid" (default: all dataset splits)'
|
||||
},
|
||||
)
|
||||
# TODO common vars below add to parent
|
||||
seed: int = II("common.seed")
|
||||
dataset_impl: Optional[ChoiceEnum(get_available_dataset_impl())] = II(
|
||||
"dataset.dataset_impl"
|
||||
)
|
||||
data_buffer_size: int = II("dataset.data_buffer_size")
|
||||
tpu: bool = II("common.tpu")
|
||||
|
||||
|
||||
@register_task("language_modeling", dataclass=LanguageModelingConfig)
|
||||
class LanguageModelingTask(LegacyFairseqTask):
|
||||
"""
|
||||
Train a language model.
|
||||
|
||||
Args:
|
||||
dictionary (~fairseq.data.Dictionary): the dictionary for the input of
|
||||
the language model
|
||||
output_dictionary (~fairseq.data.Dictionary): the dictionary for the
|
||||
output of the language model. In most cases it will be the same as
|
||||
*dictionary*, but could possibly be a more limited version of the
|
||||
dictionary (if ``--output-dictionary-size`` is used).
|
||||
targets (List[str]): list of the target types that the language model
|
||||
should predict. Can be one of "self", "future", and "past".
|
||||
Defaults to "future".
|
||||
|
||||
.. note::
|
||||
|
||||
The language modeling task is compatible with :mod:`fairseq-train`,
|
||||
:mod:`fairseq-generate`, :mod:`fairseq-interactive` and
|
||||
:mod:`fairseq-eval-lm`.
|
||||
|
||||
The language modeling task provides the following additional command-line
|
||||
arguments:
|
||||
|
||||
.. argparse::
|
||||
:ref: fairseq.tasks.language_modeling_parser
|
||||
:prog:
|
||||
"""
|
||||
|
||||
def __init__(self, args, dictionary, output_dictionary=None, targets=None):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.output_dictionary = output_dictionary or dictionary
|
||||
|
||||
if targets is None:
|
||||
targets = ["future"]
|
||||
self.targets = targets
|
||||
|
||||
@classmethod
|
||||
def setup_dictionary(cls, args, **kwargs):
|
||||
dictionary = None
|
||||
output_dictionary = None
|
||||
if args.data:
|
||||
paths = utils.split_paths(args.data)
|
||||
assert len(paths) > 0
|
||||
dictionary = Dictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
output_dictionary = dictionary
|
||||
if args.output_dictionary_size >= 0:
|
||||
output_dictionary = TruncatedDictionary(
|
||||
dictionary, args.output_dictionary_size
|
||||
)
|
||||
return (dictionary, output_dictionary)
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task (e.g., load dictionaries).
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
"""
|
||||
dictionary, output_dictionary = cls.setup_dictionary(args, **kwargs)
|
||||
|
||||
# upgrade old checkpoints
|
||||
if getattr(args, "exclude_self_target", False):
|
||||
args.self_target = False
|
||||
|
||||
targets = []
|
||||
if getattr(args, "self_target", False):
|
||||
targets.append("self")
|
||||
if getattr(args, "future_target", False):
|
||||
targets.append("future")
|
||||
if getattr(args, "past_target", False):
|
||||
targets.append("past")
|
||||
if len(targets) == 0:
|
||||
# standard language modeling
|
||||
targets = ["future"]
|
||||
|
||||
return cls(args, dictionary, output_dictionary, targets=targets)
|
||||
|
||||
def build_model(self, args):
|
||||
model = super().build_model(args)
|
||||
for target in self.targets:
|
||||
if target not in model.supported_targets:
|
||||
raise ValueError(
|
||||
"Unsupported language modeling target: {}".format(target)
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def load_dataset(
|
||||
self, split: str, epoch=1, combine=False, **kwargs
|
||||
) -> MonolingualDataset:
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
split_path = os.path.join(data_path, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path, self.dictionary, self.args.dataset_impl, combine=combine
|
||||
)
|
||||
if dataset is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
dataset = maybe_shorten_dataset(
|
||||
dataset,
|
||||
split,
|
||||
self.args.shorten_data_split_list,
|
||||
self.args.shorten_method,
|
||||
self.args.tokens_per_sample,
|
||||
self.args.seed,
|
||||
)
|
||||
|
||||
dataset = TokenBlockDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.args.tokens_per_sample,
|
||||
pad=self.dictionary.pad(),
|
||||
eos=self.dictionary.eos(),
|
||||
break_mode=self.args.sample_break_mode,
|
||||
include_targets=True,
|
||||
)
|
||||
|
||||
add_eos_for_other_targets = (
|
||||
self.args.sample_break_mode is not None
|
||||
and self.args.sample_break_mode != "none"
|
||||
)
|
||||
|
||||
self.datasets[split] = MonolingualDataset(
|
||||
dataset=dataset,
|
||||
sizes=dataset.sizes,
|
||||
src_vocab=self.dictionary,
|
||||
tgt_vocab=self.output_dictionary,
|
||||
add_eos_for_other_targets=add_eos_for_other_targets,
|
||||
shuffle=True,
|
||||
targets=self.targets,
|
||||
add_bos_token=self.args.add_bos_token,
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, **kwargs):
|
||||
"""
|
||||
Generate batches for inference. We prepend an eos token to src_tokens
|
||||
(or bos if `--add-bos-token` is set) and we append a <pad> to target.
|
||||
This is convenient both for generation with a prefix and LM scoring.
|
||||
"""
|
||||
dataset = StripTokenDataset(
|
||||
TokenBlockDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
block_size=None, # ignored for "eos" break mode
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=self.source_dictionary.eos(),
|
||||
break_mode="eos",
|
||||
),
|
||||
# remove eos from (end of) target sequence
|
||||
self.source_dictionary.eos(),
|
||||
)
|
||||
src_dataset = PrependTokenDataset(
|
||||
dataset,
|
||||
token=(
|
||||
self.source_dictionary.bos()
|
||||
if getattr(self.args, "add_bos_token", False)
|
||||
else self.source_dictionary.eos()
|
||||
),
|
||||
)
|
||||
tgt_dataset = AppendTokenDataset(dataset, token=self.source_dictionary.pad())
|
||||
return NestedDictionaryDataset(
|
||||
{
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": PadDataset(
|
||||
src_dataset,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
left_pad=False,
|
||||
),
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
},
|
||||
"target": PadDataset(
|
||||
tgt_dataset, pad_idx=self.source_dictionary.pad(), left_pad=False
|
||||
),
|
||||
},
|
||||
sizes=[np.array(src_lengths)],
|
||||
)
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
with torch.no_grad():
|
||||
# Generation will always be conditioned on bos_token
|
||||
if getattr(self.args, "add_bos_token", False):
|
||||
bos_token = self.source_dictionary.bos()
|
||||
else:
|
||||
bos_token = self.source_dictionary.eos()
|
||||
|
||||
if constraints is not None:
|
||||
raise NotImplementedError(
|
||||
"Constrained decoding with the language_modeling task is not supported"
|
||||
)
|
||||
|
||||
# SequenceGenerator doesn't use src_tokens directly, we need to
|
||||
# pass the `prefix_tokens` argument instead
|
||||
if prefix_tokens is None and sample["net_input"]["src_tokens"].nelement():
|
||||
prefix_tokens = sample["net_input"]["src_tokens"]
|
||||
if prefix_tokens[:, 0].eq(bos_token).all():
|
||||
prefix_tokens = prefix_tokens[:, 1:]
|
||||
|
||||
return generator.generate(
|
||||
models, sample, prefix_tokens=prefix_tokens, bos_token=bos_token
|
||||
)
|
||||
|
||||
def eval_lm_dataloader(
|
||||
self,
|
||||
dataset,
|
||||
max_tokens: Optional[int] = 36000,
|
||||
batch_size: Optional[int] = None,
|
||||
max_positions: Optional[int] = None,
|
||||
num_shards: int = 1,
|
||||
shard_id: int = 0,
|
||||
num_workers: int = 1,
|
||||
data_buffer_size: int = 10,
|
||||
# ensures that every evaluated token has access to a context of at least
|
||||
# this size, if possible
|
||||
context_window: int = 0,
|
||||
):
|
||||
if context_window > 0:
|
||||
dataset = LMContextWindowDataset(
|
||||
dataset=dataset,
|
||||
tokens_per_sample=self.args.tokens_per_sample,
|
||||
context_window=context_window,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
)
|
||||
return self.get_batch_iterator(
|
||||
dataset=dataset,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=batch_size,
|
||||
max_positions=max_positions,
|
||||
ignore_invalid_inputs=True,
|
||||
num_shards=num_shards,
|
||||
shard_id=shard_id,
|
||||
num_workers=num_workers,
|
||||
data_buffer_size=data_buffer_size,
|
||||
).next_epoch_itr(shuffle=False)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
"""Return the :class:`~fairseq.data.Dictionary` for the language
|
||||
model."""
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
"""Return the :class:`~fairseq.data.Dictionary` for the language
|
||||
model."""
|
||||
return self.output_dictionary
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from fairseq import tokenizer, utils
|
||||
from fairseq.data import ConcatDataset, Dictionary, data_utils, indexed_dataset
|
||||
from fairseq.data.legacy.block_pair_dataset import BlockPairDataset
|
||||
from fairseq.data.legacy.masked_lm_dataset import MaskedLMDataset
|
||||
from fairseq.data.legacy.masked_lm_dictionary import BertDictionary
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("legacy_masked_lm")
|
||||
class LegacyMaskedLMTask(LegacyFairseqTask):
|
||||
"""
|
||||
Task for training Masked LM (BERT) model.
|
||||
Args:
|
||||
dictionary (Dictionary): the dictionary for the input of the task
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data",
|
||||
help="colon separated path to data directories list, \
|
||||
will be iterated upon during epochs in round-robin manner",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments"
|
||||
" per sample for BERT dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--break-mode", default="doc", type=str, help="mode for breaking sentence"
|
||||
)
|
||||
parser.add_argument("--shuffle-dataset", action="store_true", default=False)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
return BertDictionary.load(filename)
|
||||
|
||||
@classmethod
|
||||
def build_dictionary(
|
||||
cls, filenames, workers=1, threshold=-1, nwords=-1, padding_factor=8
|
||||
):
|
||||
d = BertDictionary()
|
||||
for filename in filenames:
|
||||
Dictionary.add_file_to_dictionary(
|
||||
filename, d, tokenizer.tokenize_line, workers
|
||||
)
|
||||
d.finalize(threshold=threshold, nwords=nwords, padding_factor=padding_factor)
|
||||
return d
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task."""
|
||||
paths = utils.split_paths(args.data)
|
||||
assert len(paths) > 0
|
||||
dictionary = BertDictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
|
||||
return cls(args, dictionary)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
loaded_datasets = []
|
||||
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
logger.info("data_path", data_path)
|
||||
|
||||
for k in itertools.count():
|
||||
split_k = split + (str(k) if k > 0 else "")
|
||||
path = os.path.join(data_path, split_k)
|
||||
ds = indexed_dataset.make_dataset(
|
||||
path,
|
||||
impl=self.args.dataset_impl,
|
||||
fix_lua_indexing=True,
|
||||
dictionary=self.dictionary,
|
||||
)
|
||||
|
||||
if ds is None:
|
||||
if k > 0:
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, data_path)
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.seed + k):
|
||||
loaded_datasets.append(
|
||||
BlockPairDataset(
|
||||
ds,
|
||||
self.dictionary,
|
||||
ds.sizes,
|
||||
self.args.tokens_per_sample,
|
||||
break_mode=self.args.break_mode,
|
||||
doc_break_size=1,
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"{} {} {} examples".format(data_path, split_k, len(loaded_datasets[-1]))
|
||||
)
|
||||
|
||||
if not combine:
|
||||
break
|
||||
|
||||
if len(loaded_datasets) == 1:
|
||||
dataset = loaded_datasets[0]
|
||||
sizes = dataset.sizes
|
||||
else:
|
||||
dataset = ConcatDataset(loaded_datasets)
|
||||
sizes = np.concatenate([ds.sizes for ds in loaded_datasets])
|
||||
|
||||
self.datasets[split] = MaskedLMDataset(
|
||||
dataset=dataset,
|
||||
sizes=sizes,
|
||||
vocab=self.dictionary,
|
||||
pad_idx=self.dictionary.pad(),
|
||||
mask_idx=self.dictionary.mask(),
|
||||
classif_token_idx=self.dictionary.cls(),
|
||||
sep_token_idx=self.dictionary.sep(),
|
||||
shuffle=self.args.shuffle_dataset,
|
||||
seed=self.seed,
|
||||
)
|
||||
@@ -0,0 +1,258 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
MaskTokensDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
PrependTokenDataset,
|
||||
RightPadDataset,
|
||||
SortDataset,
|
||||
TokenBlockDataset,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.encoders.utils import get_whole_word_mask
|
||||
from fairseq.data.shorten_dataset import maybe_shorten_dataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("masked_lm")
|
||||
class MaskedLMTask(LegacyFairseqTask):
|
||||
"""Task for training masked language models (e.g., BERT, RoBERTa)."""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data",
|
||||
help="colon separated path to data directories list, \
|
||||
will be iterated upon during epochs in round-robin manner",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-break-mode",
|
||||
default="complete",
|
||||
choices=["none", "complete", "complete_doc", "eos"],
|
||||
help='If omitted or "none", fills each sample with tokens-per-sample '
|
||||
'tokens. If set to "complete", splits samples only at the end '
|
||||
"of sentence, but may include multiple sentences per sample. "
|
||||
'"complete_doc" is similar but respects doc boundaries. '
|
||||
'If set to "eos", includes only one sentence per sample.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments "
|
||||
"per sample for BERT dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-prob",
|
||||
default=0.15,
|
||||
type=float,
|
||||
help="probability of replacing a token with mask",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--leave-unmasked-prob",
|
||||
default=0.1,
|
||||
type=float,
|
||||
help="probability that a masked token is unmasked",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--random-token-prob",
|
||||
default=0.1,
|
||||
type=float,
|
||||
help="probability of replacing a token with a random token",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--freq-weighted-replacement",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="sample random replacement words based on word frequencies",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-whole-words",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="mask whole words; you may also want to set --bpe",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-multiple-length",
|
||||
default=1,
|
||||
type=int,
|
||||
help="repeat the mask indices multiple times",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-stdev", default=0.0, type=float, help="stdev of the mask length"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shorten-method",
|
||||
default="none",
|
||||
choices=["none", "truncate", "random_crop"],
|
||||
help="if not none, shorten sequences that exceed --tokens-per-sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shorten-data-split-list",
|
||||
default="",
|
||||
help="comma-separated list of dataset splits to apply shortening to, "
|
||||
'e.g., "train,valid" (default: all dataset splits)',
|
||||
)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
# add mask token
|
||||
self.mask_idx = dictionary.add_symbol("<mask>")
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
paths = utils.split_paths(args.data)
|
||||
assert len(paths) > 0
|
||||
dictionary = Dictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
return cls(args, dictionary)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
split_path = os.path.join(data_path, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
self.source_dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
if dataset is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
dataset = maybe_shorten_dataset(
|
||||
dataset,
|
||||
split,
|
||||
self.args.shorten_data_split_list,
|
||||
self.args.shorten_method,
|
||||
self.args.tokens_per_sample,
|
||||
self.args.seed,
|
||||
)
|
||||
|
||||
# create continuous blocks of tokens
|
||||
dataset = TokenBlockDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.args.tokens_per_sample - 1, # one less for <s>
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=self.source_dictionary.eos(),
|
||||
break_mode=self.args.sample_break_mode,
|
||||
)
|
||||
logger.info("loaded {} blocks from: {}".format(len(dataset), split_path))
|
||||
|
||||
# prepend beginning-of-sentence token (<s>, equiv. to [CLS] in BERT)
|
||||
dataset = PrependTokenDataset(dataset, self.source_dictionary.bos())
|
||||
|
||||
# create masked input and targets
|
||||
mask_whole_words = (
|
||||
get_whole_word_mask(self.args, self.source_dictionary)
|
||||
if self.args.mask_whole_words
|
||||
else None
|
||||
)
|
||||
|
||||
src_dataset, tgt_dataset = MaskTokensDataset.apply_mask(
|
||||
dataset,
|
||||
self.source_dictionary,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
mask_idx=self.mask_idx,
|
||||
seed=self.args.seed,
|
||||
mask_prob=self.args.mask_prob,
|
||||
leave_unmasked_prob=self.args.leave_unmasked_prob,
|
||||
random_token_prob=self.args.random_token_prob,
|
||||
freq_weighted_replacement=self.args.freq_weighted_replacement,
|
||||
mask_whole_words=mask_whole_words,
|
||||
mask_multiple_length=self.args.mask_multiple_length,
|
||||
mask_stdev=self.args.mask_stdev,
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
shuffle = np.random.permutation(len(src_dataset))
|
||||
|
||||
self.datasets[split] = SortDataset(
|
||||
NestedDictionaryDataset(
|
||||
{
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": RightPadDataset(
|
||||
src_dataset,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
),
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
},
|
||||
"target": RightPadDataset(
|
||||
tgt_dataset,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
),
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(src_dataset, reduce=True),
|
||||
},
|
||||
sizes=[src_dataset.sizes],
|
||||
),
|
||||
sort_order=[
|
||||
shuffle,
|
||||
src_dataset.sizes,
|
||||
],
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, sort=True):
|
||||
src_dataset = RightPadDataset(
|
||||
TokenBlockDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
self.args.tokens_per_sample - 1, # one less for <s>
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=self.source_dictionary.eos(),
|
||||
break_mode="eos",
|
||||
),
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
)
|
||||
src_dataset = PrependTokenDataset(src_dataset, self.source_dictionary.bos())
|
||||
src_dataset = NestedDictionaryDataset(
|
||||
{
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": src_dataset,
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
},
|
||||
},
|
||||
sizes=src_lengths,
|
||||
)
|
||||
if sort:
|
||||
src_dataset = SortDataset(src_dataset, sort_order=[src_lengths])
|
||||
return src_dataset
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
@@ -0,0 +1,254 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from fairseq.data import (
|
||||
AppendTokenDataset,
|
||||
ConcatDataset,
|
||||
DenoisingDataset,
|
||||
Dictionary,
|
||||
PrependTokenDataset,
|
||||
ResamplingDataset,
|
||||
SortDataset,
|
||||
TokenBlockDataset,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.encoders.utils import get_whole_word_mask
|
||||
from fairseq.tasks import register_task
|
||||
|
||||
from .denoising import DenoisingTask
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("multilingual_denoising")
|
||||
class MultilingualDenoisingTask(DenoisingTask):
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
DenoisingTask.add_args(parser)
|
||||
parser.add_argument(
|
||||
"--multilang-sampling-alpha",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="smoothing alpha for sample ratios across multiple datasets",
|
||||
)
|
||||
parser.add_argument("--add-lang-token", default=False, action="store_true")
|
||||
parser.add_argument(
|
||||
"--langs", type=str, help="language ids we are considering", default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-whole-word-mask-langs",
|
||||
type=str,
|
||||
default="",
|
||||
metavar="N",
|
||||
help="languages without spacing between words dont support whole word masking",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task."""
|
||||
paths = args.data.split(":")
|
||||
assert len(paths) > 0
|
||||
dictionary = Dictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
|
||||
data_path = paths[0]
|
||||
if args.langs is None:
|
||||
languages = sorted(
|
||||
[
|
||||
name
|
||||
for name in os.listdir(data_path)
|
||||
if os.path.isdir(os.path.join(data_path, name))
|
||||
]
|
||||
)
|
||||
else:
|
||||
languages = args.langs.split(",")
|
||||
|
||||
if args.add_lang_token:
|
||||
for lang in languages:
|
||||
dictionary.add_symbol("[{}]".format(lang))
|
||||
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
if not hasattr(args, "shuffle_instance"):
|
||||
args.shuffle_instance = False
|
||||
return cls(args, dictionary)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args, dictionary)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
# add mask token
|
||||
self.mask_idx = self.dictionary.add_symbol("<mask>")
|
||||
self.langs = args.langs
|
||||
self.args = args
|
||||
|
||||
def _get_sample_prob(self, dataset_lens):
|
||||
"""
|
||||
Get smoothed sampling porbability by languages. This helps low resource
|
||||
languages by upsampling them.
|
||||
"""
|
||||
prob = dataset_lens / dataset_lens.sum()
|
||||
smoothed_prob = prob ** self.args.multilang_sampling_alpha
|
||||
smoothed_prob = smoothed_prob / smoothed_prob.sum()
|
||||
return smoothed_prob
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = self.args.data.split(":")
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
split_path = os.path.join(data_path, split)
|
||||
|
||||
if self.langs is None:
|
||||
languages = sorted(
|
||||
[
|
||||
name
|
||||
for name in os.listdir(data_path)
|
||||
if os.path.isdir(os.path.join(data_path, name))
|
||||
]
|
||||
)
|
||||
else:
|
||||
languages = self.langs.split(",")
|
||||
for name in languages:
|
||||
p = os.path.join(data_path, name)
|
||||
assert os.path.exists(p), "data not found: {}".format(p)
|
||||
|
||||
logger.info("Training on {0} languages: {1}".format(len(languages), languages))
|
||||
logger.info(
|
||||
"Language to id mapping: ", {lang: id for id, lang in enumerate(languages)}
|
||||
)
|
||||
|
||||
mask_whole_words = get_whole_word_mask(self.args, self.dictionary)
|
||||
language_without_segmentations = self.args.no_whole_word_mask_langs.split(",")
|
||||
lang_datasets = []
|
||||
for language in languages:
|
||||
split_path = os.path.join(data_path, language, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
self.source_dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
if dataset is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
end_token = (
|
||||
self.source_dictionary.index("[{}]".format(language))
|
||||
if self.args.add_lang_token
|
||||
else self.source_dictionary.eos()
|
||||
)
|
||||
|
||||
# create continuous blocks of tokens
|
||||
dataset = TokenBlockDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.args.tokens_per_sample - 2, # one less for <s>
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=end_token,
|
||||
break_mode=self.args.sample_break_mode,
|
||||
)
|
||||
logger.info("loaded {} blocks from: {}".format(len(dataset), split_path))
|
||||
|
||||
# prepend beginning-of-sentence token (<s>, equiv. to [CLS] in BERT)
|
||||
dataset = PrependTokenDataset(dataset, self.source_dictionary.bos())
|
||||
dataset = AppendTokenDataset(dataset, end_token)
|
||||
|
||||
lang_mask_whole_words = (
|
||||
mask_whole_words
|
||||
if language not in language_without_segmentations
|
||||
else None
|
||||
)
|
||||
lang_dataset = DenoisingDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.dictionary,
|
||||
self.mask_idx,
|
||||
lang_mask_whole_words,
|
||||
shuffle=self.args.shuffle_instance,
|
||||
seed=self.seed,
|
||||
args=self.args,
|
||||
eos=None
|
||||
if not self.args.add_lang_token
|
||||
else self.source_dictionary.index("[{}]".format(language)),
|
||||
)
|
||||
lang_datasets.append(lang_dataset)
|
||||
|
||||
dataset_lengths = np.array(
|
||||
[len(d) for d in lang_datasets],
|
||||
dtype=float,
|
||||
)
|
||||
logger.info(
|
||||
"loaded total {} blocks for all languages".format(
|
||||
int(dataset_lengths.sum()),
|
||||
)
|
||||
)
|
||||
if split == self.args.train_subset:
|
||||
# For train subset, additionally up or down sample languages.
|
||||
sample_probs = self._get_sample_prob(dataset_lengths)
|
||||
logger.info(
|
||||
"Sample probability by language: {}".format(
|
||||
{
|
||||
lang: "{0:.4f}".format(sample_probs[id])
|
||||
for id, lang in enumerate(languages)
|
||||
}
|
||||
)
|
||||
)
|
||||
size_ratio = (sample_probs * dataset_lengths.sum()) / dataset_lengths
|
||||
logger.info(
|
||||
"Up/Down Sampling ratio by language: {}".format(
|
||||
{
|
||||
lang: "{0:.2f}".format(size_ratio[id])
|
||||
for id, lang in enumerate(languages)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
resampled_lang_datasets = [
|
||||
ResamplingDataset(
|
||||
lang_datasets[i],
|
||||
size_ratio=size_ratio[i],
|
||||
seed=self.args.seed,
|
||||
epoch=epoch,
|
||||
replace=size_ratio[i] >= 1.0,
|
||||
)
|
||||
for i, d in enumerate(lang_datasets)
|
||||
]
|
||||
dataset = ConcatDataset(
|
||||
resampled_lang_datasets,
|
||||
)
|
||||
else:
|
||||
dataset = ConcatDataset(lang_datasets)
|
||||
lang_splits = [split]
|
||||
for lang_id, lang_dataset in enumerate(lang_datasets):
|
||||
split_name = split + "_" + languages[lang_id]
|
||||
lang_splits.append(split_name)
|
||||
self.datasets[split_name] = lang_dataset
|
||||
|
||||
if split in self.args.valid_subset:
|
||||
self.args.valid_subset = self.args.valid_subset.replace(
|
||||
split, ",".join(lang_splits)
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed + epoch):
|
||||
shuffle = np.random.permutation(len(dataset))
|
||||
|
||||
self.datasets[split] = SortDataset(
|
||||
dataset,
|
||||
sort_order=[
|
||||
shuffle,
|
||||
dataset.sizes,
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,338 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
ConcatDataset,
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
MaskTokensDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
PadDataset,
|
||||
PrependTokenDataset,
|
||||
RawLabelDataset,
|
||||
ResamplingDataset,
|
||||
SortDataset,
|
||||
TokenBlockDataset,
|
||||
data_utils,
|
||||
encoders,
|
||||
)
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("multilingual_masked_lm")
|
||||
class MultiLingualMaskedLMTask(LegacyFairseqTask):
|
||||
"""Task for training masked language models (e.g., BERT, RoBERTa)."""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data",
|
||||
help="colon separated path to data directories list, \
|
||||
will be iterated upon during epochs in round-robin manner",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-break-mode",
|
||||
default="complete",
|
||||
choices=["none", "complete", "complete_doc", "eos"],
|
||||
help='If omitted or "none", fills each sample with tokens-per-sample '
|
||||
'tokens. If set to "complete", splits samples only at the end '
|
||||
"of sentence, but may include multiple sentences per sample. "
|
||||
'"complete_doc" is similar but respects doc boundaries. '
|
||||
'If set to "eos", includes only one sentence per sample.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments "
|
||||
"per sample for BERT dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-prob",
|
||||
default=0.15,
|
||||
type=float,
|
||||
help="probability of replacing a token with mask",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--leave-unmasked-prob",
|
||||
default=0.1,
|
||||
type=float,
|
||||
help="probability that a masked token is unmasked",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--random-token-prob",
|
||||
default=0.1,
|
||||
type=float,
|
||||
help="probability of replacing a token with a random token",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--freq-weighted-replacement",
|
||||
action="store_true",
|
||||
help="sample random replacement words based on word frequencies",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mask-whole-words",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="mask whole words; you may also want to set --bpe",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--multilang-sampling-alpha",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="smoothing alpha for sample rations across multiple datasets",
|
||||
)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
# add mask token
|
||||
self.mask_idx = dictionary.add_symbol("<mask>")
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
paths = utils.split_paths(args.data)
|
||||
assert len(paths) > 0
|
||||
dictionary = Dictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
return cls(args, dictionary)
|
||||
|
||||
def _get_whole_word_mask(self):
|
||||
# create masked input and targets
|
||||
if self.args.mask_whole_words:
|
||||
bpe = encoders.build_bpe(self.args)
|
||||
if bpe is not None:
|
||||
|
||||
def is_beginning_of_word(i):
|
||||
if i < self.source_dictionary.nspecial:
|
||||
# special elements are always considered beginnings
|
||||
return True
|
||||
tok = self.source_dictionary[i]
|
||||
if tok.startswith("madeupword"):
|
||||
return True
|
||||
try:
|
||||
return bpe.is_beginning_of_word(tok)
|
||||
except ValueError:
|
||||
return True
|
||||
|
||||
mask_whole_words = torch.ByteTensor(
|
||||
list(map(is_beginning_of_word, range(len(self.source_dictionary))))
|
||||
)
|
||||
else:
|
||||
mask_whole_words = None
|
||||
return mask_whole_words
|
||||
|
||||
def _get_sample_prob(self, dataset_lens):
|
||||
"""
|
||||
Get smoothed sampling porbability by languages. This helps low resource
|
||||
languages by upsampling them.
|
||||
"""
|
||||
prob = dataset_lens / dataset_lens.sum()
|
||||
smoothed_prob = prob ** self.args.multilang_sampling_alpha
|
||||
smoothed_prob = smoothed_prob / smoothed_prob.sum()
|
||||
return smoothed_prob
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
languages = sorted(
|
||||
name
|
||||
for name in os.listdir(data_path)
|
||||
if os.path.isdir(os.path.join(data_path, name))
|
||||
)
|
||||
|
||||
logger.info("Training on {0} languages: {1}".format(len(languages), languages))
|
||||
logger.info(
|
||||
"Language to id mapping: ", {lang: id for id, lang in enumerate(languages)}
|
||||
)
|
||||
|
||||
mask_whole_words = self._get_whole_word_mask()
|
||||
lang_datasets = []
|
||||
for lang_id, language in enumerate(languages):
|
||||
split_path = os.path.join(data_path, language, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
self.source_dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
if dataset is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
# create continuous blocks of tokens
|
||||
dataset = TokenBlockDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.args.tokens_per_sample - 1, # one less for <s>
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=self.source_dictionary.eos(),
|
||||
break_mode=self.args.sample_break_mode,
|
||||
)
|
||||
logger.info("loaded {} blocks from: {}".format(len(dataset), split_path))
|
||||
|
||||
# prepend beginning-of-sentence token (<s>, equiv. to [CLS] in BERT)
|
||||
dataset = PrependTokenDataset(dataset, self.source_dictionary.bos())
|
||||
|
||||
src_dataset, tgt_dataset = MaskTokensDataset.apply_mask(
|
||||
dataset,
|
||||
self.source_dictionary,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
mask_idx=self.mask_idx,
|
||||
seed=self.args.seed,
|
||||
mask_prob=self.args.mask_prob,
|
||||
leave_unmasked_prob=self.args.leave_unmasked_prob,
|
||||
random_token_prob=self.args.random_token_prob,
|
||||
freq_weighted_replacement=self.args.freq_weighted_replacement,
|
||||
mask_whole_words=mask_whole_words,
|
||||
)
|
||||
|
||||
lang_dataset = NestedDictionaryDataset(
|
||||
{
|
||||
"net_input": {
|
||||
"src_tokens": PadDataset(
|
||||
src_dataset,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
left_pad=False,
|
||||
),
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
},
|
||||
"target": PadDataset(
|
||||
tgt_dataset,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
left_pad=False,
|
||||
),
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(src_dataset, reduce=True),
|
||||
"lang_id": RawLabelDataset([lang_id] * src_dataset.sizes.shape[0]),
|
||||
},
|
||||
sizes=[src_dataset.sizes],
|
||||
)
|
||||
lang_datasets.append(lang_dataset)
|
||||
|
||||
dataset_lengths = np.array(
|
||||
[len(d) for d in lang_datasets],
|
||||
dtype=float,
|
||||
)
|
||||
logger.info(
|
||||
"loaded total {} blocks for all languages".format(
|
||||
dataset_lengths.sum(),
|
||||
)
|
||||
)
|
||||
if split == self.args.train_subset:
|
||||
# For train subset, additionally up or down sample languages.
|
||||
sample_probs = self._get_sample_prob(dataset_lengths)
|
||||
logger.info(
|
||||
"Sample probability by language: ",
|
||||
{
|
||||
lang: "{0:.4f}".format(sample_probs[id])
|
||||
for id, lang in enumerate(languages)
|
||||
},
|
||||
)
|
||||
size_ratio = (sample_probs * dataset_lengths.sum()) / dataset_lengths
|
||||
logger.info(
|
||||
"Up/Down Sampling ratio by language: ",
|
||||
{
|
||||
lang: "{0:.2f}".format(size_ratio[id])
|
||||
for id, lang in enumerate(languages)
|
||||
},
|
||||
)
|
||||
|
||||
resampled_lang_datasets = [
|
||||
ResamplingDataset(
|
||||
lang_datasets[i],
|
||||
size_ratio=size_ratio[i],
|
||||
seed=self.args.seed,
|
||||
epoch=epoch,
|
||||
replace=size_ratio[i] >= 1.0,
|
||||
)
|
||||
for i, d in enumerate(lang_datasets)
|
||||
]
|
||||
dataset = ConcatDataset(resampled_lang_datasets)
|
||||
else:
|
||||
dataset = ConcatDataset(lang_datasets)
|
||||
lang_splits = [split]
|
||||
for lang_id, lang_dataset in enumerate(lang_datasets):
|
||||
split_name = split + "_" + languages[lang_id]
|
||||
lang_splits.append(split_name)
|
||||
self.datasets[split_name] = lang_dataset
|
||||
|
||||
# [TODO]: This is hacky for now to print validation ppl for each
|
||||
# language individually. Maybe need task API changes to allow it
|
||||
# in more generic ways.
|
||||
if split in self.args.valid_subset:
|
||||
self.args.valid_subset = self.args.valid_subset.replace(
|
||||
split, ",".join(lang_splits)
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed + epoch):
|
||||
shuffle = np.random.permutation(len(dataset))
|
||||
|
||||
self.datasets[split] = SortDataset(
|
||||
dataset,
|
||||
sort_order=[
|
||||
shuffle,
|
||||
dataset.sizes,
|
||||
],
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, sort=True):
|
||||
src_dataset = PadDataset(
|
||||
TokenBlockDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
self.args.tokens_per_sample - 1, # one less for <s>
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=self.source_dictionary.eos(),
|
||||
break_mode="eos",
|
||||
),
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
left_pad=False,
|
||||
)
|
||||
src_dataset = PrependTokenDataset(src_dataset, self.source_dictionary.bos())
|
||||
src_dataset = NestedDictionaryDataset(
|
||||
{
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": src_dataset,
|
||||
"src_lengths": NumelDataset(src_dataset, reduce=False),
|
||||
},
|
||||
},
|
||||
sizes=src_lengths,
|
||||
)
|
||||
if sort:
|
||||
src_dataset = SortDataset(src_dataset, sort_order=[src_lengths])
|
||||
return src_dataset
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
@@ -0,0 +1,457 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import torch
|
||||
from fairseq import metrics, options, utils
|
||||
from fairseq.data import (
|
||||
Dictionary,
|
||||
LanguagePairDataset,
|
||||
RoundRobinZipDatasets,
|
||||
TransformEosLangPairDataset,
|
||||
)
|
||||
from fairseq.models import FairseqMultiModel
|
||||
from fairseq.tasks.translation import load_langpair_dataset
|
||||
|
||||
from . import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _lang_token(lang: str):
|
||||
return "__{}__".format(lang)
|
||||
|
||||
|
||||
def _lang_token_index(dic: Dictionary, lang: str):
|
||||
"""Return language token index."""
|
||||
idx = dic.index(_lang_token(lang))
|
||||
assert idx != dic.unk_index, "cannot find language token for lang {}".format(lang)
|
||||
return idx
|
||||
|
||||
|
||||
@register_task("multilingual_translation")
|
||||
class MultilingualTranslationTask(LegacyFairseqTask):
|
||||
"""A task for training multiple translation models simultaneously.
|
||||
|
||||
We iterate round-robin over batches from multiple language pairs, ordered
|
||||
according to the `--lang-pairs` argument.
|
||||
|
||||
The training loop is roughly:
|
||||
|
||||
for i in range(len(epoch)):
|
||||
for lang_pair in args.lang_pairs:
|
||||
batch = next_batch_for_lang_pair(lang_pair)
|
||||
loss = criterion(model_for_lang_pair(lang_pair), batch)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
In practice, `next_batch_for_lang_pair` is abstracted in a FairseqDataset
|
||||
(e.g., `RoundRobinZipDatasets`) and `model_for_lang_pair` is a model that
|
||||
implements the `FairseqMultiModel` interface.
|
||||
|
||||
During inference it is required to specify a single `--source-lang` and
|
||||
`--target-lang`, which indicates the inference langauge direction.
|
||||
`--lang-pairs`, `--encoder-langtok`, `--decoder-langtok` have to be set to
|
||||
the same value as training.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
parser.add_argument('data', metavar='DIR', help='path to data directory')
|
||||
parser.add_argument('--lang-pairs', default=None, metavar='PAIRS',
|
||||
help='comma-separated list of language pairs (in training order): en-de,en-fr,de-fr')
|
||||
parser.add_argument('-s', '--source-lang', default=None, metavar='SRC',
|
||||
help='source language (only needed for inference)')
|
||||
parser.add_argument('-t', '--target-lang', default=None, metavar='TARGET',
|
||||
help='target language (only needed for inference)')
|
||||
parser.add_argument('--left-pad-source', default='True', type=str, metavar='BOOL',
|
||||
help='pad the source on the left (default: True)')
|
||||
parser.add_argument('--left-pad-target', default='False', type=str, metavar='BOOL',
|
||||
help='pad the target on the left (default: False)')
|
||||
parser.add_argument('--max-source-positions', default=1024, type=int, metavar='N',
|
||||
help='max number of tokens in the source sequence')
|
||||
parser.add_argument('--max-target-positions', default=1024, type=int, metavar='N',
|
||||
help='max number of tokens in the target sequence')
|
||||
parser.add_argument('--upsample-primary', default=1, type=int,
|
||||
help='amount to upsample primary dataset')
|
||||
parser.add_argument('--encoder-langtok', default=None, type=str, choices=['src', 'tgt'],
|
||||
metavar='SRCTGT',
|
||||
help='replace beginning-of-sentence in source sentence with source or target '
|
||||
'language token. (src/tgt)')
|
||||
parser.add_argument('--decoder-langtok', action='store_true',
|
||||
help='replace beginning-of-sentence in target sentence with target language token')
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, args, dicts, training):
|
||||
super().__init__(args)
|
||||
self.dicts = dicts
|
||||
self.training = training
|
||||
if training:
|
||||
self.lang_pairs = args.lang_pairs
|
||||
else:
|
||||
self.lang_pairs = ["{}-{}".format(args.source_lang, args.target_lang)]
|
||||
# eval_lang_pairs for multilingual translation is usually all of the
|
||||
# lang_pairs. However for other multitask settings or when we want to
|
||||
# optimize for certain languages we want to use a different subset. Thus
|
||||
# the eval_lang_pairs class variable is provided for classes that extend
|
||||
# this class.
|
||||
self.eval_lang_pairs = self.lang_pairs
|
||||
# model_lang_pairs will be used to build encoder-decoder model pairs in
|
||||
# models.build_model(). This allows multitask type of sub-class can
|
||||
# build models other than the input lang_pairs
|
||||
self.model_lang_pairs = self.lang_pairs
|
||||
self.langs = list(dicts.keys())
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
dicts, training = cls.prepare(args, **kwargs)
|
||||
return cls(args, dicts, training)
|
||||
|
||||
@classmethod
|
||||
def update_args(cls, args):
|
||||
args.left_pad_source = utils.eval_bool(args.left_pad_source)
|
||||
args.left_pad_target = utils.eval_bool(args.left_pad_target)
|
||||
|
||||
if args.lang_pairs is None:
|
||||
raise ValueError(
|
||||
"--lang-pairs is required. List all the language pairs in the training objective."
|
||||
)
|
||||
if isinstance(args.lang_pairs, str):
|
||||
args.lang_pairs = args.lang_pairs.split(",")
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, args, **kargs):
|
||||
cls.update_args(args)
|
||||
sorted_langs = sorted(
|
||||
list({x for lang_pair in args.lang_pairs for x in lang_pair.split("-")})
|
||||
)
|
||||
if args.source_lang is not None or args.target_lang is not None:
|
||||
training = False
|
||||
else:
|
||||
training = True
|
||||
|
||||
# load dictionaries
|
||||
dicts = OrderedDict()
|
||||
for lang in sorted_langs:
|
||||
paths = utils.split_paths(args.data)
|
||||
assert len(paths) > 0
|
||||
dicts[lang] = cls.load_dictionary(
|
||||
os.path.join(paths[0], "dict.{}.txt".format(lang))
|
||||
)
|
||||
if len(dicts) > 0:
|
||||
assert dicts[lang].pad() == dicts[sorted_langs[0]].pad()
|
||||
assert dicts[lang].eos() == dicts[sorted_langs[0]].eos()
|
||||
assert dicts[lang].unk() == dicts[sorted_langs[0]].unk()
|
||||
if args.encoder_langtok is not None or args.decoder_langtok:
|
||||
for lang_to_add in sorted_langs:
|
||||
dicts[lang].add_symbol(_lang_token(lang_to_add))
|
||||
logger.info("[{}] dictionary: {} types".format(lang, len(dicts[lang])))
|
||||
return dicts, training
|
||||
|
||||
def get_encoder_langtok(self, src_lang, tgt_lang):
|
||||
if self.args.encoder_langtok is None:
|
||||
return self.dicts[src_lang].eos()
|
||||
if self.args.encoder_langtok == "src":
|
||||
return _lang_token_index(self.dicts[src_lang], src_lang)
|
||||
else:
|
||||
return _lang_token_index(self.dicts[src_lang], tgt_lang)
|
||||
|
||||
def get_decoder_langtok(self, tgt_lang):
|
||||
if not self.args.decoder_langtok:
|
||||
return self.dicts[tgt_lang].eos()
|
||||
return _lang_token_index(self.dicts[tgt_lang], tgt_lang)
|
||||
|
||||
def alter_dataset_langtok(
|
||||
self,
|
||||
lang_pair_dataset,
|
||||
src_eos=None,
|
||||
src_lang=None,
|
||||
tgt_eos=None,
|
||||
tgt_lang=None,
|
||||
):
|
||||
if self.args.encoder_langtok is None and not self.args.decoder_langtok:
|
||||
return lang_pair_dataset
|
||||
|
||||
new_src_eos = None
|
||||
if (
|
||||
self.args.encoder_langtok is not None
|
||||
and src_eos is not None
|
||||
and src_lang is not None
|
||||
and tgt_lang is not None
|
||||
):
|
||||
new_src_eos = self.get_encoder_langtok(src_lang, tgt_lang)
|
||||
else:
|
||||
src_eos = None
|
||||
|
||||
new_tgt_bos = None
|
||||
if self.args.decoder_langtok and tgt_eos is not None and tgt_lang is not None:
|
||||
new_tgt_bos = self.get_decoder_langtok(tgt_lang)
|
||||
else:
|
||||
tgt_eos = None
|
||||
|
||||
return TransformEosLangPairDataset(
|
||||
lang_pair_dataset,
|
||||
src_eos=src_eos,
|
||||
new_src_eos=new_src_eos,
|
||||
tgt_bos=tgt_eos,
|
||||
new_tgt_bos=new_tgt_bos,
|
||||
)
|
||||
|
||||
def load_dataset(self, split, epoch=1, **kwargs):
|
||||
"""Load a dataset split."""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
def language_pair_dataset(lang_pair):
|
||||
src, tgt = lang_pair.split("-")
|
||||
langpair_dataset = load_langpair_dataset(
|
||||
data_path,
|
||||
split,
|
||||
src,
|
||||
self.dicts[src],
|
||||
tgt,
|
||||
self.dicts[tgt],
|
||||
combine=True,
|
||||
dataset_impl=self.args.dataset_impl,
|
||||
upsample_primary=self.args.upsample_primary,
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
max_source_positions=self.args.max_source_positions,
|
||||
max_target_positions=self.args.max_target_positions,
|
||||
)
|
||||
return self.alter_dataset_langtok(
|
||||
langpair_dataset,
|
||||
src_eos=self.dicts[src].eos(),
|
||||
src_lang=src,
|
||||
tgt_eos=self.dicts[tgt].eos(),
|
||||
tgt_lang=tgt,
|
||||
)
|
||||
|
||||
self.datasets[split] = RoundRobinZipDatasets(
|
||||
OrderedDict(
|
||||
[
|
||||
(lang_pair, language_pair_dataset(lang_pair))
|
||||
for lang_pair in self.lang_pairs
|
||||
]
|
||||
),
|
||||
eval_key=None
|
||||
if self.training
|
||||
else "%s-%s" % (self.args.source_lang, self.args.target_lang),
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, constraints=None):
|
||||
if constraints is not None:
|
||||
raise NotImplementedError(
|
||||
"Constrained decoding with the multilingual_translation task is not supported"
|
||||
)
|
||||
|
||||
lang_pair = "%s-%s" % (self.args.source_lang, self.args.target_lang)
|
||||
return RoundRobinZipDatasets(
|
||||
OrderedDict(
|
||||
[
|
||||
(
|
||||
lang_pair,
|
||||
self.alter_dataset_langtok(
|
||||
LanguagePairDataset(
|
||||
src_tokens, src_lengths, self.source_dictionary
|
||||
),
|
||||
src_eos=self.source_dictionary.eos(),
|
||||
src_lang=self.args.source_lang,
|
||||
tgt_eos=self.target_dictionary.eos(),
|
||||
tgt_lang=self.args.target_lang,
|
||||
),
|
||||
)
|
||||
]
|
||||
),
|
||||
eval_key=lang_pair,
|
||||
)
|
||||
|
||||
def build_model(self, args):
|
||||
def check_args():
|
||||
messages = []
|
||||
if (
|
||||
len(set(self.args.lang_pairs).symmetric_difference(args.lang_pairs))
|
||||
!= 0
|
||||
):
|
||||
messages.append(
|
||||
"--lang-pairs should include all the language pairs {}.".format(
|
||||
args.lang_pairs
|
||||
)
|
||||
)
|
||||
if self.args.encoder_langtok != args.encoder_langtok:
|
||||
messages.append(
|
||||
"--encoder-langtok should be {}.".format(args.encoder_langtok)
|
||||
)
|
||||
if self.args.decoder_langtok != args.decoder_langtok:
|
||||
messages.append(
|
||||
"--decoder-langtok should {} be set.".format(
|
||||
"" if args.decoder_langtok else "not"
|
||||
)
|
||||
)
|
||||
|
||||
if len(messages) > 0:
|
||||
raise ValueError(" ".join(messages))
|
||||
|
||||
# Update args -> the fact that the constructor here
|
||||
# changes the args object doesn't mean you get the same one here
|
||||
self.update_args(args)
|
||||
|
||||
# Check if task args are consistant with model args
|
||||
check_args()
|
||||
|
||||
from fairseq import models
|
||||
|
||||
model = models.build_model(args, self)
|
||||
if not isinstance(model, FairseqMultiModel):
|
||||
raise ValueError(
|
||||
"MultilingualTranslationTask requires a FairseqMultiModel architecture"
|
||||
)
|
||||
return model
|
||||
|
||||
def _per_lang_pair_train_loss(
|
||||
self, lang_pair, model, update_num, criterion, sample, optimizer, ignore_grad
|
||||
):
|
||||
loss, sample_size, logging_output = criterion(
|
||||
model.models[lang_pair], sample[lang_pair]
|
||||
)
|
||||
if ignore_grad:
|
||||
loss *= 0
|
||||
optimizer.backward(loss)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
model.train()
|
||||
from collections import defaultdict
|
||||
|
||||
agg_loss, agg_sample_size, agg_logging_output = 0.0, 0.0, defaultdict(float)
|
||||
curr_lang_pairs = [
|
||||
lang_pair
|
||||
for lang_pair in self.model_lang_pairs
|
||||
if sample[lang_pair] is not None and len(sample[lang_pair]) != 0
|
||||
]
|
||||
|
||||
for idx, lang_pair in enumerate(curr_lang_pairs):
|
||||
|
||||
def maybe_no_sync():
|
||||
if (
|
||||
self.args.distributed_world_size > 1
|
||||
and hasattr(model, "no_sync")
|
||||
and idx < len(curr_lang_pairs) - 1
|
||||
):
|
||||
return model.no_sync()
|
||||
else:
|
||||
return contextlib.ExitStack() # dummy contextmanager
|
||||
|
||||
with maybe_no_sync():
|
||||
loss, sample_size, logging_output = self._per_lang_pair_train_loss(
|
||||
lang_pair,
|
||||
model,
|
||||
update_num,
|
||||
criterion,
|
||||
sample,
|
||||
optimizer,
|
||||
ignore_grad,
|
||||
)
|
||||
agg_loss += loss.detach().item()
|
||||
# TODO make summing of the sample sizes configurable
|
||||
agg_sample_size += sample_size
|
||||
for k in logging_output:
|
||||
agg_logging_output[k] += logging_output[k]
|
||||
agg_logging_output[f"{lang_pair}:{k}"] += logging_output[k]
|
||||
return agg_loss, agg_sample_size, agg_logging_output
|
||||
|
||||
def _per_lang_pair_valid_loss(self, lang_pair, model, criterion, sample):
|
||||
return criterion(model.models[lang_pair], sample[lang_pair])
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
from collections import defaultdict
|
||||
|
||||
agg_loss, agg_sample_size, agg_logging_output = 0.0, 0.0, defaultdict(float)
|
||||
for lang_pair in self.eval_lang_pairs:
|
||||
if (
|
||||
lang_pair not in sample
|
||||
or sample[lang_pair] is None
|
||||
or len(sample[lang_pair]) == 0
|
||||
):
|
||||
continue
|
||||
loss, sample_size, logging_output = self._per_lang_pair_valid_loss(
|
||||
lang_pair, model, criterion, sample
|
||||
)
|
||||
agg_loss += loss.data.item()
|
||||
# TODO make summing of the sample sizes configurable
|
||||
agg_sample_size += sample_size
|
||||
for k in logging_output:
|
||||
agg_logging_output[k] += logging_output[k]
|
||||
agg_logging_output[f"{lang_pair}:{k}"] += logging_output[k]
|
||||
return agg_loss, agg_sample_size, agg_logging_output
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
with torch.no_grad():
|
||||
if self.args.decoder_langtok:
|
||||
bos_token = _lang_token_index(
|
||||
self.target_dictionary, self.args.target_lang
|
||||
)
|
||||
else:
|
||||
bos_token = self.target_dictionary.eos()
|
||||
return generator.generate(
|
||||
models,
|
||||
sample,
|
||||
prefix_tokens=prefix_tokens,
|
||||
constraints=constraints,
|
||||
bos_token=bos_token,
|
||||
)
|
||||
|
||||
def reduce_metrics(self, logging_outputs, criterion):
|
||||
with metrics.aggregate():
|
||||
# pass 'sample_size', 'nsentences', 'ntokens' stats to fairseq_task
|
||||
super().reduce_metrics(logging_outputs, criterion)
|
||||
for k in ["sample_size", "nsentences", "ntokens"]:
|
||||
metrics.log_scalar(k, sum(l[k] for l in logging_outputs))
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
if self.training:
|
||||
return next(iter(self.dicts.values()))
|
||||
else:
|
||||
return self.dicts[self.args.source_lang]
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
if self.training:
|
||||
return next(iter(self.dicts.values()))
|
||||
else:
|
||||
return self.dicts[self.args.target_lang]
|
||||
|
||||
def max_positions(self):
|
||||
"""Return the max sentence length allowed by the task."""
|
||||
if len(self.datasets.values()) == 0:
|
||||
return {
|
||||
"%s-%s"
|
||||
% (self.args.source_lang, self.args.target_lang): (
|
||||
self.args.max_source_positions,
|
||||
self.args.max_target_positions,
|
||||
)
|
||||
}
|
||||
return OrderedDict(
|
||||
[
|
||||
(key, (self.args.max_source_positions, self.args.max_target_positions))
|
||||
for split in self.datasets.keys()
|
||||
for key in self.datasets[split].datasets.keys()
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,485 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
BacktranslationDataset,
|
||||
IndexedCachedDataset,
|
||||
IndexedDataset,
|
||||
IndexedRawTextDataset,
|
||||
LanguagePairDataset,
|
||||
NoisingDataset,
|
||||
RoundRobinZipDatasets,
|
||||
data_utils,
|
||||
indexed_dataset,
|
||||
)
|
||||
from fairseq.models import FairseqMultiModel
|
||||
from fairseq.sequence_generator import SequenceGenerator
|
||||
|
||||
from . import register_task
|
||||
from .multilingual_translation import MultilingualTranslationTask
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_bt_dataset_key(lang_pair):
|
||||
return "bt:" + lang_pair
|
||||
|
||||
|
||||
def _get_denoising_dataset_key(lang_pair):
|
||||
return "denoising:" + lang_pair
|
||||
|
||||
|
||||
# ported from UnsupervisedMT
|
||||
def parse_lambda_config(x):
|
||||
"""
|
||||
Parse the configuration of lambda coefficient (for scheduling).
|
||||
x = "3" # lambda will be a constant equal to x
|
||||
x = "0:1,1000:0" # lambda will start from 1 and linearly decrease
|
||||
# to 0 during the first 1000 iterations
|
||||
x = "0:0,1000:0,2000:1" # lambda will be equal to 0 for the first 1000
|
||||
# iterations, then will linearly increase to 1 until iteration 2000
|
||||
"""
|
||||
split = x.split(",")
|
||||
if len(split) == 1:
|
||||
return float(x), None
|
||||
else:
|
||||
split = [s.split(os.pathsep) for s in split]
|
||||
assert all(len(s) == 2 for s in split)
|
||||
assert all(k.isdigit() for k, _ in split)
|
||||
assert all(
|
||||
int(split[i][0]) < int(split[i + 1][0]) for i in range(len(split) - 1)
|
||||
)
|
||||
return float(split[0][1]), [(int(k), float(v)) for k, v in split]
|
||||
|
||||
|
||||
@register_task("semisupervised_translation")
|
||||
class SemisupervisedTranslationTask(MultilingualTranslationTask):
|
||||
"""A task for training multiple translation models simultaneously.
|
||||
|
||||
We iterate round-robin over batches from multiple language pairs, ordered
|
||||
according to the `--lang-pairs` argument.
|
||||
|
||||
The training loop is roughly:
|
||||
|
||||
for i in range(len(epoch)):
|
||||
for lang_pair in args.lang_pairs:
|
||||
batch = next_batch_for_lang_pair(lang_pair)
|
||||
loss = criterion(model_for_lang_pair(lang_pair), batch)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
|
||||
In practice, `next_batch_for_lang_pair` is abstracted in a FairseqDataset
|
||||
(e.g., `RoundRobinZipDatasets`) and `model_for_lang_pair` is a model that
|
||||
implements the `FairseqMultiModel` interface.
|
||||
|
||||
During inference it is required to specify a single `--source-lang` and
|
||||
`--target-lang`, instead of `--lang-pairs`.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
MultilingualTranslationTask.add_args(parser)
|
||||
parser.add_argument('--lambda-parallel-config', default="1.0", type=str, metavar='CONFIG',
|
||||
help='cross-entropy reconstruction coefficient (parallel data). '
|
||||
'use fixed weight during training if set to floating point number. '
|
||||
'use piecewise linear function over number of updates to schedule the '
|
||||
'weight with the format: w0:step0,w1:step1,...')
|
||||
parser.add_argument('--lambda-denoising-config', default="0.0", type=str, metavar='CONFIG',
|
||||
help='Cross-entropy reconstruction coefficient (denoising autoencoding)'
|
||||
'use fixed weight during training if set to floating point number. '
|
||||
'use piecewise linear function over number of updates to schedule the '
|
||||
'weight with the format: w0:step0,w1:step1,...')
|
||||
parser.add_argument('--lambda-otf-bt-config', default="0.0", type=str, metavar='CONFIG',
|
||||
help='cross-entropy reconstruction coefficient (on-the-fly back-translation parallel data)'
|
||||
'use fixed weight during training if set to floating point number. '
|
||||
'use piecewise linear function over number of updates to schedule the '
|
||||
'weight with the format: w0:step0,w1:step1,...')
|
||||
parser.add_argument('--bt-max-len-a', default=1.1, type=float, metavar='N',
|
||||
help='generate back-translated sequences of maximum length ax + b, where x is the '
|
||||
'source length')
|
||||
parser.add_argument('--bt-max-len-b', default=10.0, type=float, metavar='N',
|
||||
help='generate back-translated sequences of maximum length ax + b, where x is the '
|
||||
'source length')
|
||||
parser.add_argument('--bt-beam-size', default=1, type=int, metavar='N',
|
||||
help='beam size used in beam search of online back-translation')
|
||||
parser.add_argument('--max-word-shuffle-distance', default=3.0, type=float, metavar='N',
|
||||
help='maximum word shuffle distance for denoising autoencoding data generation')
|
||||
parser.add_argument('--word-dropout-prob', default=0.1, type=float, metavar='N',
|
||||
help='word dropout probability for denoising autoencoding data generation')
|
||||
parser.add_argument('--word-blanking-prob', default=0.2, type=float, metavar='N',
|
||||
help='word blanking probability for denoising autoencoding data generation')
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, args, dicts, training):
|
||||
super().__init__(args, dicts, training)
|
||||
self.lambda_parallel, self.lambda_parallel_steps = parse_lambda_config(
|
||||
args.lambda_parallel_config
|
||||
)
|
||||
self.lambda_otf_bt, self.lambda_otf_bt_steps = parse_lambda_config(
|
||||
args.lambda_otf_bt_config
|
||||
)
|
||||
self.lambda_denoising, self.lambda_denoising_steps = parse_lambda_config(
|
||||
args.lambda_denoising_config
|
||||
)
|
||||
if self.lambda_denoising > 0.0 or self.lambda_denoising_steps is not None:
|
||||
denoising_lang_pairs = [
|
||||
"%s-%s" % (tgt, tgt)
|
||||
for tgt in {lang_pair.split("-")[1] for lang_pair in args.lang_pairs}
|
||||
]
|
||||
self.model_lang_pairs = self.model_lang_pairs + denoising_lang_pairs
|
||||
self.backtranslate_datasets = {}
|
||||
self.backtranslators = {}
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
dicts, training = MultilingualTranslationTask.prepare(args, **kwargs)
|
||||
return cls(args, dicts, training)
|
||||
|
||||
def load_dataset(self, split, epoch=1, **kwargs):
|
||||
"""Load a dataset split."""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
def split_exists(split, src, tgt, lang):
|
||||
if src is not None:
|
||||
filename = os.path.join(
|
||||
data_path, "{}.{}-{}.{}".format(split, src, tgt, lang)
|
||||
)
|
||||
else:
|
||||
filename = os.path.join(
|
||||
data_path, "{}.{}-None.{}".format(split, src, tgt)
|
||||
)
|
||||
return indexed_dataset.dataset_exists(filename, impl=self.args.dataset_impl)
|
||||
|
||||
def load_indexed_dataset(path, dictionary):
|
||||
return data_utils.load_indexed_dataset(
|
||||
path, dictionary, self.args.dataset_impl
|
||||
)
|
||||
|
||||
# load parallel datasets
|
||||
src_datasets, tgt_datasets = {}, {}
|
||||
if (
|
||||
self.lambda_parallel > 0.0
|
||||
or self.lambda_parallel_steps is not None
|
||||
or not split.startswith("train")
|
||||
):
|
||||
for lang_pair in self.lang_pairs:
|
||||
src, tgt = lang_pair.split("-")
|
||||
if split_exists(split, src, tgt, src):
|
||||
prefix = os.path.join(
|
||||
data_path, "{}.{}-{}.".format(split, src, tgt)
|
||||
)
|
||||
elif split_exists(split, tgt, src, src):
|
||||
prefix = os.path.join(
|
||||
data_path, "{}.{}-{}.".format(split, tgt, src)
|
||||
)
|
||||
else:
|
||||
continue
|
||||
src_datasets[lang_pair] = load_indexed_dataset(
|
||||
prefix + src, self.dicts[src]
|
||||
)
|
||||
tgt_datasets[lang_pair] = load_indexed_dataset(
|
||||
prefix + tgt, self.dicts[tgt]
|
||||
)
|
||||
logger.info(
|
||||
"parallel-{} {} {} examples".format(
|
||||
data_path, split, len(src_datasets[lang_pair])
|
||||
)
|
||||
)
|
||||
if len(src_datasets) == 0:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, data_path)
|
||||
)
|
||||
|
||||
# back translation datasets
|
||||
backtranslate_datasets = {}
|
||||
if (
|
||||
self.lambda_otf_bt > 0.0 or self.lambda_otf_bt_steps is not None
|
||||
) and split.startswith("train"):
|
||||
for lang_pair in self.lang_pairs:
|
||||
src, tgt = lang_pair.split("-")
|
||||
if not split_exists(split, tgt, None, tgt):
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: backtranslation {} ({})".format(
|
||||
split, data_path
|
||||
)
|
||||
)
|
||||
filename = os.path.join(
|
||||
data_path, "{}.{}-None.{}".format(split, tgt, tgt)
|
||||
)
|
||||
dataset = load_indexed_dataset(filename, self.dicts[tgt])
|
||||
lang_pair_dataset_tgt = LanguagePairDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
self.dicts[tgt],
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
)
|
||||
lang_pair_dataset = LanguagePairDataset(
|
||||
dataset,
|
||||
dataset.sizes,
|
||||
src_dict=self.dicts[src],
|
||||
tgt=dataset,
|
||||
tgt_sizes=dataset.sizes,
|
||||
tgt_dict=self.dicts[tgt],
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
)
|
||||
backtranslate_datasets[lang_pair] = BacktranslationDataset(
|
||||
tgt_dataset=self.alter_dataset_langtok(
|
||||
lang_pair_dataset_tgt,
|
||||
src_eos=self.dicts[tgt].eos(),
|
||||
src_lang=tgt,
|
||||
tgt_lang=src,
|
||||
),
|
||||
backtranslation_fn=self.backtranslators[lang_pair],
|
||||
src_dict=self.dicts[src],
|
||||
tgt_dict=self.dicts[tgt],
|
||||
output_collater=self.alter_dataset_langtok(
|
||||
lang_pair_dataset=lang_pair_dataset,
|
||||
src_eos=self.dicts[src].eos(),
|
||||
src_lang=src,
|
||||
tgt_eos=self.dicts[tgt].eos(),
|
||||
tgt_lang=tgt,
|
||||
).collater,
|
||||
)
|
||||
logger.info(
|
||||
"backtranslate-{}: {} {} {} examples".format(
|
||||
tgt,
|
||||
data_path,
|
||||
split,
|
||||
len(backtranslate_datasets[lang_pair]),
|
||||
)
|
||||
)
|
||||
self.backtranslate_datasets[lang_pair] = backtranslate_datasets[
|
||||
lang_pair
|
||||
]
|
||||
|
||||
# denoising autoencoder
|
||||
noising_datasets = {}
|
||||
if (
|
||||
self.lambda_denoising > 0.0 or self.lambda_denoising_steps is not None
|
||||
) and split.startswith("train"):
|
||||
for lang_pair in self.lang_pairs:
|
||||
_, tgt = lang_pair.split("-")
|
||||
if not split_exists(split, tgt, None, tgt):
|
||||
continue
|
||||
filename = os.path.join(
|
||||
data_path, "{}.{}-None.{}".format(split, tgt, tgt)
|
||||
)
|
||||
tgt_dataset1 = load_indexed_dataset(filename, self.dicts[tgt])
|
||||
tgt_dataset2 = load_indexed_dataset(filename, self.dicts[tgt])
|
||||
noising_dataset = NoisingDataset(
|
||||
tgt_dataset1,
|
||||
self.dicts[tgt],
|
||||
seed=1,
|
||||
max_word_shuffle_distance=self.args.max_word_shuffle_distance,
|
||||
word_dropout_prob=self.args.word_dropout_prob,
|
||||
word_blanking_prob=self.args.word_blanking_prob,
|
||||
)
|
||||
noising_datasets[lang_pair] = self.alter_dataset_langtok(
|
||||
LanguagePairDataset(
|
||||
noising_dataset,
|
||||
tgt_dataset1.sizes,
|
||||
self.dicts[tgt],
|
||||
tgt_dataset2,
|
||||
tgt_dataset2.sizes,
|
||||
self.dicts[tgt],
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
),
|
||||
src_eos=self.dicts[tgt].eos(),
|
||||
src_lang=tgt,
|
||||
tgt_eos=self.dicts[tgt].eos(),
|
||||
tgt_lang=tgt,
|
||||
)
|
||||
logger.info(
|
||||
"denoising-{}: {} {} {} examples".format(
|
||||
tgt,
|
||||
data_path,
|
||||
split,
|
||||
len(noising_datasets[lang_pair]),
|
||||
)
|
||||
)
|
||||
|
||||
def language_pair_dataset(lang_pair):
|
||||
src, tgt = lang_pair.split("-")
|
||||
src_dataset, tgt_dataset = src_datasets[lang_pair], tgt_datasets[lang_pair]
|
||||
return self.alter_dataset_langtok(
|
||||
LanguagePairDataset(
|
||||
src_dataset,
|
||||
src_dataset.sizes,
|
||||
self.dicts[src],
|
||||
tgt_dataset,
|
||||
tgt_dataset.sizes,
|
||||
self.dicts[tgt],
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
),
|
||||
self.dicts[src].eos(),
|
||||
src,
|
||||
self.dicts[tgt].eos(),
|
||||
tgt,
|
||||
)
|
||||
|
||||
self.datasets[split] = RoundRobinZipDatasets(
|
||||
OrderedDict(
|
||||
[
|
||||
(lang_pair, language_pair_dataset(lang_pair))
|
||||
for lang_pair in src_datasets.keys()
|
||||
]
|
||||
+ [
|
||||
(_get_bt_dataset_key(lang_pair), dataset)
|
||||
for lang_pair, dataset in backtranslate_datasets.items()
|
||||
]
|
||||
+ [
|
||||
(_get_denoising_dataset_key(lang_pair), dataset)
|
||||
for lang_pair, dataset in noising_datasets.items()
|
||||
]
|
||||
),
|
||||
eval_key=None
|
||||
if self.training
|
||||
else "%s-%s" % (self.args.source_lang, self.args.target_lang),
|
||||
)
|
||||
|
||||
def build_model(self, args):
|
||||
from fairseq import models
|
||||
|
||||
model = models.build_model(args, self)
|
||||
if not isinstance(model, FairseqMultiModel):
|
||||
raise ValueError(
|
||||
"SemisupervisedTranslationTask requires a FairseqMultiModel architecture"
|
||||
)
|
||||
|
||||
# create SequenceGenerator for each model that has backtranslation dependency on it
|
||||
self.sequence_generators = {}
|
||||
if (
|
||||
self.lambda_otf_bt > 0.0 or self.lambda_otf_bt_steps is not None
|
||||
) and self.training:
|
||||
for lang_pair in self.lang_pairs:
|
||||
src, tgt = lang_pair.split("-")
|
||||
key = "{}-{}".format(tgt, src)
|
||||
self.sequence_generators[key] = SequenceGenerator(
|
||||
[model.models[key]],
|
||||
tgt_dict=self.dicts[src],
|
||||
beam_size=args.bt_beam_size,
|
||||
max_len_a=args.bt_max_len_a,
|
||||
max_len_b=args.bt_max_len_b,
|
||||
)
|
||||
decoder_lang_tok_idx = self.get_decoder_langtok(src)
|
||||
|
||||
def backtranslate_fn(
|
||||
sample,
|
||||
model=model.models[key],
|
||||
bos_token=decoder_lang_tok_idx,
|
||||
sequence_generator=self.sequence_generators[key],
|
||||
):
|
||||
return sequence_generator.generate(
|
||||
[model],
|
||||
sample,
|
||||
bos_token=bos_token,
|
||||
)
|
||||
|
||||
self.backtranslators[lang_pair] = backtranslate_fn
|
||||
|
||||
return model
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
model.train()
|
||||
|
||||
if update_num > 0:
|
||||
self.update_step(update_num)
|
||||
|
||||
agg_loss, agg_sample_size, agg_logging_output = 0.0, 0.0, {}
|
||||
|
||||
def forward_backward(model, samples, logging_output_key, weight):
|
||||
nonlocal agg_loss, agg_sample_size, agg_logging_output
|
||||
if samples is None or len(samples) == 0:
|
||||
return
|
||||
loss, sample_size, logging_output = criterion(model, samples)
|
||||
if ignore_grad:
|
||||
loss *= 0
|
||||
else:
|
||||
loss *= weight
|
||||
optimizer.backward(loss)
|
||||
agg_loss += loss.detach().item()
|
||||
# TODO make summing of the sample sizes configurable
|
||||
agg_sample_size += sample_size
|
||||
for k in logging_output:
|
||||
agg_logging_output[k] += logging_output[k]
|
||||
agg_logging_output[logging_output_key] += logging_output[k]
|
||||
|
||||
if self.lambda_parallel > 0.0:
|
||||
for lang_pair in self.lang_pairs:
|
||||
forward_backward(
|
||||
model.models[lang_pair],
|
||||
sample[lang_pair],
|
||||
lang_pair,
|
||||
self.lambda_parallel,
|
||||
)
|
||||
|
||||
if self.lambda_otf_bt > 0.0:
|
||||
for lang_pair in self.lang_pairs:
|
||||
sample_key = _get_bt_dataset_key(lang_pair)
|
||||
forward_backward(
|
||||
model.models[lang_pair],
|
||||
sample[sample_key],
|
||||
sample_key,
|
||||
self.lambda_otf_bt,
|
||||
)
|
||||
|
||||
if self.lambda_denoising > 0.0:
|
||||
for lang_pair in self.lang_pairs:
|
||||
_, tgt = lang_pair.split("-")
|
||||
sample_key = _get_denoising_dataset_key(lang_pair)
|
||||
forward_backward(
|
||||
model.models["{0}-{0}".format(tgt)],
|
||||
sample[sample_key],
|
||||
sample_key,
|
||||
self.lambda_denoising,
|
||||
)
|
||||
|
||||
return agg_loss, agg_sample_size, agg_logging_output
|
||||
|
||||
def update_step(self, num_updates):
|
||||
def lambda_step_func(config, n_iter):
|
||||
"""
|
||||
Update a lambda value according to its schedule configuration.
|
||||
"""
|
||||
ranges = [
|
||||
i
|
||||
for i in range(len(config) - 1)
|
||||
if config[i][0] <= n_iter < config[i + 1][0]
|
||||
]
|
||||
if len(ranges) == 0:
|
||||
assert n_iter >= config[-1][0]
|
||||
return config[-1][1]
|
||||
assert len(ranges) == 1
|
||||
i = ranges[0]
|
||||
x_a, y_a = config[i]
|
||||
x_b, y_b = config[i + 1]
|
||||
return y_a + (n_iter - x_a) * float(y_b - y_a) / float(x_b - x_a)
|
||||
|
||||
if self.lambda_parallel_steps is not None:
|
||||
self.lambda_parallel = lambda_step_func(
|
||||
self.lambda_parallel_steps, num_updates
|
||||
)
|
||||
if self.lambda_denoising_steps is not None:
|
||||
self.lambda_denoising = lambda_step_func(
|
||||
self.lambda_denoising_steps, num_updates
|
||||
)
|
||||
if self.lambda_otf_bt_steps is not None:
|
||||
self.lambda_otf_bt = lambda_step_func(self.lambda_otf_bt_steps, num_updates)
|
||||
@@ -0,0 +1,279 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
ConcatSentencesDataset,
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
OffsetTokensDataset,
|
||||
PrependTokenDataset,
|
||||
RawLabelDataset,
|
||||
RightPadDataset,
|
||||
RollDataset,
|
||||
SortDataset,
|
||||
StripTokenDataset,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.shorten_dataset import maybe_shorten_dataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("sentence_prediction")
|
||||
class SentencePredictionTask(LegacyFairseqTask):
|
||||
"""
|
||||
Sentence (or sentence pair) prediction (classification or regression) task.
|
||||
|
||||
Args:
|
||||
dictionary (Dictionary): the dictionary for the input of the task
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument("data", metavar="FILE", help="file prefix for data")
|
||||
parser.add_argument(
|
||||
"--num-classes",
|
||||
type=int,
|
||||
default=-1,
|
||||
help="number of classes or regression targets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-token",
|
||||
type=int,
|
||||
default=None,
|
||||
help="add token at the beginning of each batch item",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--separator-token",
|
||||
type=int,
|
||||
default=None,
|
||||
help="add separator token between inputs",
|
||||
)
|
||||
parser.add_argument("--regression-target", action="store_true", default=False)
|
||||
parser.add_argument("--no-shuffle", action="store_true", default=False)
|
||||
parser.add_argument(
|
||||
"--shorten-method",
|
||||
default="none",
|
||||
choices=["none", "truncate", "random_crop"],
|
||||
help="if not none, shorten sequences that exceed --tokens-per-sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shorten-data-split-list",
|
||||
default="",
|
||||
help="comma-separated list of dataset splits to apply shortening to, "
|
||||
'e.g., "train,valid" (default: all dataset splits)',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--add-prev-output-tokens",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="add prev_output_tokens to sample, used for encoder-decoder arch",
|
||||
)
|
||||
|
||||
def __init__(self, args, data_dictionary, label_dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = data_dictionary
|
||||
self._label_dictionary = label_dictionary
|
||||
if not hasattr(args, "max_positions"):
|
||||
self._max_positions = (
|
||||
args.max_source_positions,
|
||||
args.max_target_positions,
|
||||
)
|
||||
else:
|
||||
self._max_positions = args.max_positions
|
||||
args.tokens_per_sample = self._max_positions
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, args, filename, source=True):
|
||||
"""Load the dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
dictionary = Dictionary.load(filename)
|
||||
dictionary.add_symbol("<mask>")
|
||||
return dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
assert args.num_classes > 0, "Must set --num-classes"
|
||||
|
||||
# load data dictionary
|
||||
data_dict = cls.load_dictionary(
|
||||
args,
|
||||
os.path.join(args.data, "input0", "dict.txt"),
|
||||
source=True,
|
||||
)
|
||||
logger.info("[input] dictionary: {} types".format(len(data_dict)))
|
||||
|
||||
# load label dictionary
|
||||
if not args.regression_target:
|
||||
label_dict = cls.load_dictionary(
|
||||
args,
|
||||
os.path.join(args.data, "label", "dict.txt"),
|
||||
source=False,
|
||||
)
|
||||
logger.info("[label] dictionary: {} types".format(len(label_dict)))
|
||||
else:
|
||||
label_dict = data_dict
|
||||
return cls(args, data_dict, label_dict)
|
||||
|
||||
def load_dataset(self, split, combine=False, **kwargs):
|
||||
"""Load a given dataset split (e.g., train, valid, test)."""
|
||||
|
||||
def get_path(key, split):
|
||||
return os.path.join(self.args.data, key, split)
|
||||
|
||||
def make_dataset(key, dictionary):
|
||||
split_path = get_path(key, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
return dataset
|
||||
|
||||
input0 = make_dataset("input0", self.source_dictionary)
|
||||
assert input0 is not None, "could not find dataset: {}".format(
|
||||
get_path("input0", split)
|
||||
)
|
||||
input1 = make_dataset("input1", self.source_dictionary)
|
||||
|
||||
if self.args.init_token is not None:
|
||||
input0 = PrependTokenDataset(input0, self.args.init_token)
|
||||
|
||||
if input1 is None:
|
||||
src_tokens = input0
|
||||
else:
|
||||
if self.args.separator_token is not None:
|
||||
input1 = PrependTokenDataset(input1, self.args.separator_token)
|
||||
|
||||
src_tokens = ConcatSentencesDataset(input0, input1)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
shuffle = np.random.permutation(len(src_tokens))
|
||||
|
||||
src_tokens = maybe_shorten_dataset(
|
||||
src_tokens,
|
||||
split,
|
||||
self.args.shorten_data_split_list,
|
||||
self.args.shorten_method,
|
||||
self.max_positions(),
|
||||
self.args.seed,
|
||||
)
|
||||
|
||||
dataset = {
|
||||
"id": IdDataset(),
|
||||
"net_input": {
|
||||
"src_tokens": RightPadDataset(
|
||||
src_tokens,
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
),
|
||||
"src_lengths": NumelDataset(src_tokens, reduce=False),
|
||||
},
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(src_tokens, reduce=True),
|
||||
}
|
||||
|
||||
if self.args.add_prev_output_tokens:
|
||||
prev_tokens_dataset = RightPadDataset(
|
||||
RollDataset(src_tokens, 1),
|
||||
pad_idx=self.dictionary.pad(),
|
||||
)
|
||||
dataset["net_input"].update(
|
||||
prev_output_tokens=prev_tokens_dataset,
|
||||
)
|
||||
|
||||
if not self.args.regression_target:
|
||||
label_dataset = make_dataset("label", self.label_dictionary)
|
||||
if label_dataset is not None:
|
||||
dataset.update(
|
||||
target=OffsetTokensDataset(
|
||||
StripTokenDataset(
|
||||
label_dataset,
|
||||
id_to_strip=self.label_dictionary.eos(),
|
||||
),
|
||||
offset=-self.label_dictionary.nspecial,
|
||||
)
|
||||
)
|
||||
else:
|
||||
label_path = "{0}.label".format(get_path("label", split))
|
||||
if os.path.exists(label_path):
|
||||
|
||||
def parse_regression_target(i, line):
|
||||
values = line.split()
|
||||
assert (
|
||||
len(values) == self.args.num_classes
|
||||
), f'expected num_classes={self.args.num_classes} regression target values on line {i}, found: "{line}"'
|
||||
return [float(x) for x in values]
|
||||
|
||||
with open(label_path) as h:
|
||||
dataset.update(
|
||||
target=RawLabelDataset(
|
||||
[
|
||||
parse_regression_target(i, line.strip())
|
||||
for i, line in enumerate(h.readlines())
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
nested_dataset = NestedDictionaryDataset(
|
||||
dataset,
|
||||
sizes=[src_tokens.sizes],
|
||||
)
|
||||
|
||||
if self.args.no_shuffle:
|
||||
dataset = nested_dataset
|
||||
else:
|
||||
dataset = SortDataset(
|
||||
nested_dataset,
|
||||
# shuffle
|
||||
sort_order=[shuffle],
|
||||
)
|
||||
|
||||
logger.info("Loaded {0} with #samples: {1}".format(split, len(dataset)))
|
||||
|
||||
self.datasets[split] = dataset
|
||||
return self.datasets[split]
|
||||
|
||||
def build_model(self, args):
|
||||
from fairseq import models
|
||||
|
||||
model = models.build_model(args, self)
|
||||
|
||||
model.register_classification_head(
|
||||
getattr(args, "classification_head_name", "sentence_classification_head"),
|
||||
num_classes=self.args.num_classes,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def max_positions(self):
|
||||
return self._max_positions
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def label_dictionary(self):
|
||||
return self._label_dictionary
|
||||
@@ -0,0 +1,219 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
ConcatSentencesDataset,
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
PrependTokenDataset,
|
||||
RawLabelDataset,
|
||||
RightPadDataset,
|
||||
SortDataset,
|
||||
TruncateDataset,
|
||||
data_utils,
|
||||
)
|
||||
from fairseq.data.shorten_dataset import maybe_shorten_dataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("sentence_ranking")
|
||||
class SentenceRankingTask(LegacyFairseqTask):
|
||||
"""
|
||||
Ranking task on multiple sentences.
|
||||
|
||||
Args:
|
||||
dictionary (Dictionary): the dictionary for the input of the task
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument("data", metavar="FILE", help="file prefix for data")
|
||||
parser.add_argument(
|
||||
"--num-classes", type=int, help="number of sentences to be ranked"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-token",
|
||||
type=int,
|
||||
help="add token at the beginning of each batch item",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--separator-token", type=int, help="add separator token between inputs"
|
||||
)
|
||||
parser.add_argument("--no-shuffle", action="store_true")
|
||||
parser.add_argument(
|
||||
"--shorten-method",
|
||||
default="none",
|
||||
choices=["none", "truncate", "random_crop"],
|
||||
help="if not none, shorten sequences that exceed --tokens-per-sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shorten-data-split-list",
|
||||
default="",
|
||||
help="comma-separated list of dataset splits to apply shortening to, "
|
||||
'e.g., "train,valid" (default: all dataset splits)',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-option-length", type=int, help="max length for each option"
|
||||
)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, args, filename, source=True):
|
||||
"""Load the dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
dictionary = Dictionary.load(filename)
|
||||
dictionary.add_symbol("<mask>")
|
||||
return dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
assert (
|
||||
args.criterion == "sentence_ranking"
|
||||
), "Must set --criterion=sentence_ranking"
|
||||
|
||||
# load data dictionary
|
||||
data_dict = cls.load_dictionary(
|
||||
args,
|
||||
os.path.join(args.data, "input0", "dict.txt"),
|
||||
source=True,
|
||||
)
|
||||
logger.info("[input] dictionary: {} types".format(len(data_dict)))
|
||||
return SentenceRankingTask(args, data_dict)
|
||||
|
||||
def load_dataset(self, split, combine=False, **kwargs):
|
||||
"""Load a given dataset split (e.g., train, valid, test)."""
|
||||
|
||||
def get_path(type, split):
|
||||
return os.path.join(self.args.data, type, split)
|
||||
|
||||
def make_dataset(type, dictionary):
|
||||
split_path = get_path(type, split)
|
||||
|
||||
dataset = data_utils.load_indexed_dataset(
|
||||
split_path,
|
||||
self.source_dictionary,
|
||||
self.args.dataset_impl,
|
||||
combine=combine,
|
||||
)
|
||||
return dataset
|
||||
|
||||
input0 = make_dataset("input0", self.source_dictionary)
|
||||
input_options = [
|
||||
make_dataset("input{idx}".format(idx=idx + 1), self.source_dictionary)
|
||||
for idx in range(self.args.num_classes)
|
||||
]
|
||||
|
||||
if self.args.separator_token is not None:
|
||||
input0 = PrependTokenDataset(input0, self.args.separator_token)
|
||||
|
||||
src_tokens = []
|
||||
for input_option in input_options:
|
||||
if self.args.init_token is not None:
|
||||
input_option = PrependTokenDataset(input_option, self.args.init_token)
|
||||
if self.args.max_option_length is not None:
|
||||
input_option = TruncateDataset(
|
||||
input_option, self.args.max_option_length
|
||||
)
|
||||
src_token = ConcatSentencesDataset(input_option, input0)
|
||||
src_token = maybe_shorten_dataset(
|
||||
src_token,
|
||||
split,
|
||||
self.args.shorten_data_split_list,
|
||||
self.args.shorten_method,
|
||||
self.args.max_positions,
|
||||
self.args.seed,
|
||||
)
|
||||
src_tokens.append(src_token)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
shuffle = np.random.permutation(len(src_tokens[0]))
|
||||
|
||||
dataset = {
|
||||
"id": IdDataset(),
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(src_tokens[0], reduce=True),
|
||||
}
|
||||
|
||||
for src_token_idx in range(len(src_tokens)):
|
||||
dataset.update(
|
||||
{
|
||||
"net_input{idx}".format(idx=src_token_idx + 1): {
|
||||
"src_tokens": RightPadDataset(
|
||||
src_tokens[src_token_idx],
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
),
|
||||
"src_lengths": NumelDataset(
|
||||
src_tokens[src_token_idx], reduce=False
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
label_path = "{}.label".format(get_path("label", split))
|
||||
if os.path.exists(label_path):
|
||||
with open(label_path) as h:
|
||||
dataset.update(
|
||||
target=RawLabelDataset([int(x.strip()) for x in h.readlines()])
|
||||
)
|
||||
|
||||
nested_dataset = NestedDictionaryDataset(
|
||||
dataset,
|
||||
sizes=[np.maximum.reduce([src_token.sizes for src_token in src_tokens])],
|
||||
)
|
||||
|
||||
if self.args.no_shuffle:
|
||||
dataset = nested_dataset
|
||||
else:
|
||||
dataset = SortDataset(
|
||||
nested_dataset,
|
||||
# shuffle
|
||||
sort_order=[shuffle],
|
||||
)
|
||||
|
||||
logger.info("Loaded {0} with #samples: {1}".format(split, len(dataset)))
|
||||
|
||||
self.datasets[split] = dataset
|
||||
return self.datasets[split]
|
||||
|
||||
def build_model(self, args):
|
||||
from fairseq import models
|
||||
|
||||
model = models.build_model(args, self)
|
||||
|
||||
model.register_classification_head(
|
||||
getattr(args, "ranking_head_name", "sentence_classification_head"),
|
||||
num_classes=1,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
def max_positions(self):
|
||||
return self.args.max_positions
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
@@ -0,0 +1,149 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os.path as op
|
||||
from argparse import Namespace
|
||||
|
||||
from fairseq.data import Dictionary, encoders
|
||||
from fairseq.data.audio.speech_to_text_dataset import (
|
||||
S2TDataConfig,
|
||||
SpeechToTextDataset,
|
||||
SpeechToTextDatasetCreator,
|
||||
get_features_or_waveform
|
||||
)
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("speech_to_text")
|
||||
class SpeechToTextTask(LegacyFairseqTask):
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
parser.add_argument("data", help="manifest root path")
|
||||
parser.add_argument(
|
||||
"--config-yaml",
|
||||
type=str,
|
||||
default="config.yaml",
|
||||
help="Configuration YAML filename (under manifest root)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-source-positions",
|
||||
default=6000,
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="max number of tokens in the source sequence",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-target-positions",
|
||||
default=1024,
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="max number of tokens in the target sequence",
|
||||
)
|
||||
|
||||
def __init__(self, args, tgt_dict):
|
||||
super().__init__(args)
|
||||
self.tgt_dict = tgt_dict
|
||||
self.data_cfg = S2TDataConfig(op.join(args.data, args.config_yaml))
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
data_cfg = S2TDataConfig(op.join(args.data, args.config_yaml))
|
||||
dict_path = op.join(args.data, data_cfg.vocab_filename)
|
||||
if not op.isfile(dict_path):
|
||||
raise FileNotFoundError(f"Dict not found: {dict_path}")
|
||||
tgt_dict = Dictionary.load(dict_path)
|
||||
logger.info(
|
||||
f"dictionary size ({data_cfg.vocab_filename}): " f"{len(tgt_dict):,}"
|
||||
)
|
||||
|
||||
if getattr(args, "train_subset", None) is not None:
|
||||
if not all(s.startswith("train") for s in args.train_subset.split(",")):
|
||||
raise ValueError('Train splits should be named like "train*".')
|
||||
return cls(args, tgt_dict)
|
||||
|
||||
def build_criterion(self, args):
|
||||
from fairseq import criterions
|
||||
|
||||
if self.data_cfg.prepend_tgt_lang_tag and args.ignore_prefix_size != 1:
|
||||
raise ValueError(
|
||||
'Please set "--ignore-prefix-size 1" since '
|
||||
"target language ID token is prepended as BOS."
|
||||
)
|
||||
return criterions.build_criterion(args, self)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
is_train_split = split.startswith("train")
|
||||
pre_tokenizer = self.build_tokenizer(self.args)
|
||||
bpe_tokenizer = self.build_bpe(self.args)
|
||||
self.datasets[split] = SpeechToTextDatasetCreator.from_tsv(
|
||||
self.args.data,
|
||||
self.data_cfg,
|
||||
split,
|
||||
self.tgt_dict,
|
||||
pre_tokenizer,
|
||||
bpe_tokenizer,
|
||||
is_train_split=is_train_split,
|
||||
epoch=epoch,
|
||||
seed=self.args.seed,
|
||||
)
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.tgt_dict
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return None
|
||||
|
||||
def max_positions(self):
|
||||
return self.args.max_source_positions, self.args.max_target_positions
|
||||
|
||||
def build_model(self, args):
|
||||
args.input_feat_per_channel = self.data_cfg.input_feat_per_channel
|
||||
args.input_channels = self.data_cfg.input_channels
|
||||
return super(SpeechToTextTask, self).build_model(args)
|
||||
|
||||
def build_generator(
|
||||
self,
|
||||
models,
|
||||
args,
|
||||
seq_gen_cls=None,
|
||||
extra_gen_cls_kwargs=None,
|
||||
):
|
||||
if self.data_cfg.prepend_tgt_lang_tag and args.prefix_size != 1:
|
||||
raise ValueError(
|
||||
'Please set "--prefix-size 1" since '
|
||||
"target language ID token is prepended as BOS."
|
||||
)
|
||||
lang_token_ids = {
|
||||
i
|
||||
for s, i in self.tgt_dict.indices.items()
|
||||
if SpeechToTextDataset.is_lang_tag(s)
|
||||
}
|
||||
extra_gen_cls_kwargs = {"symbols_to_strip_from_output": lang_token_ids}
|
||||
return super().build_generator(
|
||||
models, args, seq_gen_cls=None, extra_gen_cls_kwargs=extra_gen_cls_kwargs
|
||||
)
|
||||
|
||||
def build_tokenizer(self, args):
|
||||
logger.info(f"pre-tokenizer: {self.data_cfg.pre_tokenizer}")
|
||||
return encoders.build_tokenizer(Namespace(**self.data_cfg.pre_tokenizer))
|
||||
|
||||
def build_bpe(self, args):
|
||||
logger.info(f"tokenizer: {self.data_cfg.bpe_tokenizer}")
|
||||
return encoders.build_bpe(Namespace(**self.data_cfg.bpe_tokenizer))
|
||||
|
||||
def get_interactive_tokens_and_lengths(self, lines, encode_fn):
|
||||
n_frames = [get_features_or_waveform(p).shape[0] for p in lines]
|
||||
return lines, n_frames
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, **kwargs):
|
||||
return SpeechToTextDataset(
|
||||
"interactive", False, self.data_cfg, src_tokens, src_lengths
|
||||
)
|
||||
@@ -0,0 +1,484 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from argparse import Namespace
|
||||
from omegaconf import II
|
||||
|
||||
import numpy as np
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.data import (
|
||||
AppendTokenDataset,
|
||||
ConcatDataset,
|
||||
LanguagePairDataset,
|
||||
PrependTokenDataset,
|
||||
StripTokenDataset,
|
||||
TruncateDataset,
|
||||
data_utils,
|
||||
encoders,
|
||||
indexed_dataset,
|
||||
)
|
||||
from fairseq.data.indexed_dataset import get_available_dataset_impl
|
||||
from fairseq.dataclass import ChoiceEnum, FairseqDataclass
|
||||
from fairseq.tasks import FairseqTask, register_task
|
||||
|
||||
|
||||
EVAL_BLEU_ORDER = 4
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_langpair_dataset(
|
||||
data_path,
|
||||
split,
|
||||
src,
|
||||
src_dict,
|
||||
tgt,
|
||||
tgt_dict,
|
||||
combine,
|
||||
dataset_impl,
|
||||
upsample_primary,
|
||||
left_pad_source,
|
||||
left_pad_target,
|
||||
max_source_positions,
|
||||
max_target_positions,
|
||||
prepend_bos=False,
|
||||
load_alignments=False,
|
||||
truncate_source=False,
|
||||
append_source_id=False,
|
||||
num_buckets=0,
|
||||
shuffle=True,
|
||||
pad_to_multiple=1,
|
||||
):
|
||||
def split_exists(split, src, tgt, lang, data_path):
|
||||
filename = os.path.join(data_path, "{}.{}-{}.{}".format(split, src, tgt, lang))
|
||||
return indexed_dataset.dataset_exists(filename, impl=dataset_impl)
|
||||
|
||||
src_datasets = []
|
||||
tgt_datasets = []
|
||||
|
||||
for k in itertools.count():
|
||||
split_k = split + (str(k) if k > 0 else "")
|
||||
|
||||
# infer langcode
|
||||
if split_exists(split_k, src, tgt, src, data_path):
|
||||
prefix = os.path.join(data_path, "{}.{}-{}.".format(split_k, src, tgt))
|
||||
elif split_exists(split_k, tgt, src, src, data_path):
|
||||
prefix = os.path.join(data_path, "{}.{}-{}.".format(split_k, tgt, src))
|
||||
else:
|
||||
if k > 0:
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, data_path)
|
||||
)
|
||||
|
||||
src_dataset = data_utils.load_indexed_dataset(
|
||||
prefix + src, src_dict, dataset_impl
|
||||
)
|
||||
if truncate_source:
|
||||
src_dataset = AppendTokenDataset(
|
||||
TruncateDataset(
|
||||
StripTokenDataset(src_dataset, src_dict.eos()),
|
||||
max_source_positions - 1,
|
||||
),
|
||||
src_dict.eos(),
|
||||
)
|
||||
src_datasets.append(src_dataset)
|
||||
|
||||
tgt_dataset = data_utils.load_indexed_dataset(
|
||||
prefix + tgt, tgt_dict, dataset_impl
|
||||
)
|
||||
if tgt_dataset is not None:
|
||||
tgt_datasets.append(tgt_dataset)
|
||||
|
||||
logger.info(
|
||||
"{} {} {}-{} {} examples".format(
|
||||
data_path, split_k, src, tgt, len(src_datasets[-1])
|
||||
)
|
||||
)
|
||||
|
||||
if not combine:
|
||||
break
|
||||
|
||||
assert len(src_datasets) == len(tgt_datasets) or len(tgt_datasets) == 0
|
||||
|
||||
if len(src_datasets) == 1:
|
||||
src_dataset = src_datasets[0]
|
||||
tgt_dataset = tgt_datasets[0] if len(tgt_datasets) > 0 else None
|
||||
else:
|
||||
sample_ratios = [1] * len(src_datasets)
|
||||
sample_ratios[0] = upsample_primary
|
||||
src_dataset = ConcatDataset(src_datasets, sample_ratios)
|
||||
if len(tgt_datasets) > 0:
|
||||
tgt_dataset = ConcatDataset(tgt_datasets, sample_ratios)
|
||||
else:
|
||||
tgt_dataset = None
|
||||
|
||||
if prepend_bos:
|
||||
assert hasattr(src_dict, "bos_index") and hasattr(tgt_dict, "bos_index")
|
||||
src_dataset = PrependTokenDataset(src_dataset, src_dict.bos())
|
||||
if tgt_dataset is not None:
|
||||
tgt_dataset = PrependTokenDataset(tgt_dataset, tgt_dict.bos())
|
||||
|
||||
eos = None
|
||||
if append_source_id:
|
||||
src_dataset = AppendTokenDataset(
|
||||
src_dataset, src_dict.index("[{}]".format(src))
|
||||
)
|
||||
if tgt_dataset is not None:
|
||||
tgt_dataset = AppendTokenDataset(
|
||||
tgt_dataset, tgt_dict.index("[{}]".format(tgt))
|
||||
)
|
||||
eos = tgt_dict.index("[{}]".format(tgt))
|
||||
|
||||
align_dataset = None
|
||||
if load_alignments:
|
||||
align_path = os.path.join(data_path, "{}.align.{}-{}".format(split, src, tgt))
|
||||
if indexed_dataset.dataset_exists(align_path, impl=dataset_impl):
|
||||
align_dataset = data_utils.load_indexed_dataset(
|
||||
align_path, None, dataset_impl
|
||||
)
|
||||
|
||||
tgt_dataset_sizes = tgt_dataset.sizes if tgt_dataset is not None else None
|
||||
return LanguagePairDataset(
|
||||
src_dataset,
|
||||
src_dataset.sizes,
|
||||
src_dict,
|
||||
tgt_dataset,
|
||||
tgt_dataset_sizes,
|
||||
tgt_dict,
|
||||
left_pad_source=left_pad_source,
|
||||
left_pad_target=left_pad_target,
|
||||
align_dataset=align_dataset,
|
||||
eos=eos,
|
||||
num_buckets=num_buckets,
|
||||
shuffle=shuffle,
|
||||
pad_to_multiple=pad_to_multiple,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationConfig(FairseqDataclass):
|
||||
data: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "colon separated path to data directories list, will be iterated upon during epochs "
|
||||
"in round-robin manner; however, valid and test data are always in the first directory "
|
||||
"to avoid the need for repeating them in all directories"
|
||||
},
|
||||
)
|
||||
source_lang: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "source language",
|
||||
"argparse_alias": "-s",
|
||||
},
|
||||
)
|
||||
target_lang: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "target language",
|
||||
"argparse_alias": "-t",
|
||||
},
|
||||
)
|
||||
load_alignments: bool = field(
|
||||
default=False, metadata={"help": "load the binarized alignments"}
|
||||
)
|
||||
left_pad_source: bool = field(
|
||||
default=False, metadata={"help": "pad the source on the left"}
|
||||
)
|
||||
left_pad_target: bool = field(
|
||||
default=False, metadata={"help": "pad the target on the left"}
|
||||
)
|
||||
max_source_positions: int = field(
|
||||
default=1024, metadata={"help": "max number of tokens in the source sequence"}
|
||||
)
|
||||
max_target_positions: int = field(
|
||||
default=1024, metadata={"help": "max number of tokens in the target sequence"}
|
||||
)
|
||||
upsample_primary: int = field(
|
||||
default=-1, metadata={"help": "the amount of upsample primary dataset"}
|
||||
)
|
||||
truncate_source: bool = field(
|
||||
default=False, metadata={"help": "truncate source to max-source-positions"}
|
||||
)
|
||||
num_batch_buckets: int = field(
|
||||
default=0,
|
||||
metadata={
|
||||
"help": "if >0, then bucket source and target lengths into "
|
||||
"N buckets and pad accordingly; this is useful on TPUs to minimize the number of compilations"
|
||||
},
|
||||
)
|
||||
train_subset: str = II("dataset.train_subset")
|
||||
dataset_impl: Optional[ChoiceEnum(get_available_dataset_impl())] = II(
|
||||
"dataset.dataset_impl"
|
||||
)
|
||||
required_seq_len_multiple: int = II("dataset.required_seq_len_multiple")
|
||||
|
||||
# options for reporting BLEU during validation
|
||||
eval_bleu: bool = field(
|
||||
default=False, metadata={"help": "evaluation with BLEU scores"}
|
||||
)
|
||||
eval_bleu_args: Optional[str] = field(
|
||||
default="{}",
|
||||
metadata={
|
||||
"help": 'generation args for BLUE scoring, e.g., \'{"beam": 4, "lenpen": 0.6}\', as JSON string'
|
||||
},
|
||||
)
|
||||
eval_bleu_detok: str = field(
|
||||
default="space",
|
||||
metadata={
|
||||
"help": "detokenize before computing BLEU (e.g., 'moses'); required if using --eval-bleu; "
|
||||
"use 'space' to disable detokenization; see fairseq.data.encoders for other options"
|
||||
},
|
||||
)
|
||||
eval_bleu_detok_args: Optional[str] = field(
|
||||
default="{}",
|
||||
metadata={"help": "args for building the tokenizer, if needed, as JSON string"},
|
||||
)
|
||||
eval_tokenized_bleu: bool = field(
|
||||
default=False, metadata={"help": "compute tokenized BLEU instead of sacrebleu"}
|
||||
)
|
||||
eval_bleu_remove_bpe: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "remove BPE before computing BLEU",
|
||||
"argparse_const": "@@ ",
|
||||
},
|
||||
)
|
||||
eval_bleu_print_samples: bool = field(
|
||||
default=False, metadata={"help": "print sample generations during validation"}
|
||||
)
|
||||
|
||||
|
||||
@register_task("translation", dataclass=TranslationConfig)
|
||||
class TranslationTask(FairseqTask):
|
||||
"""
|
||||
Translate from one (source) language to another (target) language.
|
||||
|
||||
Args:
|
||||
src_dict (~fairseq.data.Dictionary): dictionary for the source language
|
||||
tgt_dict (~fairseq.data.Dictionary): dictionary for the target language
|
||||
|
||||
.. note::
|
||||
|
||||
The translation task is compatible with :mod:`fairseq-train`,
|
||||
:mod:`fairseq-generate` and :mod:`fairseq-interactive`.
|
||||
"""
|
||||
|
||||
cfg: TranslationConfig
|
||||
|
||||
def __init__(self, cfg: TranslationConfig, src_dict, tgt_dict):
|
||||
super().__init__(cfg)
|
||||
self.src_dict = src_dict
|
||||
self.tgt_dict = tgt_dict
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, cfg: TranslationConfig, **kwargs):
|
||||
"""Setup the task (e.g., load dictionaries).
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
"""
|
||||
|
||||
paths = utils.split_paths(cfg.data)
|
||||
assert len(paths) > 0
|
||||
# find language pair automatically
|
||||
if cfg.source_lang is None or cfg.target_lang is None:
|
||||
cfg.source_lang, cfg.target_lang = data_utils.infer_language_pair(paths[0])
|
||||
if cfg.source_lang is None or cfg.target_lang is None:
|
||||
raise Exception(
|
||||
"Could not infer language pair, please provide it explicitly"
|
||||
)
|
||||
|
||||
# load dictionaries
|
||||
src_dict = cls.load_dictionary(
|
||||
os.path.join(paths[0], "dict.{}.txt".format(cfg.source_lang))
|
||||
)
|
||||
tgt_dict = cls.load_dictionary(
|
||||
os.path.join(paths[0], "dict.{}.txt".format(cfg.target_lang))
|
||||
)
|
||||
assert src_dict.pad() == tgt_dict.pad()
|
||||
assert src_dict.eos() == tgt_dict.eos()
|
||||
assert src_dict.unk() == tgt_dict.unk()
|
||||
logger.info("[{}] dictionary: {} types".format(cfg.source_lang, len(src_dict)))
|
||||
logger.info("[{}] dictionary: {} types".format(cfg.target_lang, len(tgt_dict)))
|
||||
|
||||
return cls(cfg, src_dict, tgt_dict)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.cfg.data)
|
||||
assert len(paths) > 0
|
||||
if split != self.cfg.train_subset:
|
||||
# if not training data set, use the first shard for valid and test
|
||||
paths = paths[:1]
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
# infer langcode
|
||||
src, tgt = self.cfg.source_lang, self.cfg.target_lang
|
||||
|
||||
self.datasets[split] = load_langpair_dataset(
|
||||
data_path,
|
||||
split,
|
||||
src,
|
||||
self.src_dict,
|
||||
tgt,
|
||||
self.tgt_dict,
|
||||
combine=combine,
|
||||
dataset_impl=self.cfg.dataset_impl,
|
||||
upsample_primary=self.cfg.upsample_primary,
|
||||
left_pad_source=self.cfg.left_pad_source,
|
||||
left_pad_target=self.cfg.left_pad_target,
|
||||
max_source_positions=self.cfg.max_source_positions,
|
||||
max_target_positions=self.cfg.max_target_positions,
|
||||
load_alignments=self.cfg.load_alignments,
|
||||
truncate_source=self.cfg.truncate_source,
|
||||
num_buckets=self.cfg.num_batch_buckets,
|
||||
shuffle=(split != "test"),
|
||||
pad_to_multiple=self.cfg.required_seq_len_multiple,
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, constraints=None):
|
||||
return LanguagePairDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
self.source_dictionary,
|
||||
tgt_dict=self.target_dictionary,
|
||||
constraints=constraints,
|
||||
)
|
||||
|
||||
def build_model(self, cfg):
|
||||
model = super().build_model(cfg)
|
||||
self.cfg.eval_bleu = False
|
||||
if self.cfg.eval_bleu:
|
||||
detok_args = json.loads(self.cfg.eval_bleu_detok_args)
|
||||
self.tokenizer = encoders.build_tokenizer(
|
||||
Namespace(tokenizer=self.cfg.eval_bleu_detok, **detok_args)
|
||||
)
|
||||
|
||||
gen_args = json.loads(self.cfg.eval_bleu_args)
|
||||
self.sequence_generator = self.build_generator(
|
||||
[model], Namespace(**gen_args)
|
||||
)
|
||||
return model
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
loss, sample_size, logging_output = super().valid_step(sample, model, criterion)
|
||||
if self.cfg.eval_bleu:
|
||||
bleu = self._inference_with_bleu(self.sequence_generator, sample, model)
|
||||
logging_output["_bleu_sys_len"] = bleu.sys_len
|
||||
logging_output["_bleu_ref_len"] = bleu.ref_len
|
||||
# we split counts into separate entries so that they can be
|
||||
# summed efficiently across workers using fast-stat-sync
|
||||
assert len(bleu.counts) == EVAL_BLEU_ORDER
|
||||
for i in range(EVAL_BLEU_ORDER):
|
||||
logging_output["_bleu_counts_" + str(i)] = bleu.counts[i]
|
||||
logging_output["_bleu_totals_" + str(i)] = bleu.totals[i]
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def reduce_metrics(self, logging_outputs, criterion):
|
||||
super().reduce_metrics(logging_outputs, criterion)
|
||||
if self.cfg.eval_bleu:
|
||||
|
||||
def sum_logs(key):
|
||||
import torch
|
||||
result = sum(log.get(key, 0) for log in logging_outputs)
|
||||
if torch.is_tensor(result):
|
||||
result = result.cpu()
|
||||
return result
|
||||
|
||||
counts, totals = [], []
|
||||
for i in range(EVAL_BLEU_ORDER):
|
||||
counts.append(sum_logs("_bleu_counts_" + str(i)))
|
||||
totals.append(sum_logs("_bleu_totals_" + str(i)))
|
||||
|
||||
if max(totals) > 0:
|
||||
# log counts as numpy arrays -- log_scalar will sum them correctly
|
||||
metrics.log_scalar("_bleu_counts", np.array(counts))
|
||||
metrics.log_scalar("_bleu_totals", np.array(totals))
|
||||
metrics.log_scalar("_bleu_sys_len", sum_logs("_bleu_sys_len"))
|
||||
metrics.log_scalar("_bleu_ref_len", sum_logs("_bleu_ref_len"))
|
||||
|
||||
def compute_bleu(meters):
|
||||
import inspect
|
||||
import sacrebleu
|
||||
|
||||
fn_sig = inspect.getfullargspec(sacrebleu.compute_bleu)[0]
|
||||
if "smooth_method" in fn_sig:
|
||||
smooth = {"smooth_method": "exp"}
|
||||
else:
|
||||
smooth = {"smooth": "exp"}
|
||||
bleu = sacrebleu.compute_bleu(
|
||||
correct=meters["_bleu_counts"].sum,
|
||||
total=meters["_bleu_totals"].sum,
|
||||
sys_len=meters["_bleu_sys_len"].sum,
|
||||
ref_len=meters["_bleu_ref_len"].sum,
|
||||
**smooth
|
||||
)
|
||||
return round(bleu.score, 2)
|
||||
|
||||
metrics.log_derived("bleu", compute_bleu)
|
||||
|
||||
def max_positions(self):
|
||||
"""Return the max sentence length allowed by the task."""
|
||||
return (self.cfg.max_source_positions, self.cfg.max_target_positions)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
"""Return the source :class:`~fairseq.data.Dictionary`."""
|
||||
return self.src_dict
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
"""Return the target :class:`~fairseq.data.Dictionary`."""
|
||||
return self.tgt_dict
|
||||
|
||||
def _inference_with_bleu(self, generator, sample, model):
|
||||
import sacrebleu
|
||||
|
||||
def decode(toks, escape_unk=False):
|
||||
s = self.tgt_dict.string(
|
||||
toks.int().cpu(),
|
||||
self.cfg.eval_bleu_remove_bpe,
|
||||
# The default unknown string in fairseq is `<unk>`, but
|
||||
# this is tokenized by sacrebleu as `< unk >`, inflating
|
||||
# BLEU scores. Instead, we use a somewhat more verbose
|
||||
# alternative that is unlikely to appear in the real
|
||||
# reference, but doesn't get split into multiple tokens.
|
||||
unk_string=("UNKNOWNTOKENINREF" if escape_unk else "UNKNOWNTOKENINHYP"),
|
||||
)
|
||||
if self.tokenizer:
|
||||
s = self.tokenizer.decode(s)
|
||||
return s
|
||||
|
||||
gen_out = self.inference_step(generator, [model], sample, prefix_tokens=None)
|
||||
hyps, refs = [], []
|
||||
for i in range(len(gen_out)):
|
||||
hyps.append(decode(gen_out[i][0]["tokens"]))
|
||||
refs.append(
|
||||
decode(
|
||||
utils.strip_pad(sample["target"][i], self.tgt_dict.pad()),
|
||||
escape_unk=True, # don't count <unk> as matches to the hypo
|
||||
)
|
||||
)
|
||||
if self.cfg.eval_bleu_print_samples:
|
||||
logger.info("example hypothesis: " + hyps[0])
|
||||
logger.info("example reference: " + refs[0])
|
||||
if self.cfg.eval_tokenized_bleu:
|
||||
return sacrebleu.corpus_bleu(hyps, [refs], tokenize="none")
|
||||
else:
|
||||
return sacrebleu.corpus_bleu(hyps, [refs])
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch
|
||||
from fairseq import utils
|
||||
from fairseq.data import LanguagePairDataset
|
||||
|
||||
from . import register_task
|
||||
from .translation import TranslationTask, load_langpair_dataset
|
||||
|
||||
|
||||
@register_task("translation_from_pretrained_bart")
|
||||
class TranslationFromPretrainedBARTTask(TranslationTask):
|
||||
"""
|
||||
Translate from source language to target language with a model initialized with a multilingual pretrain.
|
||||
|
||||
Args:
|
||||
src_dict (~fairseq.data.Dictionary): dictionary for the source language
|
||||
tgt_dict (~fairseq.data.Dictionary): dictionary for the target language
|
||||
|
||||
.. note::
|
||||
|
||||
The translation task is compatible with :mod:`fairseq-train`,
|
||||
:mod:`fairseq-generate` and :mod:`fairseq-interactive`.
|
||||
|
||||
The translation task provides the following additional command-line
|
||||
arguments:
|
||||
|
||||
.. argparse::
|
||||
:ref: fairseq.tasks.translation_parser
|
||||
:prog:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
TranslationTask.add_args(parser)
|
||||
parser.add_argument('--langs', required=True, metavar='LANG',
|
||||
help='comma-separated list of monolingual language, '
|
||||
'for example, "en,de,fr". These should match the '
|
||||
'langs from pretraining (and be in the same order). '
|
||||
'You should always add all pretraining language idx '
|
||||
'during finetuning.')
|
||||
parser.add_argument('--prepend-bos', action='store_true',
|
||||
help='prepend bos token to each sentence, which matches '
|
||||
'mBART pretraining')
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, args, src_dict, tgt_dict):
|
||||
super().__init__(args, src_dict, tgt_dict)
|
||||
self.langs = args.langs.split(",")
|
||||
for d in [src_dict, tgt_dict]:
|
||||
for l in self.langs:
|
||||
d.add_symbol("[{}]".format(l))
|
||||
d.add_symbol("<mask>")
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.args.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
# infer langcode
|
||||
src, tgt = self.args.source_lang, self.args.target_lang
|
||||
|
||||
self.datasets[split] = load_langpair_dataset(
|
||||
data_path,
|
||||
split,
|
||||
src,
|
||||
self.src_dict,
|
||||
tgt,
|
||||
self.tgt_dict,
|
||||
combine=combine,
|
||||
dataset_impl=self.args.dataset_impl,
|
||||
upsample_primary=self.args.upsample_primary,
|
||||
left_pad_source=self.args.left_pad_source,
|
||||
left_pad_target=self.args.left_pad_target,
|
||||
max_source_positions=getattr(self.args, "max_source_positions", 1024),
|
||||
max_target_positions=getattr(self.args, "max_target_positions", 1024),
|
||||
load_alignments=self.args.load_alignments,
|
||||
prepend_bos=getattr(self.args, "prepend_bos", False),
|
||||
append_source_id=True,
|
||||
)
|
||||
|
||||
def build_generator(self, models, args, **unused):
|
||||
if getattr(args, "score_reference", False):
|
||||
from fairseq.sequence_scorer import SequenceScorer
|
||||
|
||||
return SequenceScorer(
|
||||
self.target_dictionary,
|
||||
eos=self.tgt_dict.index("[{}]".format(self.args.target_lang)),
|
||||
)
|
||||
else:
|
||||
from fairseq.sequence_generator import SequenceGenerator
|
||||
|
||||
return SequenceGenerator(
|
||||
models,
|
||||
self.target_dictionary,
|
||||
beam_size=getattr(args, "beam", 5),
|
||||
max_len_a=getattr(args, "max_len_a", 0),
|
||||
max_len_b=getattr(args, "max_len_b", 200),
|
||||
min_len=getattr(args, "min_len", 1),
|
||||
normalize_scores=(not getattr(args, "unnormalized", False)),
|
||||
len_penalty=getattr(args, "lenpen", 1),
|
||||
unk_penalty=getattr(args, "unkpen", 0),
|
||||
temperature=getattr(args, "temperature", 1.0),
|
||||
match_source_len=getattr(args, "match_source_len", False),
|
||||
no_repeat_ngram_size=getattr(args, "no_repeat_ngram_size", 0),
|
||||
eos=self.tgt_dict.index("[{}]".format(self.args.target_lang)),
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, constraints=None):
|
||||
src_lang_id = self.source_dictionary.index("[{}]".format(self.args.source_lang))
|
||||
source_tokens = []
|
||||
for s_t in src_tokens:
|
||||
s_t = torch.cat([s_t, s_t.new(1).fill_(src_lang_id)])
|
||||
source_tokens.append(s_t)
|
||||
dataset = LanguagePairDataset(
|
||||
source_tokens,
|
||||
src_lengths,
|
||||
self.source_dictionary,
|
||||
tgt_dict=self.target_dictionary,
|
||||
constraints=constraints,
|
||||
)
|
||||
return dataset
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from fairseq.data.legacy.masked_lm_dictionary import MaskedLMDictionary
|
||||
from fairseq.tasks.translation import TranslationConfig, TranslationTask
|
||||
|
||||
from . import register_task
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationFromPretrainedXLMConfig(TranslationConfig):
|
||||
pass
|
||||
|
||||
|
||||
@register_task(
|
||||
"translation_from_pretrained_xlm", dataclass=TranslationFromPretrainedXLMConfig
|
||||
)
|
||||
class TranslationFromPretrainedXLMTask(TranslationTask):
|
||||
"""
|
||||
Same as TranslationTask except use the MaskedLMDictionary class so that
|
||||
we can load data that was binarized with the MaskedLMDictionary class.
|
||||
|
||||
This task should be used for the entire training pipeline when we want to
|
||||
train an NMT model from a pretrained XLM checkpoint: binarizing NMT data,
|
||||
training NMT with the pretrained XLM checkpoint, and subsequent evaluation
|
||||
of that trained model.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
"""Load the masked LM dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
return MaskedLMDictionary.load(filename)
|
||||
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import torch
|
||||
from fairseq import utils
|
||||
from fairseq.data import LanguagePairDataset
|
||||
from fairseq.dataclass import ChoiceEnum
|
||||
from fairseq.tasks import register_task
|
||||
from fairseq.tasks.translation import TranslationConfig, TranslationTask, load_langpair_dataset
|
||||
from fairseq.utils import new_arange
|
||||
|
||||
|
||||
NOISE_CHOICES = ChoiceEnum(["random_delete", "random_mask", "no_noise", "full_mask"])
|
||||
|
||||
@dataclass
|
||||
class TranslationLevenshteinConfig(TranslationConfig):
|
||||
noise: NOISE_CHOICES = field(
|
||||
default="random_delete",
|
||||
metadata={
|
||||
"help": "type of noise"
|
||||
},
|
||||
)
|
||||
|
||||
@register_task("translation_lev", dataclass=TranslationLevenshteinConfig)
|
||||
class TranslationLevenshteinTask(TranslationTask):
|
||||
"""
|
||||
Translation (Sequence Generation) task for Levenshtein Transformer
|
||||
See `"Levenshtein Transformer" <https://arxiv.org/abs/1905.11006>`_.
|
||||
"""
|
||||
|
||||
cfg: TranslationLevenshteinConfig
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
paths = utils.split_paths(self.cfg.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
|
||||
# infer langcode
|
||||
src, tgt = self.cfg.source_lang, self.cfg.target_lang
|
||||
|
||||
self.datasets[split] = load_langpair_dataset(
|
||||
data_path,
|
||||
split,
|
||||
src,
|
||||
self.src_dict,
|
||||
tgt,
|
||||
self.tgt_dict,
|
||||
combine=combine,
|
||||
dataset_impl=self.cfg.dataset_impl,
|
||||
upsample_primary=self.cfg.upsample_primary,
|
||||
left_pad_source=self.cfg.left_pad_source,
|
||||
left_pad_target=self.cfg.left_pad_target,
|
||||
max_source_positions=self.cfg.max_source_positions,
|
||||
max_target_positions=self.cfg.max_target_positions,
|
||||
prepend_bos=True,
|
||||
)
|
||||
|
||||
def inject_noise(self, target_tokens):
|
||||
def _random_delete(target_tokens):
|
||||
pad = self.tgt_dict.pad()
|
||||
bos = self.tgt_dict.bos()
|
||||
eos = self.tgt_dict.eos()
|
||||
|
||||
max_len = target_tokens.size(1)
|
||||
target_mask = target_tokens.eq(pad)
|
||||
target_score = target_tokens.clone().float().uniform_()
|
||||
target_score.masked_fill_(
|
||||
target_tokens.eq(bos) | target_tokens.eq(eos), 0.0
|
||||
)
|
||||
target_score.masked_fill_(target_mask, 1)
|
||||
target_score, target_rank = target_score.sort(1)
|
||||
target_length = target_mask.size(1) - target_mask.float().sum(
|
||||
1, keepdim=True
|
||||
)
|
||||
|
||||
# do not delete <bos> and <eos> (we assign 0 score for them)
|
||||
target_cutoff = (
|
||||
2
|
||||
+ (
|
||||
(target_length - 2)
|
||||
* target_score.new_zeros(target_score.size(0), 1).uniform_()
|
||||
).long()
|
||||
)
|
||||
target_cutoff = target_score.sort(1)[1] >= target_cutoff
|
||||
|
||||
prev_target_tokens = (
|
||||
target_tokens.gather(1, target_rank)
|
||||
.masked_fill_(target_cutoff, pad)
|
||||
.gather(1, target_rank.masked_fill_(target_cutoff, max_len).sort(1)[1])
|
||||
)
|
||||
prev_target_tokens = prev_target_tokens[
|
||||
:, : prev_target_tokens.ne(pad).sum(1).max()
|
||||
]
|
||||
|
||||
return prev_target_tokens
|
||||
|
||||
def _random_mask(target_tokens):
|
||||
pad = self.tgt_dict.pad()
|
||||
bos = self.tgt_dict.bos()
|
||||
eos = self.tgt_dict.eos()
|
||||
unk = self.tgt_dict.unk()
|
||||
|
||||
target_masks = (
|
||||
target_tokens.ne(pad) & target_tokens.ne(bos) & target_tokens.ne(eos)
|
||||
)
|
||||
target_score = target_tokens.clone().float().uniform_()
|
||||
target_score.masked_fill_(~target_masks, 2.0)
|
||||
target_length = target_masks.sum(1).float()
|
||||
target_length = target_length * target_length.clone().uniform_()
|
||||
target_length = target_length + 1 # make sure to mask at least one token.
|
||||
|
||||
_, target_rank = target_score.sort(1)
|
||||
target_cutoff = new_arange(target_rank) < target_length[:, None].long()
|
||||
prev_target_tokens = target_tokens.masked_fill(
|
||||
target_cutoff.scatter(1, target_rank, target_cutoff), unk
|
||||
)
|
||||
return prev_target_tokens
|
||||
|
||||
def _full_mask(target_tokens):
|
||||
pad = self.tgt_dict.pad()
|
||||
bos = self.tgt_dict.bos()
|
||||
eos = self.tgt_dict.eos()
|
||||
unk = self.tgt_dict.unk()
|
||||
|
||||
target_mask = (
|
||||
target_tokens.eq(bos) | target_tokens.eq(eos) | target_tokens.eq(pad)
|
||||
)
|
||||
return target_tokens.masked_fill(~target_mask, unk)
|
||||
|
||||
if self.cfg.noise == "random_delete":
|
||||
return _random_delete(target_tokens)
|
||||
elif self.cfg.noise == "random_mask":
|
||||
return _random_mask(target_tokens)
|
||||
elif self.cfg.noise == "full_mask":
|
||||
return _full_mask(target_tokens)
|
||||
elif self.cfg.noise == "no_noise":
|
||||
return target_tokens
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def build_generator(self, models, args, **unused):
|
||||
# add models input to match the API for SequenceGenerator
|
||||
from fairseq.iterative_refinement_generator import IterativeRefinementGenerator
|
||||
|
||||
return IterativeRefinementGenerator(
|
||||
self.target_dictionary,
|
||||
eos_penalty=getattr(args, "iter_decode_eos_penalty", 0.0),
|
||||
max_iter=getattr(args, "iter_decode_max_iter", 10),
|
||||
beam_size=getattr(args, "iter_decode_with_beam", 1),
|
||||
reranking=getattr(args, "iter_decode_with_external_reranker", False),
|
||||
decoding_format=getattr(args, "decoding_format", None),
|
||||
adaptive=not getattr(args, "iter_decode_force_max_iter", False),
|
||||
retain_history=getattr(args, "retain_iter_history", False),
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, constraints=None):
|
||||
if constraints is not None:
|
||||
# Though see Susanto et al. (ACL 2020): https://www.aclweb.org/anthology/2020.acl-main.325/
|
||||
raise NotImplementedError(
|
||||
"Constrained decoding with the translation_lev task is not supported"
|
||||
)
|
||||
|
||||
return LanguagePairDataset(
|
||||
src_tokens, src_lengths, self.source_dictionary, append_bos=True
|
||||
)
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
model.train()
|
||||
sample["prev_target"] = self.inject_noise(sample["target"])
|
||||
loss, sample_size, logging_output = criterion(model, sample)
|
||||
if ignore_grad:
|
||||
loss *= 0
|
||||
optimizer.backward(loss)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
sample["prev_target"] = self.inject_noise(sample["target"])
|
||||
loss, sample_size, logging_output = criterion(model, sample)
|
||||
return loss, sample_size, logging_output
|
||||
@@ -0,0 +1,430 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import torch
|
||||
from fairseq.data import (
|
||||
FairseqDataset,
|
||||
LanguagePairDataset,
|
||||
ListDataset,
|
||||
data_utils,
|
||||
iterators,
|
||||
)
|
||||
from fairseq.data.multilingual.multilingual_data_manager import (
|
||||
MultilingualDatasetManager,
|
||||
)
|
||||
from fairseq.data.multilingual.sampling_method import SamplingMethod
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
from fairseq.utils import FileContentsAction
|
||||
|
||||
|
||||
###
|
||||
def get_time_gap(s, e):
|
||||
return (
|
||||
datetime.datetime.fromtimestamp(e) - datetime.datetime.fromtimestamp(s)
|
||||
).__str__()
|
||||
|
||||
|
||||
###
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("translation_multi_simple_epoch")
|
||||
class TranslationMultiSimpleEpochTask(LegacyFairseqTask):
|
||||
"""
|
||||
Translate from one (source) language to another (target) language.
|
||||
|
||||
Args:
|
||||
langs (List[str]): a list of languages that are being supported
|
||||
dicts (Dict[str, fairseq.data.Dictionary]): mapping from supported languages to their dictionaries
|
||||
training (bool): whether the task should be configured for training or not
|
||||
|
||||
.. note::
|
||||
|
||||
The translation task is compatible with :mod:`fairseq-train`,
|
||||
:mod:`fairseq-generate` and :mod:`fairseq-interactive`.
|
||||
|
||||
The translation task provides the following additional command-line
|
||||
arguments:
|
||||
|
||||
.. argparse::
|
||||
:ref: fairseq.tasks.translation_parser
|
||||
:prog:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
parser.add_argument('-s', '--source-lang', default=None, metavar='SRC',
|
||||
help='inference source language')
|
||||
parser.add_argument('-t', '--target-lang', default=None, metavar='TARGET',
|
||||
help='inference target language')
|
||||
parser.add_argument('--lang-pairs', default=None, metavar='PAIRS',
|
||||
help='comma-separated list of language pairs (in training order): en-de,en-fr,de-fr',
|
||||
action=FileContentsAction)
|
||||
parser.add_argument('--keep-inference-langtok', action='store_true',
|
||||
help='keep language tokens in inference output (e.g. for analysis or debugging)')
|
||||
|
||||
SamplingMethod.add_arguments(parser)
|
||||
MultilingualDatasetManager.add_args(parser)
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, args, langs, dicts, training):
|
||||
super().__init__(args)
|
||||
self.langs = langs
|
||||
self.dicts = dicts
|
||||
self.training = training
|
||||
if training:
|
||||
self.lang_pairs = args.lang_pairs
|
||||
else:
|
||||
self.lang_pairs = ["{}-{}".format(args.source_lang, args.target_lang)]
|
||||
# eval_lang_pairs for multilingual translation is usually all of the
|
||||
# lang_pairs. However for other multitask settings or when we want to
|
||||
# optimize for certain languages we want to use a different subset. Thus
|
||||
# the eval_lang_pairs class variable is provided for classes that extend
|
||||
# this class.
|
||||
self.eval_lang_pairs = self.lang_pairs
|
||||
# model_lang_pairs will be used to build encoder-decoder model pairs in
|
||||
# models.build_model(). This allows multitask type of sub-class can
|
||||
# build models other than the input lang_pairs
|
||||
self.model_lang_pairs = self.lang_pairs
|
||||
self.source_langs = [d.split("-")[0] for d in self.lang_pairs]
|
||||
self.target_langs = [d.split("-")[1] for d in self.lang_pairs]
|
||||
self.check_dicts(self.dicts, self.source_langs, self.target_langs)
|
||||
|
||||
self.sampling_method = SamplingMethod.build_sampler(args, self)
|
||||
self.data_manager = MultilingualDatasetManager.setup_data_manager(
|
||||
args, self.lang_pairs, langs, dicts, self.sampling_method
|
||||
)
|
||||
|
||||
def check_dicts(self, dicts, source_langs, target_langs):
|
||||
if self.args.source_dict is not None or self.args.target_dict is not None:
|
||||
# no need to check whether the source side and target side are sharing dictionaries
|
||||
return
|
||||
src_dict = dicts[source_langs[0]]
|
||||
tgt_dict = dicts[target_langs[0]]
|
||||
for src_lang in source_langs:
|
||||
assert (
|
||||
src_dict == dicts[src_lang]
|
||||
), "Diffrent dictionary are specified for different source languages; "
|
||||
"TranslationMultiSimpleEpochTask only supports one shared dictionary across all source languages"
|
||||
for tgt_lang in target_langs:
|
||||
assert (
|
||||
tgt_dict == dicts[tgt_lang]
|
||||
), "Diffrent dictionary are specified for different target languages; "
|
||||
"TranslationMultiSimpleEpochTask only supports one shared dictionary across all target languages"
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
langs, dicts, training = MultilingualDatasetManager.prepare(
|
||||
cls.load_dictionary, args, **kwargs
|
||||
)
|
||||
return cls(args, langs, dicts, training)
|
||||
|
||||
def has_sharded_data(self, split):
|
||||
return self.data_manager.has_sharded_data(split)
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
if split in self.datasets:
|
||||
dataset = self.datasets[split]
|
||||
if self.has_sharded_data(split):
|
||||
if self.args.virtual_epoch_size is not None:
|
||||
if dataset.load_next_shard:
|
||||
shard_epoch = dataset.shard_epoch
|
||||
else:
|
||||
# no need to load next shard so skip loading
|
||||
# also this avoid always loading from beginning of the data
|
||||
return
|
||||
else:
|
||||
shard_epoch = epoch
|
||||
else:
|
||||
# estimate the shard epoch from virtual data size and virtual epoch size
|
||||
shard_epoch = self.data_manager.estimate_global_pass_epoch(epoch)
|
||||
logger.info(f"loading data for {split} epoch={epoch}/{shard_epoch}")
|
||||
logger.info(f"mem usage: {data_utils.get_mem_usage()}")
|
||||
if split in self.datasets:
|
||||
del self.datasets[split]
|
||||
logger.info("old dataset deleted manually")
|
||||
logger.info(f"mem usage: {data_utils.get_mem_usage()}")
|
||||
self.datasets[split] = self.data_manager.load_dataset(
|
||||
split,
|
||||
self.training,
|
||||
epoch=epoch,
|
||||
combine=combine,
|
||||
shard_epoch=shard_epoch,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def build_dataset_for_inference(self, src_tokens, src_lengths, constraints=None):
|
||||
if constraints is not None:
|
||||
raise NotImplementedError(
|
||||
"Constrained decoding with the multilingual_translation task is not supported"
|
||||
)
|
||||
|
||||
src_data = ListDataset(src_tokens, src_lengths)
|
||||
dataset = LanguagePairDataset(src_data, src_lengths, self.source_dictionary)
|
||||
src_langtok_spec, tgt_langtok_spec = self.args.langtoks["main"]
|
||||
if self.args.lang_tok_replacing_bos_eos:
|
||||
dataset = self.data_manager.alter_dataset_langtok(
|
||||
dataset,
|
||||
src_eos=self.source_dictionary.eos(),
|
||||
src_lang=self.args.source_lang,
|
||||
tgt_eos=self.target_dictionary.eos(),
|
||||
tgt_lang=self.args.target_lang,
|
||||
src_langtok_spec=src_langtok_spec,
|
||||
tgt_langtok_spec=tgt_langtok_spec,
|
||||
)
|
||||
else:
|
||||
dataset.src = self.data_manager.src_dataset_tranform_func(
|
||||
self.args.source_lang,
|
||||
self.args.target_lang,
|
||||
dataset=dataset.src,
|
||||
spec=src_langtok_spec,
|
||||
)
|
||||
return dataset
|
||||
|
||||
def build_generator(
|
||||
self,
|
||||
models,
|
||||
args,
|
||||
seq_gen_cls=None,
|
||||
extra_gen_cls_kwargs=None,
|
||||
):
|
||||
if not getattr(args, "keep_inference_langtok", False):
|
||||
_, tgt_langtok_spec = self.args.langtoks["main"]
|
||||
if tgt_langtok_spec:
|
||||
tgt_lang_tok = self.data_manager.get_decoder_langtok(
|
||||
self.args.target_lang, tgt_langtok_spec
|
||||
)
|
||||
extra_gen_cls_kwargs = extra_gen_cls_kwargs or {}
|
||||
extra_gen_cls_kwargs["symbols_to_strip_from_output"] = {tgt_lang_tok}
|
||||
|
||||
return super().build_generator(
|
||||
models, args, seq_gen_cls=None, extra_gen_cls_kwargs=extra_gen_cls_kwargs
|
||||
)
|
||||
|
||||
def build_model(self, args):
|
||||
return super().build_model(args)
|
||||
|
||||
def valid_step(self, sample, model, criterion):
|
||||
loss, sample_size, logging_output = super().valid_step(sample, model, criterion)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
with torch.no_grad():
|
||||
_, tgt_langtok_spec = self.args.langtoks["main"]
|
||||
if not self.args.lang_tok_replacing_bos_eos:
|
||||
if prefix_tokens is None and tgt_langtok_spec:
|
||||
tgt_lang_tok = self.data_manager.get_decoder_langtok(
|
||||
self.args.target_lang, tgt_langtok_spec
|
||||
)
|
||||
src_tokens = sample["net_input"]["src_tokens"]
|
||||
bsz = src_tokens.size(0)
|
||||
prefix_tokens = (
|
||||
torch.LongTensor([[tgt_lang_tok]]).expand(bsz, 1).to(src_tokens)
|
||||
)
|
||||
return generator.generate(
|
||||
models,
|
||||
sample,
|
||||
prefix_tokens=prefix_tokens,
|
||||
constraints=constraints,
|
||||
)
|
||||
else:
|
||||
return generator.generate(
|
||||
models,
|
||||
sample,
|
||||
prefix_tokens=prefix_tokens,
|
||||
bos_token=self.data_manager.get_decoder_langtok(
|
||||
self.args.target_lang, tgt_langtok_spec
|
||||
)
|
||||
if tgt_langtok_spec
|
||||
else self.target_dictionary.eos(),
|
||||
)
|
||||
|
||||
def reduce_metrics(self, logging_outputs, criterion):
|
||||
super().reduce_metrics(logging_outputs, criterion)
|
||||
|
||||
def max_positions(self):
|
||||
"""Return the max sentence length allowed by the task."""
|
||||
return (self.args.max_source_positions, self.args.max_target_positions)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.data_manager.get_source_dictionary(self.source_langs[0])
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.data_manager.get_target_dictionary(self.target_langs[0])
|
||||
|
||||
def create_batch_sampler_func(
|
||||
self,
|
||||
max_positions,
|
||||
ignore_invalid_inputs,
|
||||
max_tokens,
|
||||
max_sentences,
|
||||
required_batch_size_multiple=1,
|
||||
seed=1,
|
||||
):
|
||||
def construct_batch_sampler(dataset, epoch):
|
||||
splits = [
|
||||
s for s, _ in self.datasets.items() if self.datasets[s] == dataset
|
||||
]
|
||||
split = splits[0] if len(splits) > 0 else None
|
||||
# NEW implementation
|
||||
if epoch is not None:
|
||||
# initialize the dataset with the correct starting epoch
|
||||
dataset.set_epoch(epoch)
|
||||
|
||||
# get indices ordered by example size
|
||||
start_time = time.time()
|
||||
logger.info(f"start batch sampler: mem usage: {data_utils.get_mem_usage()}")
|
||||
|
||||
with data_utils.numpy_seed(seed):
|
||||
indices = dataset.ordered_indices()
|
||||
logger.info(
|
||||
f"[{split}] @batch_sampler order indices time: {get_time_gap(start_time, time.time())}"
|
||||
)
|
||||
logger.info(f"mem usage: {data_utils.get_mem_usage()}")
|
||||
|
||||
# filter examples that are too large
|
||||
if max_positions is not None:
|
||||
my_time = time.time()
|
||||
indices = self.filter_indices_by_size(
|
||||
indices, dataset, max_positions, ignore_invalid_inputs
|
||||
)
|
||||
logger.info(
|
||||
f"[{split}] @batch_sampler filter_by_size time: {get_time_gap(my_time, time.time())}"
|
||||
)
|
||||
logger.info(f"mem usage: {data_utils.get_mem_usage()}")
|
||||
|
||||
# create mini-batches with given size constraints
|
||||
my_time = time.time()
|
||||
batch_sampler = dataset.batch_by_size(
|
||||
indices,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=max_sentences,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[{split}] @batch_sampler batch_by_size time: {get_time_gap(my_time, time.time())}"
|
||||
)
|
||||
logger.info(
|
||||
f"[{split}] per epoch batch_sampler set-up time: {get_time_gap(start_time, time.time())}"
|
||||
)
|
||||
logger.info(f"mem usage: {data_utils.get_mem_usage()}")
|
||||
|
||||
return batch_sampler
|
||||
|
||||
return construct_batch_sampler
|
||||
|
||||
# we need to override get_batch_iterator because we want to reset the epoch iterator each time
|
||||
def get_batch_iterator(
|
||||
self,
|
||||
dataset,
|
||||
max_tokens=None,
|
||||
max_sentences=None,
|
||||
max_positions=None,
|
||||
ignore_invalid_inputs=False,
|
||||
required_batch_size_multiple=1,
|
||||
seed=1,
|
||||
num_shards=1,
|
||||
shard_id=0,
|
||||
num_workers=0,
|
||||
epoch=1,
|
||||
data_buffer_size=0,
|
||||
disable_iterator_cache=False,
|
||||
):
|
||||
"""
|
||||
Get an iterator that yields batches of data from the given dataset.
|
||||
|
||||
Args:
|
||||
dataset (~fairseq.data.FairseqDataset): dataset to batch
|
||||
max_tokens (int, optional): max number of tokens in each batch
|
||||
(default: None).
|
||||
max_sentences (int, optional): max number of sentences in each
|
||||
batch (default: None).
|
||||
max_positions (optional): max sentence length supported by the
|
||||
model (default: None).
|
||||
ignore_invalid_inputs (bool, optional): don't raise Exception for
|
||||
sentences that are too long (default: False).
|
||||
required_batch_size_multiple (int, optional): require batch size to
|
||||
be a multiple of N (default: 1).
|
||||
seed (int, optional): seed for random number generator for
|
||||
reproducibility (default: 1).
|
||||
num_shards (int, optional): shard the data iterator into N
|
||||
shards (default: 1).
|
||||
shard_id (int, optional): which shard of the data iterator to
|
||||
return (default: 0).
|
||||
num_workers (int, optional): how many subprocesses to use for data
|
||||
loading. 0 means the data will be loaded in the main process
|
||||
(default: 0).
|
||||
epoch (int, optional): the epoch to start the iterator from
|
||||
(default: 0).
|
||||
data_buffer_size (int, optional): number of batches to
|
||||
preload (default: 0).
|
||||
disable_iterator_cache (bool, optional): don't cache the
|
||||
EpochBatchIterator (ignores `FairseqTask::can_reuse_epoch_itr`)
|
||||
(default: False).
|
||||
Returns:
|
||||
~fairseq.iterators.EpochBatchIterator: a batched iterator over the
|
||||
given dataset split
|
||||
"""
|
||||
# initialize the dataset with the correct starting epoch
|
||||
assert isinstance(dataset, FairseqDataset)
|
||||
if dataset in self.dataset_to_epoch_iter:
|
||||
return self.dataset_to_epoch_iter[dataset]
|
||||
if self.args.sampling_method == "RoundRobin":
|
||||
batch_iter = super().get_batch_iterator(
|
||||
dataset,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=max_sentences,
|
||||
max_positions=max_positions,
|
||||
ignore_invalid_inputs=ignore_invalid_inputs,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
seed=seed,
|
||||
num_shards=num_shards,
|
||||
shard_id=shard_id,
|
||||
num_workers=num_workers,
|
||||
epoch=epoch,
|
||||
data_buffer_size=data_buffer_size,
|
||||
disable_iterator_cache=disable_iterator_cache,
|
||||
)
|
||||
self.dataset_to_epoch_iter[dataset] = batch_iter
|
||||
return batch_iter
|
||||
|
||||
construct_batch_sampler = self.create_batch_sampler_func(
|
||||
max_positions,
|
||||
ignore_invalid_inputs,
|
||||
max_tokens,
|
||||
max_sentences,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
seed=seed,
|
||||
)
|
||||
|
||||
epoch_iter = iterators.EpochBatchIterator(
|
||||
dataset=dataset,
|
||||
collate_fn=dataset.collater,
|
||||
batch_sampler=construct_batch_sampler,
|
||||
seed=seed,
|
||||
num_shards=num_shards,
|
||||
shard_id=shard_id,
|
||||
num_workers=num_workers,
|
||||
epoch=epoch,
|
||||
)
|
||||
return epoch_iter
|
||||
Reference in New Issue
Block a user