chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from .criterions import *
|
||||
from .models import *
|
||||
from .tasks import *
|
||||
|
||||
print("GAD plugins loaded...")
|
||||
@@ -0,0 +1 @@
|
||||
from .glat_loss import *
|
||||
@@ -0,0 +1,203 @@
|
||||
# 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 math
|
||||
from math import log
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from torch import Tensor
|
||||
import numpy as np
|
||||
|
||||
|
||||
@register_criterion("glat_loss")
|
||||
class LabelSmoothedDualImitationCriterion(FairseqCriterion):
|
||||
def __init__(self, task, label_smoothing):
|
||||
super().__init__(task)
|
||||
self.label_smoothing = label_smoothing
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"--label-smoothing",
|
||||
default=0.0,
|
||||
type=float,
|
||||
metavar="D",
|
||||
help="epsilon for label smoothing, 0 means no label smoothing",
|
||||
)
|
||||
parser.add_argument('--mse-lambda', default=10, type=float, metavar='D')
|
||||
|
||||
def _compute_loss(
|
||||
self, outputs, targets, masks=None, label_smoothing=0.0, name="loss", factor=1.0
|
||||
):
|
||||
"""
|
||||
outputs: batch x len x d_model
|
||||
targets: batch x len
|
||||
masks: batch x len
|
||||
|
||||
policy_logprob: if there is some policy
|
||||
depends on the likelihood score as rewards.
|
||||
"""
|
||||
|
||||
def mean_ds(x: Tensor, dim=None) -> Tensor:
|
||||
return (
|
||||
x.float().mean().type_as(x)
|
||||
if dim is None
|
||||
else x.float().mean(dim).type_as(x)
|
||||
)
|
||||
|
||||
if masks is not None:
|
||||
outputs, targets = outputs[masks], targets[masks]
|
||||
|
||||
if masks is not None and not masks.any():
|
||||
nll_loss = torch.tensor(0)
|
||||
loss = nll_loss
|
||||
else:
|
||||
logits = F.log_softmax(outputs, dim=-1)
|
||||
if targets.dim() == 1:
|
||||
losses = F.nll_loss(logits, targets.to(logits.device), reduction="none")
|
||||
|
||||
else: # soft-labels
|
||||
losses = F.kl_div(logits, targets.to(logits.device), reduction="none")
|
||||
losses = losses.sum(-1)
|
||||
|
||||
nll_loss = mean_ds(losses)
|
||||
if label_smoothing > 0:
|
||||
loss = (
|
||||
nll_loss * (1 - label_smoothing) - mean_ds(logits) * label_smoothing
|
||||
)
|
||||
else:
|
||||
loss = nll_loss
|
||||
|
||||
loss = loss * factor
|
||||
return {"name": name, "loss": loss, "nll_loss": nll_loss, "factor": factor}
|
||||
|
||||
def _custom_loss(self, loss, name="loss", factor=1.0):
|
||||
return {"name": name, "loss": loss, "factor": factor}
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
nsentences, ntokens = sample["nsentences"], sample["ntokens"]
|
||||
|
||||
# B x T
|
||||
src_tokens, src_lengths = (
|
||||
sample["net_input"]["src_tokens"],
|
||||
sample["net_input"]["src_lengths"],
|
||||
)
|
||||
tgt_tokens, prev_output_tokens = sample["target"], sample["prev_target"]
|
||||
if 'glat' in sample:
|
||||
glat = sample['glat']
|
||||
else:
|
||||
glat = None
|
||||
|
||||
outputs = model(src_tokens, src_lengths, prev_output_tokens, tgt_tokens, glat)
|
||||
losses, nll_loss = [], []
|
||||
|
||||
for obj in outputs:
|
||||
if obj.startswith('glat'):
|
||||
continue
|
||||
if outputs[obj].get("loss", None) is None:
|
||||
_losses = self._compute_loss(
|
||||
outputs[obj].get("out"),
|
||||
outputs[obj].get("tgt"),
|
||||
outputs[obj].get("mask", None),
|
||||
outputs[obj].get("ls", 0.0),
|
||||
name=obj + "-loss",
|
||||
factor=outputs[obj].get("factor", 1.0),
|
||||
)
|
||||
else:
|
||||
_losses = self._custom_loss(
|
||||
outputs[obj].get("loss"),
|
||||
name=obj + "-loss",
|
||||
factor=outputs[obj].get("factor", 1.0),
|
||||
)
|
||||
|
||||
losses += [_losses]
|
||||
if outputs[obj].get("nll_loss", False):
|
||||
nll_loss += [_losses.get("nll_loss", 0.0)]
|
||||
|
||||
loss = sum(l["loss"] for l in losses)
|
||||
nll_loss = sum(l for l in nll_loss) if len(nll_loss) > 0 else loss.new_tensor(0)
|
||||
|
||||
# NOTE:
|
||||
# we don't need to use sample_size as denominator for the gradient
|
||||
# here sample_size is just used for logging
|
||||
sample_size = 1
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"nll_loss": nll_loss.data,
|
||||
"ntokens": ntokens,
|
||||
"nsentences": nsentences,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
if "glat_accu" in outputs:
|
||||
logging_output["glat_accu"] = outputs['glat_accu']
|
||||
if "glat_context_p" in outputs:
|
||||
logging_output['glat_context_p'] = outputs['glat_context_p']
|
||||
|
||||
for l in losses:
|
||||
logging_output[l["name"]] = (
|
||||
utils.item(l["loss"].data / l["factor"])
|
||||
if reduce
|
||||
else l[["loss"]].data / l["factor"]
|
||||
)
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
loss = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
nll_loss = utils.item(sum(log.get("nll_loss", 0) for log in logging_outputs))
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"nll_loss", nll_loss / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["loss"].avg)
|
||||
)
|
||||
|
||||
log_metric("glat_accu", logging_outputs)
|
||||
log_metric("glat_context_p", logging_outputs)
|
||||
|
||||
for key in logging_outputs[0]:
|
||||
if key[-5:] == "-loss":
|
||||
val = sum(log.get(key, 0) for log in logging_outputs)
|
||||
metrics.log_scalar(
|
||||
key[:-5],
|
||||
val / sample_size / math.log(2) if sample_size > 0 else 0.0,
|
||||
sample_size,
|
||||
round=3,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def log_metric(key, logging_outputs):
|
||||
if len(logging_outputs) > 0 and key in logging_outputs[0]:
|
||||
metrics.log_scalar(
|
||||
key, utils.item(np.mean([log.get(key, 0) for log in logging_outputs])), priority=10, round=3
|
||||
)
|
||||
@@ -0,0 +1,192 @@
|
||||
# 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.iterative_refinement_generator import DecoderOut
|
||||
from fairseq.models import register_model, register_model_architecture
|
||||
from fairseq.models.nat import FairseqNATModel
|
||||
from fairseq.modules.transformer_sentence_encoder import init_bert_params
|
||||
import torch
|
||||
from fairseq.models.nat.nonautoregressive_transformer import NATransformerEncoder, NATransformerDecoder, NATransformerModel
|
||||
import logging
|
||||
import random
|
||||
from contextlib import contextmanager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def torch_seed(seed):
|
||||
state = torch.random.get_rng_state()
|
||||
state_cuda = torch.cuda.random.get_rng_state()
|
||||
torch.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
torch.random.set_rng_state(state)
|
||||
torch.cuda.random.set_rng_state(state_cuda)
|
||||
|
||||
|
||||
@register_model("block")
|
||||
class BlockNAT(FairseqNATModel):
|
||||
forward_decoder = NATransformerModel.forward_decoder
|
||||
initialize_output_tokens = NATransformerModel.initialize_output_tokens
|
||||
|
||||
def __init__(self, args, encoder, decoder):
|
||||
super().__init__(args, encoder, decoder)
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
FairseqNATModel.add_args(parser)
|
||||
|
||||
parser.add_argument(
|
||||
"--src-embedding-copy",
|
||||
action="store_true",
|
||||
help="copy encoder word embeddings as the initial input of the decoder",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_encoder(cls, args, tgt_dict, embed_tokens):
|
||||
encoder = NATransformerEncoder(args, tgt_dict, embed_tokens)
|
||||
if getattr(args, "apply_bert_init", False):
|
||||
encoder.apply(init_bert_params)
|
||||
return encoder
|
||||
|
||||
@classmethod
|
||||
def build_decoder(cls, args, tgt_dict, embed_tokens):
|
||||
decoder = NATransformerDecoder(args, tgt_dict, embed_tokens)
|
||||
if getattr(args, "apply_bert_init", False):
|
||||
decoder.apply(init_bert_params)
|
||||
return decoder
|
||||
|
||||
def forward(
|
||||
self, src_tokens, src_lengths, prev_output_tokens, tgt_tokens, glat=None, **kwargs
|
||||
):
|
||||
# encoding
|
||||
encoder_out = self.encoder(src_tokens, src_lengths=src_lengths, **kwargs)
|
||||
|
||||
nonpad_positions = tgt_tokens.ne(self.pad)
|
||||
mask_positions = prev_output_tokens.eq(self.unk) & nonpad_positions
|
||||
mask_lens = (mask_positions).sum(1)
|
||||
l2r_positions = prev_output_tokens.ne(self.unk) & prev_output_tokens.ne(self.pad)
|
||||
l2r_lens = (l2r_positions).sum(1)
|
||||
rand_seed = random.randint(0, 19260817)
|
||||
glat_info = None
|
||||
if glat and tgt_tokens is not None:
|
||||
with torch.no_grad():
|
||||
with torch_seed(rand_seed):
|
||||
word_ins_out = self.decoder(
|
||||
normalize=False,
|
||||
prev_output_tokens=prev_output_tokens,
|
||||
encoder_out=encoder_out,
|
||||
)
|
||||
pred_tokens = word_ins_out.argmax(-1)
|
||||
same_num = ((pred_tokens == tgt_tokens) & mask_positions).sum(1)
|
||||
input_mask = torch.ones_like(nonpad_positions)
|
||||
bsz, seq_len = tgt_tokens.size()
|
||||
for li in range(bsz):
|
||||
target_num = (((mask_lens[li] - same_num[li].sum()).float()) * glat['context_p']).long()
|
||||
if target_num > 0:
|
||||
input_mask[li].scatter_(dim=0, index=(torch.randperm(mask_lens[li])[:target_num].cuda() +
|
||||
l2r_lens[li]).cuda(), value=0)
|
||||
input_mask = input_mask.eq(1)
|
||||
tgt_mask = input_mask.masked_fill(~mask_positions, False)
|
||||
glat_prev_output_tokens = prev_output_tokens.masked_fill(~input_mask, 0) + tgt_tokens.masked_fill(
|
||||
input_mask, 0)
|
||||
glat_tgt_tokens = tgt_tokens.masked_fill(~tgt_mask, self.pad)
|
||||
|
||||
prev_output_tokens, tgt_tokens = glat_prev_output_tokens, glat_tgt_tokens
|
||||
glat_info = {
|
||||
"glat_accu": (same_num.sum() / mask_lens.sum()).item(),
|
||||
"glat_context_p": glat['context_p'],
|
||||
}
|
||||
with torch_seed(rand_seed):
|
||||
word_ins_out = self.decoder(
|
||||
normalize=False,
|
||||
prev_output_tokens=prev_output_tokens,
|
||||
encoder_out=encoder_out,
|
||||
)
|
||||
|
||||
ret = {
|
||||
"word_ins": {
|
||||
"out": word_ins_out,
|
||||
"tgt": tgt_tokens,
|
||||
"mask": tgt_tokens.ne(self.pad),
|
||||
"ls": self.args.label_smoothing,
|
||||
"nll_loss": True,
|
||||
}
|
||||
}
|
||||
if glat_info is not None:
|
||||
ret.update(glat_info)
|
||||
return ret
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"block", "block_6e6d512"
|
||||
)
|
||||
def base_architecture(args):
|
||||
args.encoder_embed_path = getattr(args, "encoder_embed_path", None)
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 512)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", 2048)
|
||||
args.encoder_layers = getattr(args, "encoder_layers", 6)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", 8)
|
||||
args.encoder_normalize_before = getattr(args, "encoder_normalize_before", False)
|
||||
args.encoder_learned_pos = getattr(args, "encoder_learned_pos", False)
|
||||
args.decoder_embed_path = getattr(args, "decoder_embed_path", None)
|
||||
args.decoder_embed_dim = getattr(args, "decoder_embed_dim", args.encoder_embed_dim)
|
||||
args.decoder_ffn_embed_dim = getattr(
|
||||
args, "decoder_ffn_embed_dim", args.encoder_ffn_embed_dim
|
||||
)
|
||||
args.decoder_layers = getattr(args, "decoder_layers", 6)
|
||||
args.decoder_attention_heads = getattr(args, "decoder_attention_heads", 8)
|
||||
args.decoder_normalize_before = getattr(args, "decoder_normalize_before", False)
|
||||
args.decoder_learned_pos = getattr(args, "decoder_learned_pos", False)
|
||||
args.attention_dropout = getattr(args, "attention_dropout", 0.0)
|
||||
args.activation_dropout = getattr(args, "activation_dropout", 0.0)
|
||||
args.activation_fn = getattr(args, "activation_fn", "relu")
|
||||
args.dropout = getattr(args, "dropout", 0.1)
|
||||
args.adaptive_softmax_cutoff = getattr(args, "adaptive_softmax_cutoff", None)
|
||||
args.adaptive_softmax_dropout = getattr(args, "adaptive_softmax_dropout", 0)
|
||||
args.share_decoder_input_output_embed = getattr(
|
||||
args, "share_decoder_input_output_embed", False
|
||||
)
|
||||
args.share_all_embeddings = getattr(args, "share_all_embeddings", False)
|
||||
args.no_token_positional_embeddings = getattr(
|
||||
args, "no_token_positional_embeddings", False
|
||||
)
|
||||
args.adaptive_input = getattr(args, "adaptive_input", False)
|
||||
args.apply_bert_init = getattr(args, "apply_bert_init", False)
|
||||
|
||||
args.decoder_output_dim = getattr(
|
||||
args, "decoder_output_dim", args.decoder_embed_dim
|
||||
)
|
||||
args.decoder_input_dim = getattr(args, "decoder_input_dim", args.decoder_embed_dim)
|
||||
|
||||
# --- special arguments ---
|
||||
args.src_embedding_copy = getattr(args, "src_embedding_copy", False)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"block", "block"
|
||||
)
|
||||
def block_architecture(args):
|
||||
args.encoder_layers = getattr(args, "encoder_layers", 6)
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 512)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", args.encoder_embed_dim*4)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", args.encoder_embed_dim//64)
|
||||
|
||||
args.decoder_layers = getattr(args, "decoder_layers", 6)
|
||||
args.decoder_embed_dim = getattr(args, "decoder_embed_dim", 512)
|
||||
args.decoder_ffn_embed_dim = getattr(args, "decoder_ffn_embed_dim", args.decoder_embed_dim*4)
|
||||
args.decoder_attention_heads = getattr(args, "decoder_attention_heads", args.decoder_embed_dim//64)
|
||||
base_architecture(args)
|
||||
|
||||
@register_model_architecture(
|
||||
"block", "block_base"
|
||||
)
|
||||
def base_architecture2(args):
|
||||
base_architecture(args)
|
||||
@@ -0,0 +1 @@
|
||||
from .BlockNAT import *
|
||||
@@ -0,0 +1 @@
|
||||
from .translation_lev_modified import *
|
||||
@@ -0,0 +1,289 @@
|
||||
# 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
|
||||
from math import log
|
||||
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
|
||||
import logging
|
||||
from omegaconf import II
|
||||
import numpy as np
|
||||
|
||||
NOISE_CHOICES = ChoiceEnum(["random_delete", "random_mask", "no_noise", "full_mask", "block_mask"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationLevenshteinConfig(TranslationConfig):
|
||||
noise: NOISE_CHOICES = field(
|
||||
default="random_delete",
|
||||
metadata={
|
||||
"help": "type of noise"
|
||||
},
|
||||
)
|
||||
start_p: float = field(
|
||||
default=0.5, metadata={"help": "minus prob"}
|
||||
)
|
||||
minus_p: float = field(
|
||||
default=0.2, metadata={"help": "minus prob"}
|
||||
)
|
||||
total_up: int = field(
|
||||
default=300000, metadata={"help": "total updates"}
|
||||
)
|
||||
block_size: int = field(
|
||||
default=5, metadata={"help": "block size"}
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("translation_lev_modified", dataclass=TranslationLevenshteinConfig)
|
||||
class TranslationLevenshteinModifiedTask(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,
|
||||
truncate_source=self.cfg.truncate_source,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def _block_mask(target_tokens):
|
||||
block_size = self.cfg.block_size
|
||||
pad = self.tgt_dict.pad()
|
||||
unk = self.tgt_dict.unk()
|
||||
target_masks = target_tokens.ne(pad)
|
||||
target_length = target_masks.sum(1).float()
|
||||
cutoff_length = target_length * target_length.clone().uniform_()
|
||||
cutoff_length = cutoff_length.int() + 1 # make sure to mask at least one token.
|
||||
prev_target_tokens = torch.ones((target_tokens.size(0),
|
||||
target_tokens.size(1) + block_size)).to(target_tokens)
|
||||
padded_target_tokens = torch.ones((target_tokens.size(0),
|
||||
target_tokens.size(1) + block_size)).to(target_tokens)
|
||||
for i in range(target_tokens.size(0)):
|
||||
remain_length = target_length[i].int() - cutoff_length[i]
|
||||
prev_target_tokens[i][:remain_length] = target_tokens[i][:remain_length]
|
||||
prev_target_tokens[i][remain_length:block_size + remain_length] = unk
|
||||
padded_target_tokens[i][:target_tokens.size(1)] = target_tokens[i]
|
||||
prev_target_tokens = prev_target_tokens[
|
||||
:, : prev_target_tokens.ne(pad).sum(1).max()
|
||||
]
|
||||
padded_target_tokens = padded_target_tokens[
|
||||
:, : prev_target_tokens.ne(pad).sum(1).max()
|
||||
]
|
||||
return prev_target_tokens, padded_target_tokens
|
||||
|
||||
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 == "block_mask":
|
||||
return _block_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=False
|
||||
)
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
model.train()
|
||||
train_ratio = max(0, min(1, update_num / self.cfg.total_up))
|
||||
sample["glat"] = {"context_p": self.cfg.start_p - self.cfg.minus_p * train_ratio}
|
||||
sample["prev_target"], sample["target"] = self.inject_noise(sample["target"])
|
||||
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():
|
||||
sample["prev_target"], sample["target"] = self.inject_noise(sample["target"])
|
||||
loss, sample_size, logging_output = criterion(model, sample)
|
||||
EVAL_BLEU_ORDER = 4
|
||||
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 _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])
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
#!/usr/bin/env python3 -u
|
||||
# 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.
|
||||
|
||||
"""
|
||||
|
||||
mkdir data
|
||||
|
||||
# WMT16 EN-RO
|
||||
cd data
|
||||
mkdir wmt16.en-ro
|
||||
cd wmt16.en-ro
|
||||
gdown https://drive.google.com/uc?id=1YrAwCEuktG-iDVxtEW-FE72uFTLc5QMl
|
||||
tar -zxvf wmt16.tar.gz
|
||||
mv wmt16/en-ro/train/corpus.bpe.en train.en
|
||||
mv wmt16/en-ro/train/corpus.bpe.ro train.ro
|
||||
mv wmt16/en-ro/dev/dev.bpe.en valid.en
|
||||
mv wmt16/en-ro/dev/dev.bpe.ro valid.ro
|
||||
mv wmt16/en-ro/test/test.bpe.en test.en
|
||||
mv wmt16/en-ro/test/test.bpe.ro test.ro
|
||||
rm wmt16.tar.gz
|
||||
rm -r wmt16
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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 os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from .version import __version__ # noqa
|
||||
except ImportError:
|
||||
version_txt = os.path.join(os.path.dirname(__file__), "version.txt")
|
||||
with open(version_txt) as f:
|
||||
__version__ = f.read().strip()
|
||||
|
||||
__all__ = ["pdb"]
|
||||
|
||||
# backwards compatibility to support `from fairseq.X import Y`
|
||||
from fairseq.distributed import utils as distributed_utils
|
||||
from fairseq.logging import meters, metrics, progress_bar # noqa
|
||||
|
||||
sys.modules["fairseq.distributed_utils"] = distributed_utils
|
||||
sys.modules["fairseq.meters"] = meters
|
||||
sys.modules["fairseq.metrics"] = metrics
|
||||
sys.modules["fairseq.progress_bar"] = progress_bar
|
||||
|
||||
# initialize hydra
|
||||
from fairseq.dataclass.initialize import hydra_init
|
||||
hydra_init()
|
||||
|
||||
import fairseq.criterions # noqa
|
||||
import fairseq.distributed # noqa
|
||||
import fairseq.models # noqa
|
||||
import fairseq.modules # noqa
|
||||
import fairseq.optim # noqa
|
||||
import fairseq.optim.lr_scheduler # noqa
|
||||
import fairseq.pdb # noqa
|
||||
import fairseq.scoring # noqa
|
||||
import fairseq.tasks # noqa
|
||||
import fairseq.token_generation_constraints # noqa
|
||||
|
||||
import fairseq.benchmark # noqa
|
||||
import fairseq.model_parallel # noqa
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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 models/tasks to register them
|
||||
from . import dummy_lm, dummy_masked_lm, dummy_model, dummy_mt # noqa
|
||||
@@ -0,0 +1,118 @@
|
||||
# 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
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq.data import Dictionary, FairseqDataset
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.tasks import FairseqTask, register_task
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyLMConfig(FairseqDataclass):
|
||||
dict_size: int = 49996
|
||||
dataset_size: int = 100000
|
||||
tokens_per_sample: int = field(
|
||||
default=512, metadata={"help": "max sequence length"}
|
||||
)
|
||||
add_bos_token: bool = False
|
||||
batch_size: Optional[int] = II("dataset.batch_size")
|
||||
max_tokens: Optional[int] = II("dataset.max_tokens")
|
||||
max_target_positions: int = II("task.tokens_per_sample")
|
||||
|
||||
|
||||
@register_task("dummy_lm", dataclass=DummyLMConfig)
|
||||
class DummyLMTask(FairseqTask):
|
||||
|
||||
def __init__(self, cfg: DummyLMConfig):
|
||||
super().__init__(cfg)
|
||||
|
||||
# load dictionary
|
||||
self.dictionary = Dictionary()
|
||||
for i in range(cfg.dict_size):
|
||||
self.dictionary.add_symbol("word{}".format(i))
|
||||
self.dictionary.pad_to_multiple_(8) # often faster if divisible by 8
|
||||
logger.info("dictionary: {} types".format(len(self.dictionary)))
|
||||
|
||||
seq = torch.arange(cfg.tokens_per_sample + 1) + self.dictionary.pad() + 1
|
||||
|
||||
self.dummy_src = seq[:-1]
|
||||
self.dummy_tgt = seq[1:]
|
||||
|
||||
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 self.cfg.batch_size is not None:
|
||||
bsz = self.cfg.batch_size
|
||||
else:
|
||||
bsz = max(1, self.cfg.max_tokens // self.cfg.tokens_per_sample)
|
||||
self.datasets[split] = DummyDataset(
|
||||
{
|
||||
"id": 1,
|
||||
"net_input": {
|
||||
"src_tokens": torch.stack([self.dummy_src for _ in range(bsz)]),
|
||||
"src_lengths": torch.full(
|
||||
(bsz,), self.cfg.tokens_per_sample, dtype=torch.long
|
||||
),
|
||||
},
|
||||
"target": torch.stack([self.dummy_tgt for _ in range(bsz)]),
|
||||
"nsentences": bsz,
|
||||
"ntokens": bsz * self.cfg.tokens_per_sample,
|
||||
},
|
||||
num_items=self.cfg.dataset_size,
|
||||
item_size=self.cfg.tokens_per_sample,
|
||||
)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
|
||||
class DummyDataset(FairseqDataset):
|
||||
def __init__(self, batch, num_items, item_size):
|
||||
super().__init__()
|
||||
self.batch = batch
|
||||
self.num_items = num_items
|
||||
self.item_size = item_size
|
||||
|
||||
def __getitem__(self, index):
|
||||
return index
|
||||
|
||||
def __len__(self):
|
||||
return self.num_items
|
||||
|
||||
def collater(self, samples):
|
||||
return self.batch
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return np.array([self.item_size] * self.num_items)
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.item_size
|
||||
|
||||
def size(self, index):
|
||||
return self.item_size
|
||||
|
||||
def ordered_indices(self):
|
||||
return np.arange(self.num_items)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return False
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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 numpy as np
|
||||
import torch
|
||||
from fairseq.data import Dictionary, FairseqDataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("dummy_masked_lm")
|
||||
class DummyMaskedLMTask(LegacyFairseqTask):
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument("--dict-size", default=49995, type=int)
|
||||
parser.add_argument("--dataset-size", default=100000, type=int)
|
||||
parser.add_argument(
|
||||
"--tokens-per-sample",
|
||||
default=512,
|
||||
type=int,
|
||||
help="max number of total tokens over all segments "
|
||||
"per sample for BERT dataset",
|
||||
)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
|
||||
# add mask token
|
||||
self.mask_idx = dictionary.add_symbol("<mask>")
|
||||
dictionary.pad_to_multiple_(8) # often faster if divisible by 8
|
||||
|
||||
mask_idx = 0
|
||||
pad_idx = 1
|
||||
seq = torch.arange(args.tokens_per_sample) + pad_idx + 1
|
||||
mask = torch.arange(2, args.tokens_per_sample, 7) # ~15%
|
||||
src = seq.clone()
|
||||
src[mask] = mask_idx
|
||||
tgt = torch.full_like(seq, pad_idx)
|
||||
tgt[mask] = seq[mask]
|
||||
|
||||
self.dummy_src = src
|
||||
self.dummy_tgt = tgt
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task. """
|
||||
dictionary = Dictionary()
|
||||
for i in range(args.dict_size):
|
||||
dictionary.add_symbol("word{}".format(i))
|
||||
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)
|
||||
"""
|
||||
if self.args.batch_size is not None:
|
||||
bsz = self.args.batch_size
|
||||
else:
|
||||
bsz = max(1, self.args.max_tokens // self.args.tokens_per_sample)
|
||||
self.datasets[split] = DummyDataset(
|
||||
{
|
||||
"id": 1,
|
||||
"net_input": {
|
||||
"src_tokens": torch.stack([self.dummy_src for _ in range(bsz)]),
|
||||
"src_lengths": torch.full(
|
||||
(bsz,), self.args.tokens_per_sample, dtype=torch.long
|
||||
),
|
||||
},
|
||||
"target": torch.stack([self.dummy_tgt for _ in range(bsz)]),
|
||||
"nsentences": bsz,
|
||||
"ntokens": bsz * self.args.tokens_per_sample,
|
||||
},
|
||||
num_items=self.args.dataset_size,
|
||||
item_size=self.args.tokens_per_sample,
|
||||
)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
|
||||
class DummyDataset(FairseqDataset):
|
||||
def __init__(self, batch, num_items, item_size):
|
||||
super().__init__()
|
||||
self.batch = batch
|
||||
self.num_items = num_items
|
||||
self.item_size = item_size
|
||||
|
||||
def __getitem__(self, index):
|
||||
return index
|
||||
|
||||
def __len__(self):
|
||||
return self.num_items
|
||||
|
||||
def collater(self, samples):
|
||||
return self.batch
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return np.array([self.item_size] * self.num_items)
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.item_size
|
||||
|
||||
def size(self, index):
|
||||
return self.item_size
|
||||
|
||||
def ordered_indices(self):
|
||||
return np.arange(self.num_items)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return False
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from fairseq.data import Dictionary
|
||||
from fairseq.models import (
|
||||
FairseqDecoder,
|
||||
FairseqLanguageModel,
|
||||
register_model,
|
||||
register_model_architecture,
|
||||
)
|
||||
|
||||
|
||||
@register_model("dummy_model")
|
||||
class DummyModel(FairseqLanguageModel):
|
||||
def __init__(self, args, encoder):
|
||||
super().__init__(encoder)
|
||||
self.args = args
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
parser.add_argument("--num-layers", type=int, default=24)
|
||||
parser.add_argument("--embed-dim", type=int, default=1024)
|
||||
|
||||
@classmethod
|
||||
def build_model(cls, args, task):
|
||||
encoder = DummyEncoder(
|
||||
num_embed=len(task.target_dictionary),
|
||||
embed_dim=args.embed_dim,
|
||||
num_layers=args.num_layers,
|
||||
)
|
||||
return cls(args, encoder)
|
||||
|
||||
def forward(self, src_tokens, masked_tokens=None, **kwargs):
|
||||
return self.decoder(src_tokens, masked_tokens=masked_tokens)
|
||||
|
||||
|
||||
class DummyEncoder(FairseqDecoder):
|
||||
def __init__(self, num_embed=50000, embed_dim=1024, num_layers=24):
|
||||
super().__init__(Dictionary())
|
||||
self.embed = nn.Embedding(
|
||||
num_embeddings=num_embed, embedding_dim=embed_dim, padding_idx=0
|
||||
)
|
||||
self.layers_a = nn.ModuleList(
|
||||
[
|
||||
nn.Sequential(
|
||||
nn.LayerNorm(embed_dim),
|
||||
nn.Linear(embed_dim, 3 * embed_dim), # q, k, v input projection
|
||||
nn.Linear(3 * embed_dim, embed_dim), # skip self-attention
|
||||
nn.Linear(embed_dim, embed_dim), # output projection
|
||||
nn.Dropout(),
|
||||
)
|
||||
for i in range(num_layers)
|
||||
]
|
||||
)
|
||||
self.layers_b = nn.ModuleList(
|
||||
[
|
||||
nn.Sequential(
|
||||
nn.LayerNorm(embed_dim),
|
||||
nn.Linear(embed_dim, 4 * embed_dim), # FFN
|
||||
nn.ReLU(),
|
||||
nn.Linear(4 * embed_dim, embed_dim), # FFN
|
||||
nn.Dropout(0.1),
|
||||
)
|
||||
for i in range(num_layers)
|
||||
]
|
||||
)
|
||||
self.out_proj = nn.Linear(embed_dim, num_embed)
|
||||
|
||||
def forward(self, tokens, masked_tokens=None):
|
||||
x = self.embed(tokens)
|
||||
for layer_a, layer_b in zip(self.layers_a, self.layers_b):
|
||||
x = x + layer_a(x)
|
||||
x = x + layer_b(x)
|
||||
x = self.out_proj(x)
|
||||
if masked_tokens is not None:
|
||||
x = x[masked_tokens]
|
||||
return (x,)
|
||||
|
||||
def max_positions(self):
|
||||
return 1024
|
||||
|
||||
def get_normalized_probs(self, net_output, log_probs, sample=None):
|
||||
logits = net_output[0].float()
|
||||
if log_probs:
|
||||
return F.log_softmax(logits, dim=-1)
|
||||
else:
|
||||
return F.softmax(logits, dim=-1)
|
||||
|
||||
|
||||
@register_model_architecture("dummy_model", "dummy_model")
|
||||
def base_architecture(args):
|
||||
pass
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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 numpy as np
|
||||
import torch
|
||||
from fairseq.data import Dictionary, FairseqDataset
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_task("dummy_mt")
|
||||
class DummyMTTask(LegacyFairseqTask):
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument("--dict-size", default=49996, type=int)
|
||||
parser.add_argument("--dataset-size", default=100000, type=int)
|
||||
parser.add_argument("--src-len", default=30, type=int)
|
||||
parser.add_argument("--tgt-len", default=30, type=int)
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args)
|
||||
self.dictionary = dictionary
|
||||
self.seed = args.seed
|
||||
|
||||
dictionary.pad_to_multiple_(8) # often faster if divisible by 8
|
||||
|
||||
self.dummy_src = torch.arange(args.src_len + 1) + dictionary.pad() + 1
|
||||
self.dummy_tgt = torch.arange(args.tgt_len + 1) + dictionary.pad() + 1
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
"""Setup the task. """
|
||||
dictionary = Dictionary()
|
||||
for i in range(args.dict_size):
|
||||
dictionary.add_symbol("word{}".format(i))
|
||||
logger.info("dictionary: {} types".format(len(dictionary)))
|
||||
|
||||
args.max_source_positions = args.src_len + dictionary.pad() + 2
|
||||
args.max_target_positions = args.tgt_len + dictionary.pad() + 2
|
||||
|
||||
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)
|
||||
"""
|
||||
item_size = max(self.args.src_len, self.args.tgt_len)
|
||||
if self.args.batch_size is not None:
|
||||
bsz = self.args.batch_size
|
||||
else:
|
||||
bsz = max(1, self.args.max_tokens // item_size)
|
||||
tgt = torch.stack([self.dummy_tgt for _ in range(bsz)])
|
||||
self.datasets[split] = DummyDataset(
|
||||
{
|
||||
"id": 1,
|
||||
"net_input": {
|
||||
"src_tokens": torch.stack([self.dummy_src for _ in range(bsz)]),
|
||||
"src_lengths": torch.full(
|
||||
(bsz,), self.args.src_len, dtype=torch.long
|
||||
),
|
||||
"prev_output_tokens": tgt.clone(),
|
||||
},
|
||||
"target": tgt,
|
||||
"nsentences": bsz,
|
||||
"ntokens": bsz * self.args.tgt_len,
|
||||
},
|
||||
num_items=self.args.dataset_size,
|
||||
item_size=item_size,
|
||||
)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
|
||||
class DummyDataset(FairseqDataset):
|
||||
def __init__(self, batch, num_items, item_size):
|
||||
super().__init__()
|
||||
self.batch = batch
|
||||
self.num_items = num_items
|
||||
self.item_size = item_size
|
||||
|
||||
def __getitem__(self, index):
|
||||
return index
|
||||
|
||||
def __len__(self):
|
||||
return self.num_items
|
||||
|
||||
def collater(self, samples):
|
||||
return self.batch
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return np.array([self.item_size] * self.num_items)
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.item_size
|
||||
|
||||
def size(self, index):
|
||||
return self.item_size
|
||||
|
||||
def ordered_indices(self):
|
||||
return np.arange(self.num_items)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return False
|
||||
@@ -0,0 +1,114 @@
|
||||
# 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 os
|
||||
from collections import Counter
|
||||
|
||||
import torch
|
||||
from fairseq.file_io import PathManager
|
||||
from fairseq.tokenizer import tokenize_line
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
def safe_readline(f):
|
||||
pos = f.tell()
|
||||
while True:
|
||||
try:
|
||||
return f.readline()
|
||||
except UnicodeDecodeError:
|
||||
pos -= 1
|
||||
f.seek(pos) # search where this character begins
|
||||
|
||||
|
||||
class Binarizer:
|
||||
@staticmethod
|
||||
def binarize(
|
||||
filename,
|
||||
dict,
|
||||
consumer,
|
||||
tokenize=tokenize_line,
|
||||
append_eos=True,
|
||||
reverse_order=False,
|
||||
offset=0,
|
||||
end=-1,
|
||||
already_numberized=False,
|
||||
) -> Dict[str, int]:
|
||||
nseq, ntok = 0, 0
|
||||
replaced = Counter()
|
||||
|
||||
def replaced_consumer(word, idx):
|
||||
if idx == dict.unk_index and word != dict.unk_word:
|
||||
replaced.update([word])
|
||||
|
||||
with open(PathManager.get_local_path(filename), "r", encoding="utf-8") as f:
|
||||
f.seek(offset)
|
||||
# next(f) breaks f.tell(), hence readline() must be used
|
||||
line = safe_readline(f)
|
||||
while line:
|
||||
# f.tell() does not always give the byte position in the file
|
||||
# sometimes it skips to a very large number
|
||||
# it is unlikely that through a normal read we go from
|
||||
# end bytes to end + 2**32 bytes (4 GB) and this makes it unlikely
|
||||
# that the procedure breaks by the undeterministic behavior of
|
||||
# f.tell()
|
||||
if end > 0 and f.tell() > end and f.tell() < end + 2 ** 32:
|
||||
break
|
||||
if already_numberized:
|
||||
id_strings = line.strip().split()
|
||||
id_list = [int(id_string) for id_string in id_strings]
|
||||
if reverse_order:
|
||||
id_list.reverse()
|
||||
if append_eos:
|
||||
id_list.append(dict.eos())
|
||||
ids = torch.IntTensor(id_list)
|
||||
else:
|
||||
ids = dict.encode_line(
|
||||
line=line,
|
||||
line_tokenizer=tokenize,
|
||||
add_if_not_exist=False,
|
||||
consumer=replaced_consumer,
|
||||
append_eos=append_eos,
|
||||
reverse_order=reverse_order,
|
||||
)
|
||||
nseq += 1
|
||||
ntok += len(ids)
|
||||
consumer(ids)
|
||||
line = f.readline()
|
||||
return {
|
||||
"nseq": nseq,
|
||||
"nunk": sum(replaced.values()),
|
||||
"ntok": ntok,
|
||||
"replaced": replaced,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def binarize_alignments(
|
||||
filename, alignment_parser, consumer, offset=0, end=-1
|
||||
) -> Dict[str, int]:
|
||||
nseq = 0
|
||||
|
||||
with open(PathManager.get_local_path(filename), "r") as f:
|
||||
f.seek(offset)
|
||||
line = safe_readline(f)
|
||||
while line:
|
||||
if end > 0 and f.tell() > end:
|
||||
break
|
||||
ids = alignment_parser(line)
|
||||
nseq += 1
|
||||
consumer(ids)
|
||||
line = f.readline()
|
||||
return {"nseq": nseq}
|
||||
|
||||
@staticmethod
|
||||
def find_offsets(filename, num_chunks) -> List[int]:
|
||||
with open(PathManager.get_local_path(filename), "r", encoding="utf-8") as f:
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
chunk_size = size // num_chunks
|
||||
offsets = [0 for _ in range(num_chunks + 1)]
|
||||
for i in range(1, num_chunks):
|
||||
f.seek(chunk_size * i)
|
||||
safe_readline(f)
|
||||
offsets[i] = f.tell()
|
||||
return offsets
|
||||
@@ -0,0 +1,705 @@
|
||||
# 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 ast
|
||||
import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import torch
|
||||
from fairseq.dataclass.configs import CheckpointConfig, FairseqConfig
|
||||
from fairseq.dataclass.utils import (
|
||||
convert_namespace_to_omegaconf,
|
||||
overwrite_args_by_name,
|
||||
)
|
||||
from fairseq.file_io import PathManager
|
||||
from fairseq.models import FairseqDecoder, FairseqEncoder
|
||||
from omegaconf import DictConfig, open_dict
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def save_checkpoint(cfg: CheckpointConfig, trainer, epoch_itr, val_loss):
|
||||
from fairseq import meters
|
||||
|
||||
# only one worker should attempt to create the required dir
|
||||
if cfg.distributed_rank == 0:
|
||||
os.makedirs(cfg.save_dir, exist_ok=True)
|
||||
|
||||
prev_best = getattr(save_checkpoint, "best", val_loss)
|
||||
if val_loss is not None:
|
||||
best_function = max if cfg.maximize_best_checkpoint_metric else min
|
||||
save_checkpoint.best = best_function(val_loss, prev_best)
|
||||
|
||||
if cfg.no_save:
|
||||
return
|
||||
|
||||
trainer.consolidate_optimizer()
|
||||
|
||||
if not trainer.is_data_parallel_master:
|
||||
return
|
||||
|
||||
write_timer = meters.StopwatchMeter()
|
||||
write_timer.start()
|
||||
|
||||
epoch = epoch_itr.epoch
|
||||
end_of_epoch = epoch_itr.end_of_epoch()
|
||||
updates = trainer.get_num_updates()
|
||||
|
||||
logger.info(f"Preparing to save checkpoint for epoch {epoch} @ {updates} updates")
|
||||
|
||||
def is_better(a, b):
|
||||
return a >= b if cfg.maximize_best_checkpoint_metric else a <= b
|
||||
|
||||
suffix = cfg.checkpoint_suffix or ""
|
||||
checkpoint_conds = collections.OrderedDict()
|
||||
checkpoint_conds["checkpoint{}{}.pt".format(epoch, suffix)] = (
|
||||
end_of_epoch and not cfg.no_epoch_checkpoints and epoch % cfg.save_interval == 0
|
||||
)
|
||||
checkpoint_conds["checkpoint_{}_{}{}.pt".format(epoch, updates, suffix)] = (
|
||||
not end_of_epoch
|
||||
and cfg.save_interval_updates > 0
|
||||
and updates % cfg.save_interval_updates == 0
|
||||
)
|
||||
checkpoint_conds["checkpoint_best{}.pt".format(suffix)] = val_loss is not None and (
|
||||
not hasattr(save_checkpoint, "best")
|
||||
or is_better(val_loss, save_checkpoint.best)
|
||||
)
|
||||
if val_loss is not None and cfg.keep_best_checkpoints > 0:
|
||||
checkpoint_conds[
|
||||
"checkpoint.best_{}_{:.2f}.pt".format(cfg.best_checkpoint_metric, val_loss)
|
||||
] = not hasattr(save_checkpoint, "best") or is_better(
|
||||
val_loss, save_checkpoint.best
|
||||
)
|
||||
checkpoint_conds[
|
||||
"checkpoint_last{}.pt".format(suffix)
|
||||
] = not cfg.no_last_checkpoints
|
||||
|
||||
extra_state = {"train_iterator": epoch_itr.state_dict(), "val_loss": val_loss}
|
||||
if hasattr(save_checkpoint, "best"):
|
||||
extra_state.update({"best": save_checkpoint.best})
|
||||
|
||||
checkpoints = [
|
||||
os.path.join(cfg.save_dir, fn) for fn, cond in checkpoint_conds.items() if cond
|
||||
]
|
||||
if len(checkpoints) > 0:
|
||||
trainer.save_checkpoint(checkpoints[0], extra_state)
|
||||
for cp in checkpoints[1:]:
|
||||
assert PathManager.copy(
|
||||
checkpoints[0], cp, overwrite=True
|
||||
), f"Failed to copy {checkpoints[0]} to {cp}"
|
||||
|
||||
write_timer.stop()
|
||||
logger.info(
|
||||
"Saved checkpoint {} (epoch {} @ {} updates, score {}) (writing took {} seconds)".format(
|
||||
checkpoints[0], epoch, updates, val_loss, write_timer.sum
|
||||
)
|
||||
)
|
||||
|
||||
if not end_of_epoch and cfg.keep_interval_updates > 0:
|
||||
# remove old checkpoints; checkpoints are sorted in descending order
|
||||
checkpoints = checkpoint_paths(
|
||||
cfg.save_dir, pattern=r"checkpoint_\d+_(\d+)\.pt"
|
||||
)
|
||||
for old_chk in checkpoints[cfg.keep_interval_updates :]:
|
||||
if os.path.lexists(old_chk):
|
||||
os.remove(old_chk)
|
||||
|
||||
if cfg.keep_last_epochs > 0:
|
||||
# remove old epoch checkpoints; checkpoints are sorted in descending order
|
||||
checkpoints = checkpoint_paths(cfg.save_dir, pattern=r"checkpoint(\d+)\.pt")
|
||||
for old_chk in checkpoints[cfg.keep_last_epochs :]:
|
||||
if os.path.lexists(old_chk):
|
||||
os.remove(old_chk)
|
||||
|
||||
if cfg.keep_best_checkpoints > 0:
|
||||
# only keep the best N checkpoints according to validation metric
|
||||
checkpoints = checkpoint_paths(
|
||||
cfg.save_dir,
|
||||
pattern=r"checkpoint\.best_{}_(\d+\.?\d*)\.pt".format(
|
||||
cfg.best_checkpoint_metric
|
||||
),
|
||||
)
|
||||
if not cfg.maximize_best_checkpoint_metric:
|
||||
checkpoints = checkpoints[::-1]
|
||||
for old_chk in checkpoints[cfg.keep_best_checkpoints :]:
|
||||
if os.path.lexists(old_chk):
|
||||
os.remove(old_chk)
|
||||
|
||||
|
||||
def load_checkpoint(cfg: CheckpointConfig, trainer, **passthrough_args):
|
||||
"""
|
||||
Load a checkpoint and restore the training iterator.
|
||||
|
||||
*passthrough_args* will be passed through to
|
||||
``trainer.get_train_iterator``.
|
||||
"""
|
||||
|
||||
reset_optimizer = cfg.reset_optimizer
|
||||
reset_lr_scheduler = cfg.reset_lr_scheduler
|
||||
optimizer_overrides = ast.literal_eval(cfg.optimizer_overrides)
|
||||
reset_meters = cfg.reset_meters
|
||||
reset_dataloader = cfg.reset_dataloader
|
||||
|
||||
if cfg.finetune_from_model is not None and (
|
||||
reset_optimizer or reset_lr_scheduler or reset_meters or reset_dataloader
|
||||
):
|
||||
raise ValueError(
|
||||
"--finetune-from-model can not be set together with either --reset-optimizer"
|
||||
" or reset_lr_scheduler or reset_meters or reset_dataloader"
|
||||
)
|
||||
|
||||
suffix = cfg.checkpoint_suffix
|
||||
if (
|
||||
cfg.restore_file == "checkpoint_last.pt"
|
||||
): # default value of restore_file is 'checkpoint_last.pt'
|
||||
checkpoint_path = os.path.join(
|
||||
cfg.save_dir, "checkpoint_last{}.pt".format(suffix)
|
||||
)
|
||||
first_launch = not PathManager.exists(checkpoint_path)
|
||||
if cfg.finetune_from_model is not None and first_launch:
|
||||
# if there is no last checkpoint to restore, start the finetune from pretrained model
|
||||
# else just use usual logic to load checkpoint, e.g. restart from last checkpoint and etc.
|
||||
if PathManager.exists(cfg.finetune_from_model):
|
||||
checkpoint_path = cfg.finetune_from_model
|
||||
reset_optimizer = True
|
||||
reset_lr_scheduler = True
|
||||
reset_meters = True
|
||||
reset_dataloader = True
|
||||
logger.info(
|
||||
f"loading pretrained model from {checkpoint_path}: "
|
||||
"optimizer, lr scheduler, meters, dataloader will be reset"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"--funetune-from-model {cfg.finetune_from_model} does not exist"
|
||||
)
|
||||
elif cfg.model_parallel_size > 1:
|
||||
checkpoint_path = cfg.restore_file.replace(".pt", suffix + ".pt")
|
||||
else:
|
||||
checkpoint_path = cfg.restore_file
|
||||
|
||||
if cfg.restore_file != "checkpoint_last.pt" and cfg.finetune_from_model:
|
||||
raise ValueError(
|
||||
"--finetune-from-model and --restore-file (non-default value) "
|
||||
"can not be specified together: " + str(cfg)
|
||||
)
|
||||
|
||||
extra_state = trainer.load_checkpoint(
|
||||
checkpoint_path,
|
||||
reset_optimizer,
|
||||
reset_lr_scheduler,
|
||||
optimizer_overrides,
|
||||
reset_meters=reset_meters,
|
||||
)
|
||||
|
||||
if (
|
||||
extra_state is not None
|
||||
and "best" in extra_state
|
||||
and not reset_optimizer
|
||||
and not reset_meters
|
||||
):
|
||||
save_checkpoint.best = extra_state["best"]
|
||||
|
||||
if extra_state is not None and not reset_dataloader:
|
||||
# restore iterator from checkpoint
|
||||
itr_state = extra_state["train_iterator"]
|
||||
epoch_itr = trainer.get_train_iterator(
|
||||
epoch=itr_state["epoch"], load_dataset=True, **passthrough_args
|
||||
)
|
||||
epoch_itr.load_state_dict(itr_state)
|
||||
else:
|
||||
epoch_itr = trainer.get_train_iterator(
|
||||
epoch=1, load_dataset=True, **passthrough_args
|
||||
)
|
||||
|
||||
trainer.lr_step(epoch_itr.epoch)
|
||||
|
||||
return extra_state, epoch_itr
|
||||
|
||||
|
||||
def load_checkpoint_to_cpu(path, arg_overrides=None, load_on_all_ranks=False):
|
||||
"""Loads a checkpoint to CPU (with upgrading for backward compatibility).
|
||||
|
||||
If doing single-GPU training or if the checkpoint is only being loaded by at
|
||||
most one process on each node (current default behavior is for only rank 0
|
||||
to read the checkpoint from disk), load_on_all_ranks should be False to
|
||||
avoid errors from torch.distributed not having been initialized or
|
||||
torch.distributed.barrier() hanging.
|
||||
|
||||
If all processes on each node may be loading the checkpoint
|
||||
simultaneously, load_on_all_ranks should be set to True to avoid I/O
|
||||
conflicts.
|
||||
|
||||
There's currently no support for > 1 but < all processes loading the
|
||||
checkpoint on each node.
|
||||
"""
|
||||
local_path = PathManager.get_local_path(path)
|
||||
# The locally cached file returned by get_local_path() may be stale for
|
||||
# remote files that are periodically updated/overwritten (ex:
|
||||
# checkpoint_last.pt) - so we remove the local copy, sync across processes
|
||||
# (if needed), and then download a fresh copy.
|
||||
if local_path != path and PathManager.path_requires_pathmanager(path):
|
||||
try:
|
||||
os.remove(local_path)
|
||||
except FileNotFoundError:
|
||||
# With potentially multiple processes removing the same file, the
|
||||
# file being missing is benign (missing_ok isn't available until
|
||||
# Python 3.8).
|
||||
pass
|
||||
if load_on_all_ranks:
|
||||
torch.distributed.barrier()
|
||||
local_path = PathManager.get_local_path(path)
|
||||
|
||||
with open(local_path, "rb") as f:
|
||||
state = torch.load(f, map_location=torch.device("cpu"))
|
||||
|
||||
if "args" in state and state["args"] is not None and arg_overrides is not None:
|
||||
args = state["args"]
|
||||
for arg_name, arg_val in arg_overrides.items():
|
||||
setattr(args, arg_name, arg_val)
|
||||
|
||||
if "cfg" in state and state["cfg"] is not None and arg_overrides is not None:
|
||||
overwrite_args_by_name(state["cfg"], arg_overrides)
|
||||
|
||||
state = _upgrade_state_dict(state)
|
||||
return state
|
||||
|
||||
|
||||
def load_model_ensemble(
|
||||
filenames,
|
||||
arg_overrides: Optional[Dict[str, Any]] = None,
|
||||
task=None,
|
||||
strict=True,
|
||||
suffix="",
|
||||
num_shards=1,
|
||||
state=None,
|
||||
):
|
||||
"""Loads an ensemble of models.
|
||||
|
||||
Args:
|
||||
filenames (List[str]): checkpoint files to load
|
||||
arg_overrides (Dict[str,Any], optional): override model args that
|
||||
were used during model training
|
||||
task (fairseq.tasks.FairseqTask, optional): task to use for loading
|
||||
"""
|
||||
assert not (
|
||||
strict and num_shards > 1
|
||||
), "Cannot load state dict with strict=True and checkpoint shards > 1"
|
||||
ensemble, args, _task = load_model_ensemble_and_task(
|
||||
filenames,
|
||||
arg_overrides,
|
||||
task,
|
||||
strict,
|
||||
suffix,
|
||||
num_shards,
|
||||
state,
|
||||
)
|
||||
return ensemble, args
|
||||
|
||||
|
||||
def load_model_ensemble_and_task(
|
||||
filenames,
|
||||
arg_overrides: Optional[Dict[str, Any]] = None,
|
||||
task=None,
|
||||
strict=True,
|
||||
suffix="",
|
||||
num_shards=1,
|
||||
state=None,
|
||||
):
|
||||
assert state is None or len(filenames) == 1
|
||||
|
||||
from fairseq import tasks
|
||||
|
||||
assert not (
|
||||
strict and num_shards > 1
|
||||
), "Cannot load state dict with strict=True and checkpoint shards > 1"
|
||||
ensemble = []
|
||||
cfg = None
|
||||
for filename in filenames:
|
||||
orig_filename = filename
|
||||
assert num_shards > 0
|
||||
for shard_idx in range(num_shards):
|
||||
if num_shards == 1:
|
||||
filename = filename.replace(".pt", suffix + ".pt")
|
||||
else:
|
||||
filename = orig_filename[:-3] + f"_part{shard_idx}.pt"
|
||||
|
||||
if not PathManager.exists(filename):
|
||||
raise IOError("Model file not found: {}".format(filename))
|
||||
if state is None:
|
||||
state = load_checkpoint_to_cpu(filename, arg_overrides)
|
||||
if "args" in state and state["args"] is not None:
|
||||
cfg = convert_namespace_to_omegaconf(state["args"])
|
||||
elif "cfg" in state and state["cfg"] is not None:
|
||||
cfg = state["cfg"]
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Neither args nor cfg exist in state keys = {state.keys()}"
|
||||
)
|
||||
|
||||
if task is None:
|
||||
task = tasks.setup_task(cfg.task)
|
||||
|
||||
if "task_state" in state:
|
||||
task.load_state_dict(state["task_state"])
|
||||
|
||||
# build model for ensemble
|
||||
model = task.build_model(cfg.model)
|
||||
|
||||
model.load_state_dict(state["model"], strict=strict, model_cfg=cfg.model)
|
||||
|
||||
# reset state so it gets loaded for the next model in ensemble
|
||||
state = None
|
||||
|
||||
ensemble.append(model)
|
||||
return ensemble, cfg, task
|
||||
|
||||
|
||||
def checkpoint_paths(path, pattern=r"checkpoint(\d+)\.pt"):
|
||||
"""Retrieves all checkpoints found in `path` directory.
|
||||
|
||||
Checkpoints are identified by matching filename to the specified pattern. If
|
||||
the pattern contains groups, the result will be sorted by the first group in
|
||||
descending order.
|
||||
"""
|
||||
pt_regexp = re.compile(pattern)
|
||||
files = os.listdir(path)
|
||||
|
||||
entries = []
|
||||
for i, f in enumerate(files):
|
||||
m = pt_regexp.fullmatch(f)
|
||||
if m is not None:
|
||||
idx = float(m.group(1)) if len(m.groups()) > 0 else i
|
||||
entries.append((idx, m.group(0)))
|
||||
return [os.path.join(path, x[1]) for x in sorted(entries, reverse=True)]
|
||||
|
||||
|
||||
def torch_persistent_save(obj, f):
|
||||
if isinstance(f, str):
|
||||
with PathManager.open(f, "wb") as h:
|
||||
torch_persistent_save(obj, h)
|
||||
return
|
||||
for i in range(3):
|
||||
try:
|
||||
return torch.save(obj, f)
|
||||
except Exception:
|
||||
if i == 2:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
def save_state(
|
||||
filename,
|
||||
cfg: FairseqConfig,
|
||||
model_state_dict,
|
||||
criterion,
|
||||
optimizer,
|
||||
lr_scheduler,
|
||||
num_updates,
|
||||
optim_history=None,
|
||||
extra_state=None,
|
||||
task=None,
|
||||
**kwargs,
|
||||
):
|
||||
from fairseq import utils
|
||||
|
||||
if optim_history is None:
|
||||
optim_history = []
|
||||
if extra_state is None:
|
||||
extra_state = {}
|
||||
state_dict = {
|
||||
"cfg": cfg,
|
||||
"args": kwargs.get("args", None),
|
||||
"model": model_state_dict or {},
|
||||
"optimizer_history": optim_history
|
||||
+ [
|
||||
{
|
||||
"criterion_name": criterion.__class__.__name__,
|
||||
"optimizer_name": optimizer.__class__.__name__,
|
||||
"lr_scheduler_state": lr_scheduler.state_dict(),
|
||||
"num_updates": num_updates,
|
||||
}
|
||||
],
|
||||
"extra_state": extra_state,
|
||||
"task_state": task.state_dict() if task is not None else {}
|
||||
}
|
||||
if utils.has_parameters(criterion):
|
||||
state_dict["criterion"] = criterion.state_dict()
|
||||
|
||||
if cfg is None:
|
||||
cfg = state_dict["args"]
|
||||
assert cfg is not None, "must provide cfg or args"
|
||||
|
||||
if isinstance(cfg, DictConfig):
|
||||
no_save_optimizer_state = cfg.checkpoint.no_save_optimizer_state
|
||||
else:
|
||||
no_save_optimizer_state = cfg.no_save_optimizer_state
|
||||
if not no_save_optimizer_state:
|
||||
state_dict["last_optimizer_state"] = optimizer.state_dict()
|
||||
|
||||
# keep everything on CPU
|
||||
state_dict = utils.move_to_cpu(state_dict)
|
||||
|
||||
if PathManager.supports_rename(filename):
|
||||
# do atomic save
|
||||
with PathManager.open(filename + ".tmp", "wb") as f:
|
||||
torch_persistent_save(state_dict, f)
|
||||
PathManager.rename(filename + ".tmp", filename)
|
||||
else:
|
||||
# fallback to non-atomic save
|
||||
with PathManager.open(filename, "wb") as f:
|
||||
torch_persistent_save(state_dict, f)
|
||||
|
||||
|
||||
def _upgrade_state_dict(state):
|
||||
"""Helper for upgrading old model checkpoints."""
|
||||
from fairseq import models, registry, tasks
|
||||
|
||||
# add optimizer_history
|
||||
if "optimizer_history" not in state:
|
||||
state["optimizer_history"] = [
|
||||
{"criterion_name": "CrossEntropyCriterion", "best_loss": state["best_loss"]}
|
||||
]
|
||||
state["last_optimizer_state"] = state["optimizer"]
|
||||
del state["optimizer"]
|
||||
del state["best_loss"]
|
||||
# move extra_state into sub-dictionary
|
||||
if "epoch" in state and "extra_state" not in state:
|
||||
state["extra_state"] = {
|
||||
"epoch": state["epoch"],
|
||||
"batch_offset": state["batch_offset"],
|
||||
"val_loss": state["val_loss"],
|
||||
}
|
||||
del state["epoch"]
|
||||
del state["batch_offset"]
|
||||
del state["val_loss"]
|
||||
# reduce optimizer history's memory usage (only keep the last state)
|
||||
if "optimizer" in state["optimizer_history"][-1]:
|
||||
state["last_optimizer_state"] = state["optimizer_history"][-1]["optimizer"]
|
||||
for optim_hist in state["optimizer_history"]:
|
||||
del optim_hist["optimizer"]
|
||||
# record the optimizer class name
|
||||
if "optimizer_name" not in state["optimizer_history"][-1]:
|
||||
state["optimizer_history"][-1]["optimizer_name"] = "FairseqNAG"
|
||||
# move best_loss into lr_scheduler_state
|
||||
if "lr_scheduler_state" not in state["optimizer_history"][-1]:
|
||||
state["optimizer_history"][-1]["lr_scheduler_state"] = {
|
||||
"best": state["optimizer_history"][-1]["best_loss"]
|
||||
}
|
||||
del state["optimizer_history"][-1]["best_loss"]
|
||||
# keep track of number of updates
|
||||
if "num_updates" not in state["optimizer_history"][-1]:
|
||||
state["optimizer_history"][-1]["num_updates"] = 0
|
||||
# old model checkpoints may not have separate source/target positions
|
||||
if hasattr(state["args"], "max_positions") and not hasattr(
|
||||
state["args"], "max_source_positions"
|
||||
):
|
||||
state["args"].max_source_positions = state["args"].max_positions
|
||||
state["args"].max_target_positions = state["args"].max_positions
|
||||
# use stateful training data iterator
|
||||
if "train_iterator" not in state["extra_state"]:
|
||||
state["extra_state"]["train_iterator"] = {
|
||||
"epoch": state["extra_state"]["epoch"],
|
||||
"iterations_in_epoch": state["extra_state"].get("batch_offset", 0),
|
||||
}
|
||||
|
||||
# backward compatibility, cfg updates
|
||||
if "args" in state and state["args"] is not None:
|
||||
# default to translation task
|
||||
if not hasattr(state["args"], "task"):
|
||||
state["args"].task = "translation"
|
||||
# --raw-text and --lazy-load are deprecated
|
||||
if getattr(state["args"], "raw_text", False):
|
||||
state["args"].dataset_impl = "raw"
|
||||
elif getattr(state["args"], "lazy_load", False):
|
||||
state["args"].dataset_impl = "lazy"
|
||||
# epochs start at 1
|
||||
if state["extra_state"]["train_iterator"] is not None:
|
||||
state["extra_state"]["train_iterator"]["epoch"] = max(
|
||||
state["extra_state"]["train_iterator"].get("epoch", 1), 1
|
||||
)
|
||||
# --remove-bpe ==> --postprocess
|
||||
if hasattr(state["args"], "remove_bpe"):
|
||||
state["args"].post_process = state["args"].remove_bpe
|
||||
# --min-lr ==> --stop-min-lr
|
||||
if hasattr(state["args"], "min_lr"):
|
||||
state["args"].stop_min_lr = state["args"].min_lr
|
||||
del state["args"].min_lr
|
||||
# binary_cross_entropy => wav2vec criterion
|
||||
if (
|
||||
hasattr(state["args"], "criterion")
|
||||
and state["args"].criterion == "binary_cross_entropy"
|
||||
):
|
||||
state["args"].criterion = "wav2vec"
|
||||
# speech_pretraining => audio pretraining
|
||||
if (
|
||||
hasattr(state["args"], "task")
|
||||
and state["args"].task == "speech_pretraining"
|
||||
):
|
||||
state["args"].task = "audio_pretraining"
|
||||
# audio_cpc => wav2vec
|
||||
if hasattr(state["args"], "arch") and state["args"].arch == "audio_cpc":
|
||||
state["args"].arch = "wav2vec"
|
||||
# convert legacy float learning rate to List[float]
|
||||
if hasattr(state["args"], "lr") and isinstance(state["args"].lr, float):
|
||||
state["args"].lr = [state["args"].lr]
|
||||
# convert task data arg to a string instead of List[string]
|
||||
if hasattr(state["args"], "data") and isinstance(state["args"].data, list) and len(state["args"].data) > 0:
|
||||
state["args"].data = state["args"].data[0]
|
||||
|
||||
state["cfg"] = convert_namespace_to_omegaconf(state["args"])
|
||||
|
||||
if "cfg" in state and state["cfg"] is not None:
|
||||
with open_dict(state["cfg"]):
|
||||
# any upgrades for Hydra-based configs
|
||||
pass
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def prune_state_dict(state_dict, model_cfg: Optional[DictConfig]):
|
||||
"""Prune the given state_dict if desired for LayerDrop
|
||||
(https://arxiv.org/abs/1909.11556).
|
||||
|
||||
Training with LayerDrop allows models to be robust to pruning at inference
|
||||
time. This function prunes state_dict to allow smaller models to be loaded
|
||||
from a larger model and re-maps the existing state_dict for this to occur.
|
||||
|
||||
It's called by functions that load models from checkpoints and does not
|
||||
need to be called directly.
|
||||
"""
|
||||
arch = None
|
||||
if model_cfg is not None:
|
||||
arch = (
|
||||
model_cfg._name
|
||||
if isinstance(model_cfg, DictConfig)
|
||||
else getattr(model_cfg, "arch", None)
|
||||
)
|
||||
|
||||
if not model_cfg or arch is None or arch == "ptt_transformer":
|
||||
# args should not be none, but don't crash if it is.
|
||||
return state_dict
|
||||
|
||||
encoder_layers_to_keep = getattr(model_cfg, "encoder_layers_to_keep", None)
|
||||
decoder_layers_to_keep = getattr(model_cfg, "decoder_layers_to_keep", None)
|
||||
|
||||
if not encoder_layers_to_keep and not decoder_layers_to_keep:
|
||||
return state_dict
|
||||
|
||||
# apply pruning
|
||||
logger.info(
|
||||
"Pruning model to specified layer configuration - this works best if the model was trained with LayerDrop"
|
||||
)
|
||||
|
||||
def create_pruning_pass(layers_to_keep, layer_name):
|
||||
keep_layers = sorted(
|
||||
int(layer_string) for layer_string in layers_to_keep.split(",")
|
||||
)
|
||||
mapping_dict = {}
|
||||
for i in range(len(keep_layers)):
|
||||
mapping_dict[str(keep_layers[i])] = str(i)
|
||||
|
||||
regex = re.compile(r"^{layer}.*\.layers\.(\d+)".format(layer=layer_name))
|
||||
return {"substitution_regex": regex, "mapping_dict": mapping_dict}
|
||||
|
||||
pruning_passes = []
|
||||
if encoder_layers_to_keep:
|
||||
pruning_passes.append(create_pruning_pass(encoder_layers_to_keep, "encoder"))
|
||||
if decoder_layers_to_keep:
|
||||
pruning_passes.append(create_pruning_pass(decoder_layers_to_keep, "decoder"))
|
||||
|
||||
new_state_dict = {}
|
||||
for layer_name in state_dict.keys():
|
||||
match = re.search(r"\.layers\.(\d+)\.", layer_name)
|
||||
# if layer has no number in it, it is a supporting layer, such as an
|
||||
# embedding
|
||||
if not match:
|
||||
new_state_dict[layer_name] = state_dict[layer_name]
|
||||
continue
|
||||
|
||||
# otherwise, layer should be pruned.
|
||||
original_layer_number = match.group(1)
|
||||
# figure out which mapping dict to replace from
|
||||
for pruning_pass in pruning_passes:
|
||||
if original_layer_number in pruning_pass["mapping_dict"] and pruning_pass[
|
||||
"substitution_regex"
|
||||
].search(layer_name):
|
||||
new_layer_number = pruning_pass["mapping_dict"][original_layer_number]
|
||||
substitution_match = pruning_pass["substitution_regex"].search(
|
||||
layer_name
|
||||
)
|
||||
new_state_key = (
|
||||
layer_name[: substitution_match.start(1)]
|
||||
+ new_layer_number
|
||||
+ layer_name[substitution_match.end(1) :]
|
||||
)
|
||||
new_state_dict[new_state_key] = state_dict[layer_name]
|
||||
|
||||
# Since layers are now pruned, *_layers_to_keep are no longer needed.
|
||||
# This is more of "It would make it work fix" rather than a proper fix.
|
||||
if isinstance(model_cfg, DictConfig):
|
||||
context = open_dict(model_cfg)
|
||||
else:
|
||||
context = contextlib.ExitStack()
|
||||
with context:
|
||||
if hasattr(model_cfg, "encoder_layers_to_keep"):
|
||||
model_cfg.encoder_layers_to_keep = None
|
||||
if hasattr(model_cfg, "decoder_layers_to_keep"):
|
||||
model_cfg.decoder_layers_to_keep = None
|
||||
|
||||
return new_state_dict
|
||||
|
||||
|
||||
def load_pretrained_component_from_model(
|
||||
component: Union[FairseqEncoder, FairseqDecoder], checkpoint: str
|
||||
):
|
||||
"""
|
||||
Load a pretrained FairseqEncoder or FairseqDecoder from checkpoint into the
|
||||
provided `component` object. If state_dict fails to load, there may be a
|
||||
mismatch in the architecture of the corresponding `component` found in the
|
||||
`checkpoint` file.
|
||||
"""
|
||||
if not PathManager.exists(checkpoint):
|
||||
raise IOError("Model file not found: {}".format(checkpoint))
|
||||
state = load_checkpoint_to_cpu(checkpoint)
|
||||
if isinstance(component, FairseqEncoder):
|
||||
component_type = "encoder"
|
||||
elif isinstance(component, FairseqDecoder):
|
||||
component_type = "decoder"
|
||||
else:
|
||||
raise ValueError(
|
||||
"component to load must be either a FairseqEncoder or "
|
||||
"FairseqDecoder. Loading other component types are not supported."
|
||||
)
|
||||
component_state_dict = OrderedDict()
|
||||
for key in state["model"].keys():
|
||||
if key.startswith(component_type):
|
||||
# encoder.input_layers.0.0.weight --> input_layers.0.0.weight
|
||||
component_subkey = key[len(component_type) + 1 :]
|
||||
component_state_dict[component_subkey] = state["model"][key]
|
||||
component.load_state_dict(component_state_dict, strict=True)
|
||||
return component
|
||||
|
||||
|
||||
def verify_checkpoint_directory(save_dir: str) -> None:
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
temp_file_path = os.path.join(save_dir, "dummy")
|
||||
try:
|
||||
with open(temp_file_path, "w"):
|
||||
pass
|
||||
except OSError as e:
|
||||
logger.warning(
|
||||
"Unable to access checkpoint save directory: {}".format(save_dir)
|
||||
)
|
||||
raise e
|
||||
else:
|
||||
os.remove(temp_file_path)
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
#include <torch/extension.h>
|
||||
#include <vector>
|
||||
|
||||
/*
|
||||
CPP Binding for CUDA OP
|
||||
*/
|
||||
|
||||
// CUDA forward declarations
|
||||
torch::Tensor ngram_repeat_block_cuda_forward(torch::Tensor tokens,
|
||||
torch::Tensor lprobs, int bsz,
|
||||
int step, int beam_size,
|
||||
int no_repeat_ngram_size);
|
||||
|
||||
#define CHECK_CUDA(x) \
|
||||
TORCH_CHECK(x.type().is_cuda(), #x " must be a CUDA tensor")
|
||||
#define CHECK_CONTIGUOUS(x) \
|
||||
TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
|
||||
#define CHECK_INPUT(x) \
|
||||
CHECK_CUDA(x); \
|
||||
CHECK_CONTIGUOUS(x)
|
||||
|
||||
// Input check and call to CUDA OP
|
||||
// Backward method not required
|
||||
torch::Tensor ngram_repeat_block_forward(torch::Tensor tokens,
|
||||
torch::Tensor lprobs, int bsz,
|
||||
int step, int beam_size,
|
||||
int no_repeat_ngram_size) {
|
||||
CHECK_INPUT(tokens);
|
||||
CHECK_INPUT(lprobs);
|
||||
assert(bsz > 0);
|
||||
assert(step >= 0);
|
||||
assert(beam_size > 0);
|
||||
assert(no_repeat_ngram_size > 0);
|
||||
|
||||
return ngram_repeat_block_cuda_forward(tokens, lprobs, bsz, step, beam_size,
|
||||
no_repeat_ngram_size);
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("forward", &ngram_repeat_block_forward,
|
||||
"No Repeat Ngram Block forward (CUDA)");
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Kernel implementation for blocking repeated n-grams.
|
||||
*/
|
||||
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <math.h>
|
||||
#include <torch/extension.h>
|
||||
#include <vector>
|
||||
|
||||
// Ban repeated ngrams of length = 'no_repeat_ngram_size'
|
||||
__global__ void banRepeatedTokens(long* __restrict__ tokens,
|
||||
float* __restrict__ lprobs,
|
||||
int max_predict_len, int vocab_size,
|
||||
int no_repeat_ngram_size) {
|
||||
auto row = blockIdx.x;
|
||||
auto col = threadIdx.x;
|
||||
auto start = row * (max_predict_len) + col;
|
||||
// Each thread compares ngram starting from
|
||||
// thread index with final ngram starting from
|
||||
// step - no_repeat_ngram_size +2
|
||||
auto check_start_pos = blockDim.x;
|
||||
auto lprob_start = row * vocab_size;
|
||||
bool is_banned = true;
|
||||
extern __shared__ long tokens_shm[];
|
||||
tokens_shm[col] = tokens[start];
|
||||
if (col == blockDim.x - 1) {
|
||||
for (int i=1; i<no_repeat_ngram_size; i++){
|
||||
if (col+i < max_predict_len){
|
||||
tokens_shm[col + i] = tokens[start + i];
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
for (int k = 0; k < no_repeat_ngram_size - 1; k++) {
|
||||
if (tokens_shm[col + k] != tokens_shm[check_start_pos + k]) {
|
||||
is_banned = false;
|
||||
}
|
||||
}
|
||||
if (is_banned == true) {
|
||||
auto token_to_be_banned = tokens_shm[col + no_repeat_ngram_size - 1];
|
||||
lprobs[lprob_start + token_to_be_banned] = -INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate blocks and threads based on
|
||||
// batch size and sequence length and launch
|
||||
// kernel
|
||||
torch::Tensor ngram_repeat_block_cuda_forward(const torch::Tensor tokens,
|
||||
torch::Tensor lprobs, int bsz,
|
||||
int step, int beam_size,
|
||||
int no_repeat_ngram_size) {
|
||||
int threads = step - no_repeat_ngram_size + 2;
|
||||
if (threads <= 0) return lprobs;
|
||||
int max_predict_len = tokens.size(1);
|
||||
int vocab_size = lprobs.size(1);
|
||||
auto token_ptr = tokens.data_ptr<long>();
|
||||
auto lprob_ptr = lprobs.data_ptr<float>();
|
||||
int blocks = bsz * beam_size;
|
||||
int shared_mem_size = (step + 1) * sizeof(long);
|
||||
|
||||
// Launching N blocks where N is number of samples in a batch (beams*bsz)
|
||||
// Launching T threads where T is number of previous ngrams in a sample
|
||||
// Allocating shared mem per block for fastser access of input tokens since
|
||||
// each token will be accessed N times to compare with current Ngram where
|
||||
// N is Ngram size.
|
||||
banRepeatedTokens<<<blocks, threads, shared_mem_size>>>(
|
||||
token_ptr, lprob_ptr, max_predict_len, vocab_size, no_repeat_ngram_size);
|
||||
return lprobs;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
#include <map>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
typedef struct
|
||||
{
|
||||
size_t reflen;
|
||||
size_t predlen;
|
||||
size_t match1;
|
||||
size_t count1;
|
||||
size_t match2;
|
||||
size_t count2;
|
||||
size_t match3;
|
||||
size_t count3;
|
||||
size_t match4;
|
||||
size_t count4;
|
||||
} bleu_stat;
|
||||
|
||||
// left trim (remove pad)
|
||||
void bleu_ltrim(size_t* len, int** sent, int pad) {
|
||||
size_t start = 0;
|
||||
while(start < *len) {
|
||||
if (*(*sent + start) != pad) { break; }
|
||||
start++;
|
||||
}
|
||||
*sent += start;
|
||||
*len -= start;
|
||||
}
|
||||
|
||||
// right trim remove (eos)
|
||||
void bleu_rtrim(size_t* len, int** sent, int pad, int eos) {
|
||||
size_t end = *len - 1;
|
||||
while (end > 0) {
|
||||
if (*(*sent + end) != eos && *(*sent + end) != pad) { break; }
|
||||
end--;
|
||||
}
|
||||
*len = end + 1;
|
||||
}
|
||||
|
||||
// left and right trim
|
||||
void bleu_trim(size_t* len, int** sent, int pad, int eos) {
|
||||
bleu_ltrim(len, sent, pad);
|
||||
bleu_rtrim(len, sent, pad, eos);
|
||||
}
|
||||
|
||||
size_t bleu_hash(int len, int* data) {
|
||||
size_t h = 14695981039346656037ul;
|
||||
size_t prime = 0x100000001b3;
|
||||
char* b = (char*) data;
|
||||
size_t blen = sizeof(int) * len;
|
||||
|
||||
while (blen-- > 0) {
|
||||
h ^= *b++;
|
||||
h *= prime;
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
void bleu_addngram(
|
||||
size_t *ntotal, size_t *nmatch, size_t n,
|
||||
size_t reflen, int* ref, size_t predlen, int* pred) {
|
||||
|
||||
if (predlen < n) { return; }
|
||||
|
||||
predlen = predlen - n + 1;
|
||||
(*ntotal) += predlen;
|
||||
|
||||
if (reflen < n) { return; }
|
||||
|
||||
reflen = reflen - n + 1;
|
||||
|
||||
std::map<size_t, size_t> count;
|
||||
while (predlen > 0) {
|
||||
size_t w = bleu_hash(n, pred++);
|
||||
count[w]++;
|
||||
predlen--;
|
||||
}
|
||||
|
||||
while (reflen > 0) {
|
||||
size_t w = bleu_hash(n, ref++);
|
||||
if (count[w] > 0) {
|
||||
(*nmatch)++;
|
||||
count[w] -=1;
|
||||
}
|
||||
reflen--;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
#ifdef _WIN64
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
void bleu_zero_init(bleu_stat* stat) {
|
||||
std::memset(stat, 0, sizeof(bleu_stat));
|
||||
}
|
||||
|
||||
#ifdef _WIN64
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
void bleu_one_init(bleu_stat* stat) {
|
||||
bleu_zero_init(stat);
|
||||
stat->count1 = 0;
|
||||
stat->count2 = 1;
|
||||
stat->count3 = 1;
|
||||
stat->count4 = 1;
|
||||
stat->match1 = 0;
|
||||
stat->match2 = 1;
|
||||
stat->match3 = 1;
|
||||
stat->match4 = 1;
|
||||
}
|
||||
|
||||
#ifdef _WIN64
|
||||
__declspec(dllexport)
|
||||
#endif
|
||||
void bleu_add(
|
||||
bleu_stat* stat,
|
||||
size_t reflen, int* ref, size_t predlen, int* pred, int pad, int eos) {
|
||||
|
||||
bleu_trim(&reflen, &ref, pad, eos);
|
||||
bleu_trim(&predlen, &pred, pad, eos);
|
||||
stat->reflen += reflen;
|
||||
stat->predlen += predlen;
|
||||
|
||||
bleu_addngram(&stat->count1, &stat->match1, 1, reflen, ref, predlen, pred);
|
||||
bleu_addngram(&stat->count2, &stat->match2, 2, reflen, ref, predlen, pred);
|
||||
bleu_addngram(&stat->count3, &stat->match3, 3, reflen, ref, predlen, pred);
|
||||
bleu_addngram(&stat->count4, &stat->match4, 4, reflen, ref, predlen, pred);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
|
||||
static PyMethodDef method_def[] = {
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
static struct PyModuleDef module_def = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"libbleu", /* name of module */
|
||||
NULL, /* module documentation, may be NULL */
|
||||
-1, /* size of per-interpreter state of the module,
|
||||
or -1 if the module keeps state in global variables. */
|
||||
method_def
|
||||
};
|
||||
|
||||
|
||||
#if PY_MAJOR_VERSION == 2
|
||||
PyMODINIT_FUNC init_libbleu()
|
||||
#else
|
||||
PyMODINIT_FUNC PyInit_libbleu()
|
||||
#endif
|
||||
{
|
||||
PyObject *m = PyModule_Create(&module_def);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
#include <torch/torch.h> // @manual=//caffe2:torch_extension
|
||||
#include <pybind11/detail/common.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace ::std;
|
||||
|
||||
vector<vector<uint32_t>> edit_distance2_with_dp(
|
||||
vector<uint32_t>& x,
|
||||
vector<uint32_t>& y) {
|
||||
uint32_t lx = x.size();
|
||||
uint32_t ly = y.size();
|
||||
vector<vector<uint32_t>> d(lx + 1, vector<uint32_t>(ly + 1));
|
||||
for (uint32_t i = 0; i < lx + 1; i++) {
|
||||
d[i][0] = i;
|
||||
}
|
||||
for (uint32_t j = 0; j < ly + 1; j++) {
|
||||
d[0][j] = j;
|
||||
}
|
||||
for (uint32_t i = 1; i < lx + 1; i++) {
|
||||
for (uint32_t j = 1; j < ly + 1; j++) {
|
||||
d[i][j] =
|
||||
min(min(d[i - 1][j], d[i][j - 1]) + 1,
|
||||
d[i - 1][j - 1] + 2 * (x.at(i - 1) == y.at(j - 1) ? 0 : 1));
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
vector<vector<uint32_t>> edit_distance2_backtracking(
|
||||
vector<vector<uint32_t>>& d,
|
||||
vector<uint32_t>& x,
|
||||
vector<uint32_t>& y,
|
||||
uint32_t terminal_symbol) {
|
||||
vector<uint32_t> seq;
|
||||
vector<vector<uint32_t>> edit_seqs(x.size() + 2, vector<uint32_t>());
|
||||
/*
|
||||
edit_seqs:
|
||||
0~x.size() cell is the insertion sequences
|
||||
last cell is the delete sequence
|
||||
*/
|
||||
|
||||
if (x.size() == 0) {
|
||||
edit_seqs.at(0) = y;
|
||||
return edit_seqs;
|
||||
}
|
||||
|
||||
uint32_t i = d.size() - 1;
|
||||
uint32_t j = d.at(0).size() - 1;
|
||||
|
||||
while ((i >= 0) && (j >= 0)) {
|
||||
if ((i == 0) && (j == 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((j > 0) && (d.at(i).at(j - 1) < d.at(i).at(j))) {
|
||||
seq.push_back(1); // insert
|
||||
seq.push_back(y.at(j - 1));
|
||||
j--;
|
||||
} else if ((i > 0) && (d.at(i - 1).at(j) < d.at(i).at(j))) {
|
||||
seq.push_back(2); // delete
|
||||
seq.push_back(x.at(i - 1));
|
||||
i--;
|
||||
} else {
|
||||
seq.push_back(3); // keep
|
||||
seq.push_back(x.at(i - 1));
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t prev_op, op, s, word;
|
||||
prev_op = 0, s = 0;
|
||||
for (uint32_t k = 0; k < seq.size() / 2; k++) {
|
||||
op = seq.at(seq.size() - 2 * k - 2);
|
||||
word = seq.at(seq.size() - 2 * k - 1);
|
||||
if (prev_op != 1) {
|
||||
s++;
|
||||
}
|
||||
if (op == 1) // insert
|
||||
{
|
||||
edit_seqs.at(s - 1).push_back(word);
|
||||
} else if (op == 2) // delete
|
||||
{
|
||||
edit_seqs.at(x.size() + 1).push_back(1);
|
||||
} else {
|
||||
edit_seqs.at(x.size() + 1).push_back(0);
|
||||
}
|
||||
|
||||
prev_op = op;
|
||||
}
|
||||
|
||||
for (uint32_t k = 0; k < edit_seqs.size(); k++) {
|
||||
if (edit_seqs[k].size() == 0) {
|
||||
edit_seqs[k].push_back(terminal_symbol);
|
||||
}
|
||||
}
|
||||
return edit_seqs;
|
||||
}
|
||||
|
||||
vector<vector<uint32_t>> edit_distance2_backtracking_with_delete(
|
||||
vector<vector<uint32_t>>& d,
|
||||
vector<uint32_t>& x,
|
||||
vector<uint32_t>& y,
|
||||
uint32_t terminal_symbol,
|
||||
uint32_t deletion_symbol) {
|
||||
vector<uint32_t> seq;
|
||||
vector<vector<uint32_t>> edit_seqs(x.size() + 1, vector<uint32_t>());
|
||||
/*
|
||||
edit_seqs:
|
||||
0~x.size() cell is the insertion sequences
|
||||
last cell is the delete sequence
|
||||
*/
|
||||
|
||||
if (x.size() == 0) {
|
||||
edit_seqs.at(0) = y;
|
||||
return edit_seqs;
|
||||
}
|
||||
|
||||
uint32_t i = d.size() - 1;
|
||||
uint32_t j = d.at(0).size() - 1;
|
||||
|
||||
while ((i >= 0) && (j >= 0)) {
|
||||
if ((i == 0) && (j == 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((j > 0) && (d.at(i).at(j - 1) < d.at(i).at(j))) {
|
||||
seq.push_back(1); // insert
|
||||
seq.push_back(y.at(j - 1));
|
||||
j--;
|
||||
} else if ((i > 0) && (d.at(i - 1).at(j) < d.at(i).at(j))) {
|
||||
seq.push_back(2); // delete
|
||||
seq.push_back(x.at(i - 1));
|
||||
i--;
|
||||
} else {
|
||||
seq.push_back(3); // keep
|
||||
seq.push_back(x.at(i - 1));
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t prev_op, op, s, word;
|
||||
prev_op = 0, s = 0;
|
||||
for (uint32_t k = 0; k < seq.size() / 2; k++) {
|
||||
op = seq.at(seq.size() - 2 * k - 2);
|
||||
word = seq.at(seq.size() - 2 * k - 1);
|
||||
if (prev_op != 1) {
|
||||
s++;
|
||||
}
|
||||
if (op == 1) // insert
|
||||
{
|
||||
edit_seqs.at(s - 1).push_back(word);
|
||||
} else if (op == 2) // delete
|
||||
{
|
||||
edit_seqs.at(s - 1).push_back(deletion_symbol);
|
||||
}
|
||||
|
||||
prev_op = op;
|
||||
}
|
||||
|
||||
for (uint32_t k = 0; k < edit_seqs.size(); k++) {
|
||||
if (edit_seqs.at(k).size() == 0) {
|
||||
edit_seqs.at(k).push_back(terminal_symbol);
|
||||
}
|
||||
}
|
||||
return edit_seqs;
|
||||
}
|
||||
|
||||
vector<uint32_t> compute_ed2(
|
||||
vector<vector<uint32_t>>& xs,
|
||||
vector<vector<uint32_t>>& ys) {
|
||||
vector<uint32_t> distances(xs.size());
|
||||
for (uint32_t i = 0; i < xs.size(); i++) {
|
||||
vector<vector<uint32_t>> d = edit_distance2_with_dp(xs.at(i), ys.at(i));
|
||||
distances.at(i) = d.at(xs.at(i).size()).at(ys.at(i).size());
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
|
||||
vector<vector<vector<uint32_t>>> suggested_ed2_path(
|
||||
vector<vector<uint32_t>>& xs,
|
||||
vector<vector<uint32_t>>& ys,
|
||||
uint32_t terminal_symbol) {
|
||||
vector<vector<vector<uint32_t>>> seq(xs.size());
|
||||
for (uint32_t i = 0; i < xs.size(); i++) {
|
||||
vector<vector<uint32_t>> d = edit_distance2_with_dp(xs.at(i), ys.at(i));
|
||||
seq.at(i) =
|
||||
edit_distance2_backtracking(d, xs.at(i), ys.at(i), terminal_symbol);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
vector<vector<vector<uint32_t>>> suggested_ed2_path_with_delete(
|
||||
vector<vector<uint32_t>>& xs,
|
||||
vector<vector<uint32_t>>& ys,
|
||||
uint32_t terminal_symbol,
|
||||
uint32_t deletion_symbol) {
|
||||
vector<vector<vector<uint32_t>>> seq(xs.size());
|
||||
for (uint32_t i = 0; i < xs.size(); i++) {
|
||||
vector<vector<uint32_t>> d = edit_distance2_with_dp(xs.at(i), ys.at(i));
|
||||
seq.at(i) = edit_distance2_backtracking_with_delete(
|
||||
d, xs.at(i), ys.at(i), terminal_symbol, deletion_symbol);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(libnat, m) {
|
||||
m.def("compute_ed2", &compute_ed2, "compute_ed2");
|
||||
m.def("suggested_ed2_path", &suggested_ed2_path, "suggested_ed2_path");
|
||||
m.def(
|
||||
"suggested_ed2_path_with_delete",
|
||||
&suggested_ed2_path_with_delete,
|
||||
"suggested_ed2_path_with_delete");
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
This code is partially adpoted from https://github.com/1ytic/pytorch-edit-distance
|
||||
*/
|
||||
|
||||
#include "edit_dist.h"
|
||||
#include <torch/types.h>
|
||||
|
||||
#ifndef TORCH_CHECK
|
||||
#define TORCH_CHECK AT_CHECK
|
||||
#endif
|
||||
|
||||
#define CHECK_CUDA(x) TORCH_CHECK(x.type().is_cuda(), #x " must be a CUDA tensor")
|
||||
#define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
|
||||
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)
|
||||
|
||||
|
||||
torch::Tensor LevenshteinDistance(
|
||||
torch::Tensor source,
|
||||
torch::Tensor target,
|
||||
torch::Tensor source_length,
|
||||
torch::Tensor target_length) {
|
||||
|
||||
CHECK_INPUT(source);
|
||||
CHECK_INPUT(target);
|
||||
CHECK_INPUT(source_length);
|
||||
CHECK_INPUT(target_length);
|
||||
return LevenshteinDistanceCuda(source, target, source_length, target_length);
|
||||
}
|
||||
|
||||
torch::Tensor GenerateDeletionLabel(
|
||||
torch::Tensor source,
|
||||
torch::Tensor operations) {
|
||||
|
||||
CHECK_INPUT(source);
|
||||
CHECK_INPUT(operations);
|
||||
return GenerateDeletionLabelCuda(source, operations);
|
||||
}
|
||||
|
||||
std::pair<torch::Tensor, torch::Tensor> GenerateInsertionLabel(
|
||||
torch::Tensor target,
|
||||
torch::Tensor operations) {
|
||||
|
||||
CHECK_INPUT(target);
|
||||
CHECK_INPUT(operations);
|
||||
return GenerateInsertionLabelCuda(target, operations);
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("levenshtein_distance", &LevenshteinDistance, "Levenshtein distance");
|
||||
m.def("generate_deletion_labels", &GenerateDeletionLabel, "Generate Deletion Label");
|
||||
m.def("generate_insertion_labels", &GenerateInsertionLabel, "Generate Insertion Label");
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
#include "edit_dist.h"
|
||||
#include <THC/THC.h>
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <device_launch_parameters.h>
|
||||
#include <utility> // std::pair
|
||||
|
||||
template <typename scalar_t>
|
||||
__global__ void generate_deletion_label_kernel(
|
||||
const scalar_t* __restrict__ source,
|
||||
const size_t source_size,
|
||||
const size_t operation_size,
|
||||
int* __restrict__ operations,
|
||||
int* __restrict__ labels) {
|
||||
|
||||
const int index = blockIdx.x;
|
||||
const int offset = index * operation_size;
|
||||
const int offset_label = index * source_size;
|
||||
|
||||
for (int i = 0; i < source_size; i++) {
|
||||
labels[offset_label + i] = 0;
|
||||
}
|
||||
|
||||
int k = 0;
|
||||
for (int i = 0; i < operation_size; i++){
|
||||
if (operations[offset + i] == 0){
|
||||
break;
|
||||
} else if (operations[offset + i] == 1){
|
||||
continue;
|
||||
} else {
|
||||
labels[offset_label + k] = 3 - operations[offset + i];
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename scalar_t>
|
||||
__global__ void generate_insertion_label_kernel(
|
||||
const scalar_t* __restrict__ target,
|
||||
const size_t target_size,
|
||||
const size_t operation_size,
|
||||
int* __restrict__ operations,
|
||||
int* __restrict__ labels,
|
||||
int* __restrict__ masks) {
|
||||
|
||||
const int index = blockIdx.x;
|
||||
const int offset = index * operation_size;
|
||||
const int offset_label = index * target_size;
|
||||
|
||||
int k = 0;
|
||||
int u = 0;
|
||||
int m = 0;
|
||||
|
||||
for (int i = 0; i < target_size; i++) {
|
||||
labels[offset_label + i] = 0;
|
||||
masks[offset_label + i] = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < operation_size-1; i++){
|
||||
if (operations[offset + i] == 0){
|
||||
break;
|
||||
} else if (operations[offset + i] == 2){
|
||||
continue;
|
||||
} else if (operations[offset + i] == 1){
|
||||
masks[offset_label + m] = 1;
|
||||
u++; m++;
|
||||
} else {
|
||||
labels[offset_label + k] = u;
|
||||
masks[offset_label + m] = 0;
|
||||
k++; m++;
|
||||
u = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename scalar_t>
|
||||
__global__ void levenshtein_distance_kernel(
|
||||
const scalar_t* __restrict__ source,
|
||||
const scalar_t* __restrict__ target,
|
||||
const int* __restrict__ source_length,
|
||||
const int* __restrict__ target_length,
|
||||
const size_t source_size,
|
||||
const size_t target_size,
|
||||
int* __restrict__ operations,
|
||||
int* __restrict__ errors_curr) {
|
||||
|
||||
const int index = blockIdx.x;
|
||||
const int offset = index * (source_size + target_size);
|
||||
const int d = index * (source_size + 1) * (target_size + 1);
|
||||
const int t = target_size + 1;
|
||||
|
||||
auto err_idx = [d, t](int i, int j) { return d + i * t + j; };
|
||||
auto opt_idx = [offset](int k) { return offset + k; };
|
||||
|
||||
const int hyp_len = source_length[index];
|
||||
const int ref_len = target_length[index];
|
||||
const scalar_t* hyp_begin = source + index * source_size;
|
||||
const scalar_t* ref_begin = target + index * target_size;
|
||||
|
||||
// dynamic programming
|
||||
for (int i = 0; i <= hyp_len; i++){
|
||||
errors_curr[err_idx(i, 0)] = i;
|
||||
}
|
||||
for (int j = 0; j <= ref_len; j++){
|
||||
errors_curr[err_idx(0, j)] = j;
|
||||
}
|
||||
for (int i = 1; i <= hyp_len; i++){
|
||||
for (int j = 1; j <= ref_len; j++){
|
||||
errors_curr[err_idx(i, j)] = min(
|
||||
min(
|
||||
errors_curr[err_idx(i-1, j)],
|
||||
errors_curr[err_idx(i, j-1)]
|
||||
) + 1,
|
||||
errors_curr[err_idx(i-1, j-1)] + 2 * (
|
||||
*(hyp_begin+i-1) == *(ref_begin+j-1) ? 0 : 1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// back-tracing
|
||||
int i = hyp_len;
|
||||
int j = ref_len;
|
||||
int o = hyp_len + ref_len;
|
||||
|
||||
for (int k = 0; k < source_size + target_size; k++) {
|
||||
operations[opt_idx(k)] = 0;
|
||||
}
|
||||
|
||||
while ((i >= 0) && (j >= 0)) {
|
||||
if ((i == 0) && (j == 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((j > 0) && (errors_curr[err_idx(i, j-1)] < errors_curr[err_idx(i, j)])) {
|
||||
o--; operations[opt_idx(o)] = 1; j--; // insertion
|
||||
} else if ((i > 0) && (errors_curr[err_idx(i-1, j)] < errors_curr[err_idx(i, j)])) {
|
||||
o--; operations[opt_idx(o)] = 2; i--; // deletion
|
||||
} else {
|
||||
o--; operations[opt_idx(o)] = 3; i--; j--; // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// moving to the left
|
||||
for (int k = 0; k < hyp_len + ref_len; k++) {
|
||||
if (k + o < hyp_len + ref_len){
|
||||
operations[opt_idx(k)] = operations[opt_idx(k+o)];
|
||||
} else{
|
||||
operations[opt_idx(k)] = 0; // padding
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <typename scalar_t>
|
||||
__global__ void faster_levenshtein_distance_kernel(
|
||||
const scalar_t* __restrict__ source,
|
||||
const scalar_t* __restrict__ target,
|
||||
const int* __restrict__ source_length,
|
||||
const int* __restrict__ target_length,
|
||||
const size_t source_size,
|
||||
const size_t target_size,
|
||||
int* __restrict__ operations) {
|
||||
|
||||
extern __shared__ short errors[];
|
||||
auto errors_curr = errors;
|
||||
|
||||
const int index = blockIdx.x;
|
||||
const int offset = index * (source_size + target_size);
|
||||
const int t = target_size + 1;
|
||||
|
||||
auto err_idx = [t](int i, int j) { return i * t + j; };
|
||||
auto opt_idx = [offset](int k) { return offset + k; };
|
||||
|
||||
const int hyp_len = source_length[index];
|
||||
const int ref_len = target_length[index];
|
||||
const scalar_t* hyp_begin = source + index * source_size;
|
||||
const scalar_t* ref_begin = target + index * target_size;
|
||||
|
||||
// dynamic programming
|
||||
for (int i = 0; i <= hyp_len; i++){
|
||||
errors_curr[err_idx(i, 0)] = i;
|
||||
}
|
||||
for (int j = 0; j <= ref_len; j++){
|
||||
errors_curr[err_idx(0, j)] = j;
|
||||
}
|
||||
for (int i = 1; i <= hyp_len; i++){
|
||||
for (int j = 1; j <= ref_len; j++){
|
||||
errors_curr[err_idx(i, j)] = min(
|
||||
min(
|
||||
errors_curr[err_idx(i-1, j)],
|
||||
errors_curr[err_idx(i, j-1)]
|
||||
) + 1,
|
||||
errors_curr[err_idx(i-1, j-1)] + 2 * (
|
||||
*(hyp_begin+i-1) == *(ref_begin+j-1) ? 0 : 1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// back-tracing
|
||||
int i = hyp_len;
|
||||
int j = ref_len;
|
||||
int o = hyp_len + ref_len;
|
||||
|
||||
for (int k = 0; k < source_size + target_size; k++) {
|
||||
operations[opt_idx(k)] = 0;
|
||||
}
|
||||
|
||||
while ((i >= 0) && (j >= 0)) {
|
||||
if ((i == 0) && (j == 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((j > 0) && (errors_curr[err_idx(i, j-1)] < errors_curr[err_idx(i, j)])) {
|
||||
o--; operations[opt_idx(o)] = 1; j--; // insertion
|
||||
} else if ((i > 0) && (errors_curr[err_idx(i-1, j)] < errors_curr[err_idx(i, j)])) {
|
||||
o--; operations[opt_idx(o)] = 2; i--; // deletion
|
||||
} else {
|
||||
o--; operations[opt_idx(o)] = 3; i--; j--; // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// moving to the left
|
||||
for (int k = 0; k < hyp_len + ref_len; k++) {
|
||||
if (k + o < hyp_len + ref_len){
|
||||
operations[opt_idx(k)] = operations[opt_idx(k+o)];
|
||||
} else{
|
||||
operations[opt_idx(k)] = 0; // padding
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
torch::Tensor GenerateDeletionLabelCuda(
|
||||
torch::Tensor source,
|
||||
torch::Tensor operations) {
|
||||
|
||||
const auto batch_size = source.size(0);
|
||||
at::TensorOptions options(source.device());
|
||||
options = options.dtype(at::ScalarType::Int);
|
||||
auto labels = torch::empty({batch_size, source.size(1)}, options);
|
||||
auto stream = at::cuda::getCurrentCUDAStream(source.device().index());
|
||||
|
||||
AT_DISPATCH_ALL_TYPES(source.scalar_type(), "generate_deletion_labels", ([&] {
|
||||
generate_deletion_label_kernel<scalar_t><<<batch_size, 1, 0, stream>>>(
|
||||
source.data_ptr<scalar_t>(),
|
||||
source.size(1),
|
||||
operations.size(1),
|
||||
operations.data_ptr<int>(),
|
||||
labels.data_ptr<int>());
|
||||
}));
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
std::pair<torch::Tensor, torch::Tensor> GenerateInsertionLabelCuda(
|
||||
torch::Tensor target,
|
||||
torch::Tensor operations) {
|
||||
|
||||
const auto batch_size = target.size(0);
|
||||
at::TensorOptions options(target.device());
|
||||
options = options.dtype(at::ScalarType::Int);
|
||||
auto labels = torch::empty({batch_size, target.size(1)}, options);
|
||||
auto masks = torch::empty({batch_size, target.size(1)}, options);
|
||||
auto stream = at::cuda::getCurrentCUDAStream(target.device().index());
|
||||
|
||||
AT_DISPATCH_ALL_TYPES(target.scalar_type(), "generate_insertion_labels", ([&] {
|
||||
generate_insertion_label_kernel<scalar_t><<<batch_size, 1, 0, stream>>>(
|
||||
target.data_ptr<scalar_t>(),
|
||||
target.size(1),
|
||||
operations.size(1),
|
||||
operations.data_ptr<int>(),
|
||||
labels.data_ptr<int>(),
|
||||
masks.data_ptr<int>());
|
||||
}));
|
||||
|
||||
return std::make_pair(labels, masks);
|
||||
}
|
||||
|
||||
|
||||
torch::Tensor LevenshteinDistanceCuda(
|
||||
torch::Tensor source,
|
||||
torch::Tensor target,
|
||||
torch::Tensor source_length,
|
||||
torch::Tensor target_length) {
|
||||
|
||||
const auto batch_size = source.size(0);
|
||||
const auto shared_size = (source.size(1) + 1) * (target.size(1) + 1) * sizeof(short);
|
||||
|
||||
at::TensorOptions options(source.device());
|
||||
options = options.dtype(at::ScalarType::Int);
|
||||
auto operations = torch::empty({batch_size, source.size(1) + target.size(1)}, options);
|
||||
auto stream = at::cuda::getCurrentCUDAStream(source.device().index());
|
||||
|
||||
if (shared_size > 40000) {
|
||||
auto distances = torch::empty({batch_size, (source.size(1) + 1) * (target.size(1) + 1)}, options);
|
||||
AT_DISPATCH_ALL_TYPES(source.scalar_type(), "levenshtein_distance", ([&] {
|
||||
levenshtein_distance_kernel<scalar_t><<<batch_size, 1, 0, stream>>>(
|
||||
source.data_ptr<scalar_t>(),
|
||||
target.data_ptr<scalar_t>(),
|
||||
source_length.data_ptr<int>(),
|
||||
target_length.data_ptr<int>(),
|
||||
source.size(1),
|
||||
target.size(1),
|
||||
operations.data_ptr<int>(),
|
||||
distances.data_ptr<int>());
|
||||
}));
|
||||
} else {
|
||||
AT_DISPATCH_ALL_TYPES(source.scalar_type(), "faster_levenshtein_distance", ([&] {
|
||||
faster_levenshtein_distance_kernel<scalar_t><<<batch_size, 1, shared_size, stream>>>(
|
||||
source.data_ptr<scalar_t>(),
|
||||
target.data_ptr<scalar_t>(),
|
||||
source_length.data_ptr<int>(),
|
||||
target_length.data_ptr<int>(),
|
||||
source.size(1),
|
||||
target.size(1),
|
||||
operations.data_ptr<int>());
|
||||
}));
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <torch/extension.h>
|
||||
|
||||
torch::Tensor LevenshteinDistanceCuda(
|
||||
torch::Tensor source,
|
||||
torch::Tensor target,
|
||||
torch::Tensor source_length,
|
||||
torch::Tensor target_length);
|
||||
|
||||
torch::Tensor GenerateDeletionLabelCuda(
|
||||
torch::Tensor source,
|
||||
torch::Tensor operations);
|
||||
|
||||
std::pair<torch::Tensor, torch::Tensor> GenerateInsertionLabelCuda(
|
||||
torch::Tensor source,
|
||||
torch::Tensor operations);
|
||||
@@ -0,0 +1,4 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,18 @@
|
||||
# @package _group_
|
||||
|
||||
hydra:
|
||||
run:
|
||||
dir: .
|
||||
|
||||
defaults:
|
||||
- task: null
|
||||
- model: null
|
||||
- criterion: cross_entropy
|
||||
- optimizer: null
|
||||
- lr_scheduler: fixed
|
||||
- bpe: null
|
||||
- tokenizer: null
|
||||
- scoring: null
|
||||
- generation: null
|
||||
- common_eval: null
|
||||
- eval_lm: null
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "relu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 512
|
||||
decoder_output_dim: 512
|
||||
decoder_input_dim: 512
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 12
|
||||
decoder_attention_heads: 16
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: true
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "relu"
|
||||
dropout: 0.3
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.1
|
||||
relu_dropout: 0.1
|
||||
decoder_embed_dim: 1024
|
||||
decoder_output_dim: 1024
|
||||
decoder_input_dim: 1024
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 16
|
||||
decoder_attention_heads: 8
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: true
|
||||
adaptive_softmax_cutoff: "20000,60000"
|
||||
adaptive_softmax_dropout: 0.2
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: true
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: "20000,60000"
|
||||
tie_adaptive_weights: true
|
||||
tie_adaptive_proj: true
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "relu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.0
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 1024
|
||||
decoder_output_dim: 1024
|
||||
decoder_input_dim: 1024
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 12
|
||||
decoder_attention_heads: 16
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: false
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "relu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 512
|
||||
decoder_output_dim: 512
|
||||
decoder_input_dim: 512
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 12
|
||||
decoder_attention_heads: 16
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: true
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "gelu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 768
|
||||
decoder_output_dim: 768
|
||||
decoder_input_dim: 768
|
||||
decoder_ffn_embed_dim: 3072
|
||||
decoder_layers: 12
|
||||
decoder_attention_heads: 12
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: false
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "gelu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 1600
|
||||
decoder_output_dim: 1600
|
||||
decoder_input_dim: 1600
|
||||
decoder_ffn_embed_dim: 6400
|
||||
decoder_layers: 48
|
||||
decoder_attention_heads: 25
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: false
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "gelu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 1280
|
||||
decoder_output_dim: 1280
|
||||
decoder_input_dim: 1280
|
||||
decoder_ffn_embed_dim: 5120
|
||||
decoder_layers: 36
|
||||
decoder_attention_heads: 20
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: false
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "gelu"
|
||||
dropout: 0.1
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.0
|
||||
relu_dropout: 0.0
|
||||
decoder_embed_dim: 1024
|
||||
decoder_output_dim: 1024
|
||||
decoder_input_dim: 1024
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 24
|
||||
decoder_attention_heads: 16
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: false
|
||||
adaptive_softmax_cutoff: null
|
||||
adaptive_softmax_dropout: 0
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: false
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: null
|
||||
tie_adaptive_weights: false
|
||||
tie_adaptive_proj: false
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,36 @@
|
||||
# @package _group_
|
||||
activation_fn: "relu"
|
||||
dropout: 0.3
|
||||
attention_dropout: 0.1
|
||||
activation_dropout: 0.1
|
||||
relu_dropout: 0.1
|
||||
decoder_embed_dim: 1024
|
||||
decoder_output_dim: 1024
|
||||
decoder_input_dim: 1024
|
||||
decoder_ffn_embed_dim: 4096
|
||||
decoder_layers: 16
|
||||
decoder_attention_heads: 8
|
||||
decoder_normalize_before: true
|
||||
no_decoder_final_norm: true
|
||||
adaptive_softmax_cutoff: "20000,60000"
|
||||
adaptive_softmax_dropout: 0.2
|
||||
adaptive_softmax_factor: 4
|
||||
no_token_positional_embeddings: false
|
||||
share_decoder_input_output_embed: false
|
||||
character_embeddings: false
|
||||
character_filters: "[(1, 64), (2, 128), (3, 192), (4, 256), (5, 256), (6, 256), (7, 256)]"
|
||||
character_embedding_dim: 4
|
||||
char_embedder_highway_layers: 2
|
||||
adaptive_input: true
|
||||
adaptive_input_factor: 4
|
||||
adaptive_input_cutoff: "20000,60000"
|
||||
tie_adaptive_weights: true
|
||||
tie_adaptive_proj: true
|
||||
decoder_learned_pos: false
|
||||
decoder_layerdrop: 0
|
||||
decoder_layers_to_keep: null
|
||||
layernorm_embedding: false
|
||||
no_scale_embedding: false
|
||||
quant_noise_pq: 0
|
||||
quant_noise_pq_block_size: 8
|
||||
quant_noise_scalar: 0
|
||||
@@ -0,0 +1,5 @@
|
||||
# @package _group_
|
||||
activation: gelu
|
||||
vq_type: gumbel
|
||||
vq_depth: 2
|
||||
combine_groups: true
|
||||
@@ -0,0 +1,8 @@
|
||||
# @package _group_
|
||||
|
||||
quantize_targets: true
|
||||
final_dim: 256
|
||||
encoder_layerdrop: 0.05
|
||||
dropout_input: 0.1
|
||||
dropout_features: 0.1
|
||||
feature_grad_mult: 0.1
|
||||
@@ -0,0 +1,20 @@
|
||||
# @package _group_
|
||||
|
||||
quantize_targets: true
|
||||
extractor_mode: layer_norm
|
||||
layer_norm_first: true
|
||||
final_dim: 768
|
||||
latent_temp: [2.0,0.1,0.999995]
|
||||
encoder_layerdrop: 0.0
|
||||
dropout_input: 0.0
|
||||
dropout_features: 0.0
|
||||
dropout: 0.0
|
||||
attention_dropout: 0.0
|
||||
conv_bias: true
|
||||
|
||||
encoder_layers: 24
|
||||
encoder_embed_dim: 1024
|
||||
encoder_ffn_embed_dim: 4096
|
||||
encoder_attention_heads: 16
|
||||
|
||||
feature_grad_mult: 1.0
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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 importlib
|
||||
import os
|
||||
|
||||
from fairseq import registry
|
||||
from fairseq.criterions.fairseq_criterion import ( # noqa
|
||||
FairseqCriterion,
|
||||
LegacyFairseqCriterion,
|
||||
)
|
||||
from omegaconf import DictConfig
|
||||
|
||||
|
||||
(
|
||||
build_criterion_,
|
||||
register_criterion,
|
||||
CRITERION_REGISTRY,
|
||||
CRITERION_DATACLASS_REGISTRY,
|
||||
) = registry.setup_registry(
|
||||
"--criterion", base_class=FairseqCriterion, default="cross_entropy"
|
||||
)
|
||||
|
||||
|
||||
def build_criterion(cfg: DictConfig, task):
|
||||
return build_criterion_(cfg, task)
|
||||
|
||||
|
||||
# automatically import any Python files in the criterions/ directory
|
||||
for file in os.listdir(os.path.dirname(__file__)):
|
||||
if file.endswith(".py") and not file.startswith("_"):
|
||||
file_name = file[: file.find(".py")]
|
||||
importlib.import_module("fairseq.criterions." + file_name)
|
||||
@@ -0,0 +1,123 @@
|
||||
# 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 math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.dataclass.constants import DDP_BACKEND_CHOICES
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdaptiveLossConfig(FairseqDataclass):
|
||||
sentence_avg: bool = II("optimization.sentence_avg")
|
||||
ddp_backend: DDP_BACKEND_CHOICES = II("distributed_training.ddp_backend")
|
||||
|
||||
|
||||
@register_criterion("adaptive_loss", dataclass=AdaptiveLossConfig)
|
||||
class AdaptiveLoss(FairseqCriterion):
|
||||
"""This is an implementation of the loss function accompanying the adaptive softmax approximation for
|
||||
graphical processing units (GPU), described in the paper "Efficient softmax approximation for GPUs"
|
||||
(http://arxiv.org/abs/1609.04309)."""
|
||||
|
||||
def __init__(self, task, sentence_avg):
|
||||
super().__init__(task)
|
||||
self.sentence_avg = sentence_avg
|
||||
|
||||
@classmethod
|
||||
def build_criterion(cls, cfg: AdaptiveLossConfig, task):
|
||||
if cfg.ddp_backend in {"c10d", "pytorch_ddp"}:
|
||||
raise Exception(
|
||||
"AdaptiveLoss is not compatible with the PyTorch "
|
||||
"version of DistributedDataParallel. Please use "
|
||||
"`--ddp-backend=legacy_ddp` instead."
|
||||
)
|
||||
return cls(task, cfg.sentence_avg)
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
|
||||
assert (
|
||||
hasattr(model.decoder, "adaptive_softmax")
|
||||
and model.decoder.adaptive_softmax is not None
|
||||
)
|
||||
adaptive_softmax = model.decoder.adaptive_softmax
|
||||
|
||||
net_output = model(**sample["net_input"])
|
||||
orig_target = model.get_targets(sample, net_output)
|
||||
|
||||
nsentences = orig_target.size(0)
|
||||
orig_target = orig_target.view(-1)
|
||||
|
||||
bsz = orig_target.size(0)
|
||||
|
||||
logits, target = adaptive_softmax(net_output[0], orig_target)
|
||||
assert len(target) == len(logits)
|
||||
|
||||
loss = net_output[0].new(1 if reduce else bsz).zero_()
|
||||
|
||||
for i in range(len(target)):
|
||||
if target[i] is not None:
|
||||
assert target[i].min() >= 0 and target[i].max() <= logits[i].size(1)
|
||||
loss += F.cross_entropy(
|
||||
logits[i],
|
||||
target[i],
|
||||
ignore_index=self.padding_idx,
|
||||
reduction="sum" if reduce else "none",
|
||||
)
|
||||
|
||||
orig = utils.strip_pad(orig_target, self.padding_idx)
|
||||
ntokens = orig.numel()
|
||||
sample_size = sample["target"].size(0) if self.sentence_avg else ntokens
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": ntokens,
|
||||
"nsentences": nsentences,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
ntokens = utils.item(sum(log.get("ntokens", 0) for log in logging_outputs))
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
if sample_size != ntokens:
|
||||
metrics.log_scalar(
|
||||
"nll_loss", loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["nll_loss"].avg)
|
||||
)
|
||||
else:
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["loss"].avg)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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 fairseq import utils
|
||||
from fairseq.criterions import LegacyFairseqCriterion, register_criterion
|
||||
from torch import nn
|
||||
|
||||
|
||||
@register_criterion("composite_loss")
|
||||
class CompositeLoss(LegacyFairseqCriterion):
|
||||
"""This is a composite loss that, given a list of model outputs and a list of targets,
|
||||
computes an average of losses for each output-target pair"""
|
||||
|
||||
def __init__(self, args, task):
|
||||
super().__init__(args, task)
|
||||
self.underlying_criterion = args.underlying_criterion
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
parser.add_argument('--underlying-criterion', type=str, metavar='VAL', required=True,
|
||||
help='underlying criterion to use for the composite loss')
|
||||
# fmt: on
|
||||
|
||||
@staticmethod
|
||||
def build_underlying_criterion(args, task):
|
||||
saved_criterion = args.criterion
|
||||
args.criterion = args.underlying_criterion
|
||||
assert saved_criterion != args.underlying_criterion
|
||||
underlying_criterion = task.build_criterion(args)
|
||||
args.criterion = saved_criterion
|
||||
return underlying_criterion
|
||||
|
||||
@classmethod
|
||||
def build_criterion(cls, args, task):
|
||||
underlying_criterion = CompositeLoss.build_underlying_criterion(args, task)
|
||||
|
||||
class FakeModel(nn.Module):
|
||||
def __init__(self, model, net_out, target):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.net_out = net_out
|
||||
self.target = target
|
||||
|
||||
def forward(self, **unused):
|
||||
return self.net_out
|
||||
|
||||
def get_normalized_probs(self, net_output, log_probs, sample=None):
|
||||
return self.model.get_normalized_probs(
|
||||
net_output, log_probs, sample=sample
|
||||
)
|
||||
|
||||
def get_targets(self, *unused):
|
||||
return self.target
|
||||
|
||||
@property
|
||||
def decoder(self):
|
||||
return self.model.decoder
|
||||
|
||||
class _CompositeLoss(LegacyFairseqCriterion):
|
||||
def __init__(self, args, task, underlying_criterion):
|
||||
super().__init__(args, task)
|
||||
self.underlying_criterion = underlying_criterion
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
net_outputs = model(**sample["net_input"])
|
||||
targets = sample["target"]
|
||||
|
||||
bsz = targets[0].size(0)
|
||||
loss = net_outputs[0][0].new(1 if reduce else bsz).float().zero_()
|
||||
|
||||
sample_size = 0
|
||||
logging_output = {}
|
||||
for o, t in zip(net_outputs[0], targets):
|
||||
m = FakeModel(model, (o, net_outputs[1]), t)
|
||||
sample["target"] = t
|
||||
l, ss, logging_output = self.underlying_criterion(m, sample, reduce)
|
||||
loss += l
|
||||
sample_size += ss
|
||||
|
||||
loss.div_(len(targets))
|
||||
sample_size /= len(targets)
|
||||
|
||||
logging_output["loss"] = utils.item(loss.data) if reduce else loss.data
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def aggregate_logging_outputs(logging_outputs):
|
||||
return underlying_criterion.__class__.aggregate_logging_outputs(
|
||||
logging_outputs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
underlying_criterion.__class__.reduce_metrics(logging_outputs)
|
||||
|
||||
return _CompositeLoss(args, task, underlying_criterion)
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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 math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
@dataclass
|
||||
class CrossEntropyCriterionConfig(FairseqDataclass):
|
||||
sentence_avg: bool = II("optimization.sentence_avg")
|
||||
|
||||
|
||||
@register_criterion("cross_entropy", dataclass=CrossEntropyCriterionConfig)
|
||||
class CrossEntropyCriterion(FairseqCriterion):
|
||||
def __init__(self, task, sentence_avg):
|
||||
super().__init__(task)
|
||||
self.sentence_avg = sentence_avg
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
net_output = model(**sample["net_input"])
|
||||
loss, _ = self.compute_loss(model, net_output, sample, reduce=reduce)
|
||||
sample_size = (
|
||||
sample["target"].size(0) if self.sentence_avg else sample["ntokens"]
|
||||
)
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["target"].size(0),
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def compute_loss(self, model, net_output, sample, reduce=True):
|
||||
lprobs = model.get_normalized_probs(net_output, log_probs=True)
|
||||
lprobs = lprobs.view(-1, lprobs.size(-1))
|
||||
target = model.get_targets(sample, net_output).view(-1)
|
||||
loss = F.nll_loss(
|
||||
lprobs,
|
||||
target,
|
||||
ignore_index=self.padding_idx,
|
||||
reduction="sum" if reduce else "none",
|
||||
)
|
||||
return loss, loss
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
# we divide by log(2) to convert the loss from base e to base 2
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
if sample_size != ntokens:
|
||||
metrics.log_scalar(
|
||||
"nll_loss", loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["nll_loss"].avg)
|
||||
)
|
||||
else:
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["loss"].avg)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,286 @@
|
||||
# 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 math
|
||||
from argparse import Namespace
|
||||
from dataclasses import dataclass, field
|
||||
from omegaconf import II
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.data.data_utils import post_process
|
||||
from fairseq.tasks import FairseqTask
|
||||
from fairseq.logging.meters import safe_round
|
||||
|
||||
|
||||
@dataclass
|
||||
class CtcCriterionConfig(FairseqDataclass):
|
||||
zero_infinity: bool = field(
|
||||
default=False,
|
||||
metadata={"help": "zero inf loss when source length <= target length"},
|
||||
)
|
||||
sentence_avg: bool = II("optimization.sentence_avg")
|
||||
post_process: str = field(
|
||||
default="letter",
|
||||
metadata={
|
||||
"help": "how to post process predictions into words. can be letter, "
|
||||
"wordpiece, BPE symbols, etc. "
|
||||
"See fairseq.data.data_utils.post_process() for full list of options"
|
||||
},
|
||||
)
|
||||
wer_kenlm_model: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "if this is provided, use kenlm to compute wer (along with other wer_* args)"
|
||||
},
|
||||
)
|
||||
wer_lexicon: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={"help": "lexicon to use with wer_kenlm_model"},
|
||||
)
|
||||
wer_lm_weight: float = field(
|
||||
default=2.0,
|
||||
metadata={"help": "lm weight to use with wer_kenlm_model"},
|
||||
)
|
||||
wer_word_score: float = field(
|
||||
default=-1.0,
|
||||
metadata={"help": "lm word score to use with wer_kenlm_model"},
|
||||
)
|
||||
|
||||
wer_args: Optional[str] = field(
|
||||
default=None,
|
||||
metadata={
|
||||
"help": "DEPRECATED: tuple of (wer_kenlm_model, wer_lexicon, wer_lm_weight, wer_word_score)"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@register_criterion("ctc", dataclass=CtcCriterionConfig)
|
||||
class CtcCriterion(FairseqCriterion):
|
||||
def __init__(self, cfg: CtcCriterionConfig, task: FairseqTask):
|
||||
super().__init__(task)
|
||||
self.blank_idx = task.target_dictionary.index(task.blank_symbol) if hasattr(task, 'blank_symbol') else 0
|
||||
self.pad_idx = task.target_dictionary.pad()
|
||||
self.eos_idx = task.target_dictionary.eos()
|
||||
self.post_process = cfg.post_process
|
||||
|
||||
if cfg.wer_args is not None:
|
||||
(
|
||||
cfg.wer_kenlm_model,
|
||||
cfg.wer_lexicon,
|
||||
cfg.wer_lm_weight,
|
||||
cfg.wer_word_score,
|
||||
) = eval(cfg.wer_args)
|
||||
|
||||
if cfg.wer_kenlm_model is not None:
|
||||
from examples.speech_recognition.w2l_decoder import W2lKenLMDecoder
|
||||
|
||||
dec_args = Namespace()
|
||||
dec_args.nbest = 1
|
||||
dec_args.criterion = "ctc"
|
||||
dec_args.kenlm_model = cfg.wer_kenlm_model
|
||||
dec_args.lexicon = cfg.wer_lexicon
|
||||
dec_args.beam = 50
|
||||
dec_args.beam_size_token = min(50, len(task.target_dictionary))
|
||||
dec_args.beam_threshold = min(50, len(task.target_dictionary))
|
||||
dec_args.lm_weight = cfg.wer_lm_weight
|
||||
dec_args.word_score = cfg.wer_word_score
|
||||
dec_args.unk_weight = -math.inf
|
||||
dec_args.sil_weight = 0
|
||||
|
||||
self.w2l_decoder = W2lKenLMDecoder(dec_args, task.target_dictionary)
|
||||
else:
|
||||
self.w2l_decoder = None
|
||||
|
||||
self.zero_infinity = cfg.zero_infinity
|
||||
self.sentence_avg = cfg.sentence_avg
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
net_output = model(**sample["net_input"])
|
||||
lprobs = model.get_normalized_probs(
|
||||
net_output, log_probs=True
|
||||
).contiguous() # (T, B, C) from the encoder
|
||||
|
||||
if "src_lengths" in sample["net_input"]:
|
||||
input_lengths = sample["net_input"]["src_lengths"]
|
||||
else:
|
||||
non_padding_mask = ~net_output["padding_mask"]
|
||||
input_lengths = non_padding_mask.long().sum(-1)
|
||||
|
||||
pad_mask = (sample["target"] != self.pad_idx) & (
|
||||
sample["target"] != self.eos_idx
|
||||
)
|
||||
targets_flat = sample["target"].masked_select(pad_mask)
|
||||
if "target_lengths" in sample:
|
||||
target_lengths = sample["target_lengths"]
|
||||
else:
|
||||
target_lengths = pad_mask.sum(-1)
|
||||
|
||||
with torch.backends.cudnn.flags(enabled=False):
|
||||
loss = F.ctc_loss(
|
||||
lprobs,
|
||||
targets_flat,
|
||||
input_lengths,
|
||||
target_lengths,
|
||||
blank=self.blank_idx,
|
||||
reduction="sum",
|
||||
zero_infinity=self.zero_infinity,
|
||||
)
|
||||
|
||||
ntokens = (
|
||||
sample["ntokens"] if "ntokens" in sample else target_lengths.sum().item()
|
||||
)
|
||||
|
||||
sample_size = sample["target"].size(0) if self.sentence_avg else ntokens
|
||||
logging_output = {
|
||||
"loss": utils.item(loss.data), # * sample['ntokens'],
|
||||
"ntokens": ntokens,
|
||||
"nsentences": sample["id"].numel(),
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
if not model.training:
|
||||
import editdistance
|
||||
|
||||
with torch.no_grad():
|
||||
lprobs_t = lprobs.transpose(0, 1).float().contiguous().cpu()
|
||||
|
||||
c_err = 0
|
||||
c_len = 0
|
||||
w_errs = 0
|
||||
w_len = 0
|
||||
wv_errs = 0
|
||||
for lp, t, inp_l in zip(
|
||||
lprobs_t,
|
||||
sample["target_label"]
|
||||
if "target_label" in sample
|
||||
else sample["target"],
|
||||
input_lengths,
|
||||
):
|
||||
lp = lp[:inp_l].unsqueeze(0)
|
||||
|
||||
decoded = None
|
||||
if self.w2l_decoder is not None:
|
||||
decoded = self.w2l_decoder.decode(lp)
|
||||
if len(decoded) < 1:
|
||||
decoded = None
|
||||
else:
|
||||
decoded = decoded[0]
|
||||
if len(decoded) < 1:
|
||||
decoded = None
|
||||
else:
|
||||
decoded = decoded[0]
|
||||
|
||||
p = (t != self.task.target_dictionary.pad()) & (
|
||||
t != self.task.target_dictionary.eos()
|
||||
)
|
||||
targ = t[p]
|
||||
targ_units = self.task.target_dictionary.string(targ)
|
||||
targ_units_arr = targ.tolist()
|
||||
|
||||
toks = lp.argmax(dim=-1).unique_consecutive()
|
||||
pred_units_arr = toks[toks != self.blank_idx].tolist()
|
||||
|
||||
c_err += editdistance.eval(pred_units_arr, targ_units_arr)
|
||||
c_len += len(targ_units_arr)
|
||||
|
||||
targ_words = post_process(targ_units, self.post_process).split()
|
||||
|
||||
pred_units = self.task.target_dictionary.string(pred_units_arr)
|
||||
pred_words_raw = post_process(pred_units, self.post_process).split()
|
||||
|
||||
if decoded is not None and "words" in decoded:
|
||||
pred_words = decoded["words"]
|
||||
w_errs += editdistance.eval(pred_words, targ_words)
|
||||
wv_errs += editdistance.eval(pred_words_raw, targ_words)
|
||||
else:
|
||||
dist = editdistance.eval(pred_words_raw, targ_words)
|
||||
w_errs += dist
|
||||
wv_errs += dist
|
||||
|
||||
w_len += len(targ_words)
|
||||
|
||||
logging_output["wv_errors"] = wv_errs
|
||||
logging_output["w_errors"] = w_errs
|
||||
logging_output["w_total"] = w_len
|
||||
logging_output["c_errors"] = c_err
|
||||
logging_output["c_total"] = c_len
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
|
||||
loss_sum = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
ntokens = utils.item(sum(log.get("ntokens", 0) for log in logging_outputs))
|
||||
nsentences = utils.item(
|
||||
sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
)
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar("ntokens", ntokens)
|
||||
metrics.log_scalar("nsentences", nsentences)
|
||||
if sample_size != ntokens:
|
||||
metrics.log_scalar(
|
||||
"nll_loss", loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
|
||||
c_errors = sum(log.get("c_errors", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_c_errors", c_errors)
|
||||
c_total = sum(log.get("c_total", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_c_total", c_total)
|
||||
w_errors = sum(log.get("w_errors", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_w_errors", w_errors)
|
||||
wv_errors = sum(log.get("wv_errors", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_wv_errors", wv_errors)
|
||||
w_total = sum(log.get("w_total", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_w_total", w_total)
|
||||
|
||||
if c_total > 0:
|
||||
metrics.log_derived(
|
||||
"uer",
|
||||
lambda meters: safe_round(
|
||||
meters["_c_errors"].sum * 100.0 / meters["_c_total"].sum, 3
|
||||
)
|
||||
if meters["_c_total"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
if w_total > 0:
|
||||
metrics.log_derived(
|
||||
"wer",
|
||||
lambda meters: safe_round(
|
||||
meters["_w_errors"].sum * 100.0 / meters["_w_total"].sum, 3
|
||||
)
|
||||
if meters["_w_total"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
metrics.log_derived(
|
||||
"raw_wer",
|
||||
lambda meters: safe_round(
|
||||
meters["_wv_errors"].sum * 100.0 / meters["_w_total"].sum, 3
|
||||
)
|
||||
if meters["_w_total"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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 inspect
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.dataclass.utils import gen_parser_from_dataclass
|
||||
from torch.nn.modules.loss import _Loss
|
||||
|
||||
|
||||
class FairseqCriterion(_Loss):
|
||||
def __init__(self, task):
|
||||
super().__init__()
|
||||
self.task = task
|
||||
if hasattr(task, "target_dictionary"):
|
||||
tgt_dict = task.target_dictionary
|
||||
self.padding_idx = tgt_dict.pad() if tgt_dict is not None else -100
|
||||
|
||||
@classmethod
|
||||
def add_args(cls, parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
dc = getattr(cls, "__dataclass", None)
|
||||
if dc is not None:
|
||||
gen_parser_from_dataclass(parser, dc())
|
||||
|
||||
@classmethod
|
||||
def build_criterion(cls, cfg: FairseqDataclass, task):
|
||||
"""Construct a criterion from command-line args."""
|
||||
# arguments in the __init__.
|
||||
init_args = {}
|
||||
for p in inspect.signature(cls).parameters.values():
|
||||
if (
|
||||
p.kind == p.POSITIONAL_ONLY
|
||||
or p.kind == p.VAR_POSITIONAL
|
||||
or p.kind == p.VAR_KEYWORD
|
||||
):
|
||||
# we haven't implemented inference for these argument types,
|
||||
# but PRs welcome :)
|
||||
raise NotImplementedError("{} not supported".format(p.kind))
|
||||
|
||||
assert p.kind in {p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY}
|
||||
|
||||
if p.name == "task":
|
||||
init_args["task"] = task
|
||||
elif p.name == "cfg":
|
||||
init_args["cfg"] = cfg
|
||||
elif hasattr(cfg, p.name):
|
||||
init_args[p.name] = getattr(cfg, p.name)
|
||||
elif p.default != p.empty:
|
||||
pass # we'll use the default value
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Unable to infer Criterion arguments, please implement "
|
||||
"{}.build_criterion".format(cls.__name__)
|
||||
)
|
||||
return cls(**init_args)
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def aggregate_logging_outputs(
|
||||
logging_outputs: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
utils.deprecation_warning(
|
||||
"The aggregate_logging_outputs API is deprecated. "
|
||||
"Please use the reduce_metrics API instead."
|
||||
)
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def reduce_metrics(cls, logging_outputs: List[Dict[str, Any]]) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
utils.deprecation_warning(
|
||||
"Criterions should implement the reduce_metrics API. "
|
||||
"Falling back to deprecated aggregate_logging_outputs API."
|
||||
)
|
||||
agg_logging_outputs = cls.aggregate_logging_outputs(logging_outputs)
|
||||
for k, v in agg_logging_outputs.items():
|
||||
if k in {"nsentences", "ntokens", "sample_size"}:
|
||||
continue
|
||||
metrics.log_scalar(k, v)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
class LegacyFairseqCriterion(FairseqCriterion):
|
||||
def __init__(self, args, task):
|
||||
super().__init__(task=task)
|
||||
self.args = args
|
||||
|
||||
utils.deprecation_warning(
|
||||
"Criterions should take explicit arguments instead of an "
|
||||
"argparse.Namespace object, please update your criterion by "
|
||||
"extending FairseqCriterion instead of LegacyFairseqCriterion."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_criterion(cls, args, task):
|
||||
"""Construct a criterion from command-line args."""
|
||||
return cls(args, task)
|
||||
@@ -0,0 +1,170 @@
|
||||
# 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 math
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import torch
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
@dataclass
|
||||
class LabelSmoothedCrossEntropyCriterionConfig(FairseqDataclass):
|
||||
label_smoothing: float = field(
|
||||
default=0.0,
|
||||
metadata={"help": "epsilon for label smoothing, 0 means no label smoothing"},
|
||||
)
|
||||
report_accuracy: bool = field(
|
||||
default=False,
|
||||
metadata={"help": "report accuracy metric"},
|
||||
)
|
||||
ignore_prefix_size: int = field(
|
||||
default=0,
|
||||
metadata={"help": "Ignore first N tokens"},
|
||||
)
|
||||
sentence_avg: bool = II("optimization.sentence_avg")
|
||||
|
||||
|
||||
def label_smoothed_nll_loss(lprobs, target, epsilon, ignore_index=None, reduce=True):
|
||||
if target.dim() == lprobs.dim() - 1:
|
||||
target = target.unsqueeze(-1)
|
||||
nll_loss = -lprobs.gather(dim=-1, index=target)
|
||||
smooth_loss = -lprobs.sum(dim=-1, keepdim=True)
|
||||
if ignore_index is not None:
|
||||
pad_mask = target.eq(ignore_index)
|
||||
nll_loss.masked_fill_(pad_mask, 0.0)
|
||||
smooth_loss.masked_fill_(pad_mask, 0.0)
|
||||
else:
|
||||
nll_loss = nll_loss.squeeze(-1)
|
||||
smooth_loss = smooth_loss.squeeze(-1)
|
||||
if reduce:
|
||||
nll_loss = nll_loss.sum()
|
||||
smooth_loss = smooth_loss.sum()
|
||||
eps_i = epsilon / (lprobs.size(-1) - 1)
|
||||
loss = (1.0 - epsilon - eps_i) * nll_loss + eps_i * smooth_loss
|
||||
return loss, nll_loss
|
||||
|
||||
|
||||
@register_criterion(
|
||||
"label_smoothed_cross_entropy", dataclass=LabelSmoothedCrossEntropyCriterionConfig
|
||||
)
|
||||
class LabelSmoothedCrossEntropyCriterion(FairseqCriterion):
|
||||
def __init__(
|
||||
self,
|
||||
task,
|
||||
sentence_avg,
|
||||
label_smoothing,
|
||||
ignore_prefix_size=0,
|
||||
report_accuracy=False,
|
||||
):
|
||||
super().__init__(task)
|
||||
self.sentence_avg = sentence_avg
|
||||
self.eps = label_smoothing
|
||||
self.ignore_prefix_size = ignore_prefix_size
|
||||
self.report_accuracy = report_accuracy
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
net_output = model(**sample["net_input"])
|
||||
loss, nll_loss = self.compute_loss(model, net_output, sample, reduce=reduce)
|
||||
sample_size = (
|
||||
sample["target"].size(0) if self.sentence_avg else sample["ntokens"]
|
||||
)
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"nll_loss": nll_loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["target"].size(0),
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
if self.report_accuracy:
|
||||
n_correct, total = self.compute_accuracy(model, net_output, sample)
|
||||
logging_output["n_correct"] = utils.item(n_correct.data)
|
||||
logging_output["total"] = utils.item(total.data)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def get_lprobs_and_target(self, model, net_output, sample):
|
||||
lprobs = model.get_normalized_probs(net_output, log_probs=True)
|
||||
target = model.get_targets(sample, net_output)
|
||||
if self.ignore_prefix_size > 0:
|
||||
if getattr(lprobs, "batch_first", False):
|
||||
lprobs = lprobs[:, self.ignore_prefix_size :, :].contiguous()
|
||||
target = target[:, self.ignore_prefix_size :].contiguous()
|
||||
else:
|
||||
lprobs = lprobs[self.ignore_prefix_size :, :, :].contiguous()
|
||||
target = target[self.ignore_prefix_size :, :].contiguous()
|
||||
return lprobs.view(-1, lprobs.size(-1)), target.view(-1)
|
||||
|
||||
def compute_loss(self, model, net_output, sample, reduce=True):
|
||||
lprobs, target = self.get_lprobs_and_target(model, net_output, sample)
|
||||
loss, nll_loss = label_smoothed_nll_loss(
|
||||
lprobs,
|
||||
target,
|
||||
self.eps,
|
||||
ignore_index=self.padding_idx,
|
||||
reduce=reduce,
|
||||
)
|
||||
return loss, nll_loss
|
||||
|
||||
def compute_accuracy(self, model, net_output, sample):
|
||||
lprobs, target = self.get_lprobs_and_target(model, net_output, sample)
|
||||
mask = target.ne(self.padding_idx)
|
||||
n_correct = torch.sum(
|
||||
lprobs.argmax(1).masked_select(mask).eq(target.masked_select(mask))
|
||||
)
|
||||
total = torch.sum(mask)
|
||||
return n_correct, total
|
||||
|
||||
@classmethod
|
||||
def reduce_metrics(cls, logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
nll_loss_sum = sum(log.get("nll_loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"nll_loss", nll_loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["nll_loss"].avg)
|
||||
)
|
||||
|
||||
total = utils.item(sum(log.get("total", 0) for log in logging_outputs))
|
||||
if total > 0:
|
||||
metrics.log_scalar("total", total)
|
||||
n_correct = utils.item(
|
||||
sum(log.get("n_correct", 0) for log in logging_outputs)
|
||||
)
|
||||
metrics.log_scalar("n_correct", n_correct)
|
||||
metrics.log_derived(
|
||||
"accuracy",
|
||||
lambda meters: round(
|
||||
meters["n_correct"].sum * 100.0 / meters["total"].sum, 3
|
||||
)
|
||||
if meters["total"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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 math
|
||||
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import register_criterion
|
||||
|
||||
from .label_smoothed_cross_entropy import LabelSmoothedCrossEntropyCriterion
|
||||
|
||||
|
||||
@register_criterion("label_smoothed_cross_entropy_with_alignment")
|
||||
class LabelSmoothedCrossEntropyCriterionWithAlignment(
|
||||
LabelSmoothedCrossEntropyCriterion
|
||||
):
|
||||
def __init__(self, task, sentence_avg, label_smoothing, alignment_lambda):
|
||||
super().__init__(task, sentence_avg, label_smoothing)
|
||||
self.alignment_lambda = alignment_lambda
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
LabelSmoothedCrossEntropyCriterion.add_args(parser)
|
||||
parser.add_argument(
|
||||
"--alignment-lambda",
|
||||
default=0.05,
|
||||
type=float,
|
||||
metavar="D",
|
||||
help="weight for the alignment loss",
|
||||
)
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
net_output = model(**sample["net_input"])
|
||||
loss, nll_loss = self.compute_loss(model, net_output, sample, reduce=reduce)
|
||||
sample_size = (
|
||||
sample["target"].size(0) if self.sentence_avg else sample["ntokens"]
|
||||
)
|
||||
logging_output = {
|
||||
"loss": utils.item(loss.data) if reduce else loss.data,
|
||||
"nll_loss": utils.item(nll_loss.data) if reduce else nll_loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["target"].size(0),
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
alignment_loss = None
|
||||
|
||||
# Compute alignment loss only for training set and non dummy batches.
|
||||
if "alignments" in sample and sample["alignments"] is not None:
|
||||
alignment_loss = self.compute_alignment_loss(sample, net_output)
|
||||
|
||||
if alignment_loss is not None:
|
||||
logging_output["alignment_loss"] = utils.item(alignment_loss.data)
|
||||
loss += self.alignment_lambda * alignment_loss
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def compute_alignment_loss(self, sample, net_output):
|
||||
attn_prob = net_output[1]["attn"][0]
|
||||
bsz, tgt_sz, src_sz = attn_prob.shape
|
||||
attn = attn_prob.view(bsz * tgt_sz, src_sz)
|
||||
|
||||
align = sample["alignments"]
|
||||
align_weights = sample["align_weights"].float()
|
||||
|
||||
if len(align) > 0:
|
||||
# Alignment loss computation. align (shape [:, 2]) contains the src-tgt index pairs corresponding to
|
||||
# the alignments. align_weights (shape [:]) contains the 1 / frequency of a tgt index for normalizing.
|
||||
loss = -(
|
||||
(attn[align[:, 1][:, None], align[:, 0][:, None]]).log()
|
||||
* align_weights[:, None]
|
||||
).sum()
|
||||
else:
|
||||
return None
|
||||
|
||||
return loss
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
nll_loss_sum = utils.item(
|
||||
sum(log.get("nll_loss", 0) for log in logging_outputs)
|
||||
)
|
||||
alignment_loss_sum = utils.item(
|
||||
sum(log.get("alignment_loss", 0) for log in logging_outputs)
|
||||
)
|
||||
ntokens = utils.item(sum(log.get("ntokens", 0) for log in logging_outputs))
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"nll_loss", nll_loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"alignment_loss",
|
||||
alignment_loss_sum / sample_size / math.log(2),
|
||||
sample_size,
|
||||
round=3,
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["nll_loss"].avg)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,177 @@
|
||||
# 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 math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
|
||||
|
||||
def compute_cross_entropy_loss(logits, targets, ignore_index=-100):
|
||||
"""
|
||||
Function to compute the cross entropy loss. The default value of
|
||||
ignore_index is the same as the default value for F.cross_entropy in
|
||||
pytorch.
|
||||
"""
|
||||
assert logits.size(0) == targets.size(
|
||||
-1
|
||||
), "Logits and Targets tensor shapes don't match up"
|
||||
|
||||
loss = F.nll_loss(
|
||||
F.log_softmax(logits, -1, dtype=torch.float32),
|
||||
targets,
|
||||
reduction="sum",
|
||||
ignore_index=ignore_index,
|
||||
)
|
||||
return loss
|
||||
|
||||
|
||||
@register_criterion("legacy_masked_lm_loss")
|
||||
class LegacyMaskedLmLoss(FairseqCriterion):
|
||||
"""
|
||||
Implementation for the loss used in masked language model (MLM) training.
|
||||
This optionally also computes the next sentence prediction (NSP) loss and
|
||||
adds it to the overall loss based on the specified args. There are three
|
||||
cases to consider:
|
||||
1) Generic MLM training without NSP loss. In this case sentence_targets
|
||||
and sentence_logits are both None.
|
||||
2) BERT training without NSP loss. In this case sentence_targets is
|
||||
not None but sentence_logits is None and we should not be computing
|
||||
a sentence level loss.
|
||||
3) BERT training with NSP loss. In this case both sentence_targets and
|
||||
sentence_logits are not None and we should be computing a sentence
|
||||
level loss. The weight of the sentence level loss is specified as
|
||||
an argument.
|
||||
"""
|
||||
|
||||
def __init__(self, task, masked_lm_only, nsp_loss_weight):
|
||||
super().__init__(task)
|
||||
self.masked_lm_only = masked_lm_only
|
||||
self.nsp_loss_weight = nsp_loss_weight
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Args for MaskedLM Loss"""
|
||||
# Default for masked_lm_only is False so as to not break BERT training
|
||||
parser.add_argument(
|
||||
"--masked-lm-only",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="compute MLM loss only",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nsp-loss-weight",
|
||||
default=1.0,
|
||||
type=float,
|
||||
help="weight for next sentence prediction" " loss (default 1)",
|
||||
)
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
lm_logits, output_metadata = model(**sample["net_input"])
|
||||
|
||||
# reshape lm_logits from (N,T,C) to (N*T,C)
|
||||
lm_logits = lm_logits.view(-1, lm_logits.size(-1))
|
||||
lm_targets = sample["lm_target"].view(-1)
|
||||
lm_loss = compute_cross_entropy_loss(lm_logits, lm_targets, self.padding_idx)
|
||||
|
||||
# compute the number of tokens for which loss is computed. This is used
|
||||
# to normalize the loss
|
||||
ntokens = utils.strip_pad(lm_targets, self.padding_idx).numel()
|
||||
loss = lm_loss / ntokens
|
||||
nsentences = sample["nsentences"]
|
||||
# nsentences = 0
|
||||
|
||||
# Compute sentence loss if masked_lm_only is False
|
||||
sentence_loss = None
|
||||
if not self.masked_lm_only:
|
||||
sentence_logits = output_metadata["sentence_logits"]
|
||||
sentence_targets = sample["sentence_target"].view(-1)
|
||||
# This needs to be recomputed due to some differences between
|
||||
# TokenBlock and BlockPair dataset. This can be resolved with a
|
||||
# refactor of BERTModel which we will do in the future.
|
||||
# TODO: Remove this after refactor of BERTModel
|
||||
nsentences = sentence_targets.size(0)
|
||||
|
||||
# Check for logits being none which can happen when remove_heads
|
||||
# is set to true in the BERT model. Ideally we should set
|
||||
# masked_lm_only to true in this case, but that requires some
|
||||
# refactor in the BERT model.
|
||||
if sentence_logits is not None:
|
||||
sentence_loss = compute_cross_entropy_loss(
|
||||
sentence_logits, sentence_targets
|
||||
)
|
||||
|
||||
loss += self.nsp_loss_weight * (sentence_loss / nsentences)
|
||||
|
||||
# NOTE: as we are summing up per token mlm loss and per sentence nsp loss
|
||||
# we don't need to use sample_size as denominator for the gradient
|
||||
# here sample_size is just used for logging
|
||||
sample_size = 1
|
||||
logging_output = {
|
||||
"loss": utils.item(loss.data) if reduce else loss.data,
|
||||
"lm_loss": utils.item(lm_loss.data) if reduce else lm_loss.data,
|
||||
# sentence loss is not always computed
|
||||
"sentence_loss": (
|
||||
(utils.item(sentence_loss.data) if reduce else sentence_loss.data)
|
||||
if sentence_loss is not None
|
||||
else 0.0
|
||||
),
|
||||
"ntokens": ntokens,
|
||||
"nsentences": nsentences,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
lm_loss_sum = sum(log.get("lm_loss", 0) for log in logging_outputs)
|
||||
sentence_loss_sum = sum(log.get("sentence_loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
nsentences = sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
agg_loss = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss",
|
||||
agg_loss / sample_size / math.log(2) if sample_size > 0 else 0.0,
|
||||
sample_size,
|
||||
round=3,
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"lm_loss",
|
||||
lm_loss_sum / ntokens / math.log(2) if ntokens > 0 else 0.0,
|
||||
ntokens,
|
||||
round=3,
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"sentence_loss",
|
||||
sentence_loss_sum / nsentences / math.log(2) if nsentences > 0 else 0.0,
|
||||
nsentences,
|
||||
round=3,
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"nll_loss",
|
||||
lm_loss_sum / ntokens / math.log(2) if ntokens > 0 else 0.0,
|
||||
ntokens,
|
||||
round=3,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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 math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, modules, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
|
||||
|
||||
@register_criterion("masked_lm")
|
||||
class MaskedLmLoss(FairseqCriterion):
|
||||
"""
|
||||
Implementation for the loss used in masked language model (MLM) training.
|
||||
"""
|
||||
|
||||
def __init__(self, task, tpu=False):
|
||||
super().__init__(task)
|
||||
self.tpu = tpu
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
masked_tokens = sample["target"].ne(self.padding_idx)
|
||||
sample_size = masked_tokens.int().sum()
|
||||
|
||||
# Rare: when all tokens are masked, project all tokens.
|
||||
# We use torch.where to avoid device-to-host transfers,
|
||||
# except on CPU where torch.where is not well supported
|
||||
# (see github.com/pytorch/pytorch/issues/26247).
|
||||
if self.tpu:
|
||||
masked_tokens = None # always project all tokens on TPU
|
||||
elif masked_tokens.device == torch.device("cpu"):
|
||||
if not masked_tokens.any():
|
||||
masked_tokens = None
|
||||
else:
|
||||
masked_tokens = torch.where(
|
||||
masked_tokens.any(),
|
||||
masked_tokens,
|
||||
masked_tokens.new([True]),
|
||||
)
|
||||
|
||||
logits = model(**sample["net_input"], masked_tokens=masked_tokens)[0]
|
||||
targets = model.get_targets(sample, [logits])
|
||||
if masked_tokens is not None:
|
||||
targets = targets[masked_tokens]
|
||||
|
||||
loss = modules.cross_entropy(
|
||||
logits.view(-1, logits.size(-1)),
|
||||
targets.view(-1),
|
||||
reduction="sum",
|
||||
ignore_index=self.padding_idx,
|
||||
)
|
||||
|
||||
logging_output = {
|
||||
"loss": loss if self.tpu else loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["nsentences"],
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["loss"].avg)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelCriterionConfig(FairseqDataclass):
|
||||
loss_weights: Dict[str, float] = field(
|
||||
default_factory=dict,
|
||||
metadata={"help": "weights for the loss terms"},
|
||||
)
|
||||
log_keys: List[str] = field(
|
||||
default_factory=list,
|
||||
metadata={"help": "additional output keys to log"},
|
||||
)
|
||||
|
||||
|
||||
@register_criterion("model", dataclass=ModelCriterionConfig)
|
||||
class ModelCriterion(FairseqCriterion):
|
||||
"""
|
||||
This criterion relies on the model to supply losses.
|
||||
The losses should be a dictionary of name -> scalar returned by
|
||||
the model either by including it in the net_output dict or by
|
||||
implementing a get_losses(net_output, sample) method. The final loss is
|
||||
a scaled sum of all losses according to weights in loss_weights.
|
||||
If no weights are provided, then all losses are scaled by 1.0.
|
||||
|
||||
The losses will be automatically logged. Additional keys from
|
||||
net_output dict can be logged via the log_keys parameter.
|
||||
"""
|
||||
|
||||
def __init__(self, task, loss_weights=None, log_keys=None):
|
||||
super().__init__(task)
|
||||
self.loss_weights = loss_weights
|
||||
self.log_keys = log_keys
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
net_output = model(**sample["net_input"])
|
||||
|
||||
sample_size = net_output["sample_size"]
|
||||
scaled_losses = {}
|
||||
|
||||
if hasattr(model, "get_losses"):
|
||||
losses = model.get_losses(net_output, sample)
|
||||
elif isinstance(net_output, dict) and "losses" in net_output:
|
||||
losses = net_output["losses"]
|
||||
else:
|
||||
raise Exception("Could not retrieve losses")
|
||||
|
||||
for lk, p in losses.items():
|
||||
try:
|
||||
coef = 1.0 if len(self.loss_weights) == 0 else self.loss_weights[lk]
|
||||
except KeyError:
|
||||
logger.error(
|
||||
f"weight for loss {lk} is not in loss_weights ({self.loss_weights})"
|
||||
)
|
||||
raise
|
||||
if coef != 0 and p is not None:
|
||||
scaled_losses[lk] = coef * p.float()
|
||||
|
||||
loss = sum(scaled_losses.values())
|
||||
if reduce and loss.numel() > 1:
|
||||
loss = loss.sum()
|
||||
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": sample_size,
|
||||
"nsentences": sample["id"].numel(),
|
||||
"sample_size": sample_size,
|
||||
"_world_size": 1,
|
||||
}
|
||||
|
||||
for lk in self.log_keys:
|
||||
if lk in net_output and net_output[lk] is not None:
|
||||
logging_output[lk] = float(net_output[lk])
|
||||
|
||||
if len(scaled_losses) > 1:
|
||||
for lk, l in scaled_losses.items():
|
||||
logging_output[f"loss_{lk}"] = l.item()
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
ntokens = utils.item(sum(log.get("ntokens", 0) for log in logging_outputs))
|
||||
nsentences = utils.item(
|
||||
sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
)
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
metrics.log_scalar("loss", loss_sum / sample_size, sample_size, round=3)
|
||||
metrics.log_scalar("ntokens", ntokens)
|
||||
metrics.log_scalar("nsentences", nsentences)
|
||||
|
||||
builtin_keys = {
|
||||
"loss",
|
||||
"ntokens",
|
||||
"nsentences",
|
||||
"sample_size",
|
||||
"_world_size",
|
||||
}
|
||||
|
||||
world_size = utils.item(
|
||||
sum(log.get("_world_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
for k in logging_outputs[0]:
|
||||
if k not in builtin_keys:
|
||||
val = sum(log.get(k, 0) for log in logging_outputs)
|
||||
if k.startswith("loss_"):
|
||||
metrics.log_scalar(k, val / sample_size, sample_size, round=3)
|
||||
else:
|
||||
metrics.log_scalar(k, val / world_size, round=3)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,180 @@
|
||||
# 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 math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from torch import Tensor
|
||||
|
||||
|
||||
@register_criterion("nat_loss")
|
||||
class LabelSmoothedDualImitationCriterion(FairseqCriterion):
|
||||
def __init__(self, task, label_smoothing):
|
||||
super().__init__(task)
|
||||
self.label_smoothing = label_smoothing
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"--label-smoothing",
|
||||
default=0.0,
|
||||
type=float,
|
||||
metavar="D",
|
||||
help="epsilon for label smoothing, 0 means no label smoothing",
|
||||
)
|
||||
|
||||
def _compute_loss(
|
||||
self, outputs, targets, masks=None, label_smoothing=0.0, name="loss", factor=1.0
|
||||
):
|
||||
"""
|
||||
outputs: batch x len x d_model
|
||||
targets: batch x len
|
||||
masks: batch x len
|
||||
|
||||
policy_logprob: if there is some policy
|
||||
depends on the likelihood score as rewards.
|
||||
"""
|
||||
|
||||
def mean_ds(x: Tensor, dim=None) -> Tensor:
|
||||
return (
|
||||
x.float().mean().type_as(x)
|
||||
if dim is None
|
||||
else x.float().mean(dim).type_as(x)
|
||||
)
|
||||
|
||||
if masks is not None:
|
||||
outputs, targets = outputs[masks], targets[masks]
|
||||
|
||||
if masks is not None and not masks.any():
|
||||
nll_loss = torch.tensor(0)
|
||||
loss = nll_loss
|
||||
else:
|
||||
logits = F.log_softmax(outputs, dim=-1)
|
||||
if targets.dim() == 1:
|
||||
losses = F.nll_loss(logits, targets.to(logits.device), reduction="none")
|
||||
|
||||
else: # soft-labels
|
||||
losses = F.kl_div(logits, targets.to(logits.device), reduction="none")
|
||||
losses = losses.sum(-1)
|
||||
|
||||
nll_loss = mean_ds(losses)
|
||||
if label_smoothing > 0:
|
||||
loss = (
|
||||
nll_loss * (1 - label_smoothing) - mean_ds(logits) * label_smoothing
|
||||
)
|
||||
else:
|
||||
loss = nll_loss
|
||||
|
||||
loss = loss * factor
|
||||
return {"name": name, "loss": loss, "nll_loss": nll_loss, "factor": factor}
|
||||
|
||||
def _custom_loss(self, loss, name="loss", factor=1.0):
|
||||
return {"name": name, "loss": loss, "factor": factor}
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
nsentences, ntokens = sample["nsentences"], sample["ntokens"]
|
||||
|
||||
# B x T
|
||||
src_tokens, src_lengths = (
|
||||
sample["net_input"]["src_tokens"],
|
||||
sample["net_input"]["src_lengths"],
|
||||
)
|
||||
tgt_tokens, prev_output_tokens = sample["target"], sample["prev_target"]
|
||||
|
||||
outputs = model(src_tokens, src_lengths, prev_output_tokens, tgt_tokens)
|
||||
losses, nll_loss = [], []
|
||||
|
||||
for obj in outputs:
|
||||
if outputs[obj].get("loss", None) is None:
|
||||
_losses = self._compute_loss(
|
||||
outputs[obj].get("out"),
|
||||
outputs[obj].get("tgt"),
|
||||
outputs[obj].get("mask", None),
|
||||
outputs[obj].get("ls", 0.0),
|
||||
name=obj + "-loss",
|
||||
factor=outputs[obj].get("factor", 1.0),
|
||||
)
|
||||
else:
|
||||
_losses = self._custom_loss(
|
||||
outputs[obj].get("loss"),
|
||||
name=obj + "-loss",
|
||||
factor=outputs[obj].get("factor", 1.0),
|
||||
)
|
||||
|
||||
losses += [_losses]
|
||||
if outputs[obj].get("nll_loss", False):
|
||||
nll_loss += [_losses.get("nll_loss", 0.0)]
|
||||
|
||||
loss = sum(l["loss"] for l in losses)
|
||||
nll_loss = sum(l for l in nll_loss) if len(nll_loss) > 0 else loss.new_tensor(0)
|
||||
|
||||
# NOTE:
|
||||
# we don't need to use sample_size as denominator for the gradient
|
||||
# here sample_size is just used for logging
|
||||
sample_size = 1
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"nll_loss": nll_loss.data,
|
||||
"ntokens": ntokens,
|
||||
"nsentences": nsentences,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
for l in losses:
|
||||
logging_output[l["name"]] = (
|
||||
utils.item(l["loss"].data / l["factor"])
|
||||
if reduce
|
||||
else l[["loss"]].data / l["factor"]
|
||||
)
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
loss = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
nll_loss = utils.item(sum(log.get("nll_loss", 0) for log in logging_outputs))
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar(
|
||||
"nll_loss", nll_loss / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_derived(
|
||||
"ppl", lambda meters: utils.get_perplexity(meters["loss"].avg)
|
||||
)
|
||||
|
||||
for key in logging_outputs[0]:
|
||||
if key[-5:] == "-loss":
|
||||
val = sum(log.get(key, 0) for log in logging_outputs)
|
||||
metrics.log_scalar(
|
||||
key[:-5],
|
||||
val / sample_size / math.log(2) if sample_size > 0 else 0.0,
|
||||
sample_size,
|
||||
round=3,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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 math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
|
||||
|
||||
@register_criterion("sentence_prediction")
|
||||
class SentencePredictionCriterion(FairseqCriterion):
|
||||
def __init__(self, task, classification_head_name, regression_target):
|
||||
super().__init__(task)
|
||||
self.classification_head_name = classification_head_name
|
||||
self.regression_target = regression_target
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
# fmt: off
|
||||
parser.add_argument('--classification-head-name',
|
||||
default='sentence_classification_head',
|
||||
help='name of the classification head to use')
|
||||
# fmt: on
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
assert (
|
||||
hasattr(model, "classification_heads")
|
||||
and self.classification_head_name in model.classification_heads
|
||||
), "model must provide sentence classification head for --criterion=sentence_prediction"
|
||||
|
||||
logits, _ = model(
|
||||
**sample["net_input"],
|
||||
features_only=True,
|
||||
classification_head_name=self.classification_head_name,
|
||||
)
|
||||
targets = model.get_targets(sample, [logits]).view(-1)
|
||||
sample_size = targets.numel()
|
||||
|
||||
if not self.regression_target:
|
||||
lprobs = F.log_softmax(logits, dim=-1, dtype=torch.float32)
|
||||
loss = F.nll_loss(lprobs, targets, reduction="sum")
|
||||
else:
|
||||
logits = logits.view(-1).float()
|
||||
targets = targets.float()
|
||||
loss = F.mse_loss(logits, targets, reduction="sum")
|
||||
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample_size,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
if not self.regression_target:
|
||||
preds = logits.argmax(dim=1)
|
||||
logging_output["ncorrect"] = (preds == targets).sum()
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
nsentences = sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
if sample_size != ntokens:
|
||||
metrics.log_scalar(
|
||||
"nll_loss", loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
|
||||
if len(logging_outputs) > 0 and "ncorrect" in logging_outputs[0]:
|
||||
ncorrect = sum(log.get("ncorrect", 0) for log in logging_outputs)
|
||||
metrics.log_scalar(
|
||||
"accuracy", 100.0 * ncorrect / nsentences, nsentences, round=1
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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 math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
|
||||
|
||||
@register_criterion("sentence_ranking")
|
||||
class SentenceRankingCriterion(FairseqCriterion):
|
||||
def __init__(self, task, ranking_head_name, save_predictions, num_classes):
|
||||
super().__init__(task)
|
||||
self.ranking_head_name = ranking_head_name
|
||||
if save_predictions is not None:
|
||||
self.prediction_h = open(save_predictions, "w")
|
||||
else:
|
||||
self.prediction_h = None
|
||||
self.num_classes = num_classes
|
||||
|
||||
def __del__(self):
|
||||
if self.prediction_h is not None:
|
||||
self.prediction_h.close()
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
# fmt: off
|
||||
parser.add_argument('--save-predictions', metavar='FILE',
|
||||
help='file to save predictions to')
|
||||
parser.add_argument('--ranking-head-name',
|
||||
default='sentence_classification_head',
|
||||
help='name of the ranking head to use')
|
||||
# fmt: on
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute ranking loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
assert (
|
||||
hasattr(model, "classification_heads")
|
||||
and self.ranking_head_name in model.classification_heads
|
||||
), "model must provide sentence ranking head for --criterion=sentence_ranking"
|
||||
|
||||
scores = []
|
||||
for idx in range(self.num_classes):
|
||||
score, _ = model(
|
||||
**sample["net_input{idx}".format(idx=idx + 1)],
|
||||
classification_head_name=self.ranking_head_name,
|
||||
)
|
||||
scores.append(score)
|
||||
|
||||
logits = torch.cat(scores, dim=1)
|
||||
sample_size = logits.size(0)
|
||||
|
||||
if "target" in sample:
|
||||
targets = model.get_targets(sample, [logits]).view(-1)
|
||||
lprobs = F.log_softmax(logits, dim=-1, dtype=torch.float32)
|
||||
loss = F.nll_loss(lprobs, targets, reduction="sum")
|
||||
else:
|
||||
targets = None
|
||||
loss = torch.tensor(0.0, requires_grad=True)
|
||||
|
||||
if self.prediction_h is not None:
|
||||
preds = logits.argmax(dim=1)
|
||||
for i, (id, pred) in enumerate(zip(sample["id"].tolist(), preds.tolist())):
|
||||
if targets is not None:
|
||||
label = targets[i].item()
|
||||
print("{}\t{}\t{}".format(id, pred, label), file=self.prediction_h)
|
||||
else:
|
||||
print("{}\t{}".format(id, pred), file=self.prediction_h)
|
||||
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample_size,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
if targets is not None:
|
||||
logging_output["ncorrect"] = (logits.argmax(dim=1) == targets).sum()
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
nsentences = sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / sample_size / math.log(2), sample_size, round=3
|
||||
)
|
||||
if sample_size != ntokens:
|
||||
metrics.log_scalar(
|
||||
"nll_loss", loss_sum / ntokens / math.log(2), ntokens, round=3
|
||||
)
|
||||
|
||||
if len(logging_outputs) > 0 and "ncorrect" in logging_outputs[0]:
|
||||
ncorrect = sum(log.get("ncorrect", 0) for log in logging_outputs)
|
||||
metrics.log_scalar(
|
||||
"accuracy", 100.0 * ncorrect / nsentences, nsentences, round=1
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,198 @@
|
||||
# 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 math
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.criterions import FairseqCriterion, register_criterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.logging.meters import safe_round
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wav2VecCriterionConfig(FairseqDataclass):
|
||||
infonce: bool = field(
|
||||
default=False,
|
||||
metadata={
|
||||
"help": "if set, uses cross entropy instead of binary cross entropy (i.e. InfoNCE loss)"
|
||||
},
|
||||
)
|
||||
loss_weights: Optional[List[float]] = field(
|
||||
default=None,
|
||||
metadata={"help": "weights for additional loss terms (not first one)"},
|
||||
)
|
||||
log_keys: List[str] = field(
|
||||
default_factory=lambda: [],
|
||||
metadata={"help": "output keys to log"},
|
||||
)
|
||||
|
||||
|
||||
@register_criterion("wav2vec", dataclass=Wav2VecCriterionConfig)
|
||||
class Wav2vecCriterion(FairseqCriterion):
|
||||
def __init__(self, task, infonce=False, loss_weights=None, log_keys=None):
|
||||
super().__init__(task)
|
||||
self.infonce = infonce
|
||||
self.loss_weights = loss_weights
|
||||
self.log_keys = [] if log_keys is None else log_keys
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
"""Compute the loss for the given sample.
|
||||
|
||||
Returns a tuple with three elements:
|
||||
1) the loss
|
||||
2) the sample size, which is used as the denominator for the gradient
|
||||
3) logging outputs to display while training
|
||||
"""
|
||||
net_output = model(**sample["net_input"])
|
||||
logits = model.get_logits(net_output).float()
|
||||
target = model.get_targets(sample, net_output)
|
||||
|
||||
weights = None
|
||||
if hasattr(model, "get_target_weights") and not self.infonce:
|
||||
weights = model.get_target_weights(target, net_output)
|
||||
if torch.is_tensor(weights):
|
||||
weights = weights.float()
|
||||
|
||||
losses = []
|
||||
|
||||
if self.infonce:
|
||||
loss = F.cross_entropy(
|
||||
logits,
|
||||
target,
|
||||
reduction="sum" if reduce else "none",
|
||||
)
|
||||
else:
|
||||
loss = F.binary_cross_entropy_with_logits(
|
||||
logits,
|
||||
target.float(),
|
||||
weights,
|
||||
reduction="sum" if reduce else "none",
|
||||
)
|
||||
|
||||
sample_size = target.numel() if self.infonce else target.long().sum().item()
|
||||
losses.append(loss.detach().clone())
|
||||
|
||||
if self.loss_weights is not None:
|
||||
assert hasattr(model, "get_extra_losses")
|
||||
extra_losses = model.get_extra_losses(net_output)
|
||||
if torch.is_tensor(extra_losses):
|
||||
extra_losses = [extra_losses]
|
||||
if len(self.loss_weights) == 1 and len(extra_losses) != 1:
|
||||
self.loss_weights = [self.loss_weights[0]] * len(extra_losses)
|
||||
assert len(extra_losses) == len(
|
||||
self.loss_weights
|
||||
), f"{len(extra_losses)}, {len(self.loss_weights)}"
|
||||
for p, coef in zip(extra_losses, self.loss_weights):
|
||||
if coef != 0 and p is not None:
|
||||
p = coef * p.float() * sample_size
|
||||
loss += p
|
||||
losses.append(p)
|
||||
|
||||
logging_output = {
|
||||
"loss": loss.item() if reduce else loss,
|
||||
"ntokens": sample_size,
|
||||
"nsentences": sample["id"].numel(),
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
for lk in self.log_keys:
|
||||
# Only store "logits" and "target" for computing MAP and MAUC
|
||||
# during validation
|
||||
if lk == "logits":
|
||||
if not self.training:
|
||||
logging_output["logits"] = logits.cpu().numpy()
|
||||
elif lk == "target":
|
||||
if not self.training:
|
||||
logging_output["target"] = target.cpu().numpy()
|
||||
elif lk in net_output:
|
||||
logging_output[lk] = float(net_output[lk])
|
||||
|
||||
if len(losses) > 1:
|
||||
for i, l in enumerate(losses):
|
||||
logging_output[f"loss_{i}"] = l.item()
|
||||
|
||||
if self.infonce:
|
||||
with torch.no_grad():
|
||||
if logits.numel() == 0:
|
||||
corr = 0
|
||||
count = 0
|
||||
else:
|
||||
assert logits.dim() > 1, logits.shape
|
||||
max = logits.argmax(-1) == 0
|
||||
min = logits.argmin(-1) == 0
|
||||
both = max & min
|
||||
corr = max.long().sum().item() - both.long().sum().item()
|
||||
count = max.numel()
|
||||
|
||||
logging_output["correct"] = corr
|
||||
logging_output["count"] = count
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def reduce_metrics(logging_outputs) -> None:
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = utils.item(sum(log.get("loss", 0) for log in logging_outputs))
|
||||
ntokens = utils.item(sum(log.get("ntokens", 0) for log in logging_outputs))
|
||||
nsentences = utils.item(
|
||||
sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
)
|
||||
sample_size = utils.item(
|
||||
sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
)
|
||||
|
||||
metrics.log_scalar(
|
||||
"loss", loss_sum / (sample_size or 1) / math.log(2), sample_size, round=3
|
||||
)
|
||||
metrics.log_scalar("ntokens", ntokens)
|
||||
metrics.log_scalar("nsentences", nsentences)
|
||||
|
||||
correct = sum(log.get("correct", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_correct", correct)
|
||||
|
||||
total = sum(log.get("count", 0) for log in logging_outputs)
|
||||
metrics.log_scalar("_total", total)
|
||||
|
||||
if total > 0:
|
||||
metrics.log_derived(
|
||||
"accuracy",
|
||||
lambda meters: safe_round(
|
||||
meters["_correct"].sum / meters["_total"].sum, 5
|
||||
)
|
||||
if meters["_total"].sum > 0
|
||||
else float("nan"),
|
||||
)
|
||||
|
||||
builtin_keys = {
|
||||
"loss",
|
||||
"ntokens",
|
||||
"nsentences",
|
||||
"sample_size",
|
||||
"correct",
|
||||
"count",
|
||||
}
|
||||
|
||||
for k in logging_outputs[0]:
|
||||
if k not in builtin_keys:
|
||||
val = sum(log.get(k, 0) for log in logging_outputs)
|
||||
if k.startswith("loss"):
|
||||
metrics.log_scalar(
|
||||
k, val / (sample_size or 1) / math.log(2), sample_size, round=3
|
||||
)
|
||||
else:
|
||||
metrics.log_scalar(k, val / len(logging_outputs), round=3)
|
||||
|
||||
@staticmethod
|
||||
def logging_outputs_can_be_summed() -> bool:
|
||||
"""
|
||||
Whether the logging outputs returned by `forward` can be summed
|
||||
across workers prior to calling `reduce_metrics`. Setting this
|
||||
to True will improves distributed training speed.
|
||||
"""
|
||||
return False
|
||||
@@ -0,0 +1,124 @@
|
||||
# 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"""
|
||||
|
||||
from .dictionary import Dictionary, TruncatedDictionary
|
||||
|
||||
from .fairseq_dataset import FairseqDataset, FairseqIterableDataset
|
||||
|
||||
from .base_wrapper_dataset import BaseWrapperDataset
|
||||
|
||||
from .add_target_dataset import AddTargetDataset
|
||||
from .append_token_dataset import AppendTokenDataset
|
||||
from .audio.raw_audio_dataset import FileAudioDataset
|
||||
from .backtranslation_dataset import BacktranslationDataset
|
||||
from .bucket_pad_length_dataset import BucketPadLengthDataset
|
||||
from .colorize_dataset import ColorizeDataset
|
||||
from .concat_dataset import ConcatDataset
|
||||
from .concat_sentences_dataset import ConcatSentencesDataset
|
||||
from .denoising_dataset import DenoisingDataset
|
||||
from .id_dataset import IdDataset
|
||||
from .indexed_dataset import (
|
||||
IndexedCachedDataset,
|
||||
IndexedDataset,
|
||||
IndexedRawTextDataset,
|
||||
MMapIndexedDataset,
|
||||
)
|
||||
from .language_pair_dataset import LanguagePairDataset
|
||||
from .list_dataset import ListDataset
|
||||
from .lm_context_window_dataset import LMContextWindowDataset
|
||||
from .lru_cache_dataset import LRUCacheDataset
|
||||
from .mask_tokens_dataset import MaskTokensDataset
|
||||
from .monolingual_dataset import MonolingualDataset
|
||||
from .multi_corpus_sampled_dataset import MultiCorpusSampledDataset
|
||||
from .nested_dictionary_dataset import NestedDictionaryDataset
|
||||
from .noising import NoisingDataset
|
||||
from .numel_dataset import NumelDataset
|
||||
from .num_samples_dataset import NumSamplesDataset
|
||||
from .offset_tokens_dataset import OffsetTokensDataset
|
||||
from .pad_dataset import LeftPadDataset, PadDataset, RightPadDataset
|
||||
from .prepend_dataset import PrependDataset
|
||||
from .prepend_token_dataset import PrependTokenDataset
|
||||
from .raw_label_dataset import RawLabelDataset
|
||||
from .replace_dataset import ReplaceDataset
|
||||
from .resampling_dataset import ResamplingDataset
|
||||
from .roll_dataset import RollDataset
|
||||
from .round_robin_zip_datasets import RoundRobinZipDatasets
|
||||
from .sort_dataset import SortDataset
|
||||
from .strip_token_dataset import StripTokenDataset
|
||||
from .subsample_dataset import SubsampleDataset
|
||||
from .token_block_dataset import TokenBlockDataset
|
||||
from .transform_eos_dataset import TransformEosDataset
|
||||
from .transform_eos_lang_pair_dataset import TransformEosLangPairDataset
|
||||
from .shorten_dataset import TruncateDataset, RandomCropDataset
|
||||
from .multilingual.sampled_multi_dataset import SampledMultiDataset
|
||||
from .multilingual.sampled_multi_epoch_dataset import SampledMultiEpochDataset
|
||||
from .fasta_dataset import FastaDataset, EncodedFastaDataset
|
||||
|
||||
from .iterators import (
|
||||
CountingIterator,
|
||||
EpochBatchIterator,
|
||||
GroupedIterator,
|
||||
ShardedIterator,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AddTargetDataset",
|
||||
"AppendTokenDataset",
|
||||
"BacktranslationDataset",
|
||||
"BaseWrapperDataset",
|
||||
"BucketPadLengthDataset",
|
||||
"ColorizeDataset",
|
||||
"ConcatDataset",
|
||||
"ConcatSentencesDataset",
|
||||
"CountingIterator",
|
||||
"DenoisingDataset",
|
||||
"Dictionary",
|
||||
"EncodedFastaDataset",
|
||||
"EpochBatchIterator",
|
||||
"FairseqDataset",
|
||||
"FairseqIterableDataset",
|
||||
"FastaDataset",
|
||||
"GroupedIterator",
|
||||
"IdDataset",
|
||||
"IndexedCachedDataset",
|
||||
"IndexedDataset",
|
||||
"IndexedRawTextDataset",
|
||||
"LanguagePairDataset",
|
||||
"LeftPadDataset",
|
||||
"ListDataset",
|
||||
"LMContextWindowDataset",
|
||||
"LRUCacheDataset",
|
||||
"MaskTokensDataset",
|
||||
"MMapIndexedDataset",
|
||||
"MonolingualDataset",
|
||||
"MultiCorpusSampledDataset",
|
||||
"NestedDictionaryDataset",
|
||||
"NoisingDataset",
|
||||
"NumelDataset",
|
||||
"NumSamplesDataset",
|
||||
"OffsetTokensDataset",
|
||||
"PadDataset",
|
||||
"PrependDataset",
|
||||
"PrependTokenDataset",
|
||||
"ReplaceDataset",
|
||||
"RollDataset",
|
||||
"FileAudioDataset",
|
||||
"RawLabelDataset",
|
||||
"ResamplingDataset",
|
||||
"RightPadDataset",
|
||||
"RoundRobinZipDatasets",
|
||||
"SampledMultiDataset",
|
||||
"SampledMultiEpochDataset",
|
||||
"ShardedIterator",
|
||||
"SortDataset",
|
||||
"StripTokenDataset",
|
||||
"SubsampleDataset",
|
||||
"TokenBlockDataset",
|
||||
"TransformEosDataset",
|
||||
"TransformEosLangPairDataset",
|
||||
"TruncateDataset",
|
||||
"TruncatedDictionary",
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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 . import BaseWrapperDataset, data_utils
|
||||
|
||||
|
||||
class AddTargetDataset(BaseWrapperDataset):
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
labels,
|
||||
pad,
|
||||
eos,
|
||||
batch_targets,
|
||||
process_label=None,
|
||||
add_to_input=False,
|
||||
):
|
||||
super().__init__(dataset)
|
||||
self.labels = labels
|
||||
self.batch_targets = batch_targets
|
||||
self.pad = pad
|
||||
self.eos = eos
|
||||
self.process_label = process_label
|
||||
self.add_to_input = add_to_input
|
||||
|
||||
def get_label(self, index):
|
||||
return (
|
||||
self.labels[index]
|
||||
if self.process_label is None
|
||||
else self.process_label(self.labels[index])
|
||||
)
|
||||
|
||||
def __getitem__(self, index):
|
||||
item = self.dataset[index]
|
||||
item["label"] = self.get_label(index)
|
||||
return item
|
||||
|
||||
def size(self, index):
|
||||
sz = self.dataset.size(index)
|
||||
own_sz = len(self.get_label(index))
|
||||
return (sz, own_sz)
|
||||
|
||||
def collater(self, samples):
|
||||
collated = self.dataset.collater(samples)
|
||||
if len(collated) == 0:
|
||||
return collated
|
||||
indices = set(collated["id"].tolist())
|
||||
target = [s["label"] for s in samples if s["id"] in indices]
|
||||
|
||||
if self.batch_targets:
|
||||
collated["target_lengths"] = torch.LongTensor([len(t) for t in target])
|
||||
target = data_utils.collate_tokens(target, pad_idx=self.pad, left_pad=False)
|
||||
collated["ntokens"] = collated["target_lengths"].sum().item()
|
||||
else:
|
||||
collated["ntokens"] = sum([len(t) for t in target])
|
||||
|
||||
collated["target"] = target
|
||||
|
||||
if self.add_to_input:
|
||||
eos = target.new_full((target.size(0), 1), self.eos)
|
||||
collated["target"] = torch.cat([target, eos], dim=-1).long()
|
||||
collated["net_input"]["prev_output_tokens"] = torch.cat(
|
||||
[eos, target], dim=-1
|
||||
).long()
|
||||
collated["ntokens"] += target.size(0)
|
||||
return collated
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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 numpy as np
|
||||
import torch
|
||||
|
||||
from . import BaseWrapperDataset
|
||||
|
||||
|
||||
class AppendTokenDataset(BaseWrapperDataset):
|
||||
def __init__(self, dataset, token=None):
|
||||
super().__init__(dataset)
|
||||
self.token = token
|
||||
if token is not None:
|
||||
self._sizes = np.array(dataset.sizes) + 1
|
||||
else:
|
||||
self._sizes = dataset.sizes
|
||||
|
||||
def __getitem__(self, idx):
|
||||
item = self.dataset[idx]
|
||||
if self.token is not None:
|
||||
item = torch.cat([item, item.new([self.token])])
|
||||
return item
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return self._sizes
|
||||
|
||||
def num_tokens(self, index):
|
||||
n = self.dataset.num_tokens(index)
|
||||
if self.token is not None:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
def size(self, index):
|
||||
n = self.dataset.size(index)
|
||||
if self.token is not None:
|
||||
n += 1
|
||||
return n
|
||||
@@ -0,0 +1,93 @@
|
||||
import os.path as op
|
||||
from typing import BinaryIO, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def get_waveform(
|
||||
path_or_fp: Union[str, BinaryIO], normalization=True
|
||||
) -> Tuple[np.ndarray, int]:
|
||||
"""Get the waveform and sample rate of a 16-bit mono-channel WAV or FLAC.
|
||||
|
||||
Args:
|
||||
path_or_fp (str or BinaryIO): the path or file-like object
|
||||
normalization (bool): Normalize values to [-1, 1] (Default: True)
|
||||
"""
|
||||
if isinstance(path_or_fp, str):
|
||||
ext = op.splitext(op.basename(path_or_fp))[1]
|
||||
if ext not in {".flac", ".wav"}:
|
||||
raise ValueError(f"Unsupported audio format: {ext}")
|
||||
|
||||
try:
|
||||
import soundfile as sf
|
||||
except ImportError:
|
||||
raise ImportError("Please install soundfile to load WAV/FLAC file")
|
||||
|
||||
waveform, sample_rate = sf.read(path_or_fp, dtype="float32")
|
||||
if not normalization:
|
||||
waveform *= 2 ** 15 # denormalized to 16-bit signed integers
|
||||
return waveform, sample_rate
|
||||
|
||||
|
||||
def _get_kaldi_fbank(waveform, sample_rate, n_bins=80) -> Optional[np.ndarray]:
|
||||
"""Get mel-filter bank features via PyKaldi."""
|
||||
try:
|
||||
from kaldi.feat.mel import MelBanksOptions
|
||||
from kaldi.feat.fbank import FbankOptions, Fbank
|
||||
from kaldi.feat.window import FrameExtractionOptions
|
||||
from kaldi.matrix import Vector
|
||||
|
||||
mel_opts = MelBanksOptions()
|
||||
mel_opts.num_bins = n_bins
|
||||
frame_opts = FrameExtractionOptions()
|
||||
frame_opts.samp_freq = sample_rate
|
||||
opts = FbankOptions()
|
||||
opts.mel_opts = mel_opts
|
||||
opts.frame_opts = frame_opts
|
||||
fbank = Fbank(opts=opts)
|
||||
features = fbank.compute(Vector(waveform), 1.0).numpy()
|
||||
return features
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _get_torchaudio_fbank(waveform, sample_rate, n_bins=80) -> Optional[np.ndarray]:
|
||||
"""Get mel-filter bank features via TorchAudio."""
|
||||
try:
|
||||
import torch
|
||||
import torchaudio.compliance.kaldi as ta_kaldi
|
||||
import torchaudio.sox_effects as ta_sox
|
||||
|
||||
waveform = torch.from_numpy(waveform)
|
||||
if len(waveform.shape) == 1:
|
||||
# Mono channel: D -> 1 x D
|
||||
waveform = waveform.unsqueeze(0)
|
||||
else:
|
||||
# Merge multiple channels to one: C x D -> 1 x D
|
||||
waveform, _ = ta_sox.apply_effects_tensor(waveform, sample_rate, ['channels', '1'])
|
||||
|
||||
features = ta_kaldi.fbank(
|
||||
waveform, num_mel_bins=n_bins, sample_frequency=sample_rate
|
||||
)
|
||||
return features.numpy()
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def get_fbank(path_or_fp: Union[str, BinaryIO], n_bins=80) -> np.ndarray:
|
||||
"""Get mel-filter bank features via PyKaldi or TorchAudio. Prefer PyKaldi
|
||||
(faster CPP implementation) to TorchAudio (Python implementation). Note that
|
||||
Kaldi/TorchAudio requires 16-bit signed integers as inputs and hence the
|
||||
waveform should not be normalized."""
|
||||
sound, sample_rate = get_waveform(path_or_fp, normalization=False)
|
||||
|
||||
features = _get_kaldi_fbank(sound, sample_rate, n_bins)
|
||||
if features is None:
|
||||
features = _get_torchaudio_fbank(sound, sample_rate, n_bins)
|
||||
if features is None:
|
||||
raise ImportError(
|
||||
"Please install pyKaldi or torchaudio to enable "
|
||||
"online filterbank feature extraction"
|
||||
)
|
||||
|
||||
return features
|
||||
@@ -0,0 +1,82 @@
|
||||
import importlib
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class AudioFeatureTransform(ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def from_config_dict(cls, config: Optional[Dict] = None):
|
||||
pass
|
||||
|
||||
|
||||
AUDIO_FEATURE_TRANSFORM_REGISTRY = {}
|
||||
AUDIO_FEATURE_TRANSFORM_CLASS_NAMES = set()
|
||||
|
||||
|
||||
def register_audio_feature_transform(name):
|
||||
def register_audio_feature_transform_cls(cls):
|
||||
if name in AUDIO_FEATURE_TRANSFORM_REGISTRY:
|
||||
raise ValueError(f"Cannot register duplicate transform ({name})")
|
||||
if not issubclass(cls, AudioFeatureTransform):
|
||||
raise ValueError(
|
||||
f"Transform ({name}: {cls.__name__}) must extend "
|
||||
"AudioFeatureTransform"
|
||||
)
|
||||
if cls.__name__ in AUDIO_FEATURE_TRANSFORM_CLASS_NAMES:
|
||||
raise ValueError(
|
||||
f"Cannot register audio feature transform with duplicate "
|
||||
f"class name ({cls.__name__})"
|
||||
)
|
||||
AUDIO_FEATURE_TRANSFORM_REGISTRY[name] = cls
|
||||
AUDIO_FEATURE_TRANSFORM_CLASS_NAMES.add(cls.__name__)
|
||||
return cls
|
||||
|
||||
return register_audio_feature_transform_cls
|
||||
|
||||
|
||||
def get_audio_feature_transform(name):
|
||||
return AUDIO_FEATURE_TRANSFORM_REGISTRY[name]
|
||||
|
||||
|
||||
transforms_dir = os.path.dirname(__file__)
|
||||
for file in os.listdir(transforms_dir):
|
||||
path = os.path.join(transforms_dir, file)
|
||||
if (
|
||||
not file.startswith("_")
|
||||
and not file.startswith(".")
|
||||
and (file.endswith(".py") or os.path.isdir(path))
|
||||
):
|
||||
name = file[: file.find(".py")] if file.endswith(".py") else file
|
||||
importlib.import_module("fairseq.data.audio.feature_transforms." + name)
|
||||
|
||||
|
||||
class CompositeAudioFeatureTransform(AudioFeatureTransform):
|
||||
@classmethod
|
||||
def from_config_dict(cls, config=None):
|
||||
_config = {} if config is None else config
|
||||
_transforms = _config.get("transforms")
|
||||
if _transforms is None:
|
||||
return None
|
||||
transforms = [
|
||||
get_audio_feature_transform(_t).from_config_dict(_config.get(_t))
|
||||
for _t in _transforms
|
||||
]
|
||||
return CompositeAudioFeatureTransform(transforms)
|
||||
|
||||
def __init__(self, transforms):
|
||||
self.transforms = [t for t in transforms if t is not None]
|
||||
|
||||
def __call__(self, x):
|
||||
for t in self.transforms:
|
||||
x = t(x)
|
||||
return x
|
||||
|
||||
def __repr__(self):
|
||||
format_string = (
|
||||
[self.__class__.__name__ + "("]
|
||||
+ [f" {t.__repr__()}" for t in self.transforms]
|
||||
+ [")"]
|
||||
)
|
||||
return "\n".join(format_string)
|
||||
@@ -0,0 +1,29 @@
|
||||
import numpy as np
|
||||
from fairseq.data.audio.feature_transforms import (
|
||||
AudioFeatureTransform,
|
||||
register_audio_feature_transform,
|
||||
)
|
||||
|
||||
|
||||
@register_audio_feature_transform("global_cmvn")
|
||||
class GlobalCMVN(AudioFeatureTransform):
|
||||
"""Global CMVN (cepstral mean and variance normalization). The global mean
|
||||
and variance need to be pre-computed and stored in NumPy format (.npz)."""
|
||||
|
||||
@classmethod
|
||||
def from_config_dict(cls, config=None):
|
||||
_config = {} if config is None else config
|
||||
return GlobalCMVN(_config.get("stats_npz_path"))
|
||||
|
||||
def __init__(self, stats_npz_path):
|
||||
self.stats_npz_path = stats_npz_path
|
||||
stats = np.load(stats_npz_path)
|
||||
self.mean, self.std = stats["mean"], stats["std"]
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__ + f'(stats_npz_path="{self.stats_npz_path}")'
|
||||
|
||||
def __call__(self, x):
|
||||
x = np.subtract(x, self.mean)
|
||||
x = np.divide(x, self.std)
|
||||
return x
|
||||
@@ -0,0 +1,131 @@
|
||||
import math
|
||||
import numbers
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from fairseq.data.audio.feature_transforms import (
|
||||
AudioFeatureTransform,
|
||||
register_audio_feature_transform,
|
||||
)
|
||||
|
||||
|
||||
@register_audio_feature_transform("specaugment")
|
||||
class SpecAugmentTransform(AudioFeatureTransform):
|
||||
"""SpecAugment (https://arxiv.org/abs/1904.08779)"""
|
||||
|
||||
@classmethod
|
||||
def from_config_dict(cls, config=None):
|
||||
_config = {} if config is None else config
|
||||
return SpecAugmentTransform(
|
||||
_config.get("time_warp_W", 0),
|
||||
_config.get("freq_mask_N", 0),
|
||||
_config.get("freq_mask_F", 0),
|
||||
_config.get("time_mask_N", 0),
|
||||
_config.get("time_mask_T", 0),
|
||||
_config.get("time_mask_p", 0.0),
|
||||
_config.get("mask_value", None),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
time_warp_w: int = 0,
|
||||
freq_mask_n: int = 0,
|
||||
freq_mask_f: int = 0,
|
||||
time_mask_n: int = 0,
|
||||
time_mask_t: int = 0,
|
||||
time_mask_p: float = 0.0,
|
||||
mask_value: Optional[float] = 0.0,
|
||||
):
|
||||
# Sanity checks
|
||||
assert mask_value is None or isinstance(
|
||||
mask_value, numbers.Number
|
||||
), f"mask_value (type: {type(mask_value)}) must be None or a number"
|
||||
if freq_mask_n > 0:
|
||||
assert freq_mask_f > 0, (
|
||||
f"freq_mask_F ({freq_mask_f}) "
|
||||
f"must be larger than 0 when doing freq masking."
|
||||
)
|
||||
if time_mask_n > 0:
|
||||
assert time_mask_t > 0, (
|
||||
f"time_mask_T ({time_mask_t}) must be larger than 0 when "
|
||||
f"doing time masking."
|
||||
)
|
||||
|
||||
self.time_warp_w = time_warp_w
|
||||
self.freq_mask_n = freq_mask_n
|
||||
self.freq_mask_f = freq_mask_f
|
||||
self.time_mask_n = time_mask_n
|
||||
self.time_mask_t = time_mask_t
|
||||
self.time_mask_p = time_mask_p
|
||||
self.mask_value = mask_value
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
self.__class__.__name__
|
||||
+ "("
|
||||
+ ", ".join(
|
||||
[
|
||||
f"time_warp_w={self.time_warp_w}",
|
||||
f"freq_mask_n={self.freq_mask_n}",
|
||||
f"freq_mask_f={self.freq_mask_f}",
|
||||
f"time_mask_n={self.time_mask_n}",
|
||||
f"time_mask_t={self.time_mask_t}",
|
||||
f"time_mask_p={self.time_mask_p}",
|
||||
]
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
def __call__(self, spectrogram):
|
||||
assert len(spectrogram.shape) == 2, "spectrogram must be a 2-D tensor."
|
||||
|
||||
distorted = spectrogram.copy() # make a copy of input spectrogram.
|
||||
num_frames = spectrogram.shape[0] # or 'tau' in the paper.
|
||||
num_freqs = spectrogram.shape[1] # or 'miu' in the paper.
|
||||
mask_value = self.mask_value
|
||||
|
||||
if mask_value is None: # if no value was specified, use local mean.
|
||||
mask_value = spectrogram.mean()
|
||||
|
||||
if num_frames == 0:
|
||||
return spectrogram
|
||||
|
||||
if num_freqs < self.freq_mask_f:
|
||||
return spectrogram
|
||||
|
||||
if self.time_warp_w > 0:
|
||||
if 2 * self.time_warp_w < num_frames:
|
||||
import cv2
|
||||
|
||||
w0 = np.random.randint(self.time_warp_w, num_frames - self.time_warp_w)
|
||||
w = np.random.randint(-self.time_warp_w + 1, self.time_warp_w)
|
||||
upper, lower = distorted[:w0, :], distorted[w0:, :]
|
||||
upper = cv2.resize(
|
||||
upper, dsize=(num_freqs, w0 + w), interpolation=cv2.INTER_LINEAR
|
||||
)
|
||||
lower = cv2.resize(
|
||||
lower,
|
||||
dsize=(num_freqs, num_frames - w0 - w),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
distorted = np.concatenate((upper, lower), axis=0)
|
||||
|
||||
for _i in range(self.freq_mask_n):
|
||||
f = np.random.randint(0, self.freq_mask_f)
|
||||
f0 = np.random.randint(0, num_freqs - f)
|
||||
if f != 0:
|
||||
distorted[:, f0 : f0 + f] = mask_value
|
||||
|
||||
max_time_mask_t = min(
|
||||
self.time_mask_t, math.floor(num_frames * self.time_mask_p)
|
||||
)
|
||||
if max_time_mask_t < 1:
|
||||
return distorted
|
||||
|
||||
for _i in range(self.time_mask_n):
|
||||
t = np.random.randint(0, max_time_mask_t)
|
||||
t0 = np.random.randint(0, num_frames - t)
|
||||
if t != 0:
|
||||
distorted[t0 : t0 + t, :] = mask_value
|
||||
|
||||
return distorted
|
||||
@@ -0,0 +1,40 @@
|
||||
import numpy as np
|
||||
from fairseq.data.audio.feature_transforms import (
|
||||
AudioFeatureTransform,
|
||||
register_audio_feature_transform,
|
||||
)
|
||||
|
||||
|
||||
@register_audio_feature_transform("utterance_cmvn")
|
||||
class UtteranceCMVN(AudioFeatureTransform):
|
||||
"""Utterance-level CMVN (cepstral mean and variance normalization)"""
|
||||
|
||||
@classmethod
|
||||
def from_config_dict(cls, config=None):
|
||||
_config = {} if config is None else config
|
||||
return UtteranceCMVN(
|
||||
_config.get("norm_means", True),
|
||||
_config.get("norm_vars", True),
|
||||
)
|
||||
|
||||
def __init__(self, norm_means=True, norm_vars=True):
|
||||
self.norm_means, self.norm_vars = norm_means, norm_vars
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
self.__class__.__name__
|
||||
+ f"(norm_means={self.norm_means}, norm_vars={self.norm_vars})"
|
||||
)
|
||||
|
||||
def __call__(self, x):
|
||||
mean = x.mean(axis=0)
|
||||
square_sums = (x ** 2).sum(axis=0)
|
||||
|
||||
if self.norm_means:
|
||||
x = np.subtract(x, mean)
|
||||
if self.norm_vars:
|
||||
var = square_sums / x.shape[0] - mean ** 2
|
||||
std = np.sqrt(np.maximum(var, 1e-10))
|
||||
x = np.divide(x, std)
|
||||
|
||||
return x
|
||||
@@ -0,0 +1,176 @@
|
||||
# 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 sys
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .. import FairseqDataset
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RawAudioDataset(FairseqDataset):
|
||||
def __init__(
|
||||
self,
|
||||
sample_rate,
|
||||
max_sample_size=None,
|
||||
min_sample_size=0,
|
||||
shuffle=True,
|
||||
pad=False,
|
||||
normalize=False,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.sample_rate = sample_rate
|
||||
self.sizes = []
|
||||
self.max_sample_size = (
|
||||
max_sample_size if max_sample_size is not None else sys.maxsize
|
||||
)
|
||||
self.min_sample_size = min_sample_size
|
||||
self.pad = pad
|
||||
self.shuffle = shuffle
|
||||
self.normalize = normalize
|
||||
|
||||
def __getitem__(self, index):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.sizes)
|
||||
|
||||
def postprocess(self, feats, curr_sample_rate):
|
||||
if feats.dim() == 2:
|
||||
feats = feats.mean(-1)
|
||||
|
||||
if curr_sample_rate != self.sample_rate:
|
||||
raise Exception(f"sample rate: {curr_sample_rate}, need {self.sample_rate}")
|
||||
|
||||
assert feats.dim() == 1, feats.dim()
|
||||
|
||||
if self.normalize:
|
||||
with torch.no_grad():
|
||||
feats = F.layer_norm(feats, feats.shape)
|
||||
return feats
|
||||
|
||||
def crop_to_max_size(self, wav, target_size):
|
||||
size = len(wav)
|
||||
diff = size - target_size
|
||||
if diff <= 0:
|
||||
return wav
|
||||
|
||||
start = np.random.randint(0, diff + 1)
|
||||
end = size - diff + start
|
||||
return wav[start:end]
|
||||
|
||||
def collater(self, samples):
|
||||
samples = [s for s in samples if s["source"] is not None]
|
||||
if len(samples) == 0:
|
||||
return {}
|
||||
|
||||
sources = [s["source"] for s in samples]
|
||||
sizes = [len(s) for s in sources]
|
||||
|
||||
if self.pad:
|
||||
target_size = min(max(sizes), self.max_sample_size)
|
||||
else:
|
||||
target_size = min(min(sizes), self.max_sample_size)
|
||||
|
||||
collated_sources = sources[0].new_zeros(len(sources), target_size)
|
||||
padding_mask = (
|
||||
torch.BoolTensor(collated_sources.shape).fill_(False) if self.pad else None
|
||||
)
|
||||
for i, (source, size) in enumerate(zip(sources, sizes)):
|
||||
diff = size - target_size
|
||||
if diff == 0:
|
||||
collated_sources[i] = source
|
||||
elif diff < 0:
|
||||
assert self.pad
|
||||
collated_sources[i] = torch.cat(
|
||||
[source, source.new_full((-diff,), 0.0)]
|
||||
)
|
||||
padding_mask[i, diff:] = True
|
||||
else:
|
||||
collated_sources[i] = self.crop_to_max_size(source, target_size)
|
||||
|
||||
input = {"source": collated_sources}
|
||||
if self.pad:
|
||||
input["padding_mask"] = padding_mask
|
||||
return {"id": torch.LongTensor([s["id"] for s in samples]), "net_input": input}
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.size(index)
|
||||
|
||||
def size(self, index):
|
||||
"""Return an example's size as a float or tuple. This value is used when
|
||||
filtering a dataset with ``--max-positions``."""
|
||||
if self.pad:
|
||||
return self.sizes[index]
|
||||
return min(self.sizes[index], self.max_sample_size)
|
||||
|
||||
def ordered_indices(self):
|
||||
"""Return an ordered list of indices. Batches will be constructed based
|
||||
on this order."""
|
||||
|
||||
if self.shuffle:
|
||||
order = [np.random.permutation(len(self))]
|
||||
else:
|
||||
order = [np.arange(len(self))]
|
||||
|
||||
order.append(self.sizes)
|
||||
return np.lexsort(order)[::-1]
|
||||
|
||||
|
||||
class FileAudioDataset(RawAudioDataset):
|
||||
def __init__(
|
||||
self,
|
||||
manifest_path,
|
||||
sample_rate,
|
||||
max_sample_size=None,
|
||||
min_sample_size=0,
|
||||
shuffle=True,
|
||||
pad=False,
|
||||
normalize=False,
|
||||
):
|
||||
super().__init__(
|
||||
sample_rate=sample_rate,
|
||||
max_sample_size=max_sample_size,
|
||||
min_sample_size=min_sample_size,
|
||||
shuffle=shuffle,
|
||||
pad=pad,
|
||||
normalize=normalize,
|
||||
)
|
||||
|
||||
self.fnames = []
|
||||
self.line_inds = set()
|
||||
|
||||
skipped = 0
|
||||
with open(manifest_path, "r") as f:
|
||||
self.root_dir = f.readline().strip()
|
||||
for i, line in enumerate(f):
|
||||
items = line.strip().split("\t")
|
||||
assert len(items) == 2, line
|
||||
sz = int(items[1])
|
||||
if min_sample_size is not None and sz < min_sample_size:
|
||||
skipped += 1
|
||||
continue
|
||||
self.fnames.append(items[0])
|
||||
self.line_inds.add(i)
|
||||
self.sizes.append(sz)
|
||||
logger.info(f"loaded {len(self.fnames)}, skipped {skipped} samples")
|
||||
|
||||
def __getitem__(self, index):
|
||||
import soundfile as sf
|
||||
|
||||
fname = os.path.join(self.root_dir, self.fnames[index])
|
||||
wav, curr_sample_rate = sf.read(fname)
|
||||
feats = torch.from_numpy(wav).float()
|
||||
feats = self.postprocess(feats, curr_sample_rate)
|
||||
return {"id": index, "source": feats}
|
||||
@@ -0,0 +1,528 @@
|
||||
# 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 csv
|
||||
import io
|
||||
import logging
|
||||
import os.path as op
|
||||
import re
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq.data import (
|
||||
ConcatDataset,
|
||||
Dictionary,
|
||||
FairseqDataset,
|
||||
ResamplingDataset,
|
||||
data_utils as fairseq_data_utils,
|
||||
)
|
||||
from fairseq.data.audio.audio_utils import get_fbank, get_waveform
|
||||
from fairseq.data.audio.feature_transforms import CompositeAudioFeatureTransform
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class S2TDataConfig(object):
|
||||
"""Wrapper class for data config YAML"""
|
||||
|
||||
def __init__(self, yaml_path):
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("Please install PyYAML to load YAML files for " "S2T data config")
|
||||
self.config = {}
|
||||
if op.isfile(yaml_path):
|
||||
try:
|
||||
with open(yaml_path) as f:
|
||||
self.config = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
logger.info(f"Failed to load config from {yaml_path}: {e}")
|
||||
else:
|
||||
logger.info(f"Cannot find {yaml_path}")
|
||||
|
||||
@property
|
||||
def vocab_filename(self):
|
||||
"""fairseq vocabulary file under data root"""
|
||||
return self.config.get("vocab_filename", "dict.txt")
|
||||
|
||||
@property
|
||||
def shuffle(self) -> bool:
|
||||
"""Shuffle dataset samples before batching"""
|
||||
return self.config.get("shuffle", False)
|
||||
|
||||
@property
|
||||
def pre_tokenizer(self) -> Dict:
|
||||
"""Pre-tokenizer to apply before subword tokenization. Returning
|
||||
a dictionary with `tokenizer` providing the tokenizer name and
|
||||
the other items providing the tokenizer-specific arguments.
|
||||
Tokenizers are defined in `fairseq.data.encoders.*`"""
|
||||
return self.config.get("pre_tokenizer", {"tokenizer": None})
|
||||
|
||||
@property
|
||||
def bpe_tokenizer(self) -> Dict:
|
||||
"""Subword tokenizer to apply after pre-tokenization. Returning
|
||||
a dictionary with `bpe` providing the tokenizer name and
|
||||
the other items providing the tokenizer-specific arguments.
|
||||
Tokenizers are defined in `fairseq.data.encoders.*`"""
|
||||
return self.config.get("bpe_tokenizer", {"bpe": None})
|
||||
|
||||
@property
|
||||
def prepend_tgt_lang_tag(self) -> bool:
|
||||
"""Prepend target lang ID token as the target BOS (e.g. for to-many
|
||||
multilingual setting). During inference, this requires `--prefix-size 1`
|
||||
to force BOS to be lang ID token."""
|
||||
return self.config.get("prepend_tgt_lang_tag", False)
|
||||
|
||||
@property
|
||||
def input_feat_per_channel(self):
|
||||
"""The dimension of input features (per audio channel)"""
|
||||
return self.config.get("input_feat_per_channel", 80)
|
||||
|
||||
@property
|
||||
def input_channels(self):
|
||||
"""The number of channels in the input audio"""
|
||||
return self.config.get("input_channels", 1)
|
||||
|
||||
@property
|
||||
def sampling_alpha(self):
|
||||
"""Hyper-parameter alpha = 1/T for temperature-based resampling.
|
||||
(alpha = 1 for no resampling)"""
|
||||
return self.config.get("sampling_alpha", 1.0)
|
||||
|
||||
@property
|
||||
def use_audio_input(self):
|
||||
"""Needed by the dataset loader to see if the model requires
|
||||
raw audio as inputs."""
|
||||
return self.config.get("use_audio_input", False)
|
||||
|
||||
@property
|
||||
def audio_root(self):
|
||||
"""Audio paths in the manifest TSV can be relative and this provides
|
||||
the root path. Set this to empty string when using absolute paths."""
|
||||
return self.config.get("audio_root", "")
|
||||
|
||||
def get_feature_transforms(self, split, is_train):
|
||||
"""Split-specific feature transforms. Allowing train set wildcard `_train`,
|
||||
evaluation set wildcard `_eval` and general wildcard `*` for matching."""
|
||||
from copy import deepcopy
|
||||
|
||||
cfg = deepcopy(self.config)
|
||||
_cur = cfg.get("transforms", {})
|
||||
cur = _cur.get(split)
|
||||
cur = _cur.get("_train") if cur is None and is_train else cur
|
||||
cur = _cur.get("_eval") if cur is None and not is_train else cur
|
||||
cur = _cur.get("*") if cur is None else cur
|
||||
cfg["transforms"] = cur
|
||||
return cfg
|
||||
|
||||
|
||||
def is_npy_data(data: bytes) -> bool:
|
||||
return data[0] == 147 and data[1] == 78
|
||||
|
||||
|
||||
def is_flac_or_wav_data(data: bytes) -> bool:
|
||||
is_flac = data[0] == 102 and data[1] == 76
|
||||
is_wav = data[0] == 82 and data[1] == 73
|
||||
return is_flac or is_wav
|
||||
|
||||
|
||||
def read_from_uncompressed_zip(file_path, offset, file_size) -> bytes:
|
||||
with open(file_path, "rb") as f:
|
||||
f.seek(offset)
|
||||
data = f.read(file_size)
|
||||
return data
|
||||
|
||||
|
||||
def get_features_from_npy_or_audio(path):
|
||||
ext = op.splitext(op.basename(path))[1]
|
||||
if ext not in {".npy", ".flac", ".wav"}:
|
||||
raise ValueError(f'Unsupported file format for "{path}"')
|
||||
return np.load(path) if ext == ".npy" else get_fbank(path)
|
||||
|
||||
|
||||
def get_features_or_waveform_from_uncompressed_zip(
|
||||
path, byte_offset, byte_size, need_waveform=False
|
||||
):
|
||||
assert path.endswith(".zip")
|
||||
data = read_from_uncompressed_zip(path, byte_offset, byte_size)
|
||||
f = io.BytesIO(data)
|
||||
if is_npy_data(data):
|
||||
features_or_waveform = np.load(f)
|
||||
elif is_flac_or_wav_data(data):
|
||||
features_or_waveform = get_waveform(f)[0] if need_waveform else get_fbank(f)
|
||||
else:
|
||||
raise ValueError(f'Unknown file format for "{path}"')
|
||||
return features_or_waveform
|
||||
|
||||
|
||||
def get_features_or_waveform(path: str, need_waveform=False):
|
||||
"""Get speech features from .npy file or waveform from .wav/.flac file.
|
||||
The file may be inside an uncompressed ZIP file and is accessed via byte
|
||||
offset and length.
|
||||
|
||||
Args:
|
||||
path (str): File path in the format of "<.npy/.wav/.flac path>" or
|
||||
"<zip path>:<byte offset>:<byte length>".
|
||||
need_waveform (bool): return waveform instead of features.
|
||||
|
||||
Returns:
|
||||
features_or_waveform (numpy.ndarray): speech features or waveform.
|
||||
"""
|
||||
_path, *extra = path.split(":")
|
||||
if not op.exists(_path):
|
||||
raise FileNotFoundError(f"File not found: {_path}")
|
||||
|
||||
if len(extra) == 0:
|
||||
if need_waveform:
|
||||
return get_waveform(_path)
|
||||
return get_features_from_npy_or_audio(_path)
|
||||
elif len(extra) == 2:
|
||||
extra = [int(i) for i in extra]
|
||||
features_or_waveform = get_features_or_waveform_from_uncompressed_zip(
|
||||
_path, extra[0], extra[1], need_waveform=need_waveform
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid path: {path}")
|
||||
|
||||
return features_or_waveform
|
||||
|
||||
|
||||
def _collate_frames(
|
||||
frames: List[torch.Tensor], is_audio_input: bool = False
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Convert a list of 2D frames into a padded 3D tensor
|
||||
Args:
|
||||
frames (list): list of 2D frames of size L[i]*f_dim. Where L[i] is
|
||||
length of i-th frame and f_dim is static dimension of features
|
||||
Returns:
|
||||
3D tensor of size len(frames)*len_max*f_dim where len_max is max of L[i]
|
||||
"""
|
||||
max_len = max(frame.size(0) for frame in frames)
|
||||
if is_audio_input:
|
||||
out = frames[0].new_zeros((len(frames), max_len))
|
||||
else:
|
||||
out = frames[0].new_zeros((len(frames), max_len, frames[0].size(1)))
|
||||
for i, v in enumerate(frames):
|
||||
out[i, : v.size(0)] = v
|
||||
return out
|
||||
|
||||
|
||||
class SpeechToTextDataset(FairseqDataset):
|
||||
LANG_TAG_TEMPLATE = "<lang:{}>"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
split: str,
|
||||
is_train_split: bool,
|
||||
data_cfg: S2TDataConfig,
|
||||
audio_paths: List[str],
|
||||
n_frames: List[int],
|
||||
src_texts: Optional[List[str]] = None,
|
||||
tgt_texts: Optional[List[str]] = None,
|
||||
speakers: Optional[List[str]] = None,
|
||||
src_langs: Optional[List[str]] = None,
|
||||
tgt_langs: Optional[List[str]] = None,
|
||||
ids: Optional[List[str]] = None,
|
||||
tgt_dict: Optional[Dictionary] = None,
|
||||
pre_tokenizer=None,
|
||||
bpe_tokenizer=None,
|
||||
):
|
||||
self.split, self.is_train_split = split, is_train_split
|
||||
self.data_cfg = data_cfg
|
||||
self.audio_paths, self.n_frames = audio_paths, n_frames
|
||||
self.n_samples = len(audio_paths)
|
||||
assert len(n_frames) == self.n_samples > 0
|
||||
assert src_texts is None or len(src_texts) == self.n_samples
|
||||
assert tgt_texts is None or len(tgt_texts) == self.n_samples
|
||||
assert speakers is None or len(speakers) == self.n_samples
|
||||
assert src_langs is None or len(src_langs) == self.n_samples
|
||||
assert tgt_langs is None or len(tgt_langs) == self.n_samples
|
||||
assert ids is None or len(ids) == self.n_samples
|
||||
assert (tgt_dict is None and tgt_texts is None) or (
|
||||
tgt_dict is not None and tgt_texts is not None
|
||||
)
|
||||
self.src_texts, self.tgt_texts = src_texts, tgt_texts
|
||||
self.src_langs, self.tgt_langs = src_langs, tgt_langs
|
||||
self.tgt_dict = tgt_dict
|
||||
self.check_tgt_lang_tag()
|
||||
self.ids = ids
|
||||
self.shuffle = data_cfg.shuffle if is_train_split else False
|
||||
|
||||
self.feature_transforms = CompositeAudioFeatureTransform.from_config_dict(
|
||||
self.data_cfg.get_feature_transforms(split, is_train_split)
|
||||
)
|
||||
|
||||
self.pre_tokenizer = pre_tokenizer
|
||||
self.bpe_tokenizer = bpe_tokenizer
|
||||
|
||||
logger.info(self.__repr__())
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
self.__class__.__name__
|
||||
+ f'(split="{self.split}", n_samples={self.n_samples}, '
|
||||
f"prepend_tgt_lang_tag={self.data_cfg.prepend_tgt_lang_tag}, "
|
||||
f"shuffle={self.shuffle}, transforms={self.feature_transforms})"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_lang_tag(cls, token):
|
||||
pattern = cls.LANG_TAG_TEMPLATE.replace("{}", "(.*)")
|
||||
return re.match(pattern, token)
|
||||
|
||||
def check_tgt_lang_tag(self):
|
||||
if self.data_cfg.prepend_tgt_lang_tag:
|
||||
assert self.tgt_langs is not None and self.tgt_dict is not None
|
||||
tgt_lang_tags = [
|
||||
self.LANG_TAG_TEMPLATE.format(t) for t in set(self.tgt_langs)
|
||||
]
|
||||
assert all(t in self.tgt_dict for t in tgt_lang_tags)
|
||||
|
||||
def tokenize_text(self, text: str):
|
||||
if self.pre_tokenizer is not None:
|
||||
text = self.pre_tokenizer.encode(text)
|
||||
if self.bpe_tokenizer is not None:
|
||||
text = self.bpe_tokenizer.encode(text)
|
||||
return text
|
||||
|
||||
def __getitem__(
|
||||
self, index: int
|
||||
) -> Tuple[int, torch.Tensor, Optional[torch.Tensor]]:
|
||||
source = get_features_or_waveform(
|
||||
self.audio_paths[index], need_waveform=self.data_cfg.use_audio_input
|
||||
)
|
||||
if self.feature_transforms is not None:
|
||||
assert not self.data_cfg.use_audio_input
|
||||
source = self.feature_transforms(source)
|
||||
source = torch.from_numpy(source).float()
|
||||
|
||||
target = None
|
||||
if self.tgt_texts is not None:
|
||||
tokenized = self.tokenize_text(self.tgt_texts[index])
|
||||
target = self.tgt_dict.encode_line(
|
||||
tokenized, add_if_not_exist=False, append_eos=True
|
||||
).long()
|
||||
if self.data_cfg.prepend_tgt_lang_tag:
|
||||
lang_tag = self.LANG_TAG_TEMPLATE.format(self.tgt_langs[index])
|
||||
lang_tag_idx = self.tgt_dict.index(lang_tag)
|
||||
target = torch.cat((torch.LongTensor([lang_tag_idx]), target), 0)
|
||||
return index, source, target
|
||||
|
||||
def __len__(self):
|
||||
return self.n_samples
|
||||
|
||||
def collater(self, samples: List[Tuple[int, torch.Tensor, torch.Tensor]]) -> Dict:
|
||||
if len(samples) == 0:
|
||||
return {}
|
||||
indices = torch.tensor([i for i, _, _ in samples], dtype=torch.long)
|
||||
frames = _collate_frames(
|
||||
[s for _, s, _ in samples], self.data_cfg.use_audio_input
|
||||
)
|
||||
# sort samples by descending number of frames
|
||||
n_frames = torch.tensor([s.size(0) for _, s, _ in samples], dtype=torch.long)
|
||||
n_frames, order = n_frames.sort(descending=True)
|
||||
indices = indices.index_select(0, order)
|
||||
frames = frames.index_select(0, order)
|
||||
|
||||
target, target_lengths = None, None
|
||||
prev_output_tokens = None
|
||||
ntokens = None
|
||||
if self.tgt_texts is not None:
|
||||
target = fairseq_data_utils.collate_tokens(
|
||||
[t for _, _, t in samples],
|
||||
self.tgt_dict.pad(),
|
||||
self.tgt_dict.eos(),
|
||||
left_pad=False,
|
||||
move_eos_to_beginning=False,
|
||||
)
|
||||
target = target.index_select(0, order)
|
||||
target_lengths = torch.tensor(
|
||||
[t.size(0) for _, _, t in samples], dtype=torch.long
|
||||
).index_select(0, order)
|
||||
prev_output_tokens = fairseq_data_utils.collate_tokens(
|
||||
[t for _, _, t in samples],
|
||||
self.tgt_dict.pad(),
|
||||
self.tgt_dict.eos(),
|
||||
left_pad=False,
|
||||
move_eos_to_beginning=True,
|
||||
)
|
||||
prev_output_tokens = prev_output_tokens.index_select(0, order)
|
||||
ntokens = sum(t.size(0) for _, _, t in samples)
|
||||
|
||||
out = {
|
||||
"id": indices,
|
||||
"net_input": {
|
||||
"src_tokens": frames,
|
||||
"src_lengths": n_frames,
|
||||
"prev_output_tokens": prev_output_tokens,
|
||||
},
|
||||
"target": target,
|
||||
"target_lengths": target_lengths,
|
||||
"ntokens": ntokens,
|
||||
"nsentences": len(samples),
|
||||
}
|
||||
return out
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.n_frames[index]
|
||||
|
||||
def size(self, index):
|
||||
t_len = 0
|
||||
if self.tgt_texts is not None:
|
||||
tokenized = self.tokenize_text(self.tgt_texts[index])
|
||||
t_len = len(tokenized.split(" "))
|
||||
return self.n_frames[index], t_len
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return np.array(self.n_frames)
|
||||
|
||||
@property
|
||||
def can_reuse_epoch_itr_across_epochs(self):
|
||||
return True
|
||||
|
||||
def ordered_indices(self):
|
||||
if self.shuffle:
|
||||
order = [np.random.permutation(len(self))]
|
||||
else:
|
||||
order = [np.arange(len(self))]
|
||||
# first by descending order of # of frames then by original/random order
|
||||
order.append([-n for n in self.n_frames])
|
||||
return np.lexsort(order)
|
||||
|
||||
def prefetch(self, indices):
|
||||
raise False
|
||||
|
||||
|
||||
class SpeechToTextDatasetCreator(object):
|
||||
# mandatory columns
|
||||
KEY_ID, KEY_AUDIO, KEY_N_FRAMES = "id", "audio", "n_frames"
|
||||
KEY_TGT_TEXT = "tgt_text"
|
||||
# optional columns
|
||||
KEY_SPEAKER, KEY_SRC_TEXT = "speaker", "src_text"
|
||||
KEY_SRC_LANG, KEY_TGT_LANG = "src_lang", "tgt_lang"
|
||||
# default values
|
||||
DEFAULT_SPEAKER = DEFAULT_SRC_TEXT = DEFAULT_LANG = ""
|
||||
|
||||
@classmethod
|
||||
def _from_list(
|
||||
cls,
|
||||
split_name: str,
|
||||
is_train_split,
|
||||
samples: List[List[Dict]],
|
||||
data_cfg: S2TDataConfig,
|
||||
tgt_dict,
|
||||
pre_tokenizer,
|
||||
bpe_tokenizer,
|
||||
) -> SpeechToTextDataset:
|
||||
audio_paths, n_frames, src_texts, tgt_texts, ids = [], [], [], [], []
|
||||
speakers, src_langs, tgt_langs = [], [], []
|
||||
for s in samples:
|
||||
ids.extend([ss[cls.KEY_ID] for ss in s])
|
||||
audio_paths.extend(
|
||||
[op.join(data_cfg.audio_root, ss[cls.KEY_AUDIO]) for ss in s]
|
||||
)
|
||||
n_frames.extend([int(ss[cls.KEY_N_FRAMES]) for ss in s])
|
||||
tgt_texts.extend([ss[cls.KEY_TGT_TEXT] for ss in s])
|
||||
src_texts.extend(
|
||||
[ss.get(cls.KEY_SRC_TEXT, cls.DEFAULT_SRC_TEXT) for ss in s]
|
||||
)
|
||||
speakers.extend([ss.get(cls.KEY_SPEAKER, cls.DEFAULT_SPEAKER) for ss in s])
|
||||
src_langs.extend([ss.get(cls.KEY_SRC_LANG, cls.DEFAULT_LANG) for ss in s])
|
||||
tgt_langs.extend([ss.get(cls.KEY_TGT_LANG, cls.DEFAULT_LANG) for ss in s])
|
||||
return SpeechToTextDataset(
|
||||
split_name,
|
||||
is_train_split,
|
||||
data_cfg,
|
||||
audio_paths,
|
||||
n_frames,
|
||||
src_texts,
|
||||
tgt_texts,
|
||||
speakers,
|
||||
src_langs,
|
||||
tgt_langs,
|
||||
ids,
|
||||
tgt_dict,
|
||||
pre_tokenizer,
|
||||
bpe_tokenizer,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_size_ratios(cls, ids: List[str], sizes: List[int], alpha: float = 1.0):
|
||||
"""Size ratios for temperature-based sampling
|
||||
(https://arxiv.org/abs/1907.05019)"""
|
||||
_sizes = np.array(sizes)
|
||||
prob = _sizes / _sizes.sum()
|
||||
smoothed_prob = prob ** alpha
|
||||
smoothed_prob = smoothed_prob / smoothed_prob.sum()
|
||||
size_ratio = (smoothed_prob * _sizes.sum()) / _sizes
|
||||
|
||||
o_str = str({_i: f"{prob[i]:.3f}" for i, _i in enumerate(ids)})
|
||||
logger.info(f"original sampling probability: {o_str}")
|
||||
p_str = str({_i: f"{smoothed_prob[i]:.3f}" for i, _i in enumerate(ids)})
|
||||
logger.info(f"balanced sampling probability: {p_str}")
|
||||
sr_str = str({_id: f"{size_ratio[i]:.3f}" for i, _id in enumerate(ids)})
|
||||
logger.info(f"balanced sampling size ratio: {sr_str}")
|
||||
return size_ratio.tolist()
|
||||
|
||||
@classmethod
|
||||
def from_tsv(
|
||||
cls,
|
||||
root: str,
|
||||
data_cfg: S2TDataConfig,
|
||||
splits: str,
|
||||
tgt_dict,
|
||||
pre_tokenizer,
|
||||
bpe_tokenizer,
|
||||
is_train_split: bool,
|
||||
epoch: int,
|
||||
seed: int,
|
||||
) -> SpeechToTextDataset:
|
||||
samples = []
|
||||
_splits = splits.split(",")
|
||||
for split in _splits:
|
||||
tsv_path = op.join(root, f"{split}.tsv")
|
||||
if not op.isfile(tsv_path):
|
||||
raise FileNotFoundError(f"Dataset not found: {tsv_path}")
|
||||
with open(tsv_path) as f:
|
||||
reader = csv.DictReader(
|
||||
f,
|
||||
delimiter="\t",
|
||||
quotechar=None,
|
||||
doublequote=False,
|
||||
lineterminator="\n",
|
||||
quoting=csv.QUOTE_NONE,
|
||||
)
|
||||
samples.append([dict(e) for e in reader])
|
||||
assert len(samples) > 0
|
||||
|
||||
datasets = [
|
||||
cls._from_list(
|
||||
name,
|
||||
is_train_split,
|
||||
[s],
|
||||
data_cfg,
|
||||
tgt_dict,
|
||||
pre_tokenizer,
|
||||
bpe_tokenizer,
|
||||
)
|
||||
for name, s in zip(_splits, samples)
|
||||
]
|
||||
|
||||
if is_train_split and len(_splits) > 1 and data_cfg.sampling_alpha != 1.0:
|
||||
# temperature-based sampling
|
||||
size_ratios = cls._get_size_ratios(
|
||||
_splits, [len(s) for s in samples], alpha=data_cfg.sampling_alpha
|
||||
)
|
||||
datasets = [
|
||||
ResamplingDataset(
|
||||
d, size_ratio=r, seed=seed, epoch=epoch, replace=(r >= 1.0)
|
||||
)
|
||||
for d, r in zip(datasets, size_ratios)
|
||||
]
|
||||
return ConcatDataset(datasets)
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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 . import FairseqDataset
|
||||
|
||||
|
||||
def backtranslate_samples(samples, collate_fn, generate_fn, cuda=True):
|
||||
"""Backtranslate a list of samples.
|
||||
|
||||
Given an input (*samples*) of the form:
|
||||
|
||||
[{'id': 1, 'source': 'hallo welt'}]
|
||||
|
||||
this will return:
|
||||
|
||||
[{'id': 1, 'source': 'hello world', 'target': 'hallo welt'}]
|
||||
|
||||
Args:
|
||||
samples (List[dict]): samples to backtranslate. Individual samples are
|
||||
expected to have a 'source' key, which will become the 'target'
|
||||
after backtranslation.
|
||||
collate_fn (callable): function to collate samples into a mini-batch
|
||||
generate_fn (callable): function to generate backtranslations
|
||||
cuda (bool): use GPU for generation (default: ``True``)
|
||||
|
||||
Returns:
|
||||
List[dict]: an updated list of samples with a backtranslated source
|
||||
"""
|
||||
collated_samples = collate_fn(samples)
|
||||
s = utils.move_to_cuda(collated_samples) if cuda else collated_samples
|
||||
generated_sources = generate_fn(s)
|
||||
|
||||
id_to_src = {sample["id"]: sample["source"] for sample in samples}
|
||||
|
||||
# Go through each tgt sentence in batch and its corresponding best
|
||||
# generated hypothesis and create a backtranslation data pair
|
||||
# {id: id, source: generated backtranslation, target: original tgt}
|
||||
return [
|
||||
{
|
||||
"id": id.item(),
|
||||
"target": id_to_src[id.item()],
|
||||
"source": hypos[0]["tokens"].cpu(),
|
||||
}
|
||||
for id, hypos in zip(collated_samples["id"], generated_sources)
|
||||
]
|
||||
|
||||
|
||||
class BacktranslationDataset(FairseqDataset):
|
||||
"""
|
||||
Sets up a backtranslation dataset which takes a tgt batch, generates
|
||||
a src using a tgt-src backtranslation function (*backtranslation_fn*),
|
||||
and returns the corresponding `{generated src, input tgt}` batch.
|
||||
|
||||
Args:
|
||||
tgt_dataset (~fairseq.data.FairseqDataset): the dataset to be
|
||||
backtranslated. Only the source side of this dataset will be used.
|
||||
After backtranslation, the source sentences in this dataset will be
|
||||
returned as the targets.
|
||||
src_dict (~fairseq.data.Dictionary): the dictionary of backtranslated
|
||||
sentences.
|
||||
tgt_dict (~fairseq.data.Dictionary, optional): the dictionary of
|
||||
sentences to be backtranslated.
|
||||
backtranslation_fn (callable, optional): function to call to generate
|
||||
backtranslations. This is typically the `generate` method of a
|
||||
:class:`~fairseq.sequence_generator.SequenceGenerator` object.
|
||||
Pass in None when it is not available at initialization time, and
|
||||
use set_backtranslation_fn function to set it when available.
|
||||
output_collater (callable, optional): function to call on the
|
||||
backtranslated samples to create the final batch
|
||||
(default: ``tgt_dataset.collater``).
|
||||
cuda: use GPU for generation
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tgt_dataset,
|
||||
src_dict,
|
||||
tgt_dict=None,
|
||||
backtranslation_fn=None,
|
||||
output_collater=None,
|
||||
cuda=True,
|
||||
**kwargs
|
||||
):
|
||||
self.tgt_dataset = tgt_dataset
|
||||
self.backtranslation_fn = backtranslation_fn
|
||||
self.output_collater = (
|
||||
output_collater if output_collater is not None else tgt_dataset.collater
|
||||
)
|
||||
self.cuda = cuda if torch.cuda.is_available() else False
|
||||
self.src_dict = src_dict
|
||||
self.tgt_dict = tgt_dict
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Returns a single sample from *tgt_dataset*. Note that backtranslation is
|
||||
not applied in this step; use :func:`collater` instead to backtranslate
|
||||
a batch of samples.
|
||||
"""
|
||||
return self.tgt_dataset[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.tgt_dataset)
|
||||
|
||||
def set_backtranslation_fn(self, backtranslation_fn):
|
||||
self.backtranslation_fn = backtranslation_fn
|
||||
|
||||
def collater(self, samples):
|
||||
"""Merge and backtranslate a list of samples to form a mini-batch.
|
||||
|
||||
Using the samples from *tgt_dataset*, load a collated target sample to
|
||||
feed to the backtranslation model. Then take the backtranslation with
|
||||
the best score as the source and the original input as the target.
|
||||
|
||||
Note: we expect *tgt_dataset* to provide a function `collater()` that
|
||||
will collate samples into the format expected by *backtranslation_fn*.
|
||||
After backtranslation, we will feed the new list of samples (i.e., the
|
||||
`(backtranslated source, original source)` pairs) to *output_collater*
|
||||
and return the result.
|
||||
|
||||
Args:
|
||||
samples (List[dict]): samples to backtranslate and collate
|
||||
|
||||
Returns:
|
||||
dict: a mini-batch with keys coming from *output_collater*
|
||||
"""
|
||||
if samples[0].get("is_dummy", False):
|
||||
return samples
|
||||
samples = backtranslate_samples(
|
||||
samples=samples,
|
||||
collate_fn=self.tgt_dataset.collater,
|
||||
generate_fn=(lambda net_input: self.backtranslation_fn(net_input)),
|
||||
cuda=self.cuda,
|
||||
)
|
||||
return self.output_collater(samples)
|
||||
|
||||
def num_tokens(self, index):
|
||||
"""Just use the tgt dataset num_tokens"""
|
||||
return self.tgt_dataset.num_tokens(index)
|
||||
|
||||
def ordered_indices(self):
|
||||
"""Just use the tgt dataset ordered_indices"""
|
||||
return self.tgt_dataset.ordered_indices()
|
||||
|
||||
def size(self, index):
|
||||
"""Return an example's size as a float or tuple. This value is used
|
||||
when filtering a dataset with ``--max-positions``.
|
||||
|
||||
Note: we use *tgt_dataset* to approximate the length of the source
|
||||
sentence, since we do not know the actual length until after
|
||||
backtranslation.
|
||||
"""
|
||||
tgt_size = self.tgt_dataset.size(index)[0]
|
||||
return (tgt_size, tgt_size)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return getattr(self.tgt_dataset, "supports_prefetch", False)
|
||||
|
||||
def prefetch(self, indices):
|
||||
return self.tgt_dataset.prefetch(indices)
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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 torch.utils.data.dataloader import default_collate
|
||||
|
||||
from . import FairseqDataset
|
||||
|
||||
|
||||
class BaseWrapperDataset(FairseqDataset):
|
||||
def __init__(self, dataset):
|
||||
super().__init__()
|
||||
self.dataset = dataset
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.dataset[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.dataset)
|
||||
|
||||
def collater(self, samples):
|
||||
if hasattr(self.dataset, "collater"):
|
||||
return self.dataset.collater(samples)
|
||||
else:
|
||||
return default_collate(samples)
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return self.dataset.sizes
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self.dataset.num_tokens(index)
|
||||
|
||||
def size(self, index):
|
||||
return self.dataset.size(index)
|
||||
|
||||
def ordered_indices(self):
|
||||
return self.dataset.ordered_indices()
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return getattr(self.dataset, "supports_prefetch", False)
|
||||
|
||||
def attr(self, attr: str, index: int):
|
||||
return self.dataset.attr(attr, index)
|
||||
|
||||
def prefetch(self, indices):
|
||||
self.dataset.prefetch(indices)
|
||||
|
||||
def get_batch_shapes(self):
|
||||
return self.dataset.get_batch_shapes()
|
||||
|
||||
def batch_by_size(
|
||||
self,
|
||||
indices,
|
||||
max_tokens=None,
|
||||
max_sentences=None,
|
||||
required_batch_size_multiple=1,
|
||||
):
|
||||
return self.dataset.batch_by_size(
|
||||
indices,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=max_sentences,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
)
|
||||
|
||||
def filter_indices_by_size(self, indices, max_sizes):
|
||||
return self.dataset.filter_indices_by_size(indices, max_sizes)
|
||||
|
||||
@property
|
||||
def can_reuse_epoch_itr_across_epochs(self):
|
||||
return self.dataset.can_reuse_epoch_itr_across_epochs
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
super().set_epoch(epoch)
|
||||
if hasattr(self.dataset, "set_epoch"):
|
||||
self.dataset.set_epoch(epoch)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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 numpy as np
|
||||
import torch.nn.functional as F
|
||||
from fairseq.data import BaseWrapperDataset
|
||||
|
||||
|
||||
class BucketPadLengthDataset(BaseWrapperDataset):
|
||||
"""
|
||||
Bucket and pad item lengths to the nearest bucket size. This can be used to
|
||||
reduce the number of unique batch shapes, which is important on TPUs since
|
||||
each new batch shape requires a recompilation.
|
||||
|
||||
Args:
|
||||
dataset (FairseqDatset): dataset to bucket
|
||||
sizes (List[int]): all item sizes
|
||||
num_buckets (int): number of buckets to create
|
||||
pad_idx (int): padding symbol
|
||||
left_pad (bool): if True, pad on the left; otherwise right pad
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
sizes,
|
||||
num_buckets,
|
||||
pad_idx,
|
||||
left_pad,
|
||||
):
|
||||
super().__init__(dataset)
|
||||
self.pad_idx = pad_idx
|
||||
self.left_pad = left_pad
|
||||
|
||||
assert num_buckets > 0
|
||||
self.buckets = np.unique(
|
||||
np.percentile(
|
||||
sizes,
|
||||
np.linspace(0, 100, num_buckets + 1),
|
||||
interpolation="lower",
|
||||
)[1:]
|
||||
)
|
||||
|
||||
def get_bucketed_sizes(orig_sizes, buckets):
|
||||
sizes = np.copy(orig_sizes)
|
||||
assert np.min(sizes) >= 0
|
||||
start_val = -1
|
||||
for end_val in buckets:
|
||||
mask = (sizes > start_val) & (sizes <= end_val)
|
||||
sizes[mask] = end_val
|
||||
start_val = end_val
|
||||
return sizes
|
||||
|
||||
self._bucketed_sizes = get_bucketed_sizes(sizes, self.buckets)
|
||||
|
||||
def __getitem__(self, index):
|
||||
item = self.dataset[index]
|
||||
bucket_size = self._bucketed_sizes[index]
|
||||
num_pad = bucket_size - item.size(-1)
|
||||
return F.pad(
|
||||
item,
|
||||
(num_pad if self.left_pad else 0, 0 if self.left_pad else num_pad),
|
||||
value=self.pad_idx,
|
||||
)
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return self._bucketed_sizes
|
||||
|
||||
def num_tokens(self, index):
|
||||
return self._bucketed_sizes[index]
|
||||
|
||||
def size(self, index):
|
||||
return self._bucketed_sizes[index]
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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 . import BaseWrapperDataset
|
||||
|
||||
|
||||
class ColorizeDataset(BaseWrapperDataset):
|
||||
""" Adds 'colors' property to net input that is obtained from the provided color getter for use by models """
|
||||
|
||||
def __init__(self, dataset, color_getter):
|
||||
super().__init__(dataset)
|
||||
self.color_getter = color_getter
|
||||
|
||||
def collater(self, samples):
|
||||
base_collate = super().collater(samples)
|
||||
if len(base_collate) > 0:
|
||||
base_collate["net_input"]["colors"] = torch.tensor(
|
||||
list(self.color_getter(self.dataset, s["id"]) for s in samples),
|
||||
dtype=torch.long,
|
||||
)
|
||||
return base_collate
|
||||
@@ -0,0 +1,124 @@
|
||||
# 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 bisect
|
||||
|
||||
import numpy as np
|
||||
from torch.utils.data.dataloader import default_collate
|
||||
|
||||
from . import FairseqDataset
|
||||
|
||||
|
||||
class ConcatDataset(FairseqDataset):
|
||||
@staticmethod
|
||||
def cumsum(sequence, sample_ratios):
|
||||
r, s = [], 0
|
||||
for e, ratio in zip(sequence, sample_ratios):
|
||||
curr_len = int(ratio * len(e))
|
||||
r.append(curr_len + s)
|
||||
s += curr_len
|
||||
return r
|
||||
|
||||
def __init__(self, datasets, sample_ratios=1):
|
||||
super(ConcatDataset, self).__init__()
|
||||
assert len(datasets) > 0, "datasets should not be an empty iterable"
|
||||
self.datasets = list(datasets)
|
||||
if isinstance(sample_ratios, int):
|
||||
sample_ratios = [sample_ratios] * len(self.datasets)
|
||||
self.sample_ratios = sample_ratios
|
||||
self.cumulative_sizes = self.cumsum(self.datasets, sample_ratios)
|
||||
self.real_sizes = [len(d) for d in self.datasets]
|
||||
|
||||
def __len__(self):
|
||||
return self.cumulative_sizes[-1]
|
||||
|
||||
def __getitem__(self, idx):
|
||||
dataset_idx, sample_idx = self._get_dataset_and_sample_index(idx)
|
||||
return self.datasets[dataset_idx][sample_idx]
|
||||
|
||||
def _get_dataset_and_sample_index(self, idx: int):
|
||||
dataset_idx = bisect.bisect_right(self.cumulative_sizes, idx)
|
||||
if dataset_idx == 0:
|
||||
sample_idx = idx
|
||||
else:
|
||||
sample_idx = idx - self.cumulative_sizes[dataset_idx - 1]
|
||||
sample_idx = sample_idx % self.real_sizes[dataset_idx]
|
||||
return dataset_idx, sample_idx
|
||||
|
||||
def collater(self, samples, **extra_args):
|
||||
# For now only supports datasets with same underlying collater implementations
|
||||
if hasattr(self.datasets[0], "collater"):
|
||||
return self.datasets[0].collater(samples, **extra_args)
|
||||
else:
|
||||
return default_collate(samples, **extra_args)
|
||||
|
||||
def size(self, idx: int):
|
||||
"""
|
||||
Return an example's size as a float or tuple.
|
||||
"""
|
||||
dataset_idx, sample_idx = self._get_dataset_and_sample_index(idx)
|
||||
return self.datasets[dataset_idx].size(sample_idx)
|
||||
|
||||
def num_tokens(self, index: int):
|
||||
return np.max(self.size(index))
|
||||
|
||||
def attr(self, attr: str, index: int):
|
||||
dataset_idx = bisect.bisect_right(self.cumulative_sizes, index)
|
||||
return getattr(self.datasets[dataset_idx], attr, None)
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
_dataset_sizes = []
|
||||
for ds, sr in zip(self.datasets, self.sample_ratios):
|
||||
if isinstance(ds.sizes, np.ndarray):
|
||||
_dataset_sizes.append(np.tile(ds.sizes, sr))
|
||||
else:
|
||||
# Only support underlying dataset with single size array.
|
||||
assert isinstance(ds.sizes, list)
|
||||
_dataset_sizes.append(np.tile(ds.sizes[0], sr))
|
||||
return np.concatenate(_dataset_sizes)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return all(d.supports_prefetch for d in self.datasets)
|
||||
|
||||
def ordered_indices(self):
|
||||
"""
|
||||
Returns indices sorted by length. So less padding is needed.
|
||||
"""
|
||||
if isinstance(self.sizes, np.ndarray) and len(self.sizes.shape) > 1:
|
||||
# special handling for concatenating lang_pair_datasets
|
||||
indices = np.arange(len(self))
|
||||
sizes = self.sizes
|
||||
tgt_sizes = (
|
||||
sizes[:, 1] if len(sizes.shape) > 0 and sizes.shape[1] > 1 else None
|
||||
)
|
||||
src_sizes = (
|
||||
sizes[:, 0] if len(sizes.shape) > 0 and sizes.shape[1] > 1 else sizes
|
||||
)
|
||||
# sort by target length, then source length
|
||||
if tgt_sizes is not None:
|
||||
indices = indices[np.argsort(tgt_sizes[indices], kind="mergesort")]
|
||||
return indices[np.argsort(src_sizes[indices], kind="mergesort")]
|
||||
else:
|
||||
return np.argsort(self.sizes)
|
||||
|
||||
def prefetch(self, indices):
|
||||
frm = 0
|
||||
for to, ds in zip(self.cumulative_sizes, self.datasets):
|
||||
real_size = len(ds)
|
||||
if getattr(ds, "supports_prefetch", False):
|
||||
ds.prefetch([(i - frm) % real_size for i in indices if frm <= i < to])
|
||||
frm = to
|
||||
|
||||
@property
|
||||
def can_reuse_epoch_itr_across_epochs(self):
|
||||
return all(d.can_reuse_epoch_itr_across_epochs for d in self.datasets)
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
super().set_epoch(epoch)
|
||||
for ds in self.datasets:
|
||||
if hasattr(ds, "set_epoch"):
|
||||
ds.set_epoch(epoch)
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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 . import FairseqDataset
|
||||
|
||||
|
||||
class ConcatSentencesDataset(FairseqDataset):
|
||||
def __init__(self, *datasets):
|
||||
super().__init__()
|
||||
self.datasets = datasets
|
||||
assert all(
|
||||
len(ds) == len(datasets[0]) for ds in datasets
|
||||
), "datasets must have the same length"
|
||||
|
||||
def __getitem__(self, index):
|
||||
return torch.cat([ds[index] for ds in self.datasets])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.datasets[0])
|
||||
|
||||
def collater(self, samples):
|
||||
return self.datasets[0].collater(samples)
|
||||
|
||||
@property
|
||||
def sizes(self):
|
||||
return sum(ds.sizes for ds in self.datasets)
|
||||
|
||||
def num_tokens(self, index):
|
||||
return sum(ds.num_tokens(index) for ds in self.datasets)
|
||||
|
||||
def size(self, index):
|
||||
return sum(ds.size(index) for ds in self.datasets)
|
||||
|
||||
def ordered_indices(self):
|
||||
return self.datasets[0].ordered_indices()
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return any(getattr(ds, "supports_prefetch", False) for ds in self.datasets)
|
||||
|
||||
def prefetch(self, indices):
|
||||
for ds in self.datasets:
|
||||
if getattr(ds, "supports_prefetch", False):
|
||||
ds.prefetch(indices)
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
super().set_epoch(epoch)
|
||||
for ds in self.datasets:
|
||||
if hasattr(ds, "set_epoch"):
|
||||
ds.set_epoch(epoch)
|
||||
@@ -0,0 +1,528 @@
|
||||
# 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.
|
||||
|
||||
try:
|
||||
from collections.abc import Iterable
|
||||
except ImportError:
|
||||
from collections import Iterable
|
||||
import contextlib
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from fairseq.file_io import PathManager
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def infer_language_pair(path):
|
||||
"""Infer language pair from filename: <split>.<lang1>-<lang2>.(...).idx"""
|
||||
src, dst = None, None
|
||||
for filename in PathManager.ls(path):
|
||||
parts = filename.split(".")
|
||||
if len(parts) >= 3 and len(parts[1].split("-")) == 2:
|
||||
return parts[1].split("-")
|
||||
return src, dst
|
||||
|
||||
|
||||
def collate_tokens(
|
||||
values,
|
||||
pad_idx,
|
||||
eos_idx=None,
|
||||
left_pad=False,
|
||||
move_eos_to_beginning=False,
|
||||
pad_to_length=None,
|
||||
pad_to_multiple=1,
|
||||
):
|
||||
"""Convert a list of 1d tensors into a padded 2d tensor."""
|
||||
size = max(v.size(0) for v in values)
|
||||
size = size if pad_to_length is None else max(size, pad_to_length)
|
||||
if pad_to_multiple != 1 and size % pad_to_multiple != 0:
|
||||
size = int(((size - 0.1) // pad_to_multiple + 1) * pad_to_multiple)
|
||||
res = values[0].new(len(values), size).fill_(pad_idx)
|
||||
|
||||
def copy_tensor(src, dst):
|
||||
assert dst.numel() == src.numel()
|
||||
if move_eos_to_beginning:
|
||||
if eos_idx is None:
|
||||
# if no eos_idx is specified, then use the last token in src
|
||||
dst[0] = src[-1]
|
||||
else:
|
||||
dst[0] = eos_idx
|
||||
dst[1:] = src[:-1]
|
||||
else:
|
||||
dst.copy_(src)
|
||||
|
||||
for i, v in enumerate(values):
|
||||
copy_tensor(v, res[i][size - len(v) :] if left_pad else res[i][: len(v)])
|
||||
return res
|
||||
|
||||
|
||||
def load_indexed_dataset(
|
||||
path, dictionary=None, dataset_impl=None, combine=False, default="cached"
|
||||
):
|
||||
"""A helper function for loading indexed datasets.
|
||||
|
||||
Args:
|
||||
path (str): path to indexed dataset (e.g., 'data-bin/train')
|
||||
dictionary (~fairseq.data.Dictionary): data dictionary
|
||||
dataset_impl (str, optional): which dataset implementation to use. If
|
||||
not provided, it will be inferred automatically. For legacy indexed
|
||||
data we use the 'cached' implementation by default.
|
||||
combine (bool, optional): automatically load and combine multiple
|
||||
datasets. For example, if *path* is 'data-bin/train', then we will
|
||||
combine 'data-bin/train', 'data-bin/train1', ... and return a
|
||||
single ConcatDataset instance.
|
||||
"""
|
||||
import fairseq.data.indexed_dataset as indexed_dataset
|
||||
from fairseq.data.concat_dataset import ConcatDataset
|
||||
|
||||
datasets = []
|
||||
for k in itertools.count():
|
||||
path_k = path + (str(k) if k > 0 else "")
|
||||
path_k = indexed_dataset.get_indexed_dataset_to_local(path_k)
|
||||
|
||||
dataset_impl_k = dataset_impl
|
||||
if dataset_impl_k is None:
|
||||
dataset_impl_k = indexed_dataset.infer_dataset_impl(path_k)
|
||||
dataset = indexed_dataset.make_dataset(
|
||||
path_k,
|
||||
impl=dataset_impl_k or default,
|
||||
fix_lua_indexing=True,
|
||||
dictionary=dictionary,
|
||||
)
|
||||
if dataset is None:
|
||||
break
|
||||
logger.info("loaded {:,} examples from: {}".format(len(dataset), path_k))
|
||||
datasets.append(dataset)
|
||||
if not combine:
|
||||
break
|
||||
if len(datasets) == 0:
|
||||
return None
|
||||
elif len(datasets) == 1:
|
||||
return datasets[0]
|
||||
else:
|
||||
return ConcatDataset(datasets)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def numpy_seed(seed, *addl_seeds):
|
||||
"""Context manager which seeds the NumPy PRNG with the specified seed and
|
||||
restores the state afterward"""
|
||||
if seed is None:
|
||||
yield
|
||||
return
|
||||
if len(addl_seeds) > 0:
|
||||
seed = int(hash((seed, *addl_seeds)) % 1e6)
|
||||
state = np.random.get_state()
|
||||
np.random.seed(seed)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
np.random.set_state(state)
|
||||
|
||||
|
||||
def collect_filtered(function, iterable, filtered):
|
||||
"""
|
||||
Similar to :func:`filter` but collects filtered elements in ``filtered``.
|
||||
|
||||
Args:
|
||||
function (callable): function that returns ``False`` for elements that
|
||||
should be filtered
|
||||
iterable (iterable): iterable to filter
|
||||
filtered (list): list to store filtered elements
|
||||
"""
|
||||
for el in iterable:
|
||||
if function(el):
|
||||
yield el
|
||||
else:
|
||||
filtered.append(el)
|
||||
|
||||
|
||||
def _filter_by_size_dynamic(indices, size_fn, max_positions, raise_exception=False):
|
||||
def compare_leq(a, b):
|
||||
return a <= b if not isinstance(a, tuple) else max(a) <= b
|
||||
|
||||
def check_size(idx):
|
||||
if isinstance(max_positions, float) or isinstance(max_positions, int):
|
||||
return size_fn(idx) <= max_positions
|
||||
elif isinstance(max_positions, dict):
|
||||
idx_size = size_fn(idx)
|
||||
assert isinstance(idx_size, dict)
|
||||
intersect_keys = set(max_positions.keys()) & set(idx_size.keys())
|
||||
return all(
|
||||
all(
|
||||
a is None or b is None or a <= b
|
||||
for a, b in zip(idx_size[key], max_positions[key])
|
||||
)
|
||||
for key in intersect_keys
|
||||
)
|
||||
else:
|
||||
# For MultiCorpusSampledDataset, will generalize it later
|
||||
if not isinstance(size_fn(idx), Iterable):
|
||||
return all(size_fn(idx) <= b for b in max_positions)
|
||||
return all(
|
||||
a is None or b is None or a <= b
|
||||
for a, b in zip(size_fn(idx), max_positions)
|
||||
)
|
||||
|
||||
ignored = []
|
||||
itr = collect_filtered(check_size, indices, ignored)
|
||||
indices = np.fromiter(itr, dtype=np.int64, count=-1)
|
||||
return indices, ignored
|
||||
|
||||
|
||||
def filter_by_size(indices, dataset, max_positions, raise_exception=False):
|
||||
"""
|
||||
[deprecated] Filter indices based on their size.
|
||||
Use `FairseqDataset::filter_indices_by_size` instead.
|
||||
|
||||
Args:
|
||||
indices (List[int]): ordered list of dataset indices
|
||||
dataset (FairseqDataset): fairseq dataset instance
|
||||
max_positions (tuple): filter elements larger than this size.
|
||||
Comparisons are done component-wise.
|
||||
raise_exception (bool, optional): if ``True``, raise an exception if
|
||||
any elements are filtered (default: False).
|
||||
"""
|
||||
warnings.warn(
|
||||
"data_utils.filter_by_size is deprecated. "
|
||||
"Use `FairseqDataset::filter_indices_by_size` instead.",
|
||||
stacklevel=2,
|
||||
)
|
||||
if isinstance(max_positions, float) or isinstance(max_positions, int):
|
||||
if hasattr(dataset, "sizes") and isinstance(dataset.sizes, np.ndarray):
|
||||
ignored = indices[dataset.sizes[indices] > max_positions].tolist()
|
||||
indices = indices[dataset.sizes[indices] <= max_positions]
|
||||
elif (
|
||||
hasattr(dataset, "sizes")
|
||||
and isinstance(dataset.sizes, list)
|
||||
and len(dataset.sizes) == 1
|
||||
):
|
||||
ignored = indices[dataset.sizes[0][indices] > max_positions].tolist()
|
||||
indices = indices[dataset.sizes[0][indices] <= max_positions]
|
||||
else:
|
||||
indices, ignored = _filter_by_size_dynamic(
|
||||
indices, dataset.size, max_positions
|
||||
)
|
||||
else:
|
||||
indices, ignored = _filter_by_size_dynamic(indices, dataset.size, max_positions)
|
||||
|
||||
if len(ignored) > 0 and raise_exception:
|
||||
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)
|
||||
)
|
||||
if len(ignored) > 0:
|
||||
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 filter_paired_dataset_indices_by_size(src_sizes, tgt_sizes, indices, max_sizes):
|
||||
"""Filter a list of sample indices. Remove those that are longer
|
||||
than specified in max_sizes.
|
||||
|
||||
Args:
|
||||
indices (np.array): original array of sample indices
|
||||
max_sizes (int or list[int] or tuple[int]): max sample size,
|
||||
can be defined separately for src and tgt (then list or tuple)
|
||||
|
||||
Returns:
|
||||
np.array: filtered sample array
|
||||
list: list of removed indices
|
||||
"""
|
||||
if max_sizes is None:
|
||||
return indices, []
|
||||
if type(max_sizes) in (int, float):
|
||||
max_src_size, max_tgt_size = max_sizes, max_sizes
|
||||
else:
|
||||
max_src_size, max_tgt_size = max_sizes
|
||||
if tgt_sizes is None:
|
||||
ignored = indices[src_sizes[indices] > max_src_size]
|
||||
else:
|
||||
ignored = indices[
|
||||
(src_sizes[indices] > max_src_size) | (tgt_sizes[indices] > max_tgt_size)
|
||||
]
|
||||
if len(ignored) > 0:
|
||||
if tgt_sizes is None:
|
||||
indices = indices[src_sizes[indices] <= max_src_size]
|
||||
else:
|
||||
indices = indices[
|
||||
(src_sizes[indices] <= max_src_size)
|
||||
& (tgt_sizes[indices] <= max_tgt_size)
|
||||
]
|
||||
return indices, ignored.tolist()
|
||||
|
||||
|
||||
def batch_by_size(
|
||||
indices,
|
||||
num_tokens_fn,
|
||||
num_tokens_vec=None,
|
||||
max_tokens=None,
|
||||
max_sentences=None,
|
||||
required_batch_size_multiple=1,
|
||||
fixed_shapes=None,
|
||||
):
|
||||
"""
|
||||
Yield mini-batches of indices bucketed by size. Batches may contain
|
||||
sequences of different lengths.
|
||||
|
||||
Args:
|
||||
indices (List[int]): ordered list of dataset indices
|
||||
num_tokens_fn (callable): function that returns the number of tokens at
|
||||
a given index
|
||||
num_tokens_vec (List[int], optional): precomputed vector of the number
|
||||
of tokens for each index in indices (to enable faster batch generation)
|
||||
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).
|
||||
required_batch_size_multiple (int, optional): require batch size to
|
||||
be less than N or a multiple of N (default: 1).
|
||||
fixed_shapes (List[Tuple[int, int]], optional): if given, batches will
|
||||
only be created with the given shapes. *max_sentences* and
|
||||
*required_batch_size_multiple* will be ignored (default: None).
|
||||
"""
|
||||
try:
|
||||
from fairseq.data.data_utils_fast import (
|
||||
batch_by_size_fn,
|
||||
batch_by_size_vec,
|
||||
batch_fixed_shapes_fast,
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please build Cython components with: `pip install --editable .` "
|
||||
"or `python setup.py build_ext --inplace`"
|
||||
)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"Please build (or rebuild) Cython components with: `pip install "
|
||||
" --editable .` or `python setup.py build_ext --inplace`."
|
||||
)
|
||||
|
||||
# added int() to avoid TypeError: an integer is required
|
||||
max_tokens = (
|
||||
int(max_tokens) if max_tokens is not None else -1
|
||||
)
|
||||
max_sentences = max_sentences if max_sentences is not None else -1
|
||||
bsz_mult = required_batch_size_multiple
|
||||
|
||||
if not isinstance(indices, np.ndarray):
|
||||
indices = np.fromiter(indices, dtype=np.int64, count=-1)
|
||||
|
||||
if num_tokens_vec is not None and not isinstance(num_tokens_vec, np.ndarray):
|
||||
num_tokens_vec = np.fromiter(num_tokens_vec, dtype=np.int64, count=-1)
|
||||
|
||||
if fixed_shapes is None:
|
||||
if num_tokens_vec is None:
|
||||
return batch_by_size_fn(
|
||||
indices,
|
||||
num_tokens_fn,
|
||||
max_tokens,
|
||||
max_sentences,
|
||||
bsz_mult,
|
||||
)
|
||||
else:
|
||||
return batch_by_size_vec(
|
||||
indices,
|
||||
num_tokens_vec,
|
||||
max_tokens,
|
||||
max_sentences,
|
||||
bsz_mult,
|
||||
)
|
||||
|
||||
else:
|
||||
fixed_shapes = np.array(fixed_shapes, dtype=np.int64)
|
||||
sort_order = np.lexsort(
|
||||
[
|
||||
fixed_shapes[:, 1].argsort(), # length
|
||||
fixed_shapes[:, 0].argsort(), # bsz
|
||||
]
|
||||
)
|
||||
fixed_shapes_sorted = fixed_shapes[sort_order]
|
||||
return batch_fixed_shapes_fast(indices, num_tokens_fn, fixed_shapes_sorted)
|
||||
|
||||
|
||||
def post_process(sentence: str, symbol: str):
|
||||
if symbol == "sentencepiece":
|
||||
sentence = sentence.replace(" ", "").replace("\u2581", " ").strip()
|
||||
elif symbol == "wordpiece":
|
||||
sentence = sentence.replace(" ", "").replace("_", " ").strip()
|
||||
elif symbol == "letter":
|
||||
sentence = sentence.replace(" ", "").replace("|", " ").strip()
|
||||
elif symbol == "_EOW":
|
||||
sentence = sentence.replace(" ", "").replace("_EOW", " ").strip()
|
||||
elif symbol == "bert":
|
||||
sentence = sentence.replace(" ##", "").rstrip()
|
||||
elif symbol in {"subword_nmt", "@@ ", "@@"}:
|
||||
if symbol == "subword_nmt":
|
||||
symbol = "@@ "
|
||||
sentence = (sentence + " ").replace(symbol, "").rstrip()
|
||||
elif symbol == "none":
|
||||
pass
|
||||
elif symbol is not None:
|
||||
raise NotImplementedError(f"Unknown post_process option: {symbol}")
|
||||
return sentence
|
||||
|
||||
|
||||
def compute_mask_indices(
|
||||
shape: Tuple[int, int],
|
||||
padding_mask: Optional[torch.Tensor],
|
||||
mask_prob: float,
|
||||
mask_length: int,
|
||||
mask_type: str = "static",
|
||||
mask_other: float = 0.0,
|
||||
min_masks: int = 0,
|
||||
no_overlap: bool = False,
|
||||
min_space: int = 0,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Computes random mask spans for a given shape
|
||||
|
||||
Args:
|
||||
shape: the the shape for which to compute masks.
|
||||
should be of size 2 where first element is batch size and 2nd is timesteps
|
||||
padding_mask: optional padding mask of the same size as shape, which will prevent masking padded elements
|
||||
mask_prob: probability for each token to be chosen as start of the span to be masked. this will be multiplied by
|
||||
number of timesteps divided by length of mask span to mask approximately this percentage of all elements.
|
||||
however due to overlaps, the actual number will be smaller (unless no_overlap is True)
|
||||
mask_type: how to compute mask lengths
|
||||
static = fixed size
|
||||
uniform = sample from uniform distribution [mask_other, mask_length*2]
|
||||
normal = sample from normal distribution with mean mask_length and stdev mask_other. mask is min 1 element
|
||||
poisson = sample from possion distribution with lambda = mask length
|
||||
min_masks: minimum number of masked spans
|
||||
no_overlap: if false, will switch to an alternative recursive algorithm that prevents spans from overlapping
|
||||
min_space: only used if no_overlap is True, this is how many elements to keep unmasked between spans
|
||||
"""
|
||||
|
||||
bsz, all_sz = shape
|
||||
mask = np.full((bsz, all_sz), False)
|
||||
|
||||
all_num_mask = int(
|
||||
# add a random number for probabilistic rounding
|
||||
mask_prob * all_sz / float(mask_length)
|
||||
+ np.random.rand()
|
||||
)
|
||||
|
||||
all_num_mask = max(min_masks, all_num_mask)
|
||||
|
||||
mask_idcs = []
|
||||
for i in range(bsz):
|
||||
if padding_mask is not None:
|
||||
sz = all_sz - padding_mask[i].long().sum().item()
|
||||
num_mask = int(
|
||||
# add a random number for probabilistic rounding
|
||||
mask_prob * sz / float(mask_length)
|
||||
+ np.random.rand()
|
||||
)
|
||||
num_mask = max(min_masks, num_mask)
|
||||
else:
|
||||
sz = all_sz
|
||||
num_mask = all_num_mask
|
||||
|
||||
if mask_type == "static":
|
||||
lengths = np.full(num_mask, mask_length)
|
||||
elif mask_type == "uniform":
|
||||
lengths = np.random.randint(mask_other, mask_length * 2 + 1, size=num_mask)
|
||||
elif mask_type == "normal":
|
||||
lengths = np.random.normal(mask_length, mask_other, size=num_mask)
|
||||
lengths = [max(1, int(round(x))) for x in lengths]
|
||||
elif mask_type == "poisson":
|
||||
lengths = np.random.poisson(mask_length, size=num_mask)
|
||||
lengths = [int(round(x)) for x in lengths]
|
||||
else:
|
||||
raise Exception("unknown mask selection " + mask_type)
|
||||
|
||||
if sum(lengths) == 0:
|
||||
lengths[0] = min(mask_length, sz - 1)
|
||||
|
||||
if no_overlap:
|
||||
mask_idc = []
|
||||
|
||||
def arrange(s, e, length, keep_length):
|
||||
span_start = np.random.randint(s, e - length)
|
||||
mask_idc.extend(span_start + i for i in range(length))
|
||||
|
||||
new_parts = []
|
||||
if span_start - s - min_space >= keep_length:
|
||||
new_parts.append((s, span_start - min_space + 1))
|
||||
if e - span_start - keep_length - min_space > keep_length:
|
||||
new_parts.append((span_start + length + min_space, e))
|
||||
return new_parts
|
||||
|
||||
parts = [(0, sz)]
|
||||
min_length = min(lengths)
|
||||
for length in sorted(lengths, reverse=True):
|
||||
lens = np.fromiter(
|
||||
(e - s if e - s >= length + min_space else 0 for s, e in parts),
|
||||
np.int,
|
||||
)
|
||||
l_sum = np.sum(lens)
|
||||
if l_sum == 0:
|
||||
break
|
||||
probs = lens / np.sum(lens)
|
||||
c = np.random.choice(len(parts), p=probs)
|
||||
s, e = parts.pop(c)
|
||||
parts.extend(arrange(s, e, length, min_length))
|
||||
mask_idc = np.asarray(mask_idc)
|
||||
else:
|
||||
min_len = min(lengths)
|
||||
if sz - min_len <= num_mask:
|
||||
min_len = sz - num_mask - 1
|
||||
|
||||
mask_idc = np.random.choice(sz - min_len, num_mask, replace=False)
|
||||
|
||||
mask_idc = np.asarray(
|
||||
[
|
||||
mask_idc[j] + offset
|
||||
for j in range(len(mask_idc))
|
||||
for offset in range(lengths[j])
|
||||
]
|
||||
)
|
||||
|
||||
mask_idcs.append(np.unique(mask_idc[mask_idc < sz]))
|
||||
|
||||
min_len = min([len(m) for m in mask_idcs])
|
||||
for i, mask_idc in enumerate(mask_idcs):
|
||||
if len(mask_idc) > min_len:
|
||||
mask_idc = np.random.choice(mask_idc, min_len, replace=False)
|
||||
mask[i, mask_idc] = True
|
||||
|
||||
return mask
|
||||
|
||||
|
||||
def get_mem_usage():
|
||||
try:
|
||||
import psutil
|
||||
|
||||
mb = 1024 * 1024
|
||||
return f"used={psutil.virtual_memory().used / mb}Mb; avail={psutil.virtual_memory().available / mb}Mb"
|
||||
except ImportError:
|
||||
return "N/A"
|
||||
|
||||
|
||||
def lengths_to_padding_mask(lens: torch.LongTensor) -> torch.BoolTensor:
|
||||
bsz, max_lens = lens.size(0), torch.max(lens).item()
|
||||
mask = torch.arange(max_lens).to(lens.device).view(1, max_lens)
|
||||
mask = mask.expand(bsz, -1) >= lens.view(bsz, 1).expand(-1, max_lens)
|
||||
return mask
|
||||
|
||||
|
||||
def lengths_to_mask(lens: torch.LongTensor) -> torch.BoolTensor:
|
||||
return ~lengths_to_padding_mask(lens)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
# cython: language_level=3
|
||||
# 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 numpy as np
|
||||
|
||||
cimport cython
|
||||
cimport numpy as np
|
||||
|
||||
from libc.stdint cimport int32_t, int64_t
|
||||
from libcpp cimport bool as bool_t
|
||||
|
||||
ctypedef int64_t DTYPE_t
|
||||
|
||||
@cython.cdivision(True)
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
cpdef list batch_by_size_vec(
|
||||
np.ndarray[int64_t, ndim=1] indices,
|
||||
np.ndarray[int64_t, ndim=1] num_tokens_vec,
|
||||
int64_t max_tokens,
|
||||
int64_t max_sentences,
|
||||
int32_t bsz_mult,
|
||||
):
|
||||
if indices.shape[0] == 0:
|
||||
return []
|
||||
|
||||
assert max_tokens <= 0 or np.max(num_tokens_vec) <= max_tokens, (
|
||||
f"Sentences lengths should not exceed max_tokens={max_tokens}"
|
||||
)
|
||||
|
||||
cdef int32_t indices_len = indices.shape[0]
|
||||
cdef np.ndarray[int32_t, ndim=1] batches_ends = \
|
||||
np.zeros(indices_len, dtype=np.int32)
|
||||
cdef int32_t[:] batches_ends_view = batches_ends
|
||||
cdef int64_t[:] num_tokens_view = num_tokens_vec
|
||||
|
||||
cdef int32_t pos = 0
|
||||
cdef int32_t new_batch_end = 0
|
||||
|
||||
cdef int64_t new_batch_max_tokens = 0
|
||||
cdef int32_t new_batch_sentences = 0
|
||||
cdef int64_t new_batch_num_tokens = 0
|
||||
|
||||
cdef bool_t overflow = False
|
||||
cdef bool_t size_matches_with_bsz_mult = False
|
||||
|
||||
cdef int32_t batches_count = 0
|
||||
cdef int32_t batch_start = 0
|
||||
cdef int64_t tail_max_tokens = 0
|
||||
cdef int64_t batch_max_tokens = 0
|
||||
|
||||
for pos in range(indices_len):
|
||||
# At every pos we keep stats about the last complete batch [batch_start:batch_end),
|
||||
# and tail [batch_end:pos].
|
||||
# 1) Every time when (batch + tail) forms a valid batch
|
||||
# (according to max_tokens, max_sentences and bsz_mult) we append tail to batch.
|
||||
# 2) When (batch+tail) violates max_tokens or max_sentences constraints
|
||||
# we finalize running batch, and tail becomes a new batch.
|
||||
# 3) There is a corner case when tail also violates constraints.
|
||||
# In that situation [batch_end:pos-1] (tail without the current pos)
|
||||
# gets added to the finalized batches, while [pos:pos] becomes a new tail.
|
||||
#
|
||||
# Important: For the sake of performance try to avoid using function calls within this loop.
|
||||
|
||||
tail_max_tokens = tail_max_tokens \
|
||||
if tail_max_tokens > num_tokens_view[pos] \
|
||||
else num_tokens_view[pos]
|
||||
new_batch_end = pos + 1
|
||||
new_batch_max_tokens = batch_max_tokens \
|
||||
if batch_max_tokens > tail_max_tokens \
|
||||
else tail_max_tokens
|
||||
new_batch_sentences = new_batch_end - batch_start
|
||||
new_batch_num_tokens = new_batch_sentences * new_batch_max_tokens
|
||||
|
||||
overflow = (new_batch_sentences > max_sentences > 0 or
|
||||
new_batch_num_tokens > max_tokens > 0)
|
||||
size_matches_with_bsz_mult = (new_batch_sentences < bsz_mult or
|
||||
new_batch_sentences % bsz_mult == 0)
|
||||
|
||||
if overflow:
|
||||
tail_num_tokens = tail_max_tokens * \
|
||||
(new_batch_end - batches_ends_view[batches_count])
|
||||
tail_overflow = tail_num_tokens > max_tokens > 0
|
||||
# In case of a tail overflow finalize two batches
|
||||
if tail_overflow:
|
||||
batches_count += 1
|
||||
batches_ends_view[batches_count] = pos
|
||||
tail_max_tokens = num_tokens_view[pos]
|
||||
batch_start = batches_ends_view[batches_count]
|
||||
batches_count += 1
|
||||
new_batch_max_tokens = tail_max_tokens
|
||||
|
||||
if overflow or size_matches_with_bsz_mult:
|
||||
batches_ends_view[batches_count] = new_batch_end
|
||||
batch_max_tokens = new_batch_max_tokens
|
||||
tail_max_tokens = 0
|
||||
if batches_ends_view[batches_count] != indices_len:
|
||||
batches_count += 1
|
||||
# Memory and time-efficient split
|
||||
return np.split(indices, batches_ends[:batches_count])
|
||||
|
||||
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
cpdef list batch_by_size_fn(
|
||||
np.ndarray[DTYPE_t, ndim=1] indices,
|
||||
num_tokens_fn,
|
||||
int64_t max_tokens,
|
||||
int64_t max_sentences,
|
||||
int32_t bsz_mult,
|
||||
):
|
||||
cdef int32_t indices_len = indices.shape[0]
|
||||
cdef np.ndarray[int64_t, ndim=1] num_tokens_vec = np.zeros(indices_len,
|
||||
dtype=np.int64)
|
||||
cdef DTYPE_t[:] indices_view = indices
|
||||
cdef DTYPE_t[:] num_tokens_vec_view = num_tokens_vec
|
||||
cdef int64_t pos
|
||||
for pos in range(indices_len):
|
||||
num_tokens_vec[pos] = num_tokens_fn(indices_view[pos])
|
||||
return batch_by_size_vec(indices, num_tokens_vec, max_tokens,
|
||||
max_sentences, bsz_mult,)
|
||||
|
||||
|
||||
cdef _find_valid_shape(
|
||||
DTYPE_t[:, :] shapes_view,
|
||||
int64_t num_sentences,
|
||||
int64_t num_tokens,
|
||||
):
|
||||
"""Return index of first valid shape of -1 if none is found."""
|
||||
for i in range(shapes_view.shape[0]):
|
||||
if num_sentences <= shapes_view[i][0] and num_tokens <= shapes_view[i][1]:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
@cython.cdivision(True)
|
||||
cpdef list batch_fixed_shapes_fast(
|
||||
np.ndarray[DTYPE_t, ndim=1] indices,
|
||||
num_tokens_fn,
|
||||
np.ndarray[DTYPE_t, ndim=2] fixed_shapes_sorted,
|
||||
):
|
||||
cdef int64_t sample_len = 0
|
||||
cdef list sample_lens = []
|
||||
cdef list batch = []
|
||||
cdef list batches = []
|
||||
cdef int64_t mod_len
|
||||
cdef int64_t i
|
||||
cdef int64_t idx
|
||||
cdef int64_t num_tokens
|
||||
cdef DTYPE_t[:] indices_view = indices
|
||||
cdef DTYPE_t[:, :] shapes_view = fixed_shapes_sorted
|
||||
|
||||
for i in range(len(indices_view)):
|
||||
idx = indices_view[i]
|
||||
num_tokens = num_tokens_fn(idx)
|
||||
sample_lens.append(num_tokens)
|
||||
sample_len = max(sample_len, num_tokens)
|
||||
|
||||
shape_idx = _find_valid_shape(shapes_view, len(batch) + 1, sample_len)
|
||||
if shape_idx == -1:
|
||||
batches.append(batch)
|
||||
batch = []
|
||||
sample_lens = []
|
||||
sample_len = 0
|
||||
shapes_view = fixed_shapes_sorted
|
||||
elif shape_idx > 0:
|
||||
# small optimization for the next call to _find_valid_shape
|
||||
shapes_view = shapes_view[shape_idx:]
|
||||
|
||||
batch.append(idx)
|
||||
|
||||
if len(batch) > 0:
|
||||
batches.append(batch)
|
||||
|
||||
return batches
|
||||
@@ -0,0 +1,436 @@
|
||||
# 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 math
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from . import FairseqDataset, data_utils
|
||||
|
||||
|
||||
def collate(
|
||||
samples,
|
||||
pad_idx,
|
||||
eos_idx,
|
||||
vocab,
|
||||
left_pad_source=False,
|
||||
left_pad_target=False,
|
||||
input_feeding=True,
|
||||
pad_to_length=None,
|
||||
):
|
||||
assert input_feeding
|
||||
if len(samples) == 0:
|
||||
return {}
|
||||
|
||||
def merge(key, left_pad, move_eos_to_beginning=False, pad_to_length=None):
|
||||
return data_utils.collate_tokens(
|
||||
[s[key] for s in samples],
|
||||
pad_idx,
|
||||
eos_idx=None, # use eos_idx of each sample instead of vocab.eos()
|
||||
left_pad=left_pad,
|
||||
move_eos_to_beginning=move_eos_to_beginning,
|
||||
pad_to_length=pad_to_length,
|
||||
)
|
||||
|
||||
id = torch.LongTensor([s["id"] for s in samples])
|
||||
src_tokens = merge(
|
||||
"source",
|
||||
left_pad=left_pad_source,
|
||||
pad_to_length=pad_to_length["source"] if pad_to_length is not None else None,
|
||||
)
|
||||
# sort by descending source length
|
||||
src_lengths = torch.LongTensor([s["source"].numel() for s in samples])
|
||||
src_lengths, sort_order = src_lengths.sort(descending=True)
|
||||
id = id.index_select(0, sort_order)
|
||||
src_tokens = src_tokens.index_select(0, sort_order)
|
||||
|
||||
prev_output_tokens = None
|
||||
target = None
|
||||
if samples[0].get("target", None) is not None:
|
||||
target = merge(
|
||||
"target",
|
||||
left_pad=left_pad_target,
|
||||
pad_to_length=pad_to_length["target"]
|
||||
if pad_to_length is not None
|
||||
else None,
|
||||
)
|
||||
target = target.index_select(0, sort_order)
|
||||
ntokens = sum(len(s["target"]) for s in samples)
|
||||
|
||||
if input_feeding:
|
||||
# we create a shifted version of targets for feeding the
|
||||
# previous output token(s) into the next decoder step
|
||||
prev_output_tokens = merge(
|
||||
"target",
|
||||
left_pad=left_pad_target,
|
||||
move_eos_to_beginning=True,
|
||||
pad_to_length=pad_to_length["target"]
|
||||
if pad_to_length is not None
|
||||
else None,
|
||||
)
|
||||
prev_output_tokens = prev_output_tokens.index_select(0, sort_order)
|
||||
else:
|
||||
ntokens = sum(len(s["source"]) for s in samples)
|
||||
|
||||
batch = {
|
||||
"id": id,
|
||||
"ntokens": ntokens,
|
||||
"net_input": {
|
||||
"src_tokens": src_tokens,
|
||||
"src_lengths": src_lengths,
|
||||
},
|
||||
"target": target,
|
||||
"nsentences": samples[0]["source"].size(0),
|
||||
"sort_order": sort_order,
|
||||
}
|
||||
if prev_output_tokens is not None:
|
||||
batch["net_input"]["prev_output_tokens"] = prev_output_tokens
|
||||
|
||||
return batch
|
||||
|
||||
|
||||
class DenoisingDataset(FairseqDataset):
|
||||
"""
|
||||
A wrapper around TokenBlockDataset for BART dataset.
|
||||
|
||||
Args:
|
||||
dataset (TokenBlockDataset): dataset to wrap
|
||||
sizes (List[int]): sentence lengths
|
||||
vocab (~fairseq.data.Dictionary): vocabulary
|
||||
mask_idx (int): dictionary index used for masked token
|
||||
mask_whole_words: only mask whole words. This should be a byte mask
|
||||
over vocab indices, indicating whether it is the beginning of a
|
||||
word. We will extend any mask to encompass the whole word.
|
||||
shuffle (bool, optional): shuffle the elements before batching.
|
||||
Default: ``True``
|
||||
seed: Seed for random number generator for reproducibility.
|
||||
args: argparse arguments.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
sizes,
|
||||
vocab,
|
||||
mask_idx,
|
||||
mask_whole_words,
|
||||
shuffle,
|
||||
seed,
|
||||
args,
|
||||
eos=None,
|
||||
item_transform_func=None,
|
||||
):
|
||||
self.dataset = dataset
|
||||
|
||||
self.sizes = sizes
|
||||
|
||||
self.vocab = vocab
|
||||
self.shuffle = shuffle
|
||||
self.seed = seed
|
||||
self.mask_idx = mask_idx
|
||||
self.mask_whole_word = mask_whole_words
|
||||
self.mask_ratio = args.mask
|
||||
self.random_ratio = args.mask_random
|
||||
self.insert_ratio = args.insert
|
||||
self.rotate_ratio = args.rotate
|
||||
self.permute_sentence_ratio = args.permute_sentences
|
||||
self.eos = eos if eos is not None else vocab.eos()
|
||||
self.item_transform_func = item_transform_func
|
||||
|
||||
if args.bpe != "gpt2":
|
||||
self.full_stop_index = self.vocab.eos()
|
||||
else:
|
||||
assert args.bpe == "gpt2"
|
||||
self.full_stop_index = self.vocab.index("13")
|
||||
|
||||
self.replace_length = args.replace_length
|
||||
if self.replace_length not in [-1, 0, 1]:
|
||||
raise ValueError(f"invalid arg: replace_length={self.replace_length}")
|
||||
if args.mask_length not in ["subword", "word", "span-poisson"]:
|
||||
raise ValueError(f"invalid arg: mask-length={args.mask_length}")
|
||||
if args.mask_length == "subword" and args.replace_length not in [0, 1]:
|
||||
raise ValueError(f"if using subwords, use replace-length=1 or 0")
|
||||
|
||||
self.mask_span_distribution = None
|
||||
if args.mask_length == "span-poisson":
|
||||
_lambda = args.poisson_lambda
|
||||
|
||||
lambda_to_the_k = 1
|
||||
e_to_the_minus_lambda = math.exp(-_lambda)
|
||||
k_factorial = 1
|
||||
ps = []
|
||||
for k in range(0, 128):
|
||||
ps.append(e_to_the_minus_lambda * lambda_to_the_k / k_factorial)
|
||||
lambda_to_the_k *= _lambda
|
||||
k_factorial *= k + 1
|
||||
if ps[-1] < 0.0000001:
|
||||
break
|
||||
ps = torch.FloatTensor(ps)
|
||||
self.mask_span_distribution = torch.distributions.Categorical(ps)
|
||||
|
||||
self.epoch = 0
|
||||
|
||||
@property
|
||||
def can_reuse_epoch_itr_across_epochs(self):
|
||||
return True # only the noise changes, not item sizes
|
||||
|
||||
def set_epoch(self, epoch, **unused):
|
||||
self.epoch = epoch
|
||||
|
||||
def __getitem__(self, index):
|
||||
with data_utils.numpy_seed(self.seed, self.epoch, index):
|
||||
tokens = self.dataset[index]
|
||||
assert tokens[-1] == self.eos
|
||||
source, target = tokens, tokens.clone()
|
||||
|
||||
if self.permute_sentence_ratio > 0.0:
|
||||
source = self.permute_sentences(source, self.permute_sentence_ratio)
|
||||
|
||||
if self.mask_ratio > 0:
|
||||
source = self.add_whole_word_mask(source, self.mask_ratio)
|
||||
|
||||
if self.insert_ratio > 0:
|
||||
source = self.add_insertion_noise(source, self.insert_ratio)
|
||||
|
||||
if self.rotate_ratio > 0.0 and np.random.random() < self.rotate_ratio:
|
||||
source = self.add_rolling_noise(source)
|
||||
# there can additional changes to make:
|
||||
if self.item_transform_func is not None:
|
||||
source, target = self.item_transform_func(source, target)
|
||||
|
||||
assert (source >= 0).all()
|
||||
assert (source[1:-1] >= 1).all()
|
||||
assert (source <= len(self.vocab)).all()
|
||||
assert source[0] == self.vocab.bos()
|
||||
assert source[-1] == self.eos
|
||||
return {
|
||||
"id": index,
|
||||
"source": source,
|
||||
"target": target,
|
||||
}
|
||||
|
||||
def __len__(self):
|
||||
return len(self.dataset)
|
||||
|
||||
def permute_sentences(self, source, p=1.0):
|
||||
full_stops = source == self.full_stop_index
|
||||
# Pretend it ends with a full stop so last span is a sentence
|
||||
full_stops[-2] = 1
|
||||
|
||||
# Tokens that are full stops, where the previous token is not
|
||||
sentence_ends = (full_stops[1:] * ~full_stops[:-1]).nonzero(as_tuple=False) + 2
|
||||
result = source.clone()
|
||||
|
||||
num_sentences = sentence_ends.size(0)
|
||||
num_to_permute = math.ceil((num_sentences * 2 * p) / 2.0)
|
||||
substitutions = torch.randperm(num_sentences)[:num_to_permute]
|
||||
ordering = torch.arange(0, num_sentences)
|
||||
ordering[substitutions] = substitutions[torch.randperm(num_to_permute)]
|
||||
|
||||
# Ignore <bos> at start
|
||||
index = 1
|
||||
for i in ordering:
|
||||
sentence = source[(sentence_ends[i - 1] if i > 0 else 1) : sentence_ends[i]]
|
||||
result[index : index + sentence.size(0)] = sentence
|
||||
index += sentence.size(0)
|
||||
return result
|
||||
|
||||
def word_starts(self, source):
|
||||
if self.mask_whole_word is not None:
|
||||
is_word_start = self.mask_whole_word.gather(0, source)
|
||||
else:
|
||||
is_word_start = torch.ones(source.size())
|
||||
is_word_start[0] = 0
|
||||
is_word_start[-1] = 0
|
||||
return is_word_start
|
||||
|
||||
def add_whole_word_mask(self, source, p):
|
||||
is_word_start = self.word_starts(source)
|
||||
num_to_mask = int(math.ceil(is_word_start.float().sum() * p))
|
||||
num_inserts = 0
|
||||
if num_to_mask == 0:
|
||||
return source
|
||||
|
||||
if self.mask_span_distribution is not None:
|
||||
lengths = self.mask_span_distribution.sample(sample_shape=(num_to_mask,))
|
||||
|
||||
# Make sure we have enough to mask
|
||||
cum_length = torch.cumsum(lengths, 0)
|
||||
while cum_length[-1] < num_to_mask:
|
||||
lengths = torch.cat(
|
||||
[
|
||||
lengths,
|
||||
self.mask_span_distribution.sample(sample_shape=(num_to_mask,)),
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
cum_length = torch.cumsum(lengths, 0)
|
||||
|
||||
# Trim to masking budget
|
||||
i = 0
|
||||
while cum_length[i] < num_to_mask:
|
||||
i += 1
|
||||
lengths[i] = num_to_mask - (0 if i == 0 else cum_length[i - 1])
|
||||
num_to_mask = i + 1
|
||||
lengths = lengths[:num_to_mask]
|
||||
|
||||
# Handle 0-length mask (inserts) separately
|
||||
lengths = lengths[lengths > 0]
|
||||
num_inserts = num_to_mask - lengths.size(0)
|
||||
num_to_mask -= num_inserts
|
||||
if num_to_mask == 0:
|
||||
return self.add_insertion_noise(source, num_inserts / source.size(0))
|
||||
|
||||
assert (lengths > 0).all()
|
||||
else:
|
||||
lengths = torch.ones((num_to_mask,)).long()
|
||||
assert is_word_start[-1] == 0
|
||||
word_starts = is_word_start.nonzero(as_tuple=False)
|
||||
indices = word_starts[
|
||||
torch.randperm(word_starts.size(0))[:num_to_mask]
|
||||
].squeeze(1)
|
||||
mask_random = torch.FloatTensor(num_to_mask).uniform_() < self.random_ratio
|
||||
|
||||
source_length = source.size(0)
|
||||
assert source_length - 1 not in indices
|
||||
to_keep = torch.ones(source_length, dtype=torch.bool)
|
||||
is_word_start[
|
||||
-1
|
||||
] = 255 # acts as a long length, so spans don't go over the end of doc
|
||||
if self.replace_length == 0:
|
||||
to_keep[indices] = 0
|
||||
else:
|
||||
# keep index, but replace it with [MASK]
|
||||
source[indices] = self.mask_idx
|
||||
source[indices[mask_random]] = torch.randint(
|
||||
1, len(self.vocab), size=(mask_random.sum(),)
|
||||
)
|
||||
|
||||
if self.mask_span_distribution is not None:
|
||||
assert len(lengths.size()) == 1
|
||||
assert lengths.size() == indices.size()
|
||||
lengths -= 1
|
||||
while indices.size(0) > 0:
|
||||
assert lengths.size() == indices.size()
|
||||
lengths -= is_word_start[indices + 1].long()
|
||||
uncompleted = lengths >= 0
|
||||
indices = indices[uncompleted] + 1
|
||||
mask_random = mask_random[uncompleted]
|
||||
lengths = lengths[uncompleted]
|
||||
if self.replace_length != -1:
|
||||
# delete token
|
||||
to_keep[indices] = 0
|
||||
else:
|
||||
# keep index, but replace it with [MASK]
|
||||
source[indices] = self.mask_idx
|
||||
source[indices[mask_random]] = torch.randint(
|
||||
1, len(self.vocab), size=(mask_random.sum(),)
|
||||
)
|
||||
else:
|
||||
# A bit faster when all lengths are 1
|
||||
while indices.size(0) > 0:
|
||||
uncompleted = is_word_start[indices + 1] == 0
|
||||
indices = indices[uncompleted] + 1
|
||||
mask_random = mask_random[uncompleted]
|
||||
if self.replace_length != -1:
|
||||
# delete token
|
||||
to_keep[indices] = 0
|
||||
else:
|
||||
# keep index, but replace it with [MASK]
|
||||
source[indices] = self.mask_idx
|
||||
source[indices[mask_random]] = torch.randint(
|
||||
1, len(self.vocab), size=(mask_random.sum(),)
|
||||
)
|
||||
|
||||
assert source_length - 1 not in indices
|
||||
|
||||
source = source[to_keep]
|
||||
|
||||
if num_inserts > 0:
|
||||
source = self.add_insertion_noise(source, num_inserts / source.size(0))
|
||||
|
||||
return source
|
||||
|
||||
def add_permuted_noise(self, tokens, p):
|
||||
num_words = len(tokens)
|
||||
num_to_permute = math.ceil(((num_words * 2) * p) / 2.0)
|
||||
substitutions = torch.randperm(num_words - 2)[:num_to_permute] + 1
|
||||
tokens[substitutions] = tokens[substitutions[torch.randperm(num_to_permute)]]
|
||||
return tokens
|
||||
|
||||
def add_rolling_noise(self, tokens):
|
||||
offset = np.random.randint(1, max(1, tokens.size(-1) - 1) + 1)
|
||||
tokens = torch.cat(
|
||||
(tokens[0:1], tokens[offset:-1], tokens[1:offset], tokens[-1:]),
|
||||
dim=0,
|
||||
)
|
||||
return tokens
|
||||
|
||||
def add_insertion_noise(self, tokens, p):
|
||||
if p == 0.0:
|
||||
return tokens
|
||||
|
||||
num_tokens = len(tokens)
|
||||
n = int(math.ceil(num_tokens * p))
|
||||
|
||||
noise_indices = torch.randperm(num_tokens + n - 2)[:n] + 1
|
||||
noise_mask = torch.zeros(size=(num_tokens + n,), dtype=torch.bool)
|
||||
noise_mask[noise_indices] = 1
|
||||
result = torch.LongTensor(n + len(tokens)).fill_(-1)
|
||||
|
||||
num_random = int(math.ceil(n * self.random_ratio))
|
||||
result[noise_indices[num_random:]] = self.mask_idx
|
||||
result[noise_indices[:num_random]] = torch.randint(
|
||||
low=1, high=len(self.vocab), size=(num_random,)
|
||||
)
|
||||
|
||||
result[~noise_mask] = tokens
|
||||
|
||||
assert (result >= 0).all()
|
||||
return result
|
||||
|
||||
def collater(self, samples, pad_to_length=None):
|
||||
"""Merge a list of samples to form a mini-batch.
|
||||
Args:
|
||||
samples (List[dict]): samples to collate
|
||||
Returns:
|
||||
dict: a mini-batch of data
|
||||
"""
|
||||
return collate(
|
||||
samples, self.vocab.pad(), self.eos, self.vocab, pad_to_length=pad_to_length
|
||||
)
|
||||
|
||||
def num_tokens(self, index):
|
||||
"""Return the number of tokens in a sample. This value is used to
|
||||
enforce ``--max-tokens`` during batching."""
|
||||
return self.sizes[index]
|
||||
|
||||
def size(self, index):
|
||||
"""Return an example's size as a float or tuple. This value is used when
|
||||
filtering a dataset with ``--max-positions``."""
|
||||
return self.sizes[index]
|
||||
|
||||
def ordered_indices(self):
|
||||
"""Return an ordered list of indices. Batches will be constructed based
|
||||
on this order."""
|
||||
if self.shuffle:
|
||||
indices = np.random.permutation(len(self))
|
||||
else:
|
||||
indices = np.arange(len(self))
|
||||
return indices[np.argsort(self.sizes[indices], kind="mergesort")]
|
||||
|
||||
def prefetch(self, indices):
|
||||
self.src.prefetch(indices)
|
||||
self.tgt.prefetch(indices)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
return (
|
||||
hasattr(self.src, "supports_prefetch")
|
||||
and self.src.supports_prefetch
|
||||
and hasattr(self.tgt, "supports_prefetch")
|
||||
and self.tgt.supports_prefetch
|
||||
)
|
||||
@@ -0,0 +1,394 @@
|
||||
# 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 os
|
||||
from collections import Counter
|
||||
from multiprocessing import Pool
|
||||
|
||||
import torch
|
||||
from fairseq import utils
|
||||
from fairseq.binarizer import safe_readline
|
||||
from fairseq.data import data_utils
|
||||
from fairseq.file_io import PathManager
|
||||
from fairseq.tokenizer import tokenize_line
|
||||
|
||||
|
||||
class Dictionary:
|
||||
"""A mapping from symbols to consecutive integers"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*, # begin keyword-only arguments
|
||||
bos="<s>",
|
||||
pad="<pad>",
|
||||
eos="</s>",
|
||||
unk="<unk>",
|
||||
extra_special_symbols=None,
|
||||
):
|
||||
self.bos_word, self.unk_word, self.pad_word, self.eos_word = bos, unk, pad, eos
|
||||
self.symbols = []
|
||||
self.count = []
|
||||
self.indices = {}
|
||||
self.bos_index = self.add_symbol(bos)
|
||||
self.pad_index = self.add_symbol(pad)
|
||||
self.eos_index = self.add_symbol(eos)
|
||||
self.unk_index = self.add_symbol(unk)
|
||||
if extra_special_symbols:
|
||||
for s in extra_special_symbols:
|
||||
self.add_symbol(s)
|
||||
self.nspecial = len(self.symbols)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.indices == other.indices
|
||||
|
||||
def __getitem__(self, idx):
|
||||
if idx < len(self.symbols):
|
||||
return self.symbols[idx]
|
||||
return self.unk_word
|
||||
|
||||
def __len__(self):
|
||||
"""Returns the number of symbols in the dictionary"""
|
||||
return len(self.symbols)
|
||||
|
||||
def __contains__(self, sym):
|
||||
return sym in self.indices
|
||||
|
||||
def index(self, sym):
|
||||
"""Returns the index of the specified symbol"""
|
||||
assert isinstance(sym, str)
|
||||
if sym in self.indices:
|
||||
return self.indices[sym]
|
||||
return self.unk_index
|
||||
|
||||
def string(
|
||||
self,
|
||||
tensor,
|
||||
bpe_symbol=None,
|
||||
escape_unk=False,
|
||||
extra_symbols_to_ignore=None,
|
||||
unk_string=None,
|
||||
include_eos=False,
|
||||
):
|
||||
"""Helper for converting a tensor of token indices to a string.
|
||||
|
||||
Can optionally remove BPE symbols or escape <unk> words.
|
||||
"""
|
||||
if torch.is_tensor(tensor) and tensor.dim() == 2:
|
||||
return "\n".join(
|
||||
self.string(t, bpe_symbol, escape_unk, extra_symbols_to_ignore, include_eos=include_eos)
|
||||
for t in tensor
|
||||
)
|
||||
|
||||
extra_symbols_to_ignore = set(extra_symbols_to_ignore or [])
|
||||
extra_symbols_to_ignore.add(self.eos())
|
||||
|
||||
def token_string(i):
|
||||
if i == self.unk():
|
||||
if unk_string is not None:
|
||||
return unk_string
|
||||
else:
|
||||
return self.unk_string(escape_unk)
|
||||
else:
|
||||
return self[i]
|
||||
|
||||
if hasattr(self, "bos_index"):
|
||||
extra_symbols_to_ignore.add(self.bos())
|
||||
|
||||
sent = " ".join(
|
||||
token_string(i)
|
||||
for i in tensor
|
||||
if utils.item(i) not in extra_symbols_to_ignore
|
||||
)
|
||||
|
||||
return data_utils.post_process(sent, bpe_symbol)
|
||||
|
||||
def unk_string(self, escape=False):
|
||||
"""Return unknown string, optionally escaped as: <<unk>>"""
|
||||
if escape:
|
||||
return "<{}>".format(self.unk_word)
|
||||
else:
|
||||
return self.unk_word
|
||||
|
||||
def add_symbol(self, word, n=1, overwrite=False):
|
||||
"""Adds a word to the dictionary"""
|
||||
if word in self.indices and not overwrite:
|
||||
idx = self.indices[word]
|
||||
self.count[idx] = self.count[idx] + n
|
||||
return idx
|
||||
else:
|
||||
idx = len(self.symbols)
|
||||
self.indices[word] = idx
|
||||
self.symbols.append(word)
|
||||
self.count.append(n)
|
||||
return idx
|
||||
|
||||
def update(self, new_dict):
|
||||
"""Updates counts from new dictionary."""
|
||||
for word in new_dict.symbols:
|
||||
idx2 = new_dict.indices[word]
|
||||
if word in self.indices:
|
||||
idx = self.indices[word]
|
||||
self.count[idx] = self.count[idx] + new_dict.count[idx2]
|
||||
else:
|
||||
idx = len(self.symbols)
|
||||
self.indices[word] = idx
|
||||
self.symbols.append(word)
|
||||
self.count.append(new_dict.count[idx2])
|
||||
|
||||
def finalize(self, threshold=-1, nwords=-1, padding_factor=8):
|
||||
"""Sort symbols by frequency in descending order, ignoring special ones.
|
||||
|
||||
Args:
|
||||
- threshold defines the minimum word count
|
||||
- nwords defines the total number of words in the final dictionary,
|
||||
including special symbols
|
||||
- padding_factor 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).
|
||||
"""
|
||||
if nwords <= 0:
|
||||
nwords = len(self)
|
||||
|
||||
new_indices = dict(zip(self.symbols[: self.nspecial], range(self.nspecial)))
|
||||
new_symbols = self.symbols[: self.nspecial]
|
||||
new_count = self.count[: self.nspecial]
|
||||
|
||||
c = Counter(
|
||||
dict(
|
||||
sorted(zip(self.symbols[self.nspecial :], self.count[self.nspecial :]))
|
||||
)
|
||||
)
|
||||
for symbol, count in c.most_common(nwords - self.nspecial):
|
||||
if count >= threshold:
|
||||
new_indices[symbol] = len(new_symbols)
|
||||
new_symbols.append(symbol)
|
||||
new_count.append(count)
|
||||
else:
|
||||
break
|
||||
|
||||
assert len(new_symbols) == len(new_indices)
|
||||
|
||||
self.count = list(new_count)
|
||||
self.symbols = list(new_symbols)
|
||||
self.indices = new_indices
|
||||
|
||||
self.pad_to_multiple_(padding_factor)
|
||||
|
||||
def pad_to_multiple_(self, padding_factor):
|
||||
"""Pad Dictionary size to be a multiple of *padding_factor*."""
|
||||
if padding_factor > 1:
|
||||
i = 0
|
||||
while len(self) % padding_factor != 0:
|
||||
symbol = "madeupword{:04d}".format(i)
|
||||
self.add_symbol(symbol, n=0)
|
||||
i += 1
|
||||
|
||||
def bos(self):
|
||||
"""Helper to get index of beginning-of-sentence symbol"""
|
||||
return self.bos_index
|
||||
|
||||
def pad(self):
|
||||
"""Helper to get index of pad symbol"""
|
||||
return self.pad_index
|
||||
|
||||
def eos(self):
|
||||
"""Helper to get index of end-of-sentence symbol"""
|
||||
return self.eos_index
|
||||
|
||||
def unk(self):
|
||||
"""Helper to get index of unk symbol"""
|
||||
return self.unk_index
|
||||
|
||||
@classmethod
|
||||
def load(cls, f):
|
||||
"""Loads the dictionary from a text file with the format:
|
||||
|
||||
```
|
||||
<symbol0> <count0>
|
||||
<symbol1> <count1>
|
||||
...
|
||||
```
|
||||
"""
|
||||
d = cls()
|
||||
d.add_from_file(f)
|
||||
return d
|
||||
|
||||
def add_from_file(self, f):
|
||||
"""
|
||||
Loads a pre-existing dictionary from a text file and adds its symbols
|
||||
to this instance.
|
||||
"""
|
||||
if isinstance(f, str):
|
||||
try:
|
||||
with open(PathManager.get_local_path(f), "r", encoding="utf-8") as fd:
|
||||
self.add_from_file(fd)
|
||||
except FileNotFoundError as fnfe:
|
||||
raise fnfe
|
||||
except UnicodeError:
|
||||
raise Exception(
|
||||
"Incorrect encoding detected in {}, please "
|
||||
"rebuild the dataset".format(f)
|
||||
)
|
||||
return
|
||||
|
||||
lines = f.readlines()
|
||||
indices_start_line = self._load_meta(lines)
|
||||
|
||||
for line in lines[indices_start_line:]:
|
||||
try:
|
||||
line, field = line.rstrip().rsplit(" ", 1)
|
||||
if field == "#fairseq:overwrite":
|
||||
overwrite = True
|
||||
line, field = line.rsplit(" ", 1)
|
||||
else:
|
||||
overwrite = False
|
||||
count = int(field)
|
||||
word = line
|
||||
if word in self and not overwrite:
|
||||
raise RuntimeError(
|
||||
"Duplicate word found when loading Dictionary: '{}'. "
|
||||
"Duplicate words can overwrite earlier ones by adding the "
|
||||
"#fairseq:overwrite flag at the end of the corresponding row "
|
||||
"in the dictionary file. If using the Camembert model, please "
|
||||
"download an updated copy of the model file.".format(word)
|
||||
)
|
||||
self.add_symbol(word, n=count, overwrite=overwrite)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"Incorrect dictionary format, expected '<token> <cnt> [flags]'"
|
||||
)
|
||||
|
||||
def _save(self, f, kv_iterator):
|
||||
if isinstance(f, str):
|
||||
PathManager.mkdirs(os.path.dirname(f))
|
||||
with PathManager.open(f, "w", encoding="utf-8") as fd:
|
||||
return self.save(fd)
|
||||
for k, v in kv_iterator:
|
||||
print("{} {}".format(k, v), file=f)
|
||||
|
||||
def _get_meta(self):
|
||||
return [], []
|
||||
|
||||
def _load_meta(self, lines):
|
||||
return 0
|
||||
|
||||
def save(self, f):
|
||||
"""Stores dictionary into a text file"""
|
||||
ex_keys, ex_vals = self._get_meta()
|
||||
self._save(
|
||||
f,
|
||||
zip(
|
||||
ex_keys + self.symbols[self.nspecial :],
|
||||
ex_vals + self.count[self.nspecial :],
|
||||
),
|
||||
)
|
||||
|
||||
def dummy_sentence(self, length):
|
||||
t = torch.Tensor(length).uniform_(self.nspecial + 1, len(self)).long()
|
||||
t[-1] = self.eos()
|
||||
return t
|
||||
|
||||
def encode_line(
|
||||
self,
|
||||
line,
|
||||
line_tokenizer=tokenize_line,
|
||||
add_if_not_exist=True,
|
||||
consumer=None,
|
||||
append_eos=True,
|
||||
reverse_order=False,
|
||||
) -> torch.IntTensor:
|
||||
words = line_tokenizer(line)
|
||||
if reverse_order:
|
||||
words = list(reversed(words))
|
||||
nwords = len(words)
|
||||
ids = torch.IntTensor(nwords + 1 if append_eos else nwords)
|
||||
|
||||
for i, word in enumerate(words):
|
||||
if add_if_not_exist:
|
||||
idx = self.add_symbol(word)
|
||||
else:
|
||||
idx = self.index(word)
|
||||
if consumer is not None:
|
||||
consumer(word, idx)
|
||||
ids[i] = idx
|
||||
if append_eos:
|
||||
ids[nwords] = self.eos_index
|
||||
return ids
|
||||
|
||||
@staticmethod
|
||||
def _add_file_to_dictionary_single_worker(
|
||||
filename, tokenize, eos_word, worker_id=0, num_workers=1
|
||||
):
|
||||
counter = Counter()
|
||||
with open(PathManager.get_local_path(filename), "r", encoding="utf-8") as f:
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
chunk_size = size // num_workers
|
||||
offset = worker_id * chunk_size
|
||||
end = offset + chunk_size
|
||||
f.seek(offset)
|
||||
if offset > 0:
|
||||
safe_readline(f) # drop first incomplete line
|
||||
line = f.readline()
|
||||
while line:
|
||||
for word in tokenize(line):
|
||||
counter.update([word])
|
||||
counter.update([eos_word])
|
||||
# f.tell() returns only an opaque number which can
|
||||
# return to the position in the file via f.seek()
|
||||
# and does not necessarily represent a byte position
|
||||
# in the file. However, f.tell() is faithful to the
|
||||
# byte position _most of the time_. Thus we can just
|
||||
# check against the file size to prevent early exit.
|
||||
if f.tell() > end and f.tell() < size:
|
||||
break
|
||||
line = f.readline()
|
||||
return counter
|
||||
|
||||
@staticmethod
|
||||
def add_file_to_dictionary(filename, dict, tokenize, num_workers):
|
||||
def merge_result(counter):
|
||||
for w, c in sorted(counter.items()):
|
||||
dict.add_symbol(w, c)
|
||||
|
||||
if num_workers > 1:
|
||||
pool = Pool(processes=num_workers)
|
||||
results = []
|
||||
for worker_id in range(num_workers):
|
||||
results.append(
|
||||
pool.apply_async(
|
||||
Dictionary._add_file_to_dictionary_single_worker,
|
||||
(filename, tokenize, dict.eos_word, worker_id, num_workers),
|
||||
)
|
||||
)
|
||||
pool.close()
|
||||
pool.join()
|
||||
for r in results:
|
||||
merge_result(r.get())
|
||||
else:
|
||||
merge_result(
|
||||
Dictionary._add_file_to_dictionary_single_worker(
|
||||
filename, tokenize, dict.eos_word
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TruncatedDictionary(object):
|
||||
def __init__(self, wrapped_dict, length):
|
||||
self.__class__ = type(
|
||||
wrapped_dict.__class__.__name__,
|
||||
(self.__class__, wrapped_dict.__class__),
|
||||
{},
|
||||
)
|
||||
self.__dict__ = wrapped_dict.__dict__
|
||||
self.wrapped_dict = wrapped_dict
|
||||
self.length = min(len(self.wrapped_dict), length)
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
def __getitem__(self, i):
|
||||
if i < self.length:
|
||||
return self.wrapped_dict[i]
|
||||
return self.wrapped_dict.unk()
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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 importlib
|
||||
import os
|
||||
|
||||
from fairseq import registry
|
||||
|
||||
|
||||
build_tokenizer, register_tokenizer, TOKENIZER_REGISTRY, _ = registry.setup_registry(
|
||||
"--tokenizer",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
build_bpe, register_bpe, BPE_REGISTRY, _ = registry.setup_registry(
|
||||
"--bpe",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
# automatically import any Python files in the encoders/ directory
|
||||
for file in os.listdir(os.path.dirname(__file__)):
|
||||
if file.endswith(".py") and not file.startswith("_"):
|
||||
module = file[: file.find(".py")]
|
||||
importlib.import_module("fairseq.data.encoders." + module)
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
|
||||
from fairseq import file_utils
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.data.encoders.byte_utils import (
|
||||
SPACE,
|
||||
SPACE_ESCAPE,
|
||||
byte_encode,
|
||||
smart_byte_decode,
|
||||
)
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ByteBpeConfig(FairseqDataclass):
|
||||
sentencepiece_model_path: str = field(
|
||||
default="???", metadata={"help": "path to sentencepiece model"}
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("byte_bpe", dataclass=ByteBpeConfig)
|
||||
class ByteBPE(object):
|
||||
def __init__(self, cfg):
|
||||
vocab = file_utils.cached_path(cfg.sentencepiece_model_path)
|
||||
try:
|
||||
import sentencepiece as spm
|
||||
|
||||
self.sp = spm.SentencePieceProcessor()
|
||||
self.sp.Load(vocab)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install sentencepiece with: pip install sentencepiece"
|
||||
)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
byte_encoded = byte_encode(x)
|
||||
return SPACE.join(self.sp.EncodeAsPieces(byte_encoded))
|
||||
|
||||
@staticmethod
|
||||
def decode(x: str) -> str:
|
||||
unescaped = x.replace(SPACE, "").replace(SPACE_ESCAPE, SPACE)
|
||||
return smart_byte_decode(unescaped)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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 re
|
||||
|
||||
|
||||
WHITESPACE_NORMALIZER = re.compile(r"\s+")
|
||||
SPACE = chr(32)
|
||||
SPACE_ESCAPE = chr(9601)
|
||||
# excluding non-breaking space (160) here
|
||||
PRINTABLE_LATIN = set(
|
||||
list(range(32, 126 + 1)) + list(range(161, 172 + 1)) + list(range(174, 255 + 1))
|
||||
)
|
||||
BYTE_TO_BCHAR = {
|
||||
b: chr(b) if b in PRINTABLE_LATIN else chr(256 + b) for b in range(256)
|
||||
}
|
||||
BCHAR_TO_BYTE = {bc: b for b, bc in BYTE_TO_BCHAR.items()}
|
||||
|
||||
|
||||
def byte_encode(x: str) -> str:
|
||||
normalized = WHITESPACE_NORMALIZER.sub(SPACE, x)
|
||||
return "".join([BYTE_TO_BCHAR[b] for b in normalized.encode("utf-8")])
|
||||
|
||||
|
||||
def byte_decode(x: str) -> str:
|
||||
try:
|
||||
return bytes([BCHAR_TO_BYTE[bc] for bc in x]).decode("utf-8")
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
def smart_byte_decode(x: str) -> str:
|
||||
output = byte_decode(x)
|
||||
if output == "":
|
||||
# DP the best recovery (max valid chars) if it's broken
|
||||
n_bytes = len(x)
|
||||
f = [0 for _ in range(n_bytes + 1)]
|
||||
pt = [0 for _ in range(n_bytes + 1)]
|
||||
for i in range(1, n_bytes + 1):
|
||||
f[i], pt[i] = f[i - 1], i - 1
|
||||
for j in range(1, min(4, i) + 1):
|
||||
if f[i - j] + 1 > f[i] and len(byte_decode(x[i - j : i])) > 0:
|
||||
f[i], pt[i] = f[i - j] + 1, i - j
|
||||
cur_pt = n_bytes
|
||||
while cur_pt > 0:
|
||||
if f[cur_pt] == f[pt[cur_pt]] + 1:
|
||||
output = byte_decode(x[pt[cur_pt] : cur_pt]) + output
|
||||
cur_pt = pt[cur_pt]
|
||||
return output
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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 fairseq.data.encoders import register_bpe
|
||||
from fairseq.data.encoders.byte_utils import (
|
||||
SPACE,
|
||||
SPACE_ESCAPE,
|
||||
byte_encode,
|
||||
smart_byte_decode,
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("bytes")
|
||||
class Bytes(object):
|
||||
def __init__(self, *unused):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def encode(x: str) -> str:
|
||||
encoded = byte_encode(x)
|
||||
escaped = encoded.replace(SPACE, SPACE_ESCAPE)
|
||||
return SPACE.join(list(escaped))
|
||||
|
||||
@staticmethod
|
||||
def decode(x: str) -> str:
|
||||
unescaped = x.replace(SPACE, "").replace(SPACE_ESCAPE, SPACE)
|
||||
return smart_byte_decode(unescaped)
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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 fairseq.data.encoders import register_bpe
|
||||
|
||||
|
||||
SPACE = chr(32)
|
||||
SPACE_ESCAPE = chr(9601)
|
||||
|
||||
|
||||
@register_bpe("characters")
|
||||
class Characters(object):
|
||||
def __init__(self, *unused):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def encode(x: str) -> str:
|
||||
escaped = x.replace(SPACE, SPACE_ESCAPE)
|
||||
return SPACE.join(list(escaped))
|
||||
|
||||
@staticmethod
|
||||
def decode(x: str) -> str:
|
||||
return x.replace(SPACE, "").replace(SPACE_ESCAPE, SPACE)
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
|
||||
from fairseq import file_utils
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class fastBPEConfig(FairseqDataclass):
|
||||
bpe_codes: str = field(default="???", metadata={"help": "path to fastBPE BPE"})
|
||||
|
||||
|
||||
@register_bpe("fastbpe", dataclass=fastBPEConfig)
|
||||
class fastBPE(object):
|
||||
def __init__(self, cfg):
|
||||
if cfg.bpe_codes is None:
|
||||
raise ValueError("--bpe-codes is required for --bpe=fastbpe")
|
||||
codes = file_utils.cached_path(cfg.bpe_codes)
|
||||
try:
|
||||
import fastBPE
|
||||
|
||||
self.bpe = fastBPE.fastBPE(codes)
|
||||
self.bpe_symbol = "@@ "
|
||||
except ImportError:
|
||||
raise ImportError("Please install fastBPE with: pip install fastBPE")
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return self.bpe.apply([x])[0]
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return (x + " ").replace(self.bpe_symbol, "").rstrip()
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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
|
||||
|
||||
from fairseq import file_utils
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
from .gpt2_bpe_utils import get_encoder
|
||||
|
||||
|
||||
DEFAULT_ENCODER_JSON = "https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json"
|
||||
DEFAULT_VOCAB_BPE = "https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GPT2BPEConfig(FairseqDataclass):
|
||||
gpt2_encoder_json: str = field(
|
||||
default=DEFAULT_ENCODER_JSON, metadata={"help": "path to encoder.json"}
|
||||
)
|
||||
gpt2_vocab_bpe: str = field(
|
||||
default=DEFAULT_VOCAB_BPE, metadata={"help": "path to vocab.bpe"}
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("gpt2", dataclass=GPT2BPEConfig)
|
||||
class GPT2BPE(object):
|
||||
def __init__(self, cfg):
|
||||
encoder_json = file_utils.cached_path(cfg.gpt2_encoder_json)
|
||||
vocab_bpe = file_utils.cached_path(cfg.gpt2_vocab_bpe)
|
||||
self.bpe = get_encoder(encoder_json, vocab_bpe)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return " ".join(map(str, self.bpe.encode(x)))
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return self.bpe.decode(
|
||||
[int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]
|
||||
)
|
||||
|
||||
def is_beginning_of_word(self, x: str) -> bool:
|
||||
return self.decode(x).startswith(" ")
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Byte pair encoding utilities from GPT-2.
|
||||
|
||||
Original source: https://github.com/openai/gpt-2/blob/master/src/encoder.py
|
||||
Original license: MIT
|
||||
"""
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def bytes_to_unicode():
|
||||
"""
|
||||
Returns list of utf-8 byte and a corresponding list of unicode strings.
|
||||
The reversible bpe codes work on unicode strings.
|
||||
This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
|
||||
When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
|
||||
This is a signficant percentage of your normal, say, 32K bpe vocab.
|
||||
To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
|
||||
And avoids mapping to whitespace/control characters the bpe code barfs on.
|
||||
"""
|
||||
bs = (
|
||||
list(range(ord("!"), ord("~") + 1))
|
||||
+ list(range(ord("¡"), ord("¬") + 1))
|
||||
+ list(range(ord("®"), ord("ÿ") + 1))
|
||||
)
|
||||
cs = bs[:]
|
||||
n = 0
|
||||
for b in range(2 ** 8):
|
||||
if b not in bs:
|
||||
bs.append(b)
|
||||
cs.append(2 ** 8 + n)
|
||||
n += 1
|
||||
cs = [chr(n) for n in cs]
|
||||
return dict(zip(bs, cs))
|
||||
|
||||
|
||||
def get_pairs(word):
|
||||
"""Return set of symbol pairs in a word.
|
||||
Word is represented as tuple of symbols (symbols being variable-length strings).
|
||||
"""
|
||||
pairs = set()
|
||||
prev_char = word[0]
|
||||
for char in word[1:]:
|
||||
pairs.add((prev_char, char))
|
||||
prev_char = char
|
||||
return pairs
|
||||
|
||||
|
||||
class Encoder:
|
||||
def __init__(self, encoder, bpe_merges, errors="replace"):
|
||||
self.encoder = encoder
|
||||
self.decoder = {v: k for k, v in self.encoder.items()}
|
||||
self.errors = errors # how to handle errors in decoding
|
||||
self.byte_encoder = bytes_to_unicode()
|
||||
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
|
||||
self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges))))
|
||||
self.cache = {}
|
||||
|
||||
try:
|
||||
import regex as re
|
||||
|
||||
self.re = re
|
||||
except ImportError:
|
||||
raise ImportError("Please install regex with: pip install regex")
|
||||
|
||||
# Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contractions
|
||||
self.pat = self.re.compile(
|
||||
r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
|
||||
)
|
||||
|
||||
def bpe(self, token):
|
||||
if token in self.cache:
|
||||
return self.cache[token]
|
||||
word = tuple(token)
|
||||
pairs = get_pairs(word)
|
||||
|
||||
if not pairs:
|
||||
return token
|
||||
|
||||
while True:
|
||||
bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float("inf")))
|
||||
if bigram not in self.bpe_ranks:
|
||||
break
|
||||
first, second = bigram
|
||||
new_word = []
|
||||
i = 0
|
||||
while i < len(word):
|
||||
try:
|
||||
j = word.index(first, i)
|
||||
new_word.extend(word[i:j])
|
||||
i = j
|
||||
except:
|
||||
new_word.extend(word[i:])
|
||||
break
|
||||
|
||||
if word[i] == first and i < len(word) - 1 and word[i + 1] == second:
|
||||
new_word.append(first + second)
|
||||
i += 2
|
||||
else:
|
||||
new_word.append(word[i])
|
||||
i += 1
|
||||
new_word = tuple(new_word)
|
||||
word = new_word
|
||||
if len(word) == 1:
|
||||
break
|
||||
else:
|
||||
pairs = get_pairs(word)
|
||||
word = " ".join(word)
|
||||
self.cache[token] = word
|
||||
return word
|
||||
|
||||
def encode(self, text):
|
||||
bpe_tokens = []
|
||||
for token in self.re.findall(self.pat, text):
|
||||
token = "".join(self.byte_encoder[b] for b in token.encode("utf-8"))
|
||||
bpe_tokens.extend(
|
||||
self.encoder[bpe_token] for bpe_token in self.bpe(token).split(" ")
|
||||
)
|
||||
return bpe_tokens
|
||||
|
||||
def decode(self, tokens):
|
||||
text = "".join([self.decoder.get(token, token) for token in tokens])
|
||||
text = bytearray([self.byte_decoder[c] for c in text]).decode(
|
||||
"utf-8", errors=self.errors
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def get_encoder(encoder_json_path, vocab_bpe_path):
|
||||
with open(encoder_json_path, "r") as f:
|
||||
encoder = json.load(f)
|
||||
with open(vocab_bpe_path, "r", encoding="utf-8") as f:
|
||||
bpe_data = f.read()
|
||||
bpe_merges = [tuple(merge_str.split()) for merge_str in bpe_data.split("\n")[1:-1]]
|
||||
return Encoder(
|
||||
encoder=encoder,
|
||||
bpe_merges=bpe_merges,
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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
|
||||
from typing import Optional
|
||||
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class BertBPEConfig(FairseqDataclass):
|
||||
bpe_cased: bool = field(default=False, metadata={"help": "set for cased BPE"})
|
||||
bpe_vocab_file: Optional[str] = field(
|
||||
default=None, metadata={"help": "bpe vocab file"}
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("bert", dataclass=BertBPEConfig)
|
||||
class BertBPE(object):
|
||||
def __init__(self, cfg):
|
||||
try:
|
||||
from transformers import BertTokenizer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install transformers with: pip install transformers"
|
||||
)
|
||||
|
||||
if cfg.bpe_vocab_file:
|
||||
self.bert_tokenizer = BertTokenizer(
|
||||
cfg.bpe_vocab_file, do_lower_case=not cfg.bpe_cased
|
||||
)
|
||||
else:
|
||||
vocab_file_name = (
|
||||
"bert-base-cased" if cfg.bpe_cased else "bert-base-uncased"
|
||||
)
|
||||
self.bert_tokenizer = BertTokenizer.from_pretrained(vocab_file_name)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return " ".join(self.bert_tokenizer.tokenize(x))
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return self.bert_tokenizer.clean_up_tokenization(
|
||||
self.bert_tokenizer.convert_tokens_to_string(x.split(" "))
|
||||
)
|
||||
|
||||
def is_beginning_of_word(self, x: str) -> bool:
|
||||
return not x.startswith("##")
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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
|
||||
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq import file_utils
|
||||
|
||||
|
||||
@dataclass
|
||||
class HuggingFaceByteLevelBPEConfig(FairseqDataclass):
|
||||
bpe_merges: str = field(default="???", metadata={"help": "path to merges.txt"})
|
||||
bpe_vocab: str = field(default="???", metadata={"help": "path to vocab.json"})
|
||||
bpe_add_prefix_space: bool = field(
|
||||
default=False, metadata={"help": "add prefix space before encoding"}
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("hf_byte_bpe", dataclass=HuggingFaceByteLevelBPEConfig)
|
||||
class HuggingFaceByteLevelBPE(object):
|
||||
def __init__(self, cfg):
|
||||
try:
|
||||
from tokenizers import ByteLevelBPETokenizer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install huggingface/tokenizers with: " "pip install tokenizers"
|
||||
)
|
||||
|
||||
bpe_vocab = file_utils.cached_path(cfg.bpe_vocab)
|
||||
bpe_merges = file_utils.cached_path(cfg.bpe_merges)
|
||||
|
||||
self.bpe = ByteLevelBPETokenizer(
|
||||
bpe_vocab,
|
||||
bpe_merges,
|
||||
add_prefix_space=cfg.bpe_add_prefix_space,
|
||||
)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return " ".join(map(str, self.bpe.encode(x).ids))
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return self.bpe.decode(
|
||||
[int(tok) if tok not in {"<unk>", "<mask>"} else tok for tok in x.split()]
|
||||
)
|
||||
|
||||
def is_beginning_of_word(self, x: str) -> bool:
|
||||
return self.decode(x).startswith(" ")
|
||||
@@ -0,0 +1,49 @@
|
||||
# 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
|
||||
|
||||
from fairseq.data.encoders import register_tokenizer
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MosesTokenizerConfig(FairseqDataclass):
|
||||
source_lang: str = field(default="en", metadata={"help": "source language"})
|
||||
target_lang: str = field(default="en", metadata={"help": "target language"})
|
||||
moses_no_dash_splits: bool = field(
|
||||
default=False, metadata={"help": "don't apply dash split rules"}
|
||||
)
|
||||
moses_no_escape: bool = field(
|
||||
default=False,
|
||||
metadata={"help": "don't perform HTML escaping on apostrophe, quotes, etc."},
|
||||
)
|
||||
|
||||
|
||||
@register_tokenizer("moses", dataclass=MosesTokenizerConfig)
|
||||
class MosesTokenizer(object):
|
||||
def __init__(self, cfg: MosesTokenizerConfig):
|
||||
self.cfg = cfg
|
||||
|
||||
try:
|
||||
from sacremoses import MosesTokenizer, MosesDetokenizer
|
||||
|
||||
self.tok = MosesTokenizer(cfg.source_lang)
|
||||
self.detok = MosesDetokenizer(cfg.target_lang)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install Moses tokenizer with: pip install sacremoses"
|
||||
)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return self.tok.tokenize(
|
||||
x,
|
||||
aggressive_dash_splits=(not self.cfg.moses_no_dash_splits),
|
||||
return_str=True,
|
||||
escape=(not self.cfg.moses_no_escape),
|
||||
)
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return self.detok.detokenize(x.split())
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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 fairseq.data.encoders import register_tokenizer
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@register_tokenizer("nltk", dataclass=FairseqDataclass)
|
||||
class NLTKTokenizer(object):
|
||||
def __init__(self, *unused):
|
||||
try:
|
||||
from nltk.tokenize import word_tokenize
|
||||
|
||||
self.word_tokenize = word_tokenize
|
||||
except ImportError:
|
||||
raise ImportError("Please install nltk with: pip install nltk")
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return " ".join(self.word_tokenize(x))
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return x
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
|
||||
from fairseq import file_utils
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SentencepieceConfig(FairseqDataclass):
|
||||
sentencepiece_model: str = field(
|
||||
default="???", metadata={"help": "path to sentencepiece model"}
|
||||
)
|
||||
|
||||
|
||||
@register_bpe("sentencepiece", dataclass=SentencepieceConfig)
|
||||
class SentencepieceBPE(object):
|
||||
def __init__(self, cfg):
|
||||
sentencepiece_model = file_utils.cached_path(cfg.sentencepiece_model)
|
||||
try:
|
||||
import sentencepiece as spm
|
||||
|
||||
self.sp = spm.SentencePieceProcessor()
|
||||
self.sp.Load(sentencepiece_model)
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install sentencepiece with: pip install sentencepiece"
|
||||
)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return " ".join(self.sp.EncodeAsPieces(x))
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return x.replace(" ", "").replace("\u2581", " ").strip()
|
||||
|
||||
def is_beginning_of_word(self, x: str) -> bool:
|
||||
if x in ["<unk>", "<s>", "</s>", "<pad>"]:
|
||||
# special elements are always considered beginnings
|
||||
# HACK: this logic is already present in fairseq/tasks/masked_lm.py
|
||||
# but these special tokens are also contained in the sentencepiece
|
||||
# vocabulary which causes duplicate special tokens. This hack makes
|
||||
# sure that they are all taken into account.
|
||||
return True
|
||||
return x.startswith("\u2581")
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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 re
|
||||
|
||||
from fairseq.data.encoders import register_tokenizer
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@register_tokenizer("space", dataclass=FairseqDataclass)
|
||||
class SpaceTokenizer(object):
|
||||
def __init__(self, *unused):
|
||||
self.space_tok = re.compile(r"\s+")
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return self.space_tok.sub(" ", x)
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return x
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
|
||||
from fairseq import file_utils
|
||||
from fairseq.data.encoders import register_bpe
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubwordNMTBPEConfig(FairseqDataclass):
|
||||
bpe_codes: str = field(default="???", metadata={"help": "path to subword NMT BPE"})
|
||||
bpe_separator: str = field(default="@@", metadata={"help": "BPE separator"})
|
||||
|
||||
|
||||
@register_bpe("subword_nmt", dataclass=SubwordNMTBPEConfig)
|
||||
class SubwordNMTBPE(object):
|
||||
def __init__(self, cfg):
|
||||
if cfg.bpe_codes is None:
|
||||
raise ValueError("--bpe-codes is required for --bpe=subword_nmt")
|
||||
codes = file_utils.cached_path(cfg.bpe_codes)
|
||||
try:
|
||||
from subword_nmt import apply_bpe
|
||||
|
||||
bpe_parser = apply_bpe.create_parser()
|
||||
bpe_args = bpe_parser.parse_args(
|
||||
[
|
||||
"--codes",
|
||||
codes,
|
||||
"--separator",
|
||||
cfg.bpe_separator,
|
||||
]
|
||||
)
|
||||
self.bpe = apply_bpe.BPE(
|
||||
bpe_args.codes,
|
||||
bpe_args.merges,
|
||||
bpe_args.separator,
|
||||
None,
|
||||
bpe_args.glossaries,
|
||||
)
|
||||
self.bpe_symbol = bpe_args.separator + " "
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Please install subword_nmt with: pip install subword-nmt"
|
||||
)
|
||||
|
||||
def encode(self, x: str) -> str:
|
||||
return self.bpe.process_line(x)
|
||||
|
||||
def decode(self, x: str) -> str:
|
||||
return (x + " ").replace(self.bpe_symbol, "").rstrip()
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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.data import encoders
|
||||
|
||||
|
||||
def get_whole_word_mask(args, dictionary):
|
||||
bpe = encoders.build_bpe(args)
|
||||
if bpe is not None:
|
||||
|
||||
def is_beginning_of_word(i):
|
||||
if i < dictionary.nspecial:
|
||||
# special elements are always considered beginnings
|
||||
return True
|
||||
tok = 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(dictionary))))
|
||||
)
|
||||
return mask_whole_words
|
||||
return None
|
||||
@@ -0,0 +1,205 @@
|
||||
# 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 numpy as np
|
||||
import torch.utils.data
|
||||
from fairseq.data import data_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EpochListening:
|
||||
"""Mixin for receiving updates whenever the epoch increments."""
|
||||
|
||||
@property
|
||||
def can_reuse_epoch_itr_across_epochs(self):
|
||||
"""
|
||||
Whether we can reuse the :class:`fairseq.data.EpochBatchIterator` for
|
||||
this dataset across epochs.
|
||||
|
||||
This needs to return ``False`` if the sample sizes can change across
|
||||
epochs, in which case we may need to regenerate batches at each epoch.
|
||||
If your dataset relies in ``set_epoch`` then you should consider setting
|
||||
this to ``False``.
|
||||
"""
|
||||
return True
|
||||
|
||||
def set_epoch(self, epoch):
|
||||
"""Will receive the updated epoch number at the beginning of the epoch."""
|
||||
pass
|
||||
|
||||
|
||||
class FairseqDataset(torch.utils.data.Dataset, EpochListening):
|
||||
"""A dataset that provides helpers for batching."""
|
||||
|
||||
def __getitem__(self, index):
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def collater(self, samples):
|
||||
"""Merge a list of samples to form a mini-batch.
|
||||
|
||||
Args:
|
||||
samples (List[dict]): samples to collate
|
||||
|
||||
Returns:
|
||||
dict: a mini-batch suitable for forwarding with a Model
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def num_tokens(self, index):
|
||||
"""Return the number of tokens in a sample. This value is used to
|
||||
enforce ``--max-tokens`` during batching."""
|
||||
raise NotImplementedError
|
||||
|
||||
def num_tokens_vec(self, indices):
|
||||
"""Return the number of tokens for a set of positions defined by indices.
|
||||
This value is used to enforce ``--max-tokens`` during batching."""
|
||||
raise NotImplementedError
|
||||
|
||||
def size(self, index):
|
||||
"""Return an example's size as a float or tuple. This value is used when
|
||||
filtering a dataset with ``--max-positions``."""
|
||||
raise NotImplementedError
|
||||
|
||||
def ordered_indices(self):
|
||||
"""Return an ordered list of indices. Batches will be constructed based
|
||||
on this order."""
|
||||
return np.arange(len(self), dtype=np.int64)
|
||||
|
||||
@property
|
||||
def supports_prefetch(self):
|
||||
"""Whether this dataset supports prefetching."""
|
||||
return False
|
||||
|
||||
def attr(self, attr: str, index: int):
|
||||
return getattr(self, attr, None)
|
||||
|
||||
def prefetch(self, indices):
|
||||
"""Prefetch the data required for this epoch."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_batch_shapes(self):
|
||||
"""
|
||||
Return a list of valid batch shapes, for example::
|
||||
|
||||
[(8, 512), (16, 256), (32, 128)]
|
||||
|
||||
The first dimension of each tuple is the batch size and can be ``None``
|
||||
to automatically infer the max batch size based on ``--max-tokens``.
|
||||
The second dimension of each tuple is the max supported length as given
|
||||
by :func:`fairseq.data.FairseqDataset.num_tokens`.
|
||||
|
||||
This will be used by :func:`fairseq.data.FairseqDataset.batch_by_size`
|
||||
to restrict batch shapes. This is useful on TPUs to avoid too many
|
||||
dynamic shapes (and recompilations).
|
||||
"""
|
||||
return None
|
||||
|
||||
def batch_by_size(
|
||||
self,
|
||||
indices,
|
||||
max_tokens=None,
|
||||
max_sentences=None,
|
||||
required_batch_size_multiple=1,
|
||||
):
|
||||
"""
|
||||
Given an ordered set of indices, return batches according to
|
||||
*max_tokens*, *max_sentences* and *required_batch_size_multiple*.
|
||||
"""
|
||||
from fairseq.data import data_utils
|
||||
|
||||
fixed_shapes = self.get_batch_shapes()
|
||||
if fixed_shapes is not None:
|
||||
|
||||
def adjust_bsz(bsz, num_tokens):
|
||||
if bsz is None:
|
||||
assert max_tokens is not None, "Must specify --max-tokens"
|
||||
bsz = max_tokens // num_tokens
|
||||
if max_sentences is not None:
|
||||
bsz = min(bsz, max_sentences)
|
||||
elif (
|
||||
bsz >= required_batch_size_multiple
|
||||
and bsz % required_batch_size_multiple != 0
|
||||
):
|
||||
bsz -= bsz % required_batch_size_multiple
|
||||
return bsz
|
||||
|
||||
fixed_shapes = np.array(
|
||||
[
|
||||
[adjust_bsz(bsz, num_tokens), num_tokens]
|
||||
for (bsz, num_tokens) in fixed_shapes
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
num_tokens_vec = self.num_tokens_vec(indices).astype('int64')
|
||||
except NotImplementedError:
|
||||
num_tokens_vec = None
|
||||
|
||||
return data_utils.batch_by_size(
|
||||
indices,
|
||||
num_tokens_fn=self.num_tokens,
|
||||
num_tokens_vec=num_tokens_vec,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=max_sentences,
|
||||
required_batch_size_multiple=required_batch_size_multiple,
|
||||
fixed_shapes=fixed_shapes,
|
||||
)
|
||||
|
||||
def filter_indices_by_size(self, indices, max_sizes):
|
||||
"""
|
||||
Filter a list of sample indices. Remove those that are longer than
|
||||
specified in *max_sizes*.
|
||||
|
||||
WARNING: don't update, override method in child classes
|
||||
|
||||
Args:
|
||||
indices (np.array): original array of sample indices
|
||||
max_sizes (int or list[int] or tuple[int]): max sample size,
|
||||
can be defined separately for src and tgt (then list or tuple)
|
||||
|
||||
Returns:
|
||||
np.array: filtered sample array
|
||||
list: list of removed indices
|
||||
"""
|
||||
if isinstance(max_sizes, float) or isinstance(max_sizes, int):
|
||||
if hasattr(self, "sizes") and isinstance(self.sizes, np.ndarray):
|
||||
ignored = indices[self.sizes[indices] > max_sizes].tolist()
|
||||
indices = indices[self.sizes[indices] <= max_sizes]
|
||||
elif (
|
||||
hasattr(self, "sizes")
|
||||
and isinstance(self.sizes, list)
|
||||
and len(self.sizes) == 1
|
||||
):
|
||||
ignored = indices[self.sizes[0][indices] > max_sizes].tolist()
|
||||
indices = indices[self.sizes[0][indices] <= max_sizes]
|
||||
else:
|
||||
indices, ignored = data_utils._filter_by_size_dynamic(
|
||||
indices, self.size, max_sizes
|
||||
)
|
||||
else:
|
||||
indices, ignored = data_utils._filter_by_size_dynamic(
|
||||
indices, self.size, max_sizes
|
||||
)
|
||||
return indices, ignored
|
||||
|
||||
@property
|
||||
def supports_fetch_outside_dataloader(self):
|
||||
"""Whether this dataset supports fetching outside the workers of the dataloader."""
|
||||
return True
|
||||
|
||||
|
||||
class FairseqIterableDataset(torch.utils.data.IterableDataset, EpochListening):
|
||||
"""
|
||||
For datasets that need to be read sequentially, usually because the data is
|
||||
being streamed or otherwise can't be manipulated on a single machine.
|
||||
"""
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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 os
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def fasta_file_path(prefix_path):
|
||||
return prefix_path + ".fasta"
|
||||
|
||||
|
||||
class FastaDataset(torch.utils.data.Dataset):
|
||||
"""
|
||||
For loading protein sequence datasets in the common FASTA data format
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, cache_indices=False):
|
||||
self.fn = fasta_file_path(path)
|
||||
self.threadlocal = threading.local()
|
||||
self.cache = Path(f"{path}.fasta.idx.npy")
|
||||
if cache_indices:
|
||||
if self.cache.exists():
|
||||
self.offsets, self.sizes = np.load(self.cache)
|
||||
else:
|
||||
self.offsets, self.sizes = self._build_index(path)
|
||||
np.save(self.cache, np.stack([self.offsets, self.sizes]))
|
||||
else:
|
||||
self.offsets, self.sizes = self._build_index(path)
|
||||
|
||||
def _get_file(self):
|
||||
if not hasattr(self.threadlocal, "f"):
|
||||
self.threadlocal.f = open(self.fn, "r")
|
||||
return self.threadlocal.f
|
||||
|
||||
def __getitem__(self, idx):
|
||||
f = self._get_file()
|
||||
f.seek(self.offsets[idx])
|
||||
desc = f.readline().strip()
|
||||
line = f.readline()
|
||||
seq = ""
|
||||
while line != "" and line[0] != ">":
|
||||
seq += line.strip()
|
||||
line = f.readline()
|
||||
return desc, seq
|
||||
|
||||
def __len__(self):
|
||||
return self.offsets.size
|
||||
|
||||
def _build_index(self, path: str):
|
||||
# Use grep and awk to get 100M/s on local SSD.
|
||||
# Should process your enormous 100G fasta in ~10 min single core...
|
||||
path = fasta_file_path(path)
|
||||
bytes_offsets = subprocess.check_output(
|
||||
f"cat {path} | tqdm --bytes --total $(wc -c < {path})"
|
||||
"| grep --byte-offset '^>' -o | cut -d: -f1",
|
||||
shell=True,
|
||||
)
|
||||
fasta_lengths = subprocess.check_output(
|
||||
f"cat {path} | tqdm --bytes --total $(wc -c < {path})"
|
||||
"| awk '/^>/ {print \"\";next;} { printf(\"%s\",$0);}' | tail -n+2 | awk '{print length($1)}'",
|
||||
shell=True,
|
||||
)
|
||||
bytes_np = np.fromstring(bytes_offsets, dtype=np.int64, sep=" ")
|
||||
sizes_np = np.fromstring(fasta_lengths, dtype=np.int64, sep=" ")
|
||||
return bytes_np, sizes_np
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = state
|
||||
self.threadlocal = threading.local()
|
||||
|
||||
def __getstate__(self):
|
||||
d = {}
|
||||
for i, v in self.__dict__.items():
|
||||
if i != "threadlocal":
|
||||
d[i] = v
|
||||
return d
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self.threadlocal, "f"):
|
||||
self.threadlocal.f.close()
|
||||
del self.threadlocal.f
|
||||
|
||||
@staticmethod
|
||||
def exists(path):
|
||||
return os.path.exists(fasta_file_path(path))
|
||||
|
||||
|
||||
class EncodedFastaDataset(FastaDataset):
|
||||
"""
|
||||
The FastaDataset returns raw sequences - this allows us to return
|
||||
indices with a dictionary instead.
|
||||
"""
|
||||
|
||||
def __init__(self, path, dictionary):
|
||||
super().__init__(path, cache_indices=True)
|
||||
self.dictionary = dictionary
|
||||
|
||||
def __getitem__(self, idx):
|
||||
desc, seq = super().__getitem__(idx)
|
||||
return self.dictionary.encode_line(seq, line_tokenizer=list).long()
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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 . import FairseqDataset
|
||||
|
||||
|
||||
class IdDataset(FairseqDataset):
|
||||
def __getitem__(self, index):
|
||||
return index
|
||||
|
||||
def __len__(self):
|
||||
return 0
|
||||
|
||||
def collater(self, samples):
|
||||
return torch.tensor(samples)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user