chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
!*/*.sh
|
||||
!*/*.md
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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 fairseq.version import __version__ # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,90 @@
|
||||
# Adaptive Span
|
||||
|
||||
Adaptive Span is a novel self-attention mechanism that can learn its optimal
|
||||
attention span. This allows us to extend significantly the maximum context size
|
||||
used in Transformer, while maintaining control over their memory footprint
|
||||
and computational time. It uses the Truncated BPTT technique for training,
|
||||
as in [transformerXL](https://github.com/pytorch/fairseq/blob/master/examples/truncated_bptt/README.md).
|
||||
|
||||
Adaptive Span was introduced by paper:
|
||||
[Adaptive Attention Span in Transformers](https://arxiv.org/abs/1905.07799),
|
||||
which achieved state-of-the-art language modeling results at the time of publication.
|
||||
|
||||
We manage to reproduce their result in fairseq and keep most of the
|
||||
[original implementation](https://github.com/facebookresearch/adaptive-span) untouched.
|
||||
You can refer to the their sweep file as well if any combination of hyperparameter is not clear.
|
||||
|
||||
##### 0. Setup
|
||||
|
||||
First you need to process the Enwik8 dataset, we use the pre-tokenized dataset
|
||||
from [adaptive span paper](https://github.com/facebookresearch/adaptive-span/blob/master/get_data.sh).
|
||||
You can download the dataset, and then run:
|
||||
```bash
|
||||
fairseq-preprocess --only-source --trainpref ~/data/enwik8/train.txt \
|
||||
--validpref ~/data/enwik8/valid.txt --testpref ~/data/enwik8/test.txt \
|
||||
--destdir ~/data/enwik8/data-bin/ --joined-dictionary --workers 20
|
||||
```
|
||||
|
||||
##### 1. Train a Adaptive Span model on Enwik8
|
||||
|
||||
We will train a 12-layer Adaptive Span model following the [hyperparameters
|
||||
used in the original
|
||||
paper](https://github.com/facebookresearch/adaptive-span/blob/master/experiments/enwik8.sh).
|
||||
|
||||
The following command assumes 4 GPUs, so that the total batch size is 64
|
||||
sequences (4 x 16). Training should take 2-3 days on 4 V100 GPUs:
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 fairseq-train \
|
||||
--user-dir examples/adaptive_span \
|
||||
--data ~/data/enwik8/data-bin/ \
|
||||
--fp16 --fp16-no-flatten-grads --max-update 600000 \
|
||||
--task truncated_bptt_lm --tokens-per-sample 512 --arch adaptive_span \
|
||||
--n-layer 12 --d-model 512 --n-head 8 --d-inner 2048 --dropout 0.3 \
|
||||
--attn-span 8192 --optimizer adagrad_with_grad_clip --adagrad-clip 0.03 \
|
||||
--validate-interval-updates 1000 \
|
||||
--lr-scheduler fixed --warmup-updates 32000 --batch-size-valid 32 \
|
||||
--lr 0.07 --criterion adaptive_span_loss --batch-size 16 --update-freq 1 \
|
||||
--seed 2 --log-format json --log-interval 25 --aux-loss-scaler 5e-07
|
||||
```
|
||||
This should land around 1.05 on validation, 1.03 on test. You can lower the
|
||||
--aux-loss-scaler for better performance (longer span). It gives ~0.03 bpc
|
||||
improvement to the transformerXL baseline here.
|
||||
If training on a single GPU, set `--update-freq=4` to accumulate 4x gradients
|
||||
and simulate training on 4 GPUs.
|
||||
You can also reproduce the transformerXL result on enwik8 using this code base.
|
||||
It should land around 1.06 on test,matching the [original paper](https://github.com/kimiyoung/transformer-xl/blob/master/pytorch/run_enwik8_base.sh).
|
||||
You can try by
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 fairseq-train \
|
||||
--user-dir examples/truncated_bptt \
|
||||
~/data/enwik8/data-bin/ \
|
||||
--task truncated_bptt_lm --fp16 --max-update 400000 \
|
||||
--tokens-per-sample 512 --arch transformer_xl --n-layer 12 \
|
||||
--d-model 512 --n-head 8 --d-head 64 --d-inner 2048 --dropout 0.1 \
|
||||
--dropatt 0.0 --mem-len 512 --optimizer adam --clip-norm 0.25 \
|
||||
--lr-scheduler cosine --warmup-updates 0 \
|
||||
--lr 0.0 --lr 0.00025 --batch-size 15 \
|
||||
--update-freq 1 --seed 2 --log-format json --log-interval 25 \
|
||||
--fp16
|
||||
```
|
||||
|
||||
##### 2. Evaluate
|
||||
For Adaptive Span:
|
||||
```bash
|
||||
fairseq-eval-lm ~/data/enwik8/data-bin/ --path model/checkpoint_best.pt \
|
||||
--user-dir examples/adaptive_span \
|
||||
--task truncated_bptt_lm --batch-size 8 --tokens-per-sample 512 --gen-subset test
|
||||
```
|
||||
For Transformer-XL evaluation:
|
||||
```bash
|
||||
fairseq-eval-lm ~/data/enwik8/data-bin/ --path model/checkpoint_best.pt \
|
||||
--user-dir examples/truncated_bptt/ --task truncated_bptt_lm --batch-size 8 \
|
||||
--tokens-per-sample 80 \
|
||||
--model-overrides '{"mem_len":2100,"clamp_len":820,"same_length":True}' \
|
||||
--gen-subset valid
|
||||
```
|
||||
|
||||
*Note:* During training the model saw 512 tokens of context
|
||||
(``--tokens-per-sample=512``), with batch size 8. These settings match the evaluation
|
||||
settings from [the original
|
||||
paper](https://github.com/facebookresearch/adaptive-span/blob/master/experiments/enwik8.sh).
|
||||
@@ -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 importlib
|
||||
import os
|
||||
|
||||
# automatically import any Python files in the current directory
|
||||
cur_dir = os.path.dirname(__file__)
|
||||
for file in os.listdir(cur_dir):
|
||||
path = os.path.join(cur_dir, file)
|
||||
if (
|
||||
not file.startswith("_")
|
||||
and not file.startswith(".")
|
||||
and (file.endswith(".py") or os.path.isdir(path))
|
||||
):
|
||||
mod_name = file[: file.find(".py")] if file.endswith(".py") else file
|
||||
module = importlib.import_module(__name__ + "." + mod_name)
|
||||
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from torch.optim import Adagrad
|
||||
|
||||
from fairseq.optim import LegacyFairseqOptimizer, register_optimizer
|
||||
|
||||
|
||||
@register_optimizer("adagrad_with_grad_clip")
|
||||
class FairseqAdagradWithGradClip(LegacyFairseqOptimizer):
|
||||
def __init__(self, args, params):
|
||||
super().__init__(args)
|
||||
self._optimizer = AdagradWithGradClip(params, **self.optimizer_config)
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add optimizer-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
parser.add_argument('--weight-decay', '--wd', default=0.0, type=float, metavar='WD',
|
||||
help='weight decay')
|
||||
parser.add_argument('--adagrad-clip', default=0.0, type=float, metavar='D',
|
||||
help='internal grad clip')
|
||||
# fmt: on
|
||||
|
||||
@property
|
||||
def optimizer_config(self):
|
||||
"""
|
||||
Return a kwarg dictionary that will be used to override optimizer
|
||||
args stored in checkpoints. This allows us to load a checkpoint and
|
||||
resume training using a different set of optimizer args, e.g., with a
|
||||
different learning rate.
|
||||
"""
|
||||
return {
|
||||
"lr": self.args.lr[0],
|
||||
"weight_decay": self.args.weight_decay,
|
||||
"grad_clip": self.args.adagrad_clip,
|
||||
}
|
||||
|
||||
@property
|
||||
def supports_flat_params(self):
|
||||
return False
|
||||
|
||||
|
||||
def _clip_grad(clr, grad, group_grad_clip):
|
||||
if group_grad_clip > 0:
|
||||
norm = grad.norm(2).item()
|
||||
if norm > group_grad_clip:
|
||||
clr *= group_grad_clip / (norm + 1e-10)
|
||||
return clr
|
||||
|
||||
|
||||
class AdagradWithGradClip(Adagrad):
|
||||
"""Adagrad algorithm with custom gradient clipping"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params,
|
||||
lr=1e-2,
|
||||
lr_decay=0,
|
||||
weight_decay=0,
|
||||
initial_accumulator_value=0,
|
||||
grad_clip=0,
|
||||
):
|
||||
Adagrad.__init__(
|
||||
self,
|
||||
params,
|
||||
lr=lr,
|
||||
lr_decay=lr_decay,
|
||||
weight_decay=weight_decay,
|
||||
initial_accumulator_value=initial_accumulator_value,
|
||||
)
|
||||
self.defaults["grad_clip"] = grad_clip
|
||||
self.param_groups[0].setdefault("grad_clip", grad_clip)
|
||||
|
||||
def step(self, closure=None):
|
||||
loss = None
|
||||
if closure is not None:
|
||||
loss = closure()
|
||||
|
||||
for group in self.param_groups:
|
||||
for p in group["params"]:
|
||||
if p.grad is None:
|
||||
continue
|
||||
|
||||
grad = p.grad.data
|
||||
state = self.state[p]
|
||||
|
||||
state["step"] += 1
|
||||
|
||||
if group["weight_decay"] != 0:
|
||||
if p.grad.data.is_sparse:
|
||||
raise RuntimeError(
|
||||
"weight_decay option is "
|
||||
"not compatible with sparse "
|
||||
"gradients"
|
||||
)
|
||||
grad = grad.add(group["weight_decay"], p.data)
|
||||
|
||||
clr = group["lr"] / (1 + (state["step"] - 1) * group["lr_decay"])
|
||||
|
||||
# clip
|
||||
clr = _clip_grad(clr=clr, grad=grad, group_grad_clip=group["grad_clip"])
|
||||
|
||||
if grad.is_sparse:
|
||||
# the update is non-linear so indices must be unique
|
||||
grad = grad.coalesce()
|
||||
grad_indices = grad._indices()
|
||||
grad_values = grad._values()
|
||||
size = grad.size()
|
||||
|
||||
def make_sparse(values):
|
||||
constructor = grad.new
|
||||
if grad_indices.dim() == 0 or values.dim() == 0:
|
||||
return constructor().resize_as_(grad)
|
||||
return constructor(grad_indices, values, size)
|
||||
|
||||
state["sum"].add_(make_sparse(grad_values.pow(2)))
|
||||
std = state["sum"]._sparse_mask(grad)
|
||||
std_values = std._values().sqrt_().add_(1e-10)
|
||||
p.data.add_(-clr, make_sparse(grad_values / std_values))
|
||||
else:
|
||||
state["sum"].addcmul_(1, grad, grad)
|
||||
std = state["sum"].sqrt().add_(1e-10)
|
||||
p.data.addcdiv_(-clr, grad, std)
|
||||
|
||||
return loss
|
||||
@@ -0,0 +1,160 @@
|
||||
# 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 as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class AdaptiveMask(nn.Module):
|
||||
"""Soft masking function for adaptive size.
|
||||
It masks out the last K values of an input. The masking value
|
||||
goes from 1 to 0 gradually, so K can be learned with
|
||||
back-propagation.
|
||||
Args:
|
||||
max_size: maximum size (i.e. input dimension)
|
||||
ramp_size: size of the ramp going from 0 to 1
|
||||
init_val: initial size proportion not to be masked out
|
||||
shape: learn multiple sizes independent of each other
|
||||
"""
|
||||
|
||||
def __init__(self, max_size, ramp_size, init_val=0, shape=(1,)):
|
||||
nn.Module.__init__(self)
|
||||
self._max_size = max_size
|
||||
self._ramp_size = ramp_size
|
||||
self.current_val = nn.Parameter(torch.zeros(*shape) + init_val)
|
||||
mask_template = torch.linspace(1 - max_size, 0, steps=max_size)
|
||||
self.register_buffer("mask_template", mask_template)
|
||||
|
||||
def forward(self, x):
|
||||
mask = self.mask_template.float() + self.current_val.float() * self._max_size
|
||||
mask = mask / self._ramp_size + 1
|
||||
mask = mask.clamp(0, 1)
|
||||
if x.size(-1) < self._max_size:
|
||||
# the input could have been trimmed beforehand to save computation
|
||||
mask = mask.narrow(-1, self._max_size - x.size(-1), x.size(-1))
|
||||
x = (x * mask).type_as(x)
|
||||
return x
|
||||
|
||||
def get_current_max_size(self, include_ramp=True):
|
||||
current_size = math.ceil(self.current_val.max().item() * self._max_size)
|
||||
if include_ramp:
|
||||
current_size += self._ramp_size
|
||||
current_size = max(0, min(self._max_size, current_size))
|
||||
return current_size
|
||||
|
||||
def get_current_avg_size(self, include_ramp=True):
|
||||
current_size = math.ceil(
|
||||
self.current_val.float().mean().item() * self._max_size
|
||||
)
|
||||
if include_ramp:
|
||||
current_size += self._ramp_size
|
||||
current_size = max(0, min(self._max_size, current_size))
|
||||
return current_size
|
||||
|
||||
def clamp_param(self):
|
||||
"""this need to be called after each update"""
|
||||
self.current_val.data.clamp_(0, 1)
|
||||
|
||||
|
||||
class AdaptiveSpan(nn.Module):
|
||||
"""Adaptive attention span for Transformerself.
|
||||
This module learns an attention span length from data for each
|
||||
self-attention head.
|
||||
Args:
|
||||
attn_span: maximum attention span
|
||||
adapt_span_loss: loss coefficient for the span length
|
||||
adapt_span_ramp: length of the masking ramp
|
||||
adapt_span_init: initial size ratio
|
||||
adapt_span_cache: adapt cache size to reduce memory usage
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attn_span,
|
||||
adapt_span_ramp,
|
||||
adapt_span_init,
|
||||
n_head,
|
||||
adapt_span_layer,
|
||||
**kargs
|
||||
):
|
||||
nn.Module.__init__(self)
|
||||
self._max_span = attn_span
|
||||
self._n_head = n_head
|
||||
self._adapt_span_layer = adapt_span_layer
|
||||
if self._adapt_span_layer:
|
||||
self._mask = AdaptiveMask(
|
||||
max_size=self._max_span,
|
||||
ramp_size=adapt_span_ramp,
|
||||
init_val=adapt_span_init,
|
||||
)
|
||||
else:
|
||||
self._mask = AdaptiveMask(
|
||||
max_size=self._max_span,
|
||||
ramp_size=adapt_span_ramp,
|
||||
init_val=adapt_span_init,
|
||||
shape=(n_head, 1, 1),
|
||||
)
|
||||
|
||||
def forward(self, attn, normalize=True):
|
||||
"""mask attention with the right span"""
|
||||
# batch and head dimensions are merged together, so separate them first
|
||||
self.clamp_param()
|
||||
if self._adapt_span_layer:
|
||||
attn = self._mask(attn)
|
||||
else:
|
||||
B = attn.size(0) # batch size
|
||||
M = attn.size(1) # block size
|
||||
attn = attn.reshape(B // self._n_head, self._n_head, M, -1)
|
||||
attn = self._mask(attn)
|
||||
attn = attn.view(B, M, -1)
|
||||
return attn
|
||||
|
||||
def get_trim_len(self):
|
||||
"""how much of memory can be trimmed to reduce computation"""
|
||||
L = self._max_span
|
||||
trim_len = min(L - 1, L - self._mask.get_current_max_size())
|
||||
# too fine granularity might be bad for the memory management
|
||||
trim_len = math.floor(trim_len / 64) * 64
|
||||
return trim_len
|
||||
|
||||
def trim_memory(self, query, key, value, key_pe):
|
||||
"""trim out unnecessary memory beforehand to reduce computation"""
|
||||
trim_len = self.get_trim_len()
|
||||
cache_size = key.size(1) - query.size(1)
|
||||
trim_len_cache = trim_len - (self._max_span - cache_size)
|
||||
if trim_len_cache > 0:
|
||||
key = key[:, trim_len_cache:, :]
|
||||
value = value[:, trim_len_cache:, :]
|
||||
elif trim_len_cache < 0:
|
||||
# cache is too short! this happens when validation resumes
|
||||
# after a lot of updates.
|
||||
key = F.pad(key, [0, 0, -trim_len_cache, 0])
|
||||
value = F.pad(value, [0, 0, -trim_len_cache, 0])
|
||||
if trim_len > 0:
|
||||
if key_pe is not None:
|
||||
key_pe = key_pe[:, :, trim_len:]
|
||||
return key, value, key_pe
|
||||
|
||||
def get_cache_size(self):
|
||||
"""determine how long the cache should be"""
|
||||
trim_len = self.get_trim_len()
|
||||
# give a buffer of 64 steps since a span might increase
|
||||
# in future updates
|
||||
return min(self._max_span, self._max_span - trim_len + 64)
|
||||
|
||||
def get_loss(self):
|
||||
"""a loss term for regularizing the span length"""
|
||||
return self._max_span * self._mask.current_val.float().mean()
|
||||
|
||||
def get_current_max_span(self):
|
||||
return self._mask.get_current_max_size()
|
||||
|
||||
def get_current_avg_span(self):
|
||||
return self._mask.get_current_avg_size()
|
||||
|
||||
def clamp_param(self):
|
||||
self._mask.clamp_param()
|
||||
@@ -0,0 +1,106 @@
|
||||
# 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 register_criterion
|
||||
from fairseq.criterions.cross_entropy import CrossEntropyCriterion
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdaptiveSpanCriterionConfig(FairseqDataclass):
|
||||
sentence_avg: bool = II("optimization.sentence_avg")
|
||||
|
||||
|
||||
@register_criterion("adaptive_span_loss", dataclass=AdaptiveSpanCriterionConfig)
|
||||
class AdaptiveSpanCriterion(CrossEntropyCriterion):
|
||||
def __init__(self, task, sentence_avg):
|
||||
super().__init__(task, 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 here is summed, different from the adaptive span code
|
||||
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, aux_loss, avg_span, max_span = self.compute_loss(
|
||||
model, net_output, sample, reduce=reduce
|
||||
)
|
||||
sample_size = (
|
||||
sample["target"].size(0) if self.sentence_avg else sample["ntokens"]
|
||||
)
|
||||
loss /= sample_size
|
||||
total_loss = loss + aux_loss
|
||||
sample_size = 1
|
||||
|
||||
logging_output = {
|
||||
"loss": loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["target"].size(0),
|
||||
"sample_size": sample_size,
|
||||
"total_loss": total_loss.data,
|
||||
"avg_span": avg_span * sample_size,
|
||||
"max_span": max_span * sample_size,
|
||||
}
|
||||
return total_loss, sample_size, logging_output
|
||||
|
||||
def compute_loss(self, model, net_output, sample, reduce=True):
|
||||
loss, _ = super().compute_loss(model, net_output, sample, reduce)
|
||||
aux_loss = model.get_aux_loss()
|
||||
avg_span = model.get_current_avg_span()
|
||||
max_span = model.get_current_max_span()
|
||||
return loss, aux_loss, avg_span, max_span
|
||||
|
||||
@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)
|
||||
total_loss_sum = sum(log.get("total_loss", 0) for log in logging_outputs)
|
||||
avg_span_sum = sum(log.get("avg_span", 0) for log in logging_outputs)
|
||||
max_span_sum = sum(log.get("max_span", 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
|
||||
)
|
||||
metrics.log_scalar("avg_span", avg_span_sum / sample_size, sample_size, round=3)
|
||||
metrics.log_scalar("max_span", max_span_sum / sample_size, sample_size, round=3)
|
||||
# total loss contains the L1 norm on adaptive-span
|
||||
metrics.log_scalar(
|
||||
"total_loss",
|
||||
total_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,263 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fairseq.modules.layer_norm import LayerNorm
|
||||
|
||||
from .adaptive_span_attention import AdaptiveSpan
|
||||
|
||||
# Size notations:
|
||||
# B = batch_size, H = d_model, M = block_size, L = attn_span
|
||||
|
||||
|
||||
def _skew(X, pad_value):
|
||||
"""shift every row 1 step to right"""
|
||||
# X = B x M x L
|
||||
B, M, L = X.size()
|
||||
X = F.pad(X, (0, M + 1), value=pad_value) # B x M x (L+M+1)
|
||||
X = X.view(B, -1) # B x ML+MM+M
|
||||
X = X[:, :-M] # B x ML+MM
|
||||
X = X.view(B, M, M + L) # B x M x L+M
|
||||
return X
|
||||
|
||||
|
||||
def _unskew(X):
|
||||
"""reverse _skew operation"""
|
||||
# X = B x M x L+M
|
||||
B, M, L = X.size()
|
||||
L -= M
|
||||
X = X.view(B, -1) # B x ML+MM
|
||||
X = F.pad(X, (0, M)) # B x ML+MM+M
|
||||
X = X.view(B, M, M + L + 1) # B x M x L+M+1
|
||||
X = X[:, :, :L] # B x M x L
|
||||
return X
|
||||
|
||||
|
||||
class SeqAttention(nn.Module):
|
||||
"""Sequential self-attention layer.
|
||||
Each token will attend to its previous fixed number of steps.
|
||||
Note that attention doesn't include the current step itself.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model, n_head, attn_span, dropout, adapt_span_layer, **kargs):
|
||||
nn.Module.__init__(self)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
self.d_model = d_model # size of a single head
|
||||
self.attn_span = attn_span
|
||||
self.adaptive_span = AdaptiveSpan(
|
||||
attn_span=attn_span,
|
||||
n_head=n_head,
|
||||
adapt_span_layer=adapt_span_layer,
|
||||
**kargs
|
||||
)
|
||||
|
||||
def forward(self, query, key, value, key_pe):
|
||||
# query size = B x M x H
|
||||
# key, value sizes = B x (M+L) x H
|
||||
|
||||
key, value, key_pe = self.adaptive_span.trim_memory(query, key, value, key_pe)
|
||||
|
||||
# compute attention from context
|
||||
# B x M (dest) x (M+L) (src)
|
||||
attn_cont = torch.matmul(query, key.transpose(-1, -2))
|
||||
attn_cont = _unskew(attn_cont) # B x M x L
|
||||
|
||||
# compute the effect of position embedding
|
||||
attn_pos = torch.matmul(query, key_pe) # B x M x L_pos
|
||||
attn = attn_cont + attn_pos
|
||||
|
||||
attn = attn / math.sqrt(self.d_model) # B x M X L_pos
|
||||
|
||||
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
|
||||
|
||||
# trim attention lengths according to the learned span
|
||||
attn = self.adaptive_span(attn)
|
||||
|
||||
attn = self.dropout(attn) # B x M X L_pos
|
||||
|
||||
attn_cont = _skew(attn, 0) # B x M X (L+M)
|
||||
out = torch.matmul(attn_cont, value) # B x M x H
|
||||
return out
|
||||
|
||||
def get_cache_size(self):
|
||||
return self.adaptive_span.get_cache_size()
|
||||
|
||||
|
||||
class MultiHeadSeqAttention(nn.Module):
|
||||
def __init__(self, d_model, n_head, **kargs):
|
||||
nn.Module.__init__(self)
|
||||
assert d_model % n_head == 0
|
||||
self.n_head = n_head
|
||||
self.head_dim = d_model // n_head
|
||||
self.attn = SeqAttention(d_model=self.head_dim, n_head=n_head, **kargs)
|
||||
self.proj_query = nn.Linear(d_model, d_model, bias=False)
|
||||
nn.init.xavier_normal_(self.proj_query.weight)
|
||||
self.proj_out = nn.Linear(d_model, d_model, bias=False)
|
||||
nn.init.xavier_normal_(self.proj_out.weight)
|
||||
self.proj_val = nn.Linear(d_model, d_model, bias=False)
|
||||
nn.init.xavier_normal_(self.proj_val.weight)
|
||||
self.proj_key = nn.Linear(d_model, d_model, bias=False)
|
||||
nn.init.xavier_normal_(self.proj_key.weight)
|
||||
|
||||
def head_reshape(self, x):
|
||||
K = self.n_head
|
||||
D = self.head_dim
|
||||
x = x.view(x.size()[:-1] + (K, D)) # B x (M+L) x K x D
|
||||
x = x.transpose(1, 2).contiguous() # B x K x (M+L) x D
|
||||
x = x.view(-1, x.size(-2), x.size(-1)) # B_K x (M+L) x D
|
||||
return x
|
||||
|
||||
def forward(self, query, key, value, key_pe):
|
||||
B = query.size(0)
|
||||
K = self.n_head
|
||||
D = self.head_dim
|
||||
M = query.size(1)
|
||||
|
||||
query = self.proj_query(query)
|
||||
query = self.head_reshape(query)
|
||||
value = self.proj_val(value)
|
||||
value = self.head_reshape(value)
|
||||
key = self.proj_key(key)
|
||||
key = self.head_reshape(key)
|
||||
|
||||
out = self.attn(query, key, value, key_pe) # B_K x M x D
|
||||
out = out.view(B, K, M, D) # B x K x M x D
|
||||
out = out.transpose(1, 2).contiguous() # B x M x K x D
|
||||
out = out.view(B, M, -1) # B x M x K_D
|
||||
out = self.proj_out(out)
|
||||
return out
|
||||
|
||||
|
||||
class FeedForwardLayer(nn.Module):
|
||||
def __init__(self, d_model, d_inner, dropout, **kargs):
|
||||
nn.Module.__init__(self)
|
||||
self.fc1 = nn.Linear(d_model, d_inner)
|
||||
self.fc2 = nn.Linear(d_inner, d_model)
|
||||
nn.init.xavier_uniform_(self.fc1.weight)
|
||||
nn.init.xavier_uniform_(self.fc2.weight)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(self, h):
|
||||
h1 = F.relu(self.fc1(h))
|
||||
h1 = self.dropout(h1)
|
||||
h2 = self.fc2(h1)
|
||||
return h2
|
||||
|
||||
|
||||
class TransformerSeqLayer(nn.Module):
|
||||
def __init__(self, d_model, **kargs):
|
||||
nn.Module.__init__(self)
|
||||
self.attn = MultiHeadSeqAttention(d_model=d_model, **kargs)
|
||||
self.norm1 = LayerNorm(d_model)
|
||||
self.ff = FeedForwardLayer(d_model=d_model, **kargs)
|
||||
self.norm2 = LayerNorm(d_model)
|
||||
|
||||
def forward(self, h, h_cache, key_pe):
|
||||
# h = B x M x H
|
||||
# h_cache = B x L x H
|
||||
h_all = torch.cat([h_cache, h], dim=1) # B x (M+L) x H
|
||||
attn_out = self.attn(h, h_all, h_all, key_pe)
|
||||
h = self.norm1(h + attn_out) # B x M x H
|
||||
if self.ff is not None:
|
||||
ff_out = self.ff(h)
|
||||
out = self.norm2(h + ff_out) # B x M x H
|
||||
else:
|
||||
out = h
|
||||
return out
|
||||
|
||||
def get_cache_size(self):
|
||||
return self.attn.attn.get_cache_size()
|
||||
|
||||
|
||||
class TransformerSeq(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size,
|
||||
d_model,
|
||||
n_head,
|
||||
n_layer,
|
||||
attn_span,
|
||||
emb_dropout,
|
||||
aux_loss_scaler,
|
||||
adapt_span_layer,
|
||||
**kargs
|
||||
):
|
||||
nn.Module.__init__(self)
|
||||
# token embeddings
|
||||
self.in_emb = nn.Embedding(vocab_size, d_model)
|
||||
nn.init.normal_(self.in_emb.weight, mean=0, std=d_model ** -0.5)
|
||||
self.out_emb = nn.Linear(d_model, vocab_size)
|
||||
self.aux_loss_scaler = aux_loss_scaler
|
||||
if emb_dropout > 0:
|
||||
self.emb_dropout = nn.Dropout(emb_dropout)
|
||||
else:
|
||||
self.emb_dropout = None
|
||||
# position embeddings
|
||||
self.key_pe = nn.Parameter(torch.randn(1, d_model // n_head, attn_span))
|
||||
|
||||
self.layers = nn.ModuleList()
|
||||
self.layers.extend(
|
||||
TransformerSeqLayer(
|
||||
d_model=d_model,
|
||||
n_head=n_head,
|
||||
attn_span=attn_span,
|
||||
adapt_span_layer=adapt_span_layer,
|
||||
**kargs
|
||||
)
|
||||
for _ in range(n_layer)
|
||||
)
|
||||
|
||||
def forward(self, x, h_cache, target=None):
|
||||
# x size = B x M
|
||||
block_size = x.size(1)
|
||||
h = self.in_emb(x) # B x M x H
|
||||
if self.emb_dropout is not None:
|
||||
h = self.emb_dropout(h)
|
||||
|
||||
h_cache_next = []
|
||||
for l, layer in enumerate(self.layers):
|
||||
cache_size = layer.attn.attn.get_cache_size()
|
||||
if cache_size > block_size:
|
||||
h_cache_next_l = torch.cat(
|
||||
[h_cache[l][:, -cache_size + block_size :, :], h], dim=1
|
||||
).detach()
|
||||
else:
|
||||
h_cache_next_l = h[:, -cache_size:, :].detach()
|
||||
h_cache_next.append(h_cache_next_l)
|
||||
h = layer(h, h_cache[l], self.key_pe) # B x M x H
|
||||
|
||||
if self.emb_dropout is not None:
|
||||
h = self.emb_dropout(h)
|
||||
|
||||
out = F.log_softmax(self.out_emb(h).float(), dim=-1).type_as(h)
|
||||
dummy_loss = None
|
||||
|
||||
return out, h_cache_next, dummy_loss
|
||||
|
||||
def get_aux_loss(self):
|
||||
loss = 0.0
|
||||
for layer in self.layers:
|
||||
loss += layer.attn.attn.adaptive_span.get_loss()
|
||||
return self.aux_loss_scaler * loss
|
||||
|
||||
def get_current_max_span(self):
|
||||
max_span = 0.0
|
||||
for layer in self.layers:
|
||||
max_span = max(
|
||||
max_span, layer.attn.attn.adaptive_span.get_current_max_span()
|
||||
)
|
||||
return max_span
|
||||
|
||||
def get_current_avg_span(self):
|
||||
avg_span = 0.0
|
||||
for layer in self.layers:
|
||||
avg_span += layer.attn.attn.adaptive_span.get_current_avg_span()
|
||||
return avg_span / len(self.layers)
|
||||
@@ -0,0 +1,145 @@
|
||||
# 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
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import torch
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.models import (
|
||||
FairseqIncrementalDecoder,
|
||||
FairseqLanguageModel,
|
||||
register_model,
|
||||
)
|
||||
from .adaptive_span_model import TransformerSeq as AdaptiveSpanTransformerModel
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdaptiveSpanSmallConfig(FairseqDataclass):
|
||||
# defaults come from https://github.com/facebookresearch/adaptive-span/blob/master/experiments/enwik8_small.sh
|
||||
vocab_size: int = 50
|
||||
d_model: int = 256
|
||||
n_head: int = 4
|
||||
d_inner: int = 1024
|
||||
n_layer: int = 8
|
||||
attn_span: int = 1024
|
||||
dropout: float = 0.0
|
||||
emb_dropout: float = 0.0
|
||||
adapt_span_ramp: int = 32
|
||||
adapt_span_init: float = 0.0
|
||||
aux_loss_scaler: float = 0.000002
|
||||
adapt_span_layer: bool = False
|
||||
|
||||
|
||||
@register_model("adaptive_span", dataclass=AdaptiveSpanSmallConfig)
|
||||
class AdaptiveSpanTransformer(FairseqLanguageModel):
|
||||
@classmethod
|
||||
def build_model(cls, cfg: AdaptiveSpanSmallConfig, task):
|
||||
return cls(AdaptiveSpanDecoder(cfg, task))
|
||||
|
||||
def get_aux_loss(self):
|
||||
return self.decoder.get_aux_loss()
|
||||
|
||||
def get_current_max_span(self):
|
||||
return self.decoder.get_current_max_span()
|
||||
|
||||
def get_current_avg_span(self):
|
||||
return self.decoder.get_current_avg_span()
|
||||
|
||||
|
||||
class AdaptiveSpanDecoder(FairseqIncrementalDecoder):
|
||||
def __init__(self, cfg, task):
|
||||
|
||||
super().__init__(task.target_dictionary)
|
||||
|
||||
self.config = cfg
|
||||
config = AdaptiveSpanSmallConfig(
|
||||
vocab_size=len(task.target_dictionary),
|
||||
d_model=cfg.d_model,
|
||||
n_head=cfg.n_head,
|
||||
d_inner=cfg.d_inner,
|
||||
n_layer=cfg.n_layer,
|
||||
attn_span=cfg.attn_span,
|
||||
dropout=cfg.dropout,
|
||||
emb_dropout=cfg.emb_dropout,
|
||||
adapt_span_ramp=cfg.adapt_span_ramp,
|
||||
adapt_span_init=cfg.adapt_span_init,
|
||||
aux_loss_scaler=cfg.aux_loss_scaler,
|
||||
adapt_span_layer=cfg.adapt_span_layer,
|
||||
)
|
||||
logger.info(config)
|
||||
self.model = AdaptiveSpanTransformerModel(**config.__dict__)
|
||||
|
||||
self._mems = None
|
||||
|
||||
def forward(
|
||||
self,
|
||||
src_tokens,
|
||||
incremental_state: Optional[Dict[str, List[torch.Tensor]]] = None,
|
||||
encoder_out=None,
|
||||
):
|
||||
bsz = src_tokens.size(0)
|
||||
if incremental_state is not None: # used during inference
|
||||
mems = self.get_incremental_state("mems")
|
||||
src_tokens = src_tokens[:, -1:] # only keep the most recent token
|
||||
else:
|
||||
mems = self._mems
|
||||
|
||||
if mems is None:
|
||||
# first time init
|
||||
mems = self.init_hid_cache(bsz)
|
||||
output = self.model(x=src_tokens, h_cache=mems,)
|
||||
if incremental_state is not None:
|
||||
self.set_incremental_state(incremental_state, "mems", output[1])
|
||||
else:
|
||||
self._mems = output[1]
|
||||
return (output[0],)
|
||||
|
||||
def max_positions(self):
|
||||
return self.config.attn_span
|
||||
|
||||
def init_hid_cache(self, batch_sz):
|
||||
hid = []
|
||||
for layer in self.model.layers:
|
||||
param = next(self.model.parameters())
|
||||
h = torch.zeros(
|
||||
batch_sz,
|
||||
layer.get_cache_size(),
|
||||
self.config.d_model,
|
||||
dtype=param.dtype,
|
||||
device=param.device,
|
||||
)
|
||||
hid.append(h)
|
||||
return hid
|
||||
|
||||
def get_aux_loss(self):
|
||||
return self.model.get_aux_loss()
|
||||
|
||||
def get_current_max_span(self):
|
||||
return self.model.get_current_max_span()
|
||||
|
||||
def get_current_avg_span(self):
|
||||
return self.model.get_current_avg_span()
|
||||
|
||||
def reorder_incremental_state(
|
||||
self,
|
||||
incremental_state: Dict[str, Dict[str, Optional[torch.Tensor]]],
|
||||
new_order: torch.Tensor,
|
||||
):
|
||||
"""Reorder incremental state.
|
||||
|
||||
This will be called when the order of the input has changed from the
|
||||
previous time step. A typical use case is beam search, where the input
|
||||
order changes between time steps based on the selection of beams.
|
||||
"""
|
||||
raise NotImplementedError("This is required for generation/beam search")
|
||||
# mems = self.get_incremental_state(incremental_state, "mems")
|
||||
# if mems is not None:
|
||||
# new_mems = [mems_i.index_select(1, new_order) for mems_i in mems]
|
||||
# self.set_incremental_state(incremental_state, "mems", new_mems)
|
||||
@@ -0,0 +1,280 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
from fairseq import distributed_utils as dist_utils, utils
|
||||
from fairseq.data import (
|
||||
Dictionary,
|
||||
TokenBlockDataset,
|
||||
data_utils,
|
||||
iterators,
|
||||
)
|
||||
from fairseq.dataclass import FairseqDataclass
|
||||
from fairseq.tasks import FairseqTask, register_task
|
||||
from omegaconf import II
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TruncatedBPTTLMConfig(FairseqDataclass):
|
||||
data: str = field(default="???", metadata={"help": "path to data directory"})
|
||||
tokens_per_sample: int = field(
|
||||
default=1024,
|
||||
metadata={"help": "max number of tokens per sequence"},
|
||||
)
|
||||
batch_size: int = II("dataset.batch_size")
|
||||
# Some models use *max_target_positions* to know how many positional
|
||||
# embeddings to learn. We use II(...) to make it default to
|
||||
# *tokens_per_sample*, but in principle there could be more positional
|
||||
# embeddings than tokens in a single batch. This may also be irrelevant for
|
||||
# custom model implementations.
|
||||
max_target_positions: int = II("task.tokens_per_sample")
|
||||
# these will be populated automatically if not provided
|
||||
data_parallel_rank: Optional[int] = None
|
||||
data_parallel_size: Optional[int] = None
|
||||
|
||||
|
||||
@register_task("truncated_bptt_lm", dataclass=TruncatedBPTTLMConfig)
|
||||
class TruncatedBPTTLMTask(FairseqTask):
|
||||
def __init__(self, cfg: TruncatedBPTTLMConfig):
|
||||
super().__init__(cfg)
|
||||
|
||||
if cfg.data_parallel_rank is None or cfg.data_parallel_size is None:
|
||||
if torch.distributed.is_initialized():
|
||||
cfg.data_parallel_rank = dist_utils.get_data_parallel_rank()
|
||||
cfg.data_parallel_size = dist_utils.get_data_parallel_world_size()
|
||||
else:
|
||||
cfg.data_parallel_rank = 0
|
||||
cfg.data_parallel_size = 1
|
||||
|
||||
# load the dictionary
|
||||
paths = utils.split_paths(cfg.data)
|
||||
assert len(paths) > 0
|
||||
self.dictionary = Dictionary.load(os.path.join(paths[0], "dict.txt"))
|
||||
logger.info("dictionary: {} types".format(len(self.dictionary)))
|
||||
|
||||
def load_dataset(self, split, epoch=1, combine=False, **kwargs):
|
||||
"""Load a given dataset split (e.g., train, valid, test)"""
|
||||
|
||||
# support sharded datasets
|
||||
paths = utils.split_paths(self.cfg.data)
|
||||
assert len(paths) > 0
|
||||
data_path = paths[(epoch - 1) % len(paths)]
|
||||
split_path = os.path.join(data_path, split)
|
||||
|
||||
# each element of *data* will be a tensorized line from the original
|
||||
# text dataset, similar to ``open(split_path).readlines()``
|
||||
data = data_utils.load_indexed_dataset(
|
||||
split_path, self.dictionary, combine=combine
|
||||
)
|
||||
if data is None:
|
||||
raise FileNotFoundError(
|
||||
"Dataset not found: {} ({})".format(split, split_path)
|
||||
)
|
||||
|
||||
# this is similar to ``data.view(-1).split(tokens_per_sample)``
|
||||
data = TokenBlockDataset(
|
||||
data,
|
||||
data.sizes,
|
||||
block_size=self.cfg.tokens_per_sample,
|
||||
pad=None, # unused
|
||||
eos=None, # unused
|
||||
break_mode="none",
|
||||
)
|
||||
|
||||
self.datasets[split] = TruncatedBPTTDataset(
|
||||
data=data,
|
||||
bsz_per_shard=self.cfg.batch_size,
|
||||
shard_id=self.cfg.data_parallel_rank,
|
||||
num_shards=self.cfg.data_parallel_size,
|
||||
)
|
||||
|
||||
def dataset(self, split):
|
||||
return self.datasets[split]
|
||||
|
||||
def get_batch_iterator(
|
||||
self, dataset, num_workers=0, epoch=1, data_buffer_size=0, **kwargs
|
||||
):
|
||||
return iterators.EpochBatchIterator(
|
||||
dataset=dataset,
|
||||
collate_fn=self._collate_fn,
|
||||
num_workers=num_workers,
|
||||
epoch=epoch,
|
||||
buffer_size=data_buffer_size,
|
||||
# we don't use the batching functionality from EpochBatchIterator;
|
||||
# instead every item in *dataset* is a whole batch
|
||||
batch_sampler=[[i] for i in range(len(dataset))],
|
||||
disable_shuffling=True,
|
||||
)
|
||||
|
||||
def _collate_fn(self, items: List[List[torch.Tensor]]):
|
||||
# we don't use fairseq's batching functionality, so we expect a single
|
||||
# Tensor of type List[torch.Tensor]
|
||||
assert len(items) == 1
|
||||
|
||||
# item will have shape B x T (the last batch may have length < T)
|
||||
id, item = items[0]
|
||||
item = data_utils.collate_tokens(item, pad_idx=self.source_dictionary.pad())
|
||||
B, T = item.size()
|
||||
|
||||
# shift item one position over and append a padding token for the target
|
||||
target = torch.nn.functional.pad(
|
||||
item[:, 1:], (0, 1, 0, 0), value=self.target_dictionary.pad()
|
||||
)
|
||||
|
||||
# fairseq expects batches to have the following structure
|
||||
return {
|
||||
"id": torch.tensor([id]*item.size(0)),
|
||||
"net_input": {
|
||||
"src_tokens": item,
|
||||
},
|
||||
"target": target,
|
||||
"nsentences": item.size(0),
|
||||
"ntokens": item.numel(),
|
||||
}
|
||||
|
||||
def build_dataset_for_inference(
|
||||
self, src_tokens: List[torch.Tensor], src_lengths: List[int], **kwargs
|
||||
) -> torch.utils.data.Dataset:
|
||||
eos = self.source_dictionary.eos()
|
||||
dataset = TokenBlockDataset(
|
||||
src_tokens,
|
||||
src_lengths,
|
||||
block_size=None, # ignored for "eos" break mode
|
||||
pad=self.source_dictionary.pad(),
|
||||
eos=eos,
|
||||
break_mode="eos",
|
||||
)
|
||||
|
||||
class Dataset(torch.utils.data.Dataset):
|
||||
def __getitem__(self, i):
|
||||
item = dataset[i]
|
||||
if item[-1] == eos:
|
||||
# remove eos to support generating with a prefix
|
||||
item = item[:-1]
|
||||
return (i, [item])
|
||||
|
||||
def __len__(self):
|
||||
return len(dataset)
|
||||
|
||||
return Dataset()
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
with torch.no_grad():
|
||||
if constraints is not None:
|
||||
raise NotImplementedError
|
||||
|
||||
# SequenceGenerator doesn't use *src_tokens* directly, we need to
|
||||
# pass the *prefix_tokens* argument instead.
|
||||
if prefix_tokens is None and sample["net_input"]["src_tokens"].nelement():
|
||||
prefix_tokens = sample["net_input"]["src_tokens"]
|
||||
|
||||
# begin generation with the end-of-sentence token
|
||||
bos_token = self.source_dictionary.eos()
|
||||
|
||||
return generator.generate(
|
||||
models, sample, prefix_tokens=prefix_tokens, bos_token=bos_token
|
||||
)
|
||||
|
||||
def eval_lm_dataloader(
|
||||
self,
|
||||
dataset,
|
||||
max_tokens: Optional[int] = 36000,
|
||||
batch_size: Optional[int] = None,
|
||||
max_positions: Optional[int] = None,
|
||||
num_shards: int = 1,
|
||||
shard_id: int = 0,
|
||||
num_workers: int = 1,
|
||||
data_buffer_size: int = 10,
|
||||
context_window: int = 0,
|
||||
):
|
||||
if context_window > 0:
|
||||
raise NotImplementedError(
|
||||
"Transformer-XL doesn't need --context-window, try "
|
||||
"--model-overrides '{\"mem_len\":42}' instead "
|
||||
)
|
||||
return self.get_batch_iterator(
|
||||
dataset=dataset,
|
||||
max_tokens=max_tokens,
|
||||
max_sentences=batch_size,
|
||||
max_positions=max_positions,
|
||||
ignore_invalid_inputs=True,
|
||||
num_shards=num_shards,
|
||||
shard_id=shard_id,
|
||||
num_workers=num_workers,
|
||||
data_buffer_size=data_buffer_size,
|
||||
).next_epoch_itr(shuffle=False)
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.dictionary
|
||||
|
||||
|
||||
class TruncatedBPTTDataset(torch.utils.data.Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
data: List[torch.Tensor], # ordered list of items
|
||||
bsz_per_shard, # number of items processed per GPUs per forward
|
||||
shard_id, # current GPU ID
|
||||
num_shards, # number of GPUs
|
||||
):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
|
||||
def batchify(data, bsz):
|
||||
# Work out how cleanly we can divide the dataset into bsz parts.
|
||||
nbatch = data.size(0) // bsz
|
||||
# Trim off any extra elements that wouldn't cleanly fit (remainders).
|
||||
data = data.narrow(0, 0, nbatch * bsz)
|
||||
# Evenly divide the data across the bsz batches.
|
||||
data = data.view(bsz, -1).contiguous()
|
||||
return data
|
||||
|
||||
# total number of sequences processed by all GPUs in each forward pass
|
||||
global_batch_size = bsz_per_shard * num_shards
|
||||
|
||||
"""
|
||||
With a 16 item dataset, bsz_per_shard=2 and num_shards=3,
|
||||
*indices* might look like:
|
||||
|
||||
indices = [[0, 1],
|
||||
[2, 3],
|
||||
[4, 5],
|
||||
[6, 7],
|
||||
[8, 9],
|
||||
[10, 11]]
|
||||
|
||||
The size of the TruncatedBPTTDataset instance will be 2,
|
||||
and shard 1 will see items:
|
||||
|
||||
[(0, [data[4], data[6]]),
|
||||
(1, [data[5], data[7]])]
|
||||
"""
|
||||
indices = batchify(torch.arange(len(data)), global_batch_size)
|
||||
assert indices.size(0) == global_batch_size
|
||||
|
||||
self.my_indices = indices[
|
||||
shard_id * bsz_per_shard : (shard_id + 1) * bsz_per_shard
|
||||
]
|
||||
assert self.my_indices.size(0) == bsz_per_shard
|
||||
|
||||
def __len__(self):
|
||||
return self.my_indices.size(1)
|
||||
|
||||
def __getitem__(self, i) -> Tuple[int, List[torch.Tensor]]:
|
||||
return (i, [self.data[idx] for idx in self.my_indices[:, i]])
|
||||
@@ -0,0 +1,297 @@
|
||||
# Understanding Back-Translation at Scale (Edunov et al., 2018)
|
||||
|
||||
This page includes pre-trained models from the paper [Understanding Back-Translation at Scale (Edunov et al., 2018)](https://arxiv.org/abs/1808.09381).
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | Dataset | Download
|
||||
---|---|---|---
|
||||
`transformer.wmt18.en-de` | Transformer <br> ([Edunov et al., 2018](https://arxiv.org/abs/1808.09381)) <br> WMT'18 winner | [WMT'18 English-German](http://www.statmt.org/wmt18/translation-task.html) | [download (.tar.gz)](https://dl.fbaipublicfiles.com/fairseq/models/wmt18.en-de.ensemble.tar.gz) <br> See NOTE in the archive
|
||||
|
||||
## Example usage (torch.hub)
|
||||
|
||||
We require a few additional Python dependencies for preprocessing:
|
||||
```bash
|
||||
pip install subword_nmt sacremoses
|
||||
```
|
||||
|
||||
Then to generate translations from the full model ensemble:
|
||||
```python
|
||||
import torch
|
||||
|
||||
# List available models
|
||||
torch.hub.list('pytorch/fairseq') # [..., 'transformer.wmt18.en-de', ... ]
|
||||
|
||||
# Load the WMT'18 En-De ensemble
|
||||
en2de_ensemble = torch.hub.load(
|
||||
'pytorch/fairseq', 'transformer.wmt18.en-de',
|
||||
checkpoint_file='wmt18.model1.pt:wmt18.model2.pt:wmt18.model3.pt:wmt18.model4.pt:wmt18.model5.pt',
|
||||
tokenizer='moses', bpe='subword_nmt')
|
||||
|
||||
# The ensemble contains 5 models
|
||||
len(en2de_ensemble.models)
|
||||
# 5
|
||||
|
||||
# Translate
|
||||
en2de_ensemble.translate('Hello world!')
|
||||
# 'Hallo Welt!'
|
||||
```
|
||||
|
||||
## Training your own model (WMT'18 English-German)
|
||||
|
||||
The following instructions can be adapted to reproduce the models from the paper.
|
||||
|
||||
|
||||
#### Step 1. Prepare parallel data and optionally train a baseline (English-German) model
|
||||
|
||||
First download and preprocess the data:
|
||||
```bash
|
||||
# Download and prepare the data
|
||||
cd examples/backtranslation/
|
||||
bash prepare-wmt18en2de.sh
|
||||
cd ../..
|
||||
|
||||
# Binarize the data
|
||||
TEXT=examples/backtranslation/wmt18_en_de
|
||||
fairseq-preprocess \
|
||||
--joined-dictionary \
|
||||
--source-lang en --target-lang de \
|
||||
--trainpref $TEXT/train --validpref $TEXT/valid --testpref $TEXT/test \
|
||||
--destdir data-bin/wmt18_en_de --thresholdtgt 0 --thresholdsrc 0 \
|
||||
--workers 20
|
||||
|
||||
# Copy the BPE code into the data-bin directory for future use
|
||||
cp examples/backtranslation/wmt18_en_de/code data-bin/wmt18_en_de/code
|
||||
```
|
||||
|
||||
(Optionally) Train a baseline model (English-German) using just the parallel data:
|
||||
```bash
|
||||
CHECKPOINT_DIR=checkpoints_en_de_parallel
|
||||
fairseq-train --fp16 \
|
||||
data-bin/wmt18_en_de \
|
||||
--source-lang en --target-lang de \
|
||||
--arch transformer_wmt_en_de_big --share-all-embeddings \
|
||||
--dropout 0.3 --weight-decay 0.0 \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --clip-norm 0.0 \
|
||||
--lr 0.001 --lr-scheduler inverse_sqrt --warmup-updates 4000 \
|
||||
--max-tokens 3584 --update-freq 16 \
|
||||
--max-update 30000 \
|
||||
--save-dir $CHECKPOINT_DIR
|
||||
# Note: the above command assumes 8 GPUs. Adjust `--update-freq` if you have a
|
||||
# different number of GPUs.
|
||||
```
|
||||
|
||||
Average the last 10 checkpoints:
|
||||
```bash
|
||||
python scripts/average_checkpoints.py \
|
||||
--inputs $CHECKPOINT_DIR \
|
||||
--num-epoch-checkpoints 10 \
|
||||
--output $CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
```
|
||||
|
||||
Evaluate BLEU:
|
||||
```bash
|
||||
# tokenized BLEU on newstest2017:
|
||||
bash examples/backtranslation/tokenized_bleu.sh \
|
||||
wmt17 \
|
||||
en-de \
|
||||
data-bin/wmt18_en_de \
|
||||
data-bin/wmt18_en_de/code \
|
||||
$CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
# BLEU4 = 29.57, 60.9/35.4/22.9/15.5 (BP=1.000, ratio=1.014, syslen=63049, reflen=62152)
|
||||
# compare to 29.46 in Table 1, which is also for tokenized BLEU
|
||||
|
||||
# generally it's better to report (detokenized) sacrebleu though:
|
||||
bash examples/backtranslation/sacrebleu.sh \
|
||||
wmt17 \
|
||||
en-de \
|
||||
data-bin/wmt18_en_de \
|
||||
data-bin/wmt18_en_de/code \
|
||||
$CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
# BLEU+case.mixed+lang.en-de+numrefs.1+smooth.exp+test.wmt17+tok.13a+version.1.4.3 = 29.0 60.6/34.7/22.4/14.9 (BP = 1.000 ratio = 1.013 hyp_len = 62099 ref_len = 61287)
|
||||
```
|
||||
|
||||
|
||||
#### Step 2. Back-translate monolingual German data
|
||||
|
||||
Train a reverse model (German-English) to do the back-translation:
|
||||
```bash
|
||||
CHECKPOINT_DIR=checkpoints_de_en_parallel
|
||||
fairseq-train --fp16 \
|
||||
data-bin/wmt18_en_de \
|
||||
--source-lang de --target-lang en \
|
||||
--arch transformer_wmt_en_de_big --share-all-embeddings \
|
||||
--dropout 0.3 --weight-decay 0.0 \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --clip-norm 0.0 \
|
||||
--lr 0.001 --lr-scheduler inverse_sqrt --warmup-updates 4000 \
|
||||
--max-tokens 3584 --update-freq 16 \
|
||||
--max-update 30000 \
|
||||
--save-dir $CHECKPOINT_DIR
|
||||
# Note: the above command assumes 8 GPUs. Adjust `--update-freq` if you have a
|
||||
# different number of GPUs.
|
||||
```
|
||||
|
||||
Let's evaluate the back-translation (BT) model to make sure it is well trained:
|
||||
```bash
|
||||
bash examples/backtranslation/sacrebleu.sh \
|
||||
wmt17 \
|
||||
de-en \
|
||||
data-bin/wmt18_en_de \
|
||||
data-bin/wmt18_en_de/code \
|
||||
$CHECKPOINT_DIR/checkpoint_best.py
|
||||
# BLEU+case.mixed+lang.de-en+numrefs.1+smooth.exp+test.wmt17+tok.13a+version.1.4.3 = 34.9 66.9/41.8/28.5/19.9 (BP = 0.983 ratio = 0.984 hyp_len = 63342 ref_len = 64399)
|
||||
# compare to the best system from WMT'17 which scored 35.1: http://matrix.statmt.org/matrix/systems_list/1868
|
||||
```
|
||||
|
||||
Next prepare the monolingual data:
|
||||
```bash
|
||||
# Download and prepare the monolingual data
|
||||
# By default the script samples 25M monolingual sentences, which after
|
||||
# deduplication should be just over 24M sentences. These are split into 25
|
||||
# shards, each with 1M sentences (except for the last shard).
|
||||
cd examples/backtranslation/
|
||||
bash prepare-de-monolingual.sh
|
||||
cd ../..
|
||||
|
||||
# Binarize each shard of the monolingual data
|
||||
TEXT=examples/backtranslation/wmt18_de_mono
|
||||
for SHARD in $(seq -f "%02g" 0 24); do \
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--source-lang de --target-lang en \
|
||||
--joined-dictionary \
|
||||
--srcdict data-bin/wmt18_en_de/dict.de.txt \
|
||||
--testpref $TEXT/bpe.monolingual.dedup.${SHARD} \
|
||||
--destdir data-bin/wmt18_de_mono/shard${SHARD} \
|
||||
--workers 20; \
|
||||
cp data-bin/wmt18_en_de/dict.en.txt data-bin/wmt18_de_mono/shard${SHARD}/; \
|
||||
done
|
||||
```
|
||||
|
||||
Now we're ready to perform back-translation over the monolingual data. The
|
||||
following command generates via sampling, but it's possible to use greedy
|
||||
decoding (`--beam 1`), beam search (`--beam 5`),
|
||||
top-k sampling (`--sampling --beam 1 --sampling-topk 10`), etc.:
|
||||
```bash
|
||||
mkdir backtranslation_output
|
||||
for SHARD in $(seq -f "%02g" 0 24); do \
|
||||
fairseq-generate --fp16 \
|
||||
data-bin/wmt18_de_mono/shard${SHARD} \
|
||||
--path $CHECKPOINT_DIR/checkpoint_best.pt \
|
||||
--skip-invalid-size-inputs-valid-test \
|
||||
--max-tokens 4096 \
|
||||
--sampling --beam 1 \
|
||||
> backtranslation_output/sampling.shard${SHARD}.out; \
|
||||
done
|
||||
```
|
||||
|
||||
After BT, use the `extract_bt_data.py` script to re-combine the shards, extract
|
||||
the back-translations and apply length ratio filters:
|
||||
```bash
|
||||
python examples/backtranslation/extract_bt_data.py \
|
||||
--minlen 1 --maxlen 250 --ratio 1.5 \
|
||||
--output backtranslation_output/bt_data --srclang en --tgtlang de \
|
||||
backtranslation_output/sampling.shard*.out
|
||||
|
||||
# Ensure lengths are the same:
|
||||
# wc -l backtranslation_output/bt_data.{en,de}
|
||||
# 21795614 backtranslation_output/bt_data.en
|
||||
# 21795614 backtranslation_output/bt_data.de
|
||||
# 43591228 total
|
||||
```
|
||||
|
||||
Binarize the filtered BT data and combine it with the parallel data:
|
||||
```bash
|
||||
TEXT=backtranslation_output
|
||||
fairseq-preprocess \
|
||||
--source-lang en --target-lang de \
|
||||
--joined-dictionary \
|
||||
--srcdict data-bin/wmt18_en_de/dict.en.txt \
|
||||
--trainpref $TEXT/bt_data \
|
||||
--destdir data-bin/wmt18_en_de_bt \
|
||||
--workers 20
|
||||
|
||||
# We want to train on the combined data, so we'll symlink the parallel + BT data
|
||||
# in the wmt18_en_de_para_plus_bt directory. We link the parallel data as "train"
|
||||
# and the BT data as "train1", so that fairseq will combine them automatically
|
||||
# and so that we can use the `--upsample-primary` option to upsample the
|
||||
# parallel data (if desired).
|
||||
PARA_DATA=$(readlink -f data-bin/wmt18_en_de)
|
||||
BT_DATA=$(readlink -f data-bin/wmt18_en_de_bt)
|
||||
COMB_DATA=data-bin/wmt18_en_de_para_plus_bt
|
||||
mkdir -p $COMB_DATA
|
||||
for LANG in en de; do \
|
||||
ln -s ${PARA_DATA}/dict.$LANG.txt ${COMB_DATA}/dict.$LANG.txt; \
|
||||
for EXT in bin idx; do \
|
||||
ln -s ${PARA_DATA}/train.en-de.$LANG.$EXT ${COMB_DATA}/train.en-de.$LANG.$EXT; \
|
||||
ln -s ${BT_DATA}/train.en-de.$LANG.$EXT ${COMB_DATA}/train1.en-de.$LANG.$EXT; \
|
||||
ln -s ${PARA_DATA}/valid.en-de.$LANG.$EXT ${COMB_DATA}/valid.en-de.$LANG.$EXT; \
|
||||
ln -s ${PARA_DATA}/test.en-de.$LANG.$EXT ${COMB_DATA}/test.en-de.$LANG.$EXT; \
|
||||
done; \
|
||||
done
|
||||
```
|
||||
|
||||
|
||||
#### 3. Train an English-German model over the combined parallel + BT data
|
||||
|
||||
Finally we can train a model over the parallel + BT data:
|
||||
```bash
|
||||
CHECKPOINT_DIR=checkpoints_en_de_parallel_plus_bt
|
||||
fairseq-train --fp16 \
|
||||
data-bin/wmt18_en_de_para_plus_bt \
|
||||
--upsample-primary 16 \
|
||||
--source-lang en --target-lang de \
|
||||
--arch transformer_wmt_en_de_big --share-all-embeddings \
|
||||
--dropout 0.3 --weight-decay 0.0 \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --clip-norm 0.0 \
|
||||
--lr 0.0007 --lr-scheduler inverse_sqrt --warmup-updates 4000 \
|
||||
--max-tokens 3584 --update-freq 16 \
|
||||
--max-update 100000 \
|
||||
--save-dir $CHECKPOINT_DIR
|
||||
# Note: the above command assumes 8 GPUs. Adjust `--update-freq` if you have a
|
||||
# different number of GPUs.
|
||||
```
|
||||
|
||||
Average the last 10 checkpoints:
|
||||
```bash
|
||||
python scripts/average_checkpoints.py \
|
||||
--inputs $CHECKPOINT_DIR \
|
||||
--num-epoch-checkpoints 10 \
|
||||
--output $CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
```
|
||||
|
||||
Evaluate BLEU:
|
||||
```bash
|
||||
# tokenized BLEU on newstest2017:
|
||||
bash examples/backtranslation/tokenized_bleu.sh \
|
||||
wmt17 \
|
||||
en-de \
|
||||
data-bin/wmt18_en_de \
|
||||
data-bin/wmt18_en_de/code \
|
||||
$CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
# BLEU4 = 32.35, 64.4/38.9/26.2/18.3 (BP=0.977, ratio=0.977, syslen=60729, reflen=62152)
|
||||
# compare to 32.35 in Table 1, which is also for tokenized BLEU
|
||||
|
||||
# generally it's better to report (detokenized) sacrebleu:
|
||||
bash examples/backtranslation/sacrebleu.sh \
|
||||
wmt17 \
|
||||
en-de \
|
||||
data-bin/wmt18_en_de \
|
||||
data-bin/wmt18_en_de/code \
|
||||
$CHECKPOINT_DIR/checkpoint.avg10.pt
|
||||
# BLEU+case.mixed+lang.en-de+numrefs.1+smooth.exp+test.wmt17+tok.13a+version.1.4.3 = 31.5 64.3/38.2/25.6/17.6 (BP = 0.971 ratio = 0.971 hyp_len = 59515 ref_len = 61287)
|
||||
```
|
||||
|
||||
|
||||
## Citation
|
||||
```bibtex
|
||||
@inproceedings{edunov2018backtranslation,
|
||||
title = {Understanding Back-Translation at Scale},
|
||||
author = {Edunov, Sergey and Ott, Myle and Auli, Michael and Grangier, David},
|
||||
booktitle = {Conference of the Association for Computational Linguistics (ACL)},
|
||||
year = 2018,
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/python3
|
||||
# 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 argparse
|
||||
import fileinput
|
||||
import hashlib
|
||||
import sys
|
||||
from multiprocessing import Pool
|
||||
|
||||
|
||||
def get_hashes_and_lines(raw_line):
|
||||
hash = hashlib.md5(raw_line).hexdigest()
|
||||
return hash, raw_line
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--workers", type=int, default=10)
|
||||
parser.add_argument("files", nargs="*", help="input files")
|
||||
args = parser.parse_args()
|
||||
|
||||
seen = set()
|
||||
with fileinput.input(args.files, mode="rb") as h:
|
||||
pool = Pool(args.workers)
|
||||
results = pool.imap_unordered(get_hashes_and_lines, h, 1000)
|
||||
for i, (hash, raw_line) in enumerate(results):
|
||||
if hash not in seen:
|
||||
seen.add(hash)
|
||||
sys.stdout.buffer.write(raw_line)
|
||||
if i % 1000000 == 0:
|
||||
print(i, file=sys.stderr, end="", flush=True)
|
||||
elif i % 100000 == 0:
|
||||
print(".", file=sys.stderr, end="", flush=True)
|
||||
print(file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
# 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 argparse
|
||||
import fileinput
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Extract back-translations from the stdout of fairseq-generate. "
|
||||
"If there are multiply hypotheses for a source, we only keep the first one. "
|
||||
)
|
||||
)
|
||||
parser.add_argument("--output", required=True, help="output prefix")
|
||||
parser.add_argument(
|
||||
"--srclang", required=True, help="source language (extracted from H-* lines)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tgtlang", required=True, help="target language (extracted from S-* lines)"
|
||||
)
|
||||
parser.add_argument("--minlen", type=int, help="min length filter")
|
||||
parser.add_argument("--maxlen", type=int, help="max length filter")
|
||||
parser.add_argument("--ratio", type=float, help="ratio filter")
|
||||
parser.add_argument("files", nargs="*", help="input files")
|
||||
args = parser.parse_args()
|
||||
|
||||
def validate(src, tgt):
|
||||
srclen = len(src.split(" ")) if src != "" else 0
|
||||
tgtlen = len(tgt.split(" ")) if tgt != "" else 0
|
||||
if (
|
||||
(args.minlen is not None and (srclen < args.minlen or tgtlen < args.minlen))
|
||||
or (
|
||||
args.maxlen is not None
|
||||
and (srclen > args.maxlen or tgtlen > args.maxlen)
|
||||
)
|
||||
or (
|
||||
args.ratio is not None
|
||||
and (max(srclen, tgtlen) / float(min(srclen, tgtlen)) > args.ratio)
|
||||
)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def safe_index(toks, index, default):
|
||||
try:
|
||||
return toks[index]
|
||||
except IndexError:
|
||||
return default
|
||||
|
||||
with open(args.output + "." + args.srclang, "w") as src_h, open(
|
||||
args.output + "." + args.tgtlang, "w"
|
||||
) as tgt_h:
|
||||
for line in tqdm(fileinput.input(args.files)):
|
||||
if line.startswith("S-"):
|
||||
tgt = safe_index(line.rstrip().split("\t"), 1, "")
|
||||
elif line.startswith("H-"):
|
||||
if tgt is not None:
|
||||
src = safe_index(line.rstrip().split("\t"), 2, "")
|
||||
if validate(src, tgt):
|
||||
print(src, file=src_h)
|
||||
print(tgt, file=tgt_h)
|
||||
tgt = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPTS=mosesdecoder/scripts
|
||||
TOKENIZER=$SCRIPTS/tokenizer/tokenizer.perl
|
||||
NORM_PUNC=$SCRIPTS/tokenizer/normalize-punctuation.perl
|
||||
REM_NON_PRINT_CHAR=$SCRIPTS/tokenizer/remove-non-printing-char.perl
|
||||
BPEROOT=subword-nmt/subword_nmt
|
||||
|
||||
|
||||
BPE_CODE=wmt18_en_de/code
|
||||
SUBSAMPLE_SIZE=25000000
|
||||
LANG=de
|
||||
|
||||
|
||||
OUTDIR=wmt18_${LANG}_mono
|
||||
orig=orig
|
||||
tmp=$OUTDIR/tmp
|
||||
mkdir -p $OUTDIR $tmp
|
||||
|
||||
|
||||
URLS=(
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2007.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2008.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2009.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2010.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2011.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2012.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt14/training-monolingual-news-crawl/news.2013.de.shuffled.gz"
|
||||
"http://www.statmt.org/wmt15/training-monolingual-news-crawl-v2/news.2014.de.shuffled.v2.gz"
|
||||
"http://data.statmt.org/wmt16/translation-task/news.2015.de.shuffled.gz"
|
||||
"http://data.statmt.org/wmt17/translation-task/news.2016.de.shuffled.gz"
|
||||
"http://data.statmt.org/wmt18/translation-task/news.2017.de.shuffled.deduped.gz"
|
||||
)
|
||||
FILES=(
|
||||
"news.2007.de.shuffled.gz"
|
||||
"news.2008.de.shuffled.gz"
|
||||
"news.2009.de.shuffled.gz"
|
||||
"news.2010.de.shuffled.gz"
|
||||
"news.2011.de.shuffled.gz"
|
||||
"news.2012.de.shuffled.gz"
|
||||
"news.2013.de.shuffled.gz"
|
||||
"news.2014.de.shuffled.v2.gz"
|
||||
"news.2015.de.shuffled.gz"
|
||||
"news.2016.de.shuffled.gz"
|
||||
"news.2017.de.shuffled.deduped.gz"
|
||||
)
|
||||
|
||||
|
||||
cd $orig
|
||||
for ((i=0;i<${#URLS[@]};++i)); do
|
||||
file=${FILES[i]}
|
||||
if [ -f $file ]; then
|
||||
echo "$file already exists, skipping download"
|
||||
else
|
||||
url=${URLS[i]}
|
||||
wget "$url"
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
|
||||
|
||||
if [ -f $tmp/monolingual.${SUBSAMPLE_SIZE}.${LANG} ]; then
|
||||
echo "found monolingual sample, skipping shuffle/sample/tokenize"
|
||||
else
|
||||
gzip -c -d -k $(for FILE in "${FILES[@]}"; do echo $orig/$FILE; done) \
|
||||
| shuf -n $SUBSAMPLE_SIZE \
|
||||
| perl $NORM_PUNC $LANG \
|
||||
| perl $REM_NON_PRINT_CHAR \
|
||||
| perl $TOKENIZER -threads 8 -a -l $LANG \
|
||||
> $tmp/monolingual.${SUBSAMPLE_SIZE}.${LANG}
|
||||
fi
|
||||
|
||||
|
||||
if [ -f $tmp/bpe.monolingual.${SUBSAMPLE_SIZE}.${LANG} ]; then
|
||||
echo "found BPE monolingual sample, skipping BPE step"
|
||||
else
|
||||
python $BPEROOT/apply_bpe.py -c $BPE_CODE \
|
||||
< $tmp/monolingual.${SUBSAMPLE_SIZE}.${LANG} \
|
||||
> $tmp/bpe.monolingual.${SUBSAMPLE_SIZE}.${LANG}
|
||||
fi
|
||||
|
||||
|
||||
if [ -f $tmp/bpe.monolingual.dedup.${SUBSAMPLE_SIZE}.${LANG} ]; then
|
||||
echo "found deduplicated monolingual sample, skipping deduplication step"
|
||||
else
|
||||
python deduplicate_lines.py $tmp/bpe.monolingual.${SUBSAMPLE_SIZE}.${LANG} \
|
||||
> $tmp/bpe.monolingual.dedup.${SUBSAMPLE_SIZE}.${LANG}
|
||||
fi
|
||||
|
||||
|
||||
if [ -f $OUTDIR/bpe.monolingual.dedup.00.de ]; then
|
||||
echo "found sharded data, skipping sharding step"
|
||||
else
|
||||
split --lines 1000000 --numeric-suffixes \
|
||||
--additional-suffix .${LANG} \
|
||||
$tmp/bpe.monolingual.dedup.${SUBSAMPLE_SIZE}.${LANG} \
|
||||
$OUTDIR/bpe.monolingual.dedup.
|
||||
fi
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
# Adapted from https://github.com/facebookresearch/MIXER/blob/master/prepareData.sh
|
||||
|
||||
echo 'Cloning Moses github repository (for tokenization scripts)...'
|
||||
git clone https://github.com/moses-smt/mosesdecoder.git
|
||||
|
||||
echo 'Cloning Subword NMT repository (for BPE pre-processing)...'
|
||||
git clone https://github.com/rsennrich/subword-nmt.git
|
||||
|
||||
SCRIPTS=mosesdecoder/scripts
|
||||
TOKENIZER=$SCRIPTS/tokenizer/tokenizer.perl
|
||||
CLEAN=$SCRIPTS/training/clean-corpus-n.perl
|
||||
NORM_PUNC=$SCRIPTS/tokenizer/normalize-punctuation.perl
|
||||
REM_NON_PRINT_CHAR=$SCRIPTS/tokenizer/remove-non-printing-char.perl
|
||||
BPEROOT=subword-nmt/subword_nmt
|
||||
BPE_TOKENS=32000
|
||||
|
||||
URLS=(
|
||||
"http://statmt.org/wmt13/training-parallel-europarl-v7.tgz"
|
||||
"http://statmt.org/wmt13/training-parallel-commoncrawl.tgz"
|
||||
"http://data.statmt.org/wmt18/translation-task/training-parallel-nc-v13.tgz"
|
||||
"http://data.statmt.org/wmt18/translation-task/rapid2016.tgz"
|
||||
"http://data.statmt.org/wmt17/translation-task/dev.tgz"
|
||||
"http://statmt.org/wmt14/test-full.tgz"
|
||||
)
|
||||
FILES=(
|
||||
"training-parallel-europarl-v7.tgz"
|
||||
"training-parallel-commoncrawl.tgz"
|
||||
"training-parallel-nc-v13.tgz"
|
||||
"rapid2016.tgz"
|
||||
"dev.tgz"
|
||||
"test-full.tgz"
|
||||
)
|
||||
CORPORA=(
|
||||
"training/europarl-v7.de-en"
|
||||
"commoncrawl.de-en"
|
||||
"training-parallel-nc-v13/news-commentary-v13.de-en"
|
||||
"rapid2016.de-en"
|
||||
)
|
||||
|
||||
if [ ! -d "$SCRIPTS" ]; then
|
||||
echo "Please set SCRIPTS variable correctly to point to Moses scripts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTDIR=wmt18_en_de
|
||||
|
||||
src=en
|
||||
tgt=de
|
||||
lang=en-de
|
||||
prep=$OUTDIR
|
||||
tmp=$prep/tmp
|
||||
orig=orig
|
||||
|
||||
mkdir -p $orig $tmp $prep
|
||||
|
||||
cd $orig
|
||||
|
||||
for ((i=0;i<${#URLS[@]};++i)); do
|
||||
file=${FILES[i]}
|
||||
if [ -f $file ]; then
|
||||
echo "$file already exists, skipping download"
|
||||
else
|
||||
url=${URLS[i]}
|
||||
wget "$url"
|
||||
if [ -f $file ]; then
|
||||
echo "$url successfully downloaded."
|
||||
else
|
||||
echo "$url not successfully downloaded."
|
||||
exit 1
|
||||
fi
|
||||
if [ ${file: -4} == ".tgz" ]; then
|
||||
tar zxvf $file
|
||||
elif [ ${file: -4} == ".tar" ]; then
|
||||
tar xvf $file
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
|
||||
echo "pre-processing train data..."
|
||||
for l in $src $tgt; do
|
||||
rm $tmp/train.tags.$lang.tok.$l
|
||||
for f in "${CORPORA[@]}"; do
|
||||
cat $orig/$f.$l | \
|
||||
perl $NORM_PUNC $l | \
|
||||
perl $REM_NON_PRINT_CHAR | \
|
||||
perl $TOKENIZER -threads 8 -a -l $l >> $tmp/train.tags.$lang.tok.$l
|
||||
done
|
||||
done
|
||||
|
||||
echo "pre-processing test data..."
|
||||
for l in $src $tgt; do
|
||||
if [ "$l" == "$src" ]; then
|
||||
t="src"
|
||||
else
|
||||
t="ref"
|
||||
fi
|
||||
grep '<seg id' $orig/test-full/newstest2014-deen-$t.$l.sgm | \
|
||||
sed -e 's/<seg id="[0-9]*">\s*//g' | \
|
||||
sed -e 's/\s*<\/seg>\s*//g' | \
|
||||
sed -e "s/\’/\'/g" | \
|
||||
perl $TOKENIZER -threads 8 -a -l $l > $tmp/test.$l
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "splitting train and valid..."
|
||||
for l in $src $tgt; do
|
||||
awk '{if (NR%100 == 0) print $0; }' $tmp/train.tags.$lang.tok.$l > $tmp/valid.$l
|
||||
awk '{if (NR%100 != 0) print $0; }' $tmp/train.tags.$lang.tok.$l > $tmp/train.$l
|
||||
done
|
||||
|
||||
TRAIN=$tmp/train.de-en
|
||||
BPE_CODE=$prep/code
|
||||
rm -f $TRAIN
|
||||
for l in $src $tgt; do
|
||||
cat $tmp/train.$l >> $TRAIN
|
||||
done
|
||||
|
||||
echo "learn_bpe.py on ${TRAIN}..."
|
||||
python $BPEROOT/learn_bpe.py -s $BPE_TOKENS < $TRAIN > $BPE_CODE
|
||||
|
||||
for L in $src $tgt; do
|
||||
for f in train.$L valid.$L test.$L; do
|
||||
echo "apply_bpe.py to ${f}..."
|
||||
python $BPEROOT/apply_bpe.py -c $BPE_CODE < $tmp/$f > $tmp/bpe.$f
|
||||
done
|
||||
done
|
||||
|
||||
perl $CLEAN -ratio 1.5 $tmp/bpe.train $src $tgt $prep/train 1 250
|
||||
perl $CLEAN -ratio 1.5 $tmp/bpe.valid $src $tgt $prep/valid 1 250
|
||||
|
||||
for L in $src $tgt; do
|
||||
cp $tmp/bpe.test.$L $prep/test.$L
|
||||
done
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -ne 5 ]; then
|
||||
echo "usage: $0 [dataset=wmt14/full] [langpair=en-de] [databin] [bpecode] [model]"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
DATASET=$1
|
||||
LANGPAIR=$2
|
||||
DATABIN=$3
|
||||
BPECODE=$4
|
||||
MODEL=$5
|
||||
|
||||
SRCLANG=$(echo $LANGPAIR | cut -d '-' -f 1)
|
||||
TGTLANG=$(echo $LANGPAIR | cut -d '-' -f 2)
|
||||
|
||||
|
||||
BPEROOT=examples/backtranslation/subword-nmt/subword_nmt
|
||||
if [ ! -e $BPEROOT ]; then
|
||||
BPEROOT=subword-nmt/subword_nmt
|
||||
if [ ! -e $BPEROOT ]; then
|
||||
echo 'Cloning Subword NMT repository (for BPE pre-processing)...'
|
||||
git clone https://github.com/rsennrich/subword-nmt.git
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
sacrebleu -t $DATASET -l $LANGPAIR --echo src \
|
||||
| sacremoses tokenize -a -l $SRCLANG -q \
|
||||
| python $BPEROOT/apply_bpe.py -c $BPECODE \
|
||||
| fairseq-interactive $DATABIN --path $MODEL \
|
||||
-s $SRCLANG -t $TGTLANG \
|
||||
--beam 5 --remove-bpe --buffer-size 1024 --max-tokens 8000 \
|
||||
| grep ^H- | cut -f 3- \
|
||||
| sacremoses detokenize -l $TGTLANG -q \
|
||||
| sacrebleu -t $DATASET -l $LANGPAIR
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -ne 5 ]; then
|
||||
echo "usage: $0 [dataset=wmt14/full] [langpair=en-de] [databin] [bpecode] [model]"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
DATASET=$1
|
||||
LANGPAIR=$2
|
||||
DATABIN=$3
|
||||
BPECODE=$4
|
||||
MODEL=$5
|
||||
|
||||
SRCLANG=$(echo $LANGPAIR | cut -d '-' -f 1)
|
||||
TGTLANG=$(echo $LANGPAIR | cut -d '-' -f 2)
|
||||
|
||||
|
||||
BPEROOT=examples/backtranslation/subword-nmt/subword_nmt
|
||||
if [ ! -e $BPEROOT ]; then
|
||||
BPEROOT=subword-nmt/subword_nmt
|
||||
if [ ! -e $BPEROOT ]; then
|
||||
echo 'Cloning Subword NMT repository (for BPE pre-processing)...'
|
||||
git clone https://github.com/rsennrich/subword-nmt.git
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
TMP_REF=$(mktemp)
|
||||
|
||||
sacrebleu -t $DATASET -l $LANGPAIR --echo ref -q \
|
||||
| sacremoses normalize -l $TGTLANG -q \
|
||||
| sacremoses tokenize -a -l $TGTLANG -q \
|
||||
> $TMP_REF
|
||||
|
||||
sacrebleu -t $DATASET -l $LANGPAIR --echo src -q \
|
||||
| sacremoses normalize -l $SRCLANG -q \
|
||||
| sacremoses tokenize -a -l $SRCLANG -q \
|
||||
| python $BPEROOT/apply_bpe.py -c $BPECODE \
|
||||
| fairseq-interactive $DATABIN --path $MODEL \
|
||||
-s $SRCLANG -t $TGTLANG \
|
||||
--beam 5 --remove-bpe --buffer-size 1024 --max-tokens 8000 \
|
||||
| grep ^H- | cut -f 3- \
|
||||
| fairseq-score --ref $TMP_REF
|
||||
|
||||
rm -f $TMP_REF
|
||||
@@ -0,0 +1,99 @@
|
||||
# Fine-tuning BART on GLUE tasks
|
||||
|
||||
### 1) Download the data from GLUE website (https://gluebenchmark.com/tasks) using following commands:
|
||||
```bash
|
||||
wget https://gist.githubusercontent.com/W4ngatang/60c2bdb54d156a41194446737ce03e2e/raw/17b8dd0d724281ed7c3b2aeeda662b92809aadd5/download_glue_data.py
|
||||
python download_glue_data.py --data_dir glue_data --tasks all
|
||||
```
|
||||
|
||||
### 2) Preprocess GLUE task data (same as RoBERTa):
|
||||
```bash
|
||||
./examples/roberta/preprocess_GLUE_tasks.sh glue_data <glue_task_name>
|
||||
```
|
||||
`glue_task_name` is one of the following:
|
||||
`{ALL, QQP, MNLI, QNLI, MRPC, RTE, STS-B, SST-2, CoLA}`
|
||||
Use `ALL` for preprocessing all the glue tasks.
|
||||
|
||||
### 3) Fine-tuning on GLUE task:
|
||||
Example fine-tuning cmd for `RTE` task
|
||||
```bash
|
||||
TOTAL_NUM_UPDATES=2036 # 10 epochs through RTE for bsz 16
|
||||
WARMUP_UPDATES=61 # 6 percent of the number of updates
|
||||
LR=1e-05 # Peak LR for polynomial LR scheduler.
|
||||
NUM_CLASSES=2
|
||||
MAX_SENTENCES=16 # Batch size.
|
||||
BART_PATH=/path/to/bart/model.pt
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1 fairseq-train RTE-bin/ \
|
||||
--restore-file $BART_PATH \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-tokens 4400 \
|
||||
--task sentence_prediction \
|
||||
--add-prev-output-tokens \
|
||||
--layernorm-embedding \
|
||||
--share-all-embeddings \
|
||||
--share-decoder-input-output-embed \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--required-batch-size-multiple 1 \
|
||||
--init-token 0 \
|
||||
--arch bart_large \
|
||||
--criterion sentence_prediction \
|
||||
--num-classes $NUM_CLASSES \
|
||||
--dropout 0.1 --attention-dropout 0.1 \
|
||||
--weight-decay 0.01 --optimizer adam --adam-betas "(0.9, 0.98)" --adam-eps 1e-08 \
|
||||
--clip-norm 0.0 \
|
||||
--lr-scheduler polynomial_decay --lr $LR --total-num-update $TOTAL_NUM_UPDATES --warmup-updates $WARMUP_UPDATES \
|
||||
--fp16 --fp16-init-scale 4 --threshold-loss-scale 1 --fp16-scale-window 128 \
|
||||
--max-epoch 10 \
|
||||
--find-unused-parameters \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric;
|
||||
```
|
||||
|
||||
For each of the GLUE task, you will need to use following cmd-line arguments:
|
||||
|
||||
Model | MNLI | QNLI | QQP | RTE | SST-2 | MRPC | CoLA | STS-B
|
||||
---|---|---|---|---|---|---|---|---
|
||||
`--num-classes` | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 1
|
||||
`--lr` | 5e-6 | 1e-5 | 1e-5 | 1e-5 | 5e-6 | 2e-5 | 2e-5 | 2e-5
|
||||
`bsz` | 128 | 32 | 32 | 32 | 128 | 64 | 64 | 32
|
||||
`--total-num-update` | 30968 | 33112 | 113272 | 1018 | 5233 | 1148 | 1334 | 1799
|
||||
`--warmup-updates` | 1858 | 1986 | 6796 | 61 | 314 | 68 | 80 | 107
|
||||
|
||||
For `STS-B` additionally add `--regression-target --best-checkpoint-metric loss` and remove `--maximize-best-checkpoint-metric`.
|
||||
|
||||
**Note:**
|
||||
|
||||
a) `--total-num-updates` is used by `--polynomial_decay` scheduler and is calculated for `--max-epoch=10` and `--batch-size=32/64/128` depending on the task.
|
||||
|
||||
b) Above cmd-args and hyperparams are tested on Nvidia `V100` GPU with `32gb` of memory for each task. Depending on the GPU memory resources available to you, you can use increase `--update-freq` and reduce `--batch-size`.
|
||||
|
||||
### Inference on GLUE task
|
||||
After training the model as mentioned in previous step, you can perform inference with checkpoints in `checkpoints/` directory using following python code snippet:
|
||||
|
||||
```python
|
||||
from fairseq.models.bart import BARTModel
|
||||
|
||||
bart = BARTModel.from_pretrained(
|
||||
'checkpoints/',
|
||||
checkpoint_file='checkpoint_best.pt',
|
||||
data_name_or_path='RTE-bin'
|
||||
)
|
||||
|
||||
label_fn = lambda label: bart.task.label_dictionary.string(
|
||||
[label + bart.task.label_dictionary.nspecial]
|
||||
)
|
||||
ncorrect, nsamples = 0, 0
|
||||
bart.cuda()
|
||||
bart.eval()
|
||||
with open('glue_data/RTE/dev.tsv') as fin:
|
||||
fin.readline()
|
||||
for index, line in enumerate(fin):
|
||||
tokens = line.strip().split('\t')
|
||||
sent1, sent2, target = tokens[1], tokens[2], tokens[3]
|
||||
tokens = bart.encode(sent1, sent2)
|
||||
prediction = bart.predict('sentence_classification_head', tokens).argmax().item()
|
||||
prediction_label = label_fn(prediction)
|
||||
ncorrect += int(prediction_label == target)
|
||||
nsamples += 1
|
||||
print('| Accuracy: ', float(ncorrect)/float(nsamples))
|
||||
```
|
||||
@@ -0,0 +1,243 @@
|
||||
# BART: Denoising Sequence-to-Sequence Pre-training for Natural Language Generation, Translation, and Comprehension
|
||||
|
||||
[https://arxiv.org/pdf/1910.13461.pdf]
|
||||
|
||||
## Introduction
|
||||
|
||||
BART is sequence-to-sequence model trained with denoising as pretraining objective. We show that this pretraining objective is more generic and show that we can match [RoBERTa](../roberta) results on SQuAD and GLUE and gain state-of-the-art results on summarization (XSum, CNN dataset), long form generative question answering (ELI5) and dialog response genration (ConvAI2). See the associated paper for more details.
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | # params | Download
|
||||
---|---|---|---
|
||||
`bart.base` | BART model with 6 encoder and decoder layers | 140M | [bart.base.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/bart.base.tar.gz)
|
||||
`bart.large` | BART model with 12 encoder and decoder layers | 400M | [bart.large.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/bart.large.tar.gz)
|
||||
`bart.large.mnli` | `bart.large` finetuned on `MNLI` | 400M | [bart.large.mnli.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/bart.large.mnli.tar.gz)
|
||||
`bart.large.cnn` | `bart.large` finetuned on `CNN-DM` | 400M | [bart.large.cnn.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/bart.large.cnn.tar.gz)
|
||||
`bart.large.xsum` | `bart.large` finetuned on `Xsum` | 400M | [bart.large.xsum.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/bart.large.xsum.tar.gz)
|
||||
|
||||
## Results
|
||||
|
||||
**[GLUE (Wang et al., 2019)](https://gluebenchmark.com/)**
|
||||
_(dev set, single model, single-task finetuning)_
|
||||
|
||||
Model | MNLI | QNLI | QQP | RTE | SST-2 | MRPC | CoLA | STS-B
|
||||
---|---|---|---|---|---|---|---|---
|
||||
`roberta.large` | 90.2 | 94.7 | 92.2 | 86.6 | 96.4 | 90.9 | 68.0 | 92.4
|
||||
`bart.large` | 89.9 | 94.9 | 92.5 | 87.0 | 96.6 | 90.4 | 62.8 | 91.2
|
||||
|
||||
**[SQuAD (Rajpurkar et al., 2018)](https://rajpurkar.github.io/SQuAD-explorer/)**
|
||||
_(dev set, no additional data used)_
|
||||
|
||||
Model | SQuAD 1.1 EM/F1 | SQuAD 2.0 EM/F1
|
||||
---|---|---
|
||||
`roberta.large` | 88.9/94.6 | 86.5/89.4
|
||||
`bart.large` | 88.8/94.6 | 86.1/89.2
|
||||
|
||||
**[CNN/Daily Mail](http://nlpprogress.com/english/summarization.html)**
|
||||
_(test set, no additional data used)_
|
||||
|
||||
Model | R1 | R2 | RL
|
||||
---|---|---|---
|
||||
`BERTSUMEXTABS` | 42.13 | 19.60 | 39.18
|
||||
`bart.large` | 44.16 | 21.28 | 40.90
|
||||
|
||||
## Example usage
|
||||
|
||||
##### Load BART from torch.hub (PyTorch >= 1.1):
|
||||
```python
|
||||
import torch
|
||||
bart = torch.hub.load('pytorch/fairseq', 'bart.large')
|
||||
bart.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Load BART (for PyTorch 1.0 or custom models):
|
||||
```python
|
||||
# Download bart.large model
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/bart.large.tar.gz
|
||||
tar -xzvf bart.large.tar.gz
|
||||
|
||||
# Load the model in fairseq
|
||||
from fairseq.models.bart import BARTModel
|
||||
bart = BARTModel.from_pretrained('/path/to/bart.large', checkpoint_file='model.pt')
|
||||
bart.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Apply Byte-Pair Encoding (BPE) to input text:
|
||||
```python
|
||||
tokens = bart.encode('Hello world!')
|
||||
assert tokens.tolist() == [0, 31414, 232, 328, 2]
|
||||
bart.decode(tokens) # 'Hello world!'
|
||||
```
|
||||
|
||||
##### Extract features from BART:
|
||||
```python
|
||||
# Extract the last layer's features
|
||||
last_layer_features = bart.extract_features(tokens)
|
||||
assert last_layer_features.size() == torch.Size([1, 5, 1024])
|
||||
|
||||
# Extract all layer's features from decoder (layer 0 is the embedding layer)
|
||||
all_layers = bart.extract_features(tokens, return_all_hiddens=True)
|
||||
assert len(all_layers) == 13
|
||||
assert torch.all(all_layers[-1] == last_layer_features)
|
||||
```
|
||||
|
||||
##### Use BART for sentence-pair classification tasks:
|
||||
```python
|
||||
# Download BART already finetuned for MNLI
|
||||
bart = torch.hub.load('pytorch/fairseq', 'bart.large.mnli')
|
||||
bart.eval() # disable dropout for evaluation
|
||||
|
||||
# Encode a pair of sentences and make a prediction
|
||||
tokens = bart.encode('BART is a seq2seq model.', 'BART is not sequence to sequence.')
|
||||
bart.predict('mnli', tokens).argmax() # 0: contradiction
|
||||
|
||||
# Encode another pair of sentences
|
||||
tokens = bart.encode('BART is denoising autoencoder.', 'BART is version of autoencoder.')
|
||||
bart.predict('mnli', tokens).argmax() # 2: entailment
|
||||
```
|
||||
|
||||
##### Register a new (randomly initialized) classification head:
|
||||
```python
|
||||
bart.register_classification_head('new_task', num_classes=3)
|
||||
logprobs = bart.predict('new_task', tokens)
|
||||
```
|
||||
|
||||
##### Batched prediction:
|
||||
```python
|
||||
import torch
|
||||
from fairseq.data.data_utils import collate_tokens
|
||||
|
||||
bart = torch.hub.load('pytorch/fairseq', 'bart.large.mnli')
|
||||
bart.eval()
|
||||
|
||||
batch_of_pairs = [
|
||||
['BART is a seq2seq model.', 'BART is not sequence to sequence.'],
|
||||
['BART is denoising autoencoder.', 'BART is version of autoencoder.'],
|
||||
]
|
||||
|
||||
batch = collate_tokens(
|
||||
[bart.encode(pair[0], pair[1]) for pair in batch_of_pairs], pad_idx=1
|
||||
)
|
||||
|
||||
logprobs = bart.predict('mnli', batch)
|
||||
print(logprobs.argmax(dim=1))
|
||||
# tensor([0, 2])
|
||||
```
|
||||
|
||||
##### Using the GPU:
|
||||
```python
|
||||
bart.cuda()
|
||||
bart.predict('new_task', tokens)
|
||||
```
|
||||
|
||||
#### Filling masks:
|
||||
|
||||
BART can be used to fill multiple `<mask>` tokens in the input.
|
||||
```python
|
||||
bart = torch.hub.load('pytorch/fairseq', 'bart.base')
|
||||
bart.eval()
|
||||
bart.fill_mask(['The cat <mask> on the <mask>.'], topk=3, beam=10)
|
||||
# [[('The cat was on the ground.', tensor(-0.6183)), ('The cat was on the floor.', tensor(-0.6798)), ('The cat sleeps on the couch.', tensor(-0.6830))]]
|
||||
```
|
||||
|
||||
Note that by default we enforce the output length to match the input length.
|
||||
This can be disabled by setting ``match_source_len=False``:
|
||||
```
|
||||
bart.fill_mask(['The cat <mask> on the <mask>.'], topk=3, beam=10, match_source_len=False)
|
||||
# [[('The cat was on the ground.', tensor(-0.6185)), ('The cat was asleep on the couch.', tensor(-0.6276)), ('The cat was on the floor.', tensor(-0.6800))]]
|
||||
```
|
||||
|
||||
Example code to fill masks for a batch of sentences using GPU
|
||||
```
|
||||
bart.cuda()
|
||||
bart.fill_mask(['The cat <mask> on the <mask>.', 'The dog <mask> on the <mask>.'], topk=3, beam=10)
|
||||
# [[('The cat was on the ground.', tensor(-0.6183)), ('The cat was on the floor.', tensor(-0.6798)), ('The cat sleeps on the couch.', tensor(-0.6830))], [('The dog was on the ground.', tensor(-0.6190)), ('The dog lay on the ground.', tensor(-0.6711)),
|
||||
('The dog was asleep on the couch', tensor(-0.6796))]]
|
||||
```
|
||||
|
||||
#### Evaluating the `bart.large.mnli` model:
|
||||
|
||||
Example python code snippet to evaluate accuracy on the MNLI `dev_matched` set.
|
||||
```python
|
||||
label_map = {0: 'contradiction', 1: 'neutral', 2: 'entailment'}
|
||||
ncorrect, nsamples = 0, 0
|
||||
bart.cuda()
|
||||
bart.eval()
|
||||
with open('glue_data/MNLI/dev_matched.tsv') as fin:
|
||||
fin.readline()
|
||||
for index, line in enumerate(fin):
|
||||
tokens = line.strip().split('\t')
|
||||
sent1, sent2, target = tokens[8], tokens[9], tokens[-1]
|
||||
tokens = bart.encode(sent1, sent2)
|
||||
prediction = bart.predict('mnli', tokens).argmax().item()
|
||||
prediction_label = label_map[prediction]
|
||||
ncorrect += int(prediction_label == target)
|
||||
nsamples += 1
|
||||
print('| Accuracy: ', float(ncorrect)/float(nsamples))
|
||||
# Expected output: 0.9010
|
||||
```
|
||||
|
||||
#### Evaluating the `bart.large.cnn` model:
|
||||
Follow instructions [here](https://github.com/abisee/cnn-dailymail) to download and process into data-files such that `test.source` and `test.target` has one line for each non-tokenized sample.
|
||||
|
||||
```python
|
||||
bart = torch.hub.load('pytorch/fairseq', 'bart.large.cnn')
|
||||
bart.cuda()
|
||||
bart.eval()
|
||||
bart.half()
|
||||
count = 1
|
||||
bsz = 32
|
||||
with open('test.source') as source, open('test.hypo', 'w') as fout:
|
||||
sline = source.readline().strip()
|
||||
slines = [sline]
|
||||
for sline in source:
|
||||
if count % bsz == 0:
|
||||
with torch.no_grad():
|
||||
hypotheses_batch = bart.sample(slines, beam=4, lenpen=2.0, max_len_b=140, min_len=55, no_repeat_ngram_size=3)
|
||||
|
||||
for hypothesis in hypotheses_batch:
|
||||
fout.write(hypothesis + '\n')
|
||||
fout.flush()
|
||||
slines = []
|
||||
|
||||
slines.append(sline.strip())
|
||||
count += 1
|
||||
if slines != []:
|
||||
hypotheses_batch = bart.sample(slines, beam=4, lenpen=2.0, max_len_b=140, min_len=55, no_repeat_ngram_size=3)
|
||||
for hypothesis in hypotheses_batch:
|
||||
fout.write(hypothesis + '\n')
|
||||
fout.flush()
|
||||
```
|
||||
|
||||
Install `files2rouge` from [here](https://github.com/pltrdy/files2rouge).
|
||||
|
||||
```bash
|
||||
export CLASSPATH=/path/to/stanford-corenlp-full-2016-10-31/stanford-corenlp-3.7.0.jar
|
||||
|
||||
# Tokenize hypothesis and target files.
|
||||
cat test.hypo | java edu.stanford.nlp.process.PTBTokenizer -ioFileList -preserveLines > test.hypo.tokenized
|
||||
cat test.target | java edu.stanford.nlp.process.PTBTokenizer -ioFileList -preserveLines > test.hypo.target
|
||||
files2rouge test.hypo.tokenized test.hypo.target
|
||||
# Expected output: (ROUGE-2 Average_F: 0.21238)
|
||||
```
|
||||
|
||||
|
||||
## Finetuning
|
||||
|
||||
- [Finetuning on GLUE](README.glue.md)
|
||||
- [Finetuning on CNN-DM](README.summarization.md)
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@article{lewis2019bart,
|
||||
title = {BART: Denoising Sequence-to-Sequence Pre-training for Natural
|
||||
Language Generation, Translation, and Comprehension},
|
||||
author = {Mike Lewis and Yinhan Liu and Naman Goyal and Marjan Ghazvininejad and
|
||||
Abdelrahman Mohamed and Omer Levy and Veselin Stoyanov
|
||||
and Luke Zettlemoyer },
|
||||
journal={arXiv preprint arXiv:1910.13461},
|
||||
year = {2019},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
# Fine-tuning BART on CNN-Dailymail summarization task
|
||||
|
||||
### 1) Download the CNN and Daily Mail data and preprocess it into data files with non-tokenized cased samples.
|
||||
|
||||
Follow the instructions [here](https://github.com/abisee/cnn-dailymail) to download the original CNN and Daily Mail datasets. To preprocess the data, refer to the pointers in [this issue](https://github.com/pytorch/fairseq/issues/1391) or check out the code [here](https://github.com/artmatsak/cnn-dailymail).
|
||||
|
||||
Follow the instructions [here](https://github.com/EdinburghNLP/XSum) to download the original Extreme Summarization datasets, or check out the code [here](https://github.com/EdinburghNLP/XSum/tree/master/XSum-Dataset), Please keep the raw dataset and make sure no tokenization nor BPE on the dataset.
|
||||
|
||||
### 2) BPE preprocess:
|
||||
|
||||
```bash
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json'
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe'
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt'
|
||||
|
||||
TASK=cnn_dm
|
||||
for SPLIT in train val
|
||||
do
|
||||
for LANG in source target
|
||||
do
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json encoder.json \
|
||||
--vocab-bpe vocab.bpe \
|
||||
--inputs "$TASK/$SPLIT.$LANG" \
|
||||
--outputs "$TASK/$SPLIT.bpe.$LANG" \
|
||||
--workers 60 \
|
||||
--keep-empty;
|
||||
done
|
||||
done
|
||||
```
|
||||
|
||||
### 3) Binarize dataset:
|
||||
```bash
|
||||
fairseq-preprocess \
|
||||
--source-lang "source" \
|
||||
--target-lang "target" \
|
||||
--trainpref "${TASK}/train.bpe" \
|
||||
--validpref "${TASK}/val.bpe" \
|
||||
--destdir "${TASK}-bin/" \
|
||||
--workers 60 \
|
||||
--srcdict dict.txt \
|
||||
--tgtdict dict.txt;
|
||||
```
|
||||
|
||||
### 4) Fine-tuning on CNN-DM summarization task:
|
||||
Example fine-tuning CNN-DM
|
||||
```bash
|
||||
TOTAL_NUM_UPDATES=20000
|
||||
WARMUP_UPDATES=500
|
||||
LR=3e-05
|
||||
MAX_TOKENS=2048
|
||||
UPDATE_FREQ=4
|
||||
BART_PATH=/path/to/bart/model.pt
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 fairseq-train cnn_dm-bin \
|
||||
--restore-file $BART_PATH \
|
||||
--max-tokens $MAX_TOKENS \
|
||||
--task translation \
|
||||
--source-lang source --target-lang target \
|
||||
--truncate-source \
|
||||
--layernorm-embedding \
|
||||
--share-all-embeddings \
|
||||
--share-decoder-input-output-embed \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--required-batch-size-multiple 1 \
|
||||
--arch bart_large \
|
||||
--criterion label_smoothed_cross_entropy \
|
||||
--label-smoothing 0.1 \
|
||||
--dropout 0.1 --attention-dropout 0.1 \
|
||||
--weight-decay 0.01 --optimizer adam --adam-betas "(0.9, 0.999)" --adam-eps 1e-08 \
|
||||
--clip-norm 0.1 \
|
||||
--lr-scheduler polynomial_decay --lr $LR --total-num-update $TOTAL_NUM_UPDATES --warmup-updates $WARMUP_UPDATES \
|
||||
--fp16 --update-freq $UPDATE_FREQ \
|
||||
--skip-invalid-size-inputs-valid-test \
|
||||
--find-unused-parameters;
|
||||
```
|
||||
Above is expected to run on `1` node with `8 32gb-V100`.
|
||||
Expected training time is about `5 hours`. Training time can be reduced with distributed training on `4` nodes and `--update-freq 1`.
|
||||
|
||||
Use TOTAL_NUM_UPDATES=15000 UPDATE_FREQ=2 for Xsum task
|
||||
|
||||
### Inference for CNN-DM test data using above trained checkpoint.
|
||||
After training the model as mentioned in previous step, you can perform inference with checkpoints in `checkpoints/` directory using following python code snippet:
|
||||
|
||||
```python
|
||||
import torch
|
||||
from fairseq.models.bart import BARTModel
|
||||
|
||||
bart = BARTModel.from_pretrained(
|
||||
'checkpoints/',
|
||||
checkpoint_file='checkpoint_best.pt',
|
||||
data_name_or_path='cnn_dm-bin'
|
||||
)
|
||||
|
||||
bart.cuda()
|
||||
bart.eval()
|
||||
bart.half()
|
||||
count = 1
|
||||
bsz = 32
|
||||
with open('cnn_dm/test.source') as source, open('cnn_dm/test.hypo', 'w') as fout:
|
||||
sline = source.readline().strip()
|
||||
slines = [sline]
|
||||
for sline in source:
|
||||
if count % bsz == 0:
|
||||
with torch.no_grad():
|
||||
hypotheses_batch = bart.sample(slines, beam=4, lenpen=2.0, max_len_b=140, min_len=55, no_repeat_ngram_size=3)
|
||||
|
||||
for hypothesis in hypotheses_batch:
|
||||
fout.write(hypothesis + '\n')
|
||||
fout.flush()
|
||||
slines = []
|
||||
|
||||
slines.append(sline.strip())
|
||||
count += 1
|
||||
if slines != []:
|
||||
hypotheses_batch = bart.sample(slines, beam=4, lenpen=2.0, max_len_b=140, min_len=55, no_repeat_ngram_size=3)
|
||||
for hypothesis in hypotheses_batch:
|
||||
fout.write(hypothesis + '\n')
|
||||
fout.flush()
|
||||
```
|
||||
Use beam=6, lenpen=1.0, max_len_b=60, min_len=10 for Xsum Generation
|
||||
@@ -0,0 +1,88 @@
|
||||
# Neural Machine Translation with Byte-Level Subwords
|
||||
|
||||
https://arxiv.org/abs/1909.03341
|
||||
|
||||
We provide an implementation of byte-level byte-pair encoding (BBPE), taking IWSLT 2017 Fr-En translation as
|
||||
example.
|
||||
|
||||
## Data
|
||||
Get data and generate fairseq binary dataset:
|
||||
```bash
|
||||
bash ./get_data.sh
|
||||
```
|
||||
|
||||
## Model Training
|
||||
Train Transformer model with Bi-GRU embedding contextualization (implemented in `gru_transformer.py`):
|
||||
```bash
|
||||
# VOCAB=bytes
|
||||
# VOCAB=chars
|
||||
VOCAB=bbpe2048
|
||||
# VOCAB=bpe2048
|
||||
# VOCAB=bbpe4096
|
||||
# VOCAB=bpe4096
|
||||
# VOCAB=bpe16384
|
||||
```
|
||||
```bash
|
||||
fairseq-train "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \
|
||||
--arch gru_transformer --encoder-layers 2 --decoder-layers 2 --dropout 0.3 --share-all-embeddings \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' \
|
||||
--lr 5e-4 --lr-scheduler inverse_sqrt --warmup-updates 4000 \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
|
||||
--log-format 'simple' --log-interval 100 --save-dir "checkpoints/${VOCAB}" \
|
||||
--batch-size 100 --max-update 100000 --update-freq 2
|
||||
```
|
||||
|
||||
## Generation
|
||||
`fairseq-generate` requires bytes (BBPE) decoder to convert byte-level representation back to characters:
|
||||
```bash
|
||||
# BPE=--bpe bytes
|
||||
# BPE=--bpe characters
|
||||
BPE=--bpe byte_bpe --sentencepiece-model-path data/spm_bbpe2048.model
|
||||
# BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe2048.model
|
||||
# BPE=--bpe byte_bpe --sentencepiece-model-path data/spm_bbpe4096.model
|
||||
# BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe4096.model
|
||||
# BPE=--bpe sentencepiece --sentencepiece-model data/spm_bpe16384.model
|
||||
```
|
||||
|
||||
```bash
|
||||
fairseq-generate "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \
|
||||
--source-lang fr --gen-subset test --sacrebleu --path "checkpoints/${VOCAB}/checkpoint_last.pt" \
|
||||
--tokenizer moses --moses-target-lang en ${BPE}
|
||||
```
|
||||
When using `fairseq-interactive`, bytes (BBPE) encoder/decoder is required to tokenize input data and detokenize model predictions:
|
||||
```bash
|
||||
fairseq-interactive "data/bin_${VOCAB}" --task translation --user-dir examples/byte_level_bpe/gru_transformer \
|
||||
--path "checkpoints/${VOCAB}/checkpoint_last.pt" --input data/test.fr --tokenizer moses --moses-source-lang fr \
|
||||
--moses-target-lang en ${BPE} --buffer-size 1000 --max-tokens 10000
|
||||
```
|
||||
|
||||
## Results
|
||||
| Vocabulary | Model | BLEU |
|
||||
|:-------------:|:-------------:|:-------------:|
|
||||
| Joint BPE 16k ([Kudo, 2018](https://arxiv.org/abs/1804.10959)) | 512d LSTM 2+2 | 33.81 |
|
||||
| Joint BPE 16k | Transformer base 2+2 (w/ GRU) | 36.64 (36.72) |
|
||||
| Joint BPE 4k | Transformer base 2+2 (w/ GRU) | 35.49 (36.10) |
|
||||
| Joint BBPE 4k | Transformer base 2+2 (w/ GRU) | 35.61 (35.82) |
|
||||
| Joint BPE 2k | Transformer base 2+2 (w/ GRU) | 34.87 (36.13) |
|
||||
| Joint BBPE 2k | Transformer base 2+2 (w/ GRU) | 34.98 (35.43) |
|
||||
| Characters | Transformer base 2+2 (w/ GRU) | 31.78 (33.30) |
|
||||
| Bytes | Transformer base 2+2 (w/ GRU) | 31.57 (33.62) |
|
||||
|
||||
|
||||
## Citation
|
||||
```
|
||||
@misc{wang2019neural,
|
||||
title={Neural Machine Translation with Byte-Level Subwords},
|
||||
author={Changhan Wang and Kyunghyun Cho and Jiatao Gu},
|
||||
year={2019},
|
||||
eprint={1909.03341},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CL}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Contact
|
||||
Changhan Wang ([changhan@fb.com](mailto:changhan@fb.com)),
|
||||
Kyunghyun Cho ([kyunghyuncho@fb.com](mailto:kyunghyuncho@fb.com)),
|
||||
Jiatao Gu ([jgu@fb.com](mailto:jgu@fb.com))
|
||||
@@ -0,0 +1,254 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import os.path as op
|
||||
from collections import namedtuple
|
||||
from multiprocessing import cpu_count
|
||||
from typing import List, Optional
|
||||
|
||||
import sentencepiece as sp
|
||||
from fairseq.data.encoders.byte_bpe import ByteBPE
|
||||
from fairseq.data.encoders.byte_utils import byte_encode
|
||||
from fairseq.data.encoders.bytes import Bytes
|
||||
from fairseq.data.encoders.characters import Characters
|
||||
from fairseq.data.encoders.moses_tokenizer import MosesTokenizer
|
||||
from fairseq.data.encoders.sentencepiece_bpe import SentencepieceBPE
|
||||
|
||||
|
||||
SPLITS = ["train", "valid", "test"]
|
||||
|
||||
|
||||
def _convert_xml(in_path: str, out_path: str):
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
ss = s.strip()
|
||||
if not ss.startswith("<seg"):
|
||||
continue
|
||||
ss = ss.replace("</seg>", "").split('">')
|
||||
assert len(ss) == 2
|
||||
f_o.write(ss[1].strip() + "\n")
|
||||
|
||||
|
||||
def _convert_train(in_path: str, out_path: str):
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
ss = s.strip()
|
||||
if ss.startswith("<"):
|
||||
continue
|
||||
f_o.write(ss.strip() + "\n")
|
||||
|
||||
|
||||
def _get_bytes(in_path: str, out_path: str):
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
f_o.write(Bytes.encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def _get_chars(in_path: str, out_path: str):
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
f_o.write(Characters.encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def pretokenize(in_path: str, out_path: str, src: str, tgt: str):
|
||||
Args = namedtuple(
|
||||
"Args",
|
||||
[
|
||||
"moses_source_lang",
|
||||
"moses_target_lang",
|
||||
"moses_no_dash_splits",
|
||||
"moses_no_escape",
|
||||
],
|
||||
)
|
||||
args = Args(
|
||||
moses_source_lang=src,
|
||||
moses_target_lang=tgt,
|
||||
moses_no_dash_splits=False,
|
||||
moses_no_escape=False,
|
||||
)
|
||||
pretokenizer = MosesTokenizer(args)
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
f_o.write(pretokenizer.encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def _convert_to_bchar(in_path_prefix: str, src: str, tgt: str, out_path: str):
|
||||
with open(out_path, "w") as f_o:
|
||||
for lang in [src, tgt]:
|
||||
with open(f"{in_path_prefix}.{lang}") as f:
|
||||
for s in f:
|
||||
f_o.write(byte_encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def _get_bpe(in_path: str, model_prefix: str, vocab_size: int):
|
||||
arguments = [
|
||||
f"--input={in_path}",
|
||||
f"--model_prefix={model_prefix}",
|
||||
f"--model_type=bpe",
|
||||
f"--vocab_size={vocab_size}",
|
||||
"--character_coverage=1.0",
|
||||
"--normalization_rule_name=identity",
|
||||
f"--num_threads={cpu_count()}",
|
||||
]
|
||||
sp.SentencePieceTrainer.Train(" ".join(arguments))
|
||||
|
||||
|
||||
def _apply_bbpe(model_path: str, in_path: str, out_path: str):
|
||||
Args = namedtuple("Args", ["sentencepiece_model_path"])
|
||||
args = Args(sentencepiece_model_path=model_path)
|
||||
tokenizer = ByteBPE(args)
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
f_o.write(tokenizer.encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def _apply_bpe(model_path: str, in_path: str, out_path: str):
|
||||
Args = namedtuple("Args", ["sentencepiece_model"])
|
||||
args = Args(sentencepiece_model=model_path)
|
||||
tokenizer = SentencepieceBPE(args)
|
||||
with open(in_path) as f, open(out_path, "w") as f_o:
|
||||
for s in f:
|
||||
f_o.write(tokenizer.encode(s.strip()) + "\n")
|
||||
|
||||
|
||||
def _concat_files(in_paths: List[str], out_path: str):
|
||||
with open(out_path, "w") as f_o:
|
||||
for p in in_paths:
|
||||
with open(p) as f:
|
||||
for r in f:
|
||||
f_o.write(r)
|
||||
|
||||
|
||||
def preprocess_iwslt17(
|
||||
root: str,
|
||||
src: str,
|
||||
tgt: str,
|
||||
bpe_size: Optional[int],
|
||||
need_chars: bool,
|
||||
bbpe_size: Optional[int],
|
||||
need_bytes: bool,
|
||||
):
|
||||
# extract bitext
|
||||
in_root = op.join(root, f"{src}-{tgt}")
|
||||
for lang in [src, tgt]:
|
||||
_convert_train(
|
||||
op.join(in_root, f"train.tags.{src}-{tgt}.{lang}"),
|
||||
op.join(root, f"train.{lang}"),
|
||||
)
|
||||
_convert_xml(
|
||||
op.join(in_root, f"IWSLT17.TED.dev2010.{src}-{tgt}.{lang}.xml"),
|
||||
op.join(root, f"valid.{lang}"),
|
||||
)
|
||||
_convert_xml(
|
||||
op.join(in_root, f"IWSLT17.TED.tst2015.{src}-{tgt}.{lang}.xml"),
|
||||
op.join(root, f"test.{lang}"),
|
||||
)
|
||||
# pre-tokenize
|
||||
for lang in [src, tgt]:
|
||||
for split in SPLITS:
|
||||
pretokenize(
|
||||
op.join(root, f"{split}.{lang}"),
|
||||
op.join(root, f"{split}.moses.{lang}"),
|
||||
src,
|
||||
tgt,
|
||||
)
|
||||
# tokenize with BPE vocabulary
|
||||
if bpe_size is not None:
|
||||
# learn vocabulary
|
||||
concated_train_path = op.join(root, "train.all")
|
||||
_concat_files(
|
||||
[op.join(root, "train.moses.fr"), op.join(root, "train.moses.en")],
|
||||
concated_train_path,
|
||||
)
|
||||
bpe_model_prefix = op.join(root, f"spm_bpe{bpe_size}")
|
||||
_get_bpe(concated_train_path, bpe_model_prefix, bpe_size)
|
||||
os.remove(concated_train_path)
|
||||
# apply
|
||||
for lang in [src, tgt]:
|
||||
for split in SPLITS:
|
||||
_apply_bpe(
|
||||
bpe_model_prefix + ".model",
|
||||
op.join(root, f"{split}.moses.{lang}"),
|
||||
op.join(root, f"{split}.moses.bpe{bpe_size}.{lang}"),
|
||||
)
|
||||
# tokenize with bytes vocabulary
|
||||
if need_bytes:
|
||||
for lang in [src, tgt]:
|
||||
for split in SPLITS:
|
||||
_get_bytes(
|
||||
op.join(root, f"{split}.moses.{lang}"),
|
||||
op.join(root, f"{split}.moses.bytes.{lang}"),
|
||||
)
|
||||
# tokenize with characters vocabulary
|
||||
if need_chars:
|
||||
for lang in [src, tgt]:
|
||||
for split in SPLITS:
|
||||
_get_chars(
|
||||
op.join(root, f"{split}.moses.{lang}"),
|
||||
op.join(root, f"{split}.moses.chars.{lang}"),
|
||||
)
|
||||
# tokenize with byte-level BPE vocabulary
|
||||
if bbpe_size is not None:
|
||||
# learn vocabulary
|
||||
bchar_path = op.join(root, "train.bchar")
|
||||
_convert_to_bchar(op.join(root, "train.moses"), src, tgt, bchar_path)
|
||||
bbpe_model_prefix = op.join(root, f"spm_bbpe{bbpe_size}")
|
||||
_get_bpe(bchar_path, bbpe_model_prefix, bbpe_size)
|
||||
os.remove(bchar_path)
|
||||
# apply
|
||||
for lang in [src, tgt]:
|
||||
for split in SPLITS:
|
||||
_apply_bbpe(
|
||||
bbpe_model_prefix + ".model",
|
||||
op.join(root, f"{split}.moses.{lang}"),
|
||||
op.join(root, f"{split}.moses.bbpe{bbpe_size}.{lang}"),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=str, default="data")
|
||||
parser.add_argument(
|
||||
"--bpe-vocab",
|
||||
default=None,
|
||||
type=int,
|
||||
help="Generate tokenized bitext with BPE of size K."
|
||||
"Default to None (disabled).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bbpe-vocab",
|
||||
default=None,
|
||||
type=int,
|
||||
help="Generate tokenized bitext with BBPE of size K."
|
||||
"Default to None (disabled).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--byte-vocab",
|
||||
action="store_true",
|
||||
help="Generate tokenized bitext with bytes vocabulary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--char-vocab",
|
||||
action="store_true",
|
||||
help="Generate tokenized bitext with chars vocabulary",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
preprocess_iwslt17(
|
||||
args.root,
|
||||
"fr",
|
||||
"en",
|
||||
args.bpe_vocab,
|
||||
args.char_vocab,
|
||||
args.bbpe_vocab,
|
||||
args.byte_vocab,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
PY_BIN_ROOT=
|
||||
|
||||
# PyPI dependency
|
||||
${PY_BIN_ROOT}pip install sentencepiece sacremoses
|
||||
|
||||
# Get data
|
||||
if [ ! -d "data" ]; then
|
||||
mkdir data
|
||||
fi
|
||||
|
||||
if [ ! -f "data/fr-en.tgz" ]; then
|
||||
wget https://wit3.fbk.eu/archive/2017-01-trnted/texts/fr/en/fr-en.tgz -P data
|
||||
tar xvf data/fr-en.tgz -C data
|
||||
fi
|
||||
${PY_BIN_ROOT}python get_bitext.py --bpe-vocab 16384 --byte-vocab --char-vocab
|
||||
for VOCAB_SIZE in 2048 4096; do
|
||||
${PY_BIN_ROOT}python get_bitext.py --bpe-vocab ${VOCAB_SIZE} --bbpe-vocab ${VOCAB_SIZE}
|
||||
done
|
||||
rm -r data/fr-en data/fr-en.tgz
|
||||
|
||||
# Generate binary dataset
|
||||
${PY_BIN_ROOT}/fairseq-preprocess --source-lang fr --target-lang en --destdir data/bin_bpe16384 --joined-dictionary \
|
||||
--workers "$(nproc)" --trainpref data/train.moses.bpe16384 --validpref data/valid.moses.bpe16384 \
|
||||
--testpref data/test.moses.bpe16384
|
||||
|
||||
${PY_BIN_ROOT}/fairseq-preprocess --source-lang fr --target-lang en --destdir data/bin_bytes --joined-dictionary \
|
||||
--workers "$(nproc)" --trainpref data/train.moses.bytes --validpref data/valid.moses.bytes \
|
||||
--testpref data/test.moses.bytes
|
||||
|
||||
${PY_BIN_ROOT}/fairseq-preprocess --source-lang fr --target-lang en --destdir data/bin_chars --joined-dictionary \
|
||||
--workers "$(nproc)" --trainpref data/train.moses.chars --validpref data/valid.moses.chars \
|
||||
--testpref data/test.moses.chars
|
||||
|
||||
for VOCAB_SIZE in 2048 4096; do
|
||||
for TYPE in bbpe bpe; do
|
||||
${PY_BIN_ROOT}/fairseq-preprocess --source-lang fr --target-lang en --destdir "data/bin_${TYPE}${VOCAB_SIZE}" \
|
||||
--joined-dictionary --workers "$(nproc)" --trainpref "data/train.moses.${TYPE}${VOCAB_SIZE}" \
|
||||
--validpref "data/valid.moses.${TYPE}${VOCAB_SIZE}" --testpref "data/test.moses.${TYPE}${VOCAB_SIZE}"
|
||||
done
|
||||
done
|
||||
@@ -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.
|
||||
|
||||
# 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.models import register_model, register_model_architecture
|
||||
from fairseq.models.transformer import TransformerEncoder, TransformerModel
|
||||
|
||||
|
||||
@register_model("gru_transformer")
|
||||
class GRUTransformerModel(TransformerModel):
|
||||
@classmethod
|
||||
def build_encoder(cls, args, src_dict, embed_tokens):
|
||||
return GRUTransformerEncoder(args, src_dict, embed_tokens)
|
||||
|
||||
|
||||
class GRUTransformerEncoder(TransformerEncoder):
|
||||
def __init__(self, args, dictionary, embed_tokens):
|
||||
super().__init__(args, dictionary, embed_tokens)
|
||||
self.emb_ctx = nn.GRU(
|
||||
input_size=embed_tokens.embedding_dim,
|
||||
hidden_size=embed_tokens.embedding_dim // 2,
|
||||
num_layers=1,
|
||||
bidirectional=True,
|
||||
)
|
||||
|
||||
def forward_embedding(self, src_tokens):
|
||||
# embed tokens and positions
|
||||
x = embed = self.embed_scale * self.embed_tokens(src_tokens)
|
||||
if self.embed_positions is not None:
|
||||
x = embed + self.embed_positions(src_tokens)
|
||||
|
||||
# contextualize embeddings
|
||||
x = x.transpose(0, 1)
|
||||
x = self.dropout_module(x)
|
||||
x, _ = self.emb_ctx.forward(x)
|
||||
x = x.transpose(0, 1)
|
||||
|
||||
if self.layernorm_embedding is not None:
|
||||
x = self.layernorm_embedding(x)
|
||||
x = self.dropout_module(x)
|
||||
return x, embed
|
||||
|
||||
|
||||
@register_model_architecture("gru_transformer", "gru_transformer")
|
||||
def gru_transformer_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.no_cross_attention = getattr(args, "no_cross_attention", False)
|
||||
args.cross_self_attention = getattr(args, "cross_self_attention", False)
|
||||
args.layer_wise_attention = getattr(args, "layer_wise_attention", 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)
|
||||
|
||||
args.no_scale_embedding = getattr(args, "no_scale_embedding", False)
|
||||
args.layernorm_embedding = getattr(args, "layernorm_embedding", False)
|
||||
|
||||
|
||||
@register_model_architecture("gru_transformer", "gru_transformer_big")
|
||||
def gru_transformer_big(args):
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 1024)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", 4096)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", 16)
|
||||
args.encoder_normalize_before = getattr(args, "encoder_normalize_before", False)
|
||||
args.decoder_embed_dim = getattr(args, "decoder_embed_dim", 1024)
|
||||
args.decoder_ffn_embed_dim = getattr(args, "decoder_ffn_embed_dim", 4096)
|
||||
args.decoder_attention_heads = getattr(args, "decoder_attention_heads", 16)
|
||||
args.dropout = getattr(args, "dropout", 0.3)
|
||||
gru_transformer_base_architecture(args)
|
||||
@@ -0,0 +1,75 @@
|
||||
# CamemBERT: a Tasty French Language Model
|
||||
|
||||
## Introduction
|
||||
|
||||
[CamemBERT](https://arxiv.org/abs/1911.03894) is a pretrained language model trained on 138GB of French text based on RoBERTa.
|
||||
|
||||
Also available in [github.com/huggingface/transformers](https://github.com/huggingface/transformers/).
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
| Model | #params | Download | Arch. | Training data |
|
||||
|--------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------|-------|-----------------------------------|
|
||||
| `camembert` / `camembert-base` | 110M | [camembert-base.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-base.tar.gz) | Base | OSCAR (138 GB of text) |
|
||||
| `camembert-large` | 335M | [camembert-large.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-large.tar.gz) | Large | CCNet (135 GB of text) |
|
||||
| `camembert-base-ccnet` | 110M | [camembert-base-ccnet.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-base-ccnet.tar.gz) | Base | CCNet (135 GB of text) |
|
||||
| `camembert-base-wikipedia-4gb` | 110M | [camembert-base-wikipedia-4gb.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-base-wikipedia-4gb.tar.gz) | Base | Wikipedia (4 GB of text) |
|
||||
| `camembert-base-oscar-4gb` | 110M | [camembert-base-oscar-4gb.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-base-oscar-4gb.tar.gz) | Base | Subsample of OSCAR (4 GB of text) |
|
||||
| `camembert-base-ccnet-4gb` | 110M | [camembert-base-ccnet-4gb.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/camembert-base-ccnet-4gb.tar.gz) | Base | Subsample of CCNet (4 GB of text) |
|
||||
|
||||
## Example usage
|
||||
|
||||
### fairseq
|
||||
##### Load CamemBERT from torch.hub (PyTorch >= 1.1):
|
||||
```python
|
||||
import torch
|
||||
camembert = torch.hub.load('pytorch/fairseq', 'camembert')
|
||||
camembert.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Load CamemBERT (for PyTorch 1.0 or custom models):
|
||||
```python
|
||||
# Download camembert model
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/camembert-base.tar.gz
|
||||
tar -xzvf camembert.tar.gz
|
||||
|
||||
# Load the model in fairseq
|
||||
from fairseq.models.roberta import CamembertModel
|
||||
camembert = CamembertModel.from_pretrained('/path/to/camembert')
|
||||
camembert.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Filling masks:
|
||||
```python
|
||||
masked_line = 'Le camembert est <mask> :)'
|
||||
camembert.fill_mask(masked_line, topk=3)
|
||||
# [('Le camembert est délicieux :)', 0.4909118115901947, ' délicieux'),
|
||||
# ('Le camembert est excellent :)', 0.10556942224502563, ' excellent'),
|
||||
# ('Le camembert est succulent :)', 0.03453322499990463, ' succulent')]
|
||||
```
|
||||
|
||||
##### Extract features from Camembert:
|
||||
```python
|
||||
# Extract the last layer's features
|
||||
line = "J'aime le camembert !"
|
||||
tokens = camembert.encode(line)
|
||||
last_layer_features = camembert.extract_features(tokens)
|
||||
assert last_layer_features.size() == torch.Size([1, 10, 768])
|
||||
|
||||
# Extract all layer's features (layer 0 is the embedding layer)
|
||||
all_layers = camembert.extract_features(tokens, return_all_hiddens=True)
|
||||
assert len(all_layers) == 13
|
||||
assert torch.all(all_layers[-1] == last_layer_features)
|
||||
```
|
||||
|
||||
## Citation
|
||||
If you use our work, please cite:
|
||||
|
||||
```bibtex
|
||||
@inproceedings{martin2020camembert,
|
||||
title={CamemBERT: a Tasty French Language Model},
|
||||
author={Martin, Louis and Muller, Benjamin and Su{\'a}rez, Pedro Javier Ortiz and Dupont, Yoann and Romary, Laurent and de la Clergerie, {\'E}ric Villemonte and Seddah, Djam{\'e} and Sagot, Beno{\^\i}t},
|
||||
booktitle={Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics},
|
||||
year={2020}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# (Vectorized) Lexically constrained decoding with dynamic beam allocation
|
||||
|
||||
This page provides instructions for how to use lexically constrained decoding in Fairseq.
|
||||
Fairseq implements the code described in the following papers:
|
||||
|
||||
* [Fast Lexically Constrained Decoding With Dynamic Beam Allocation](https://www.aclweb.org/anthology/N18-1119/) (Post & Vilar, 2018)
|
||||
* [Improved Lexically Constrained Decoding for Translation and Monolingual Rewriting](https://www.aclweb.org/anthology/N19-1090/) (Hu et al., 2019)
|
||||
|
||||
## Quick start
|
||||
|
||||
Constrained search is enabled by adding the command-line argument `--constraints` to `fairseq-interactive`.
|
||||
Constraints are appended to each line of input, separated by tabs. Each constraint (one or more tokens)
|
||||
is a separate field.
|
||||
|
||||
The following command, using [Fairseq's WMT19 German--English model](https://github.com/pytorch/fairseq/blob/master/examples/wmt19/README.md),
|
||||
translates the sentence *Die maschinelle Übersetzung ist schwer zu kontrollieren.* with the constraints
|
||||
"hard" and "to influence".
|
||||
|
||||
echo -e "Die maschinelle Übersetzung ist schwer zu kontrollieren.\thard\ttoinfluence" \
|
||||
| normalize.py | tok.py \
|
||||
| fairseq-interactive /path/to/model \
|
||||
--path /path/to/model/model1.pt \
|
||||
--bpe fastbpe \
|
||||
--bpe-codes /path/to/model/bpecodes \
|
||||
--constraints \
|
||||
-s de -t en \
|
||||
--beam 10
|
||||
|
||||
(tok.py and normalize.py can be found in the same directory as this README; they are just shortcuts around Fairseq's WMT19 preprocessing).
|
||||
This will generate the following output:
|
||||
|
||||
[snip]
|
||||
S-0 Die masch@@ in@@ elle Über@@ setzung ist schwer zu kontrollieren .
|
||||
W-0 1.844 seconds
|
||||
C-0 hard
|
||||
C-0 influence
|
||||
H-0 -1.5333266258239746 Mach@@ ine trans@@ lation is hard to influence .
|
||||
D-0 -1.5333266258239746 Machine translation is hard to influence .
|
||||
P-0 -0.5434 -0.1423 -0.1930 -0.1415 -0.2346 -1.8031 -0.1701 -11.7727 -0.1815 -0.1511
|
||||
|
||||
By default, constraints are generated in the order supplied, with any number (zero or more) of tokens generated
|
||||
between constraints. If you wish for the decoder to order the constraints, then use `--constraints unordered`.
|
||||
Note that you may want to use a larger beam.
|
||||
|
||||
## Implementation details
|
||||
|
||||
The heart of the implementation is in `fairseq/search.py`, which adds a `LexicallyConstrainedBeamSearch` instance.
|
||||
This instance of beam search tracks the progress of each hypothesis in the beam through the set of constraints
|
||||
provided for each input sentence. It does this using one of two classes, both found in `fairseq/token_generation_contstraints.py`:
|
||||
|
||||
* OrderedConstraintState: assumes the `C` input constraints will be generated in the provided order
|
||||
* UnorderedConstraintState: tries to apply `C` (phrasal) constraints in all `C!` orders
|
||||
|
||||
## Differences from Sockeye
|
||||
|
||||
There are a number of [differences from Sockeye's implementation](https://awslabs.github.io/sockeye/inference.html#lexical-constraints).
|
||||
|
||||
* Generating constraints in the order supplied (the default option here) is not available in Sockeye.
|
||||
* Due to an improved beam allocation method, there is no need to prune the beam.
|
||||
* Again due to better allocation, beam sizes as low as 10 or even 5 are often sufficient.
|
||||
* [The vector extensions described in Hu et al.](https://github.com/edwardjhu/sockeye/tree/trie_constraints) (NAACL 2019) were never merged
|
||||
into the main Sockeye branch.
|
||||
|
||||
## Citation
|
||||
|
||||
The paper first describing lexical constraints for seq2seq decoding is:
|
||||
|
||||
```bibtex
|
||||
@inproceedings{hokamp-liu-2017-lexically,
|
||||
title = "Lexically Constrained Decoding for Sequence Generation Using Grid Beam Search",
|
||||
author = "Hokamp, Chris and
|
||||
Liu, Qun",
|
||||
booktitle = "Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)",
|
||||
month = jul,
|
||||
year = "2017",
|
||||
address = "Vancouver, Canada",
|
||||
publisher = "Association for Computational Linguistics",
|
||||
url = "https://www.aclweb.org/anthology/P17-1141",
|
||||
doi = "10.18653/v1/P17-1141",
|
||||
pages = "1535--1546",
|
||||
}
|
||||
```
|
||||
|
||||
The fairseq implementation uses the extensions described in
|
||||
|
||||
```bibtex
|
||||
@inproceedings{post-vilar-2018-fast,
|
||||
title = "Fast Lexically Constrained Decoding with Dynamic Beam Allocation for Neural Machine Translation",
|
||||
author = "Post, Matt and
|
||||
Vilar, David",
|
||||
booktitle = "Proceedings of the 2018 Conference of the North {A}merican Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long Papers)",
|
||||
month = jun,
|
||||
year = "2018",
|
||||
address = "New Orleans, Louisiana",
|
||||
publisher = "Association for Computational Linguistics",
|
||||
url = "https://www.aclweb.org/anthology/N18-1119",
|
||||
doi = "10.18653/v1/N18-1119",
|
||||
pages = "1314--1324",
|
||||
}
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```bibtex
|
||||
@inproceedings{hu-etal-2019-improved,
|
||||
title = "Improved Lexically Constrained Decoding for Translation and Monolingual Rewriting",
|
||||
author = "Hu, J. Edward and
|
||||
Khayrallah, Huda and
|
||||
Culkin, Ryan and
|
||||
Xia, Patrick and
|
||||
Chen, Tongfei and
|
||||
Post, Matt and
|
||||
Van Durme, Benjamin",
|
||||
booktitle = "Proceedings of the 2019 Conference of the North {A}merican Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers)",
|
||||
month = jun,
|
||||
year = "2019",
|
||||
address = "Minneapolis, Minnesota",
|
||||
publisher = "Association for Computational Linguistics",
|
||||
url = "https://www.aclweb.org/anthology/N19-1090",
|
||||
doi = "10.18653/v1/N19-1090",
|
||||
pages = "839--850",
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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 sys
|
||||
|
||||
from sacremoses.normalize import MosesPunctNormalizer
|
||||
|
||||
|
||||
def main(args):
|
||||
normalizer = MosesPunctNormalizer(lang=args.lang, penn=args.penn)
|
||||
for line in sys.stdin:
|
||||
print(normalizer.normalize(line.rstrip()), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--lang", "-l", default="en")
|
||||
parser.add_argument("--penn", "-p", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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 sys
|
||||
|
||||
import sacremoses
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Tokenizes, preserving tabs"""
|
||||
mt = sacremoses.MosesTokenizer(lang=args.lang)
|
||||
|
||||
def tok(s):
|
||||
return mt.tokenize(s, return_str=True)
|
||||
|
||||
for line in sys.stdin:
|
||||
parts = list(map(tok, line.split("\t")))
|
||||
print(*parts, sep="\t", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--lang", "-l", default="en")
|
||||
parser.add_argument("--penn", "-p", action="store_true")
|
||||
parser.add_argument("--fields", "-f", help="fields to tokenize")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Convolutional Sequence to Sequence Learning (Gehring et al., 2017)
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Description | Dataset | Model | Test set(s)
|
||||
---|---|---|---
|
||||
Convolutional <br> ([Gehring et al., 2017](https://arxiv.org/abs/1705.03122)) | [WMT14 English-French](http://statmt.org/wmt14/translation-task.html#Download) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/wmt14.v2.en-fr.fconv-py.tar.bz2) | newstest2014: <br> [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/data/wmt14.v2.en-fr.newstest2014.tar.bz2) <br> newstest2012/2013: <br> [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/data/wmt14.v2.en-fr.ntst1213.tar.bz2)
|
||||
Convolutional <br> ([Gehring et al., 2017](https://arxiv.org/abs/1705.03122)) | [WMT14 English-German](http://statmt.org/wmt14/translation-task.html#Download) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/wmt14.en-de.fconv-py.tar.bz2) | newstest2014: <br> [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/data/wmt14.en-de.newstest2014.tar.bz2)
|
||||
Convolutional <br> ([Gehring et al., 2017](https://arxiv.org/abs/1705.03122)) | [WMT17 English-German](http://statmt.org/wmt17/translation-task.html#Download) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/wmt17.v2.en-de.fconv-py.tar.bz2) | newstest2014: <br> [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/data/wmt17.v2.en-de.newstest2014.tar.bz2)
|
||||
|
||||
## Example usage
|
||||
|
||||
See the [translation README](../translation/README.md) for instructions on reproducing results for WMT'14 En-De and
|
||||
WMT'14 En-Fr using the `fconv_wmt_en_de` and `fconv_wmt_en_fr` model architectures.
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@inproceedings{gehring2017convs2s,
|
||||
title = {Convolutional Sequence to Sequence Learning},
|
||||
author = {Gehring, Jonas, and Auli, Michael and Grangier, David and Yarats, Denis and Dauphin, Yann N},
|
||||
booktitle = {Proc. of ICML},
|
||||
year = 2017,
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Cross-lingual Retrieval for Iterative Self-Supervised Training
|
||||
|
||||
https://arxiv.org/pdf/2006.09526.pdf
|
||||
|
||||
## Introduction
|
||||
|
||||
CRISS is a multilingual sequence-to-sequnce pretraining method where mining and training processes are applied iteratively, improving cross-lingual alignment and translation ability at the same time.
|
||||
|
||||
## Requirements:
|
||||
|
||||
* faiss: https://github.com/facebookresearch/faiss
|
||||
* mosesdecoder: https://github.com/moses-smt/mosesdecoder
|
||||
* flores: https://github.com/facebookresearch/flores
|
||||
* LASER: https://github.com/facebookresearch/LASER
|
||||
|
||||
## Unsupervised Machine Translation
|
||||
##### 1. Download and decompress CRISS checkpoints
|
||||
```
|
||||
cd examples/criss
|
||||
wget https://dl.fbaipublicfiles.com/criss/criss_3rd_checkpoints.tar.gz
|
||||
tar -xf criss_checkpoints.tar.gz
|
||||
```
|
||||
##### 2. Download and preprocess Flores test dataset
|
||||
Make sure to run all scripts from examples/criss directory
|
||||
```
|
||||
bash download_and_preprocess_flores_test.sh
|
||||
```
|
||||
|
||||
##### 3. Run Evaluation on Sinhala-English
|
||||
```
|
||||
bash unsupervised_mt/eval.sh
|
||||
```
|
||||
|
||||
## Sentence Retrieval
|
||||
##### 1. Download and preprocess Tatoeba dataset
|
||||
```
|
||||
bash download_and_preprocess_tatoeba.sh
|
||||
```
|
||||
|
||||
##### 2. Run Sentence Retrieval on Tatoeba Kazakh-English
|
||||
```
|
||||
bash sentence_retrieval/sentence_retrieval_tatoeba.sh
|
||||
```
|
||||
|
||||
## Mining
|
||||
##### 1. Install faiss
|
||||
Follow instructions on https://github.com/facebookresearch/faiss/blob/master/INSTALL.md
|
||||
##### 2. Mine pseudo-parallel data between Kazakh and English
|
||||
```
|
||||
bash mining/mine_example.sh
|
||||
```
|
||||
|
||||
## Citation
|
||||
```bibtex
|
||||
@article{tran2020cross,
|
||||
title={Cross-lingual retrieval for iterative self-supervised training},
|
||||
author={Tran, Chau and Tang, Yuqing and Li, Xian and Gu, Jiatao},
|
||||
journal={arXiv preprint arXiv:2006.09526},
|
||||
year={2020}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
SPM_ENCODE=flores/scripts/spm_encode.py
|
||||
DATA=data_tmp
|
||||
SPM_MODEL=criss_checkpoints/sentence.bpe.model
|
||||
DICT=criss_checkpoints/dict.txt
|
||||
|
||||
download_data() {
|
||||
CORPORA=$1
|
||||
URL=$2
|
||||
|
||||
if [ -f $CORPORA ]; then
|
||||
echo "$CORPORA already exists, skipping download"
|
||||
else
|
||||
echo "Downloading $URL"
|
||||
wget $URL -O $CORPORA --no-check-certificate || rm -f $CORPORA
|
||||
if [ -f $CORPORA ]; then
|
||||
echo "$URL successfully downloaded."
|
||||
else
|
||||
echo "$URL not successfully downloaded."
|
||||
rm -f $CORPORA
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -f flores ]]; then
|
||||
echo "flores already cloned"
|
||||
else
|
||||
git clone https://github.com/facebookresearch/flores
|
||||
fi
|
||||
|
||||
mkdir -p $DATA
|
||||
download_data $DATA/wikipedia_en_ne_si_test_sets.tgz "https://github.com/facebookresearch/flores/raw/master/data/wikipedia_en_ne_si_test_sets.tgz"
|
||||
pushd $DATA
|
||||
pwd
|
||||
tar -vxf wikipedia_en_ne_si_test_sets.tgz
|
||||
popd
|
||||
|
||||
|
||||
for lang in ne_NP si_LK; do
|
||||
datadir=$DATA/${lang}-en_XX-flores
|
||||
rm -rf $datadir
|
||||
mkdir -p $datadir
|
||||
TEST_PREFIX=$DATA/wikipedia_en_ne_si_test_sets/wikipedia.test
|
||||
python $SPM_ENCODE \
|
||||
--model ${SPM_MODEL} \
|
||||
--output_format=piece \
|
||||
--inputs ${TEST_PREFIX}.${lang:0:2}-en.${lang:0:2} ${TEST_PREFIX}.${lang:0:2}-en.en \
|
||||
--outputs $datadir/test.bpe.${lang}-en_XX.${lang} $datadir/test.bpe.${lang}-en_XX.en_XX
|
||||
|
||||
# binarize data
|
||||
fairseq-preprocess \
|
||||
--source-lang ${lang} --target-lang en_XX \
|
||||
--testpref $datadir/test.bpe.${lang}-en_XX \
|
||||
--destdir $datadir \
|
||||
--srcdict ${DICT} \
|
||||
--joined-dictionary \
|
||||
--workers 4
|
||||
done
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
SPM_ENCODE=flores/scripts/spm_encode.py
|
||||
DATA=data_tmp
|
||||
SPM_MODEL=criss_checkpoints/sentence.bpe.model
|
||||
DICT=criss_checkpoints/dict.txt
|
||||
|
||||
if [[ -f flores ]]; then
|
||||
echo "flores already cloned"
|
||||
else
|
||||
git clone https://github.com/facebookresearch/flores
|
||||
fi
|
||||
if [[ -f LASER ]]; then
|
||||
echo "LASER already cloned"
|
||||
else
|
||||
git clone https://github.com/facebookresearch/LASER
|
||||
fi
|
||||
mkdir -p data_tmp
|
||||
declare -A lang_tatoeba_map=( ["ar_AR"]="ara" ["de_DE"]="deu" ["es_XX"]="spa" ["et_EE"]="est" ["fi_FI"]="fin" ["fr_XX"]="fra" ["hi_IN"]="hin" ["it_IT"]="ita" ["ja_XX"]="jpn" ["ko_KR"]="kor" ["kk_KZ"]="kaz" ["nl_XX"]="nld" ["ru_RU"]="rus" ["tr_TR"]="tur" ["vi_VN"]="vie" ["zh_CN"]="cmn")
|
||||
for lang in ar_AR de_DE es_XX et_EE fi_FI fr_XX hi_IN it_IT ja_XX kk_KZ ko_KR nl_XX ru_RU tr_TR vi_VN zh_CN; do
|
||||
lang_tatoeba=${lang_tatoeba_map[$lang]}
|
||||
echo $lang_tatoeba
|
||||
datadir=$DATA/${lang}-en_XX-tatoeba
|
||||
rm -rf $datadir
|
||||
mkdir -p $datadir
|
||||
TEST_PREFIX=LASER/data/tatoeba/v1/tatoeba
|
||||
python $SPM_ENCODE \
|
||||
--model ${SPM_MODEL} \
|
||||
--output_format=piece \
|
||||
--inputs ${TEST_PREFIX}.${lang_tatoeba}-eng.${lang_tatoeba} ${TEST_PREFIX}.${lang_tatoeba}-eng.eng \
|
||||
--outputs $datadir/test.bpe.${lang}-en_XX.${lang} $datadir/test.bpe.${lang}-en_XX.en_XX
|
||||
|
||||
# binarize data
|
||||
fairseq-preprocess \
|
||||
--source-lang ${lang} --target-lang en_XX \
|
||||
--testpref $datadir/test.bpe.${lang}-en_XX \
|
||||
--destdir $datadir \
|
||||
--srcdict ${DICT} \
|
||||
--joined-dictionary \
|
||||
--workers 4
|
||||
done
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3 -u
|
||||
# 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 argparse
|
||||
import glob
|
||||
from subprocess import check_call
|
||||
|
||||
try:
|
||||
import faiss
|
||||
|
||||
has_faiss = True
|
||||
except ImportError:
|
||||
has_faiss = False
|
||||
import numpy as np
|
||||
|
||||
|
||||
GB = 1024 * 1024 * 1024
|
||||
|
||||
|
||||
def call(cmd):
|
||||
print(cmd)
|
||||
check_call(cmd, shell=True)
|
||||
|
||||
|
||||
def get_batches(directory, lang, prefix="all_avg_pool"):
|
||||
print(f"Finding in {directory}/{prefix}.{lang}*")
|
||||
files = glob.glob(f"{directory}/{prefix}.{lang}*")
|
||||
emb_files = []
|
||||
txt_files = []
|
||||
for emb_fi in files:
|
||||
emb_files.append(emb_fi)
|
||||
txt_fi = emb_fi.replace(prefix, "sentences")
|
||||
txt_files.append(txt_fi)
|
||||
return emb_files, txt_files
|
||||
|
||||
|
||||
def load_batch(emb_file, dim):
|
||||
embeddings = np.fromfile(emb_file, dtype=np.float32)
|
||||
num_rows = int(embeddings.shape[0] / dim)
|
||||
embeddings = embeddings.reshape((num_rows, dim))
|
||||
faiss.normalize_L2(embeddings)
|
||||
return embeddings
|
||||
|
||||
|
||||
def knnGPU_sharded(x_batches_f, y_batches_f, dim, k, direction="x2y"):
|
||||
if not has_faiss:
|
||||
raise ImportError("Please install Faiss")
|
||||
sims = []
|
||||
inds = []
|
||||
xfrom = 0
|
||||
xto = 0
|
||||
for x_batch_f in x_batches_f:
|
||||
yfrom = 0
|
||||
yto = 0
|
||||
x_batch = load_batch(x_batch_f, dim)
|
||||
xto = xfrom + x_batch.shape[0]
|
||||
bsims, binds = [], []
|
||||
for y_batch_f in y_batches_f:
|
||||
y_batch = load_batch(y_batch_f, dim)
|
||||
neighbor_size = min(k, y_batch.shape[0])
|
||||
yto = yfrom + y_batch.shape[0]
|
||||
print("{}-{} -> {}-{}".format(xfrom, xto, yfrom, yto))
|
||||
idx = faiss.IndexFlatIP(dim)
|
||||
idx = faiss.index_cpu_to_all_gpus(idx)
|
||||
idx.add(y_batch)
|
||||
bsim, bind = idx.search(x_batch, neighbor_size)
|
||||
|
||||
bsims.append(bsim)
|
||||
binds.append(bind + yfrom)
|
||||
yfrom += y_batch.shape[0]
|
||||
del idx
|
||||
del y_batch
|
||||
bsims = np.concatenate(bsims, axis=1)
|
||||
binds = np.concatenate(binds, axis=1)
|
||||
aux = np.argsort(-bsims, axis=1)
|
||||
sim_batch = np.zeros((x_batch.shape[0], k), dtype=np.float32)
|
||||
ind_batch = np.zeros((x_batch.shape[0], k), dtype=np.int64)
|
||||
for i in range(x_batch.shape[0]):
|
||||
for j in range(k):
|
||||
sim_batch[i, j] = bsims[i, aux[i, j]]
|
||||
ind_batch[i, j] = binds[i, aux[i, j]]
|
||||
sims.append(sim_batch)
|
||||
inds.append(ind_batch)
|
||||
xfrom += x_batch.shape[0]
|
||||
del x_batch
|
||||
sim = np.concatenate(sims, axis=0)
|
||||
ind = np.concatenate(inds, axis=0)
|
||||
return sim, ind
|
||||
|
||||
|
||||
def score(sim, fwd_mean, bwd_mean, margin):
|
||||
return margin(sim, (fwd_mean + bwd_mean) / 2)
|
||||
|
||||
|
||||
def score_candidates(
|
||||
sim_mat, candidate_inds, fwd_mean, bwd_mean, margin, verbose=False
|
||||
):
|
||||
print(" - scoring {:d} candidates".format(sim_mat.shape[0]))
|
||||
scores = np.zeros(candidate_inds.shape)
|
||||
for i in range(scores.shape[0]):
|
||||
for j in range(scores.shape[1]):
|
||||
k = int(candidate_inds[i, j])
|
||||
scores[i, j] = score(sim_mat[i, j], fwd_mean[i], bwd_mean[k], margin)
|
||||
return scores
|
||||
|
||||
|
||||
def load_text(files):
|
||||
all_sentences = []
|
||||
for fi in files:
|
||||
with open(fi) as sentence_fi:
|
||||
for line in sentence_fi:
|
||||
all_sentences.append(line.strip())
|
||||
print(f"Read {len(all_sentences)} sentences")
|
||||
return all_sentences
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Mine bitext")
|
||||
parser.add_argument("--src-lang", help="Source language")
|
||||
parser.add_argument("--tgt-lang", help="Target language")
|
||||
parser.add_argument(
|
||||
"--dict-path", help="Path to dictionary file", default="dict.txt"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--spm-path", help="Path to SPM model file", default="sentence.bpe.model"
|
||||
)
|
||||
parser.add_argument("--dim", type=int, default=1024, help="Embedding dimension")
|
||||
parser.add_argument("--mem", type=int, default=5, help="Memory in GB")
|
||||
parser.add_argument("--src-dir", help="Source directory")
|
||||
parser.add_argument("--tgt-dir", help="Target directory")
|
||||
parser.add_argument("--output", help="Output path")
|
||||
parser.add_argument(
|
||||
"--neighborhood", type=int, default=4, help="Embedding dimension"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--threshold", type=float, default=1.06, help="Threshold on mined bitext"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--valid-size",
|
||||
type=int,
|
||||
default=2000,
|
||||
help="Number of sentences used for validation set",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-count",
|
||||
type=int,
|
||||
default=50000,
|
||||
help="Min num sentences used for each language",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
x_batches_f, x_sents_f = get_batches(args.src_dir, args.src_lang)
|
||||
y_batches_f, y_sents_f = get_batches(args.tgt_dir, args.tgt_lang)
|
||||
margin = lambda a, b: a / b
|
||||
y2x_sim, y2x_ind = knnGPU_sharded(
|
||||
y_batches_f, x_batches_f, args.dim, args.neighborhood, direction="y2x"
|
||||
)
|
||||
x2y_sim, x2y_ind = knnGPU_sharded(
|
||||
x_batches_f, y_batches_f, args.dim, args.neighborhood, direction="x2y"
|
||||
)
|
||||
|
||||
x2y_mean = x2y_sim.mean(axis=1)
|
||||
y2x_mean = y2x_sim.mean(axis=1)
|
||||
fwd_scores = score_candidates(x2y_sim, x2y_ind, x2y_mean, y2x_mean, margin)
|
||||
bwd_scores = score_candidates(y2x_sim, y2x_ind, y2x_mean, x2y_mean, margin)
|
||||
fwd_best = x2y_ind[np.arange(x2y_sim.shape[0]), fwd_scores.argmax(axis=1)]
|
||||
bwd_best = y2x_ind[np.arange(y2x_sim.shape[0]), bwd_scores.argmax(axis=1)]
|
||||
indices = np.stack(
|
||||
(
|
||||
np.concatenate((np.arange(x2y_ind.shape[0]), bwd_best)),
|
||||
np.concatenate((fwd_best, np.arange(y2x_ind.shape[0]))),
|
||||
),
|
||||
axis=1,
|
||||
)
|
||||
scores = np.concatenate((fwd_scores.max(axis=1), bwd_scores.max(axis=1)))
|
||||
|
||||
x_sentences = load_text(x_sents_f)
|
||||
y_sentences = load_text(y_sents_f)
|
||||
|
||||
threshold = args.threshold
|
||||
min_count = args.min_count
|
||||
seen_src, seen_trg = set(), set()
|
||||
directory = args.output
|
||||
call(f"mkdir -p {directory}")
|
||||
src_out = open(
|
||||
f"{directory}/all.{args.src_lang}",
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
errors="surrogateescape",
|
||||
)
|
||||
tgt_out = open(
|
||||
f"{directory}/all.{args.tgt_lang}",
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
errors="surrogateescape",
|
||||
)
|
||||
scores_out = open(
|
||||
f"{directory}/all.scores", mode="w", encoding="utf-8", errors="surrogateescape"
|
||||
)
|
||||
count = 0
|
||||
for i in np.argsort(-scores):
|
||||
src_ind, trg_ind = indices[i]
|
||||
if src_ind not in seen_src and trg_ind not in seen_trg:
|
||||
seen_src.add(src_ind)
|
||||
seen_trg.add(trg_ind)
|
||||
if scores[i] > threshold or count < min_count:
|
||||
if x_sentences[src_ind]:
|
||||
print(scores[i], file=scores_out)
|
||||
print(x_sentences[src_ind], file=src_out)
|
||||
print(y_sentences[trg_ind], file=tgt_out)
|
||||
count += 1
|
||||
else:
|
||||
print(f"Ignoring sentence: {x_sentences[src_ind]}")
|
||||
src_out.close()
|
||||
tgt_out.close()
|
||||
scores_out.close()
|
||||
|
||||
print(f"Found {count} pairs for threshold={threshold}")
|
||||
with open(f"{directory}/all.{args.src_lang}") as all_s, open(
|
||||
f"{directory}/all.{args.tgt_lang}"
|
||||
) as all_t, open(f"{directory}/valid.{args.src_lang}", "w") as valid_s, open(
|
||||
f"{directory}/valid.{args.tgt_lang}", "w"
|
||||
) as valid_t, open(
|
||||
f"{directory}/train.{args.src_lang}", "w"
|
||||
) as train_s, open(
|
||||
f"{directory}/train.{args.tgt_lang}", "w"
|
||||
) as train_t:
|
||||
count = 0
|
||||
for s_line, t_line in zip(all_s, all_t):
|
||||
s_line = s_line.split("\t")[1]
|
||||
t_line = t_line.split("\t")[1]
|
||||
if count >= args.valid_size:
|
||||
train_s.write(s_line)
|
||||
train_t.write(t_line)
|
||||
else:
|
||||
valid_s.write(s_line)
|
||||
valid_t.write(t_line)
|
||||
count += 1
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
source_lang=kk_KZ
|
||||
target_lang=en_XX
|
||||
MODEL=criss_checkpoints/criss.3rd.pt
|
||||
SPM=criss_checkpoints/sentence.bpe.model
|
||||
SPLIT=test
|
||||
LANG_DICT=criss_checkpoints/lang_dict.txt
|
||||
SPM_ENCODE=flores/scripts/spm_encode.py
|
||||
SAVE_ENCODER=save_encoder.py
|
||||
ENCODER_SAVE_ROOT=sentence_embeddings/$MODEL
|
||||
DICT=criss_checkpoints/dict.txt
|
||||
THRESHOLD=1.02
|
||||
MIN_COUNT=500
|
||||
|
||||
DATA_DIR=data_tmp
|
||||
SAVE_DIR=mining/${source_lang}_${target_lang}_mined
|
||||
ENCODER_SAVE_DIR=${ENCODER_SAVE_ROOT}/${source_lang}-${target_lang}
|
||||
INPUT_DIR=$DATA_DIR/${source_lang}-${target_lang}-tatoeba
|
||||
|
||||
mkdir -p $ENCODER_SAVE_DIR/${target_lang}
|
||||
mkdir -p $ENCODER_SAVE_DIR/${source_lang}
|
||||
mkdir -p $SAVE_DIR
|
||||
|
||||
## Save encoder outputs
|
||||
|
||||
# Save encoder outputs for source sentences
|
||||
python $SAVE_ENCODER \
|
||||
${INPUT_DIR} \
|
||||
--path ${MODEL} \
|
||||
--task translation_multi_simple_epoch \
|
||||
--lang-pairs ${source_lang}-${target_lang} \
|
||||
--lang-dict ${LANG_DICT} \
|
||||
--gen-subset ${SPLIT} \
|
||||
--bpe 'sentencepiece' \
|
||||
-s ${source_lang} -t ${target_lang} \
|
||||
--sentencepiece-model ${SPM} \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--beam 1 \
|
||||
--lang-tok-style mbart \
|
||||
--encoder-save-dir ${ENCODER_SAVE_DIR}/${source_lang}
|
||||
|
||||
## Save encoder outputs for target sentences
|
||||
python $SAVE_ENCODER \
|
||||
${INPUT_DIR} \
|
||||
--path ${MODEL} \
|
||||
--lang-pairs ${source_lang}-${target_lang} \
|
||||
--lang-dict ${LANG_DICT} \
|
||||
--task translation_multi_simple_epoch \
|
||||
--gen-subset ${SPLIT} \
|
||||
--bpe 'sentencepiece' \
|
||||
-t ${source_lang} -s ${target_lang} \
|
||||
--sentencepiece-model ${SPM} \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--beam 1 \
|
||||
--lang-tok-style mbart \
|
||||
--encoder-save-dir ${ENCODER_SAVE_DIR}/${target_lang}
|
||||
|
||||
## Mining
|
||||
python mining/mine.py \
|
||||
--src-lang ${source_lang} \
|
||||
--tgt-lang ${target_lang} \
|
||||
--dim 1024 \
|
||||
--mem 10 \
|
||||
--neighborhood 4 \
|
||||
--src-dir ${ENCODER_SAVE_DIR}/${source_lang} \
|
||||
--tgt-dir ${ENCODER_SAVE_DIR}/${target_lang} \
|
||||
--output $SAVE_DIR \
|
||||
--threshold ${THRESHOLD} \
|
||||
--min-count ${MIN_COUNT} \
|
||||
--valid-size 100 \
|
||||
--dict-path ${DICT} \
|
||||
--spm-path ${SPM} \
|
||||
|
||||
|
||||
## Process and binarize mined data
|
||||
python $SPM_ENCODE \
|
||||
--model ${SPM} \
|
||||
--output_format=piece \
|
||||
--inputs mining/${source_lang}_${target_lang}_mined/train.${source_lang} mining/${source_lang}_${target_lang}_mined/train.${target_lang} \
|
||||
--outputs mining/${source_lang}_${target_lang}_mined/train.bpe.${source_lang} mining/${source_lang}_${target_lang}_mined/train.bpe.${target_lang}
|
||||
|
||||
python $SPM_ENCODE \
|
||||
--model ${SPM} \
|
||||
--output_format=piece \
|
||||
--inputs mining/${source_lang}_${target_lang}_mined/valid.${source_lang} mining/${source_lang}_${target_lang}_mined/valid.${target_lang} \
|
||||
--outputs mining/${source_lang}_${target_lang}_mined/valid.bpe.${source_lang} mining/${source_lang}_${target_lang}_mined/valid.bpe.${target_lang}
|
||||
|
||||
|
||||
fairseq-preprocess \
|
||||
--source-lang ${source_lang} \
|
||||
--target-lang ${target_lang} \
|
||||
--trainpref mining/${source_lang}_${target_lang}_mined/train.bpe \
|
||||
--validpref mining/${source_lang}_${target_lang}_mined/valid.bpe \
|
||||
--destdir mining/${source_lang}_${target_lang}_mined \
|
||||
--srcdict ${DICT} \
|
||||
--joined-dictionary \
|
||||
--workers 8
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3 -u
|
||||
# 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.
|
||||
"""
|
||||
Translate pre-processed data with a trained model.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq import checkpoint_utils, options, progress_bar, tasks, utils
|
||||
from fairseq.sequence_generator import EnsembleModel
|
||||
|
||||
|
||||
def get_avg_pool(
|
||||
models, sample, prefix_tokens, src_dict, remove_bpe, has_langtok=False
|
||||
):
|
||||
model = EnsembleModel(models)
|
||||
|
||||
# model.forward normally channels prev_output_tokens into the decoder
|
||||
# separately, but SequenceGenerator directly calls model.encoder
|
||||
encoder_input = {
|
||||
k: v for k, v in sample["net_input"].items() if k != "prev_output_tokens"
|
||||
}
|
||||
|
||||
# compute the encoder output for each beam
|
||||
encoder_outs = model.forward_encoder(encoder_input)
|
||||
np_encoder_outs = encoder_outs[0].encoder_out.cpu().numpy().astype(np.float32)
|
||||
encoder_mask = 1 - encoder_outs[0].encoder_padding_mask.cpu().numpy().astype(
|
||||
np.float32
|
||||
)
|
||||
encoder_mask = np.expand_dims(encoder_mask.T, axis=2)
|
||||
if has_langtok:
|
||||
encoder_mask = encoder_mask[1:, :, :]
|
||||
np_encoder_outs = np_encoder_outs[1, :, :]
|
||||
masked_encoder_outs = encoder_mask * np_encoder_outs
|
||||
avg_pool = (masked_encoder_outs / encoder_mask.sum(axis=0)).sum(axis=0)
|
||||
return avg_pool
|
||||
|
||||
|
||||
def main(args):
|
||||
assert args.path is not None, "--path required for generation!"
|
||||
assert (
|
||||
not args.sampling or args.nbest == args.beam
|
||||
), "--sampling requires --nbest to be equal to --beam"
|
||||
assert (
|
||||
args.replace_unk is None or args.raw_text
|
||||
), "--replace-unk requires a raw text dataset (--raw-text)"
|
||||
|
||||
args.beam = 1
|
||||
utils.import_user_module(args)
|
||||
|
||||
if args.max_tokens is None:
|
||||
args.max_tokens = 12000
|
||||
print(args)
|
||||
use_cuda = torch.cuda.is_available() and not args.cpu
|
||||
|
||||
# Load dataset splits
|
||||
task = tasks.setup_task(args)
|
||||
task.load_dataset(args.gen_subset)
|
||||
|
||||
# Set dictionaries
|
||||
try:
|
||||
src_dict = getattr(task, "source_dictionary", None)
|
||||
except NotImplementedError:
|
||||
src_dict = None
|
||||
tgt_dict = task.target_dictionary
|
||||
|
||||
# Load ensemble
|
||||
print("| loading model(s) from {}".format(args.path))
|
||||
models, _model_args = checkpoint_utils.load_model_ensemble(
|
||||
args.path.split(":"),
|
||||
arg_overrides=eval(args.model_overrides),
|
||||
task=task,
|
||||
)
|
||||
|
||||
# Optimize ensemble for generation
|
||||
for model in models:
|
||||
model.make_generation_fast_(
|
||||
beamable_mm_beam_size=None if args.no_beamable_mm else args.beam,
|
||||
need_attn=args.print_alignment,
|
||||
)
|
||||
if args.fp16:
|
||||
model.half()
|
||||
if use_cuda:
|
||||
model.cuda()
|
||||
|
||||
# Load alignment dictionary for unknown word replacement
|
||||
# (None if no unknown word replacement, empty if no path to align dictionary)
|
||||
align_dict = utils.load_align_dict(args.replace_unk)
|
||||
|
||||
# Load dataset (possibly sharded)
|
||||
itr = task.get_batch_iterator(
|
||||
dataset=task.dataset(args.gen_subset),
|
||||
max_tokens=args.max_tokens,
|
||||
max_positions=utils.resolve_max_positions(
|
||||
task.max_positions(),
|
||||
),
|
||||
ignore_invalid_inputs=args.skip_invalid_size_inputs_valid_test,
|
||||
required_batch_size_multiple=args.required_batch_size_multiple,
|
||||
num_shards=args.num_shards,
|
||||
shard_id=args.shard_id,
|
||||
num_workers=args.num_workers,
|
||||
).next_epoch_itr(shuffle=False)
|
||||
|
||||
num_sentences = 0
|
||||
source_sentences = []
|
||||
shard_id = 0
|
||||
all_avg_pool = None
|
||||
encoder_has_langtok = (
|
||||
hasattr(task.args, "encoder_langtok")
|
||||
and task.args.encoder_langtok is not None
|
||||
and hasattr(task.args, "lang_tok_replacing_bos_eos")
|
||||
and not task.args.lang_tok_replacing_bos_eos
|
||||
)
|
||||
with progress_bar.build_progress_bar(args, itr) as t:
|
||||
for sample in t:
|
||||
if sample is None:
|
||||
print("Skipping None")
|
||||
continue
|
||||
sample = utils.move_to_cuda(sample) if use_cuda else sample
|
||||
if "net_input" not in sample:
|
||||
continue
|
||||
|
||||
prefix_tokens = None
|
||||
if args.prefix_size > 0:
|
||||
prefix_tokens = sample["target"][:, : args.prefix_size]
|
||||
|
||||
with torch.no_grad():
|
||||
avg_pool = get_avg_pool(
|
||||
models,
|
||||
sample,
|
||||
prefix_tokens,
|
||||
src_dict,
|
||||
args.post_process,
|
||||
has_langtok=encoder_has_langtok,
|
||||
)
|
||||
if all_avg_pool is not None:
|
||||
all_avg_pool = np.concatenate((all_avg_pool, avg_pool))
|
||||
else:
|
||||
all_avg_pool = avg_pool
|
||||
|
||||
if not isinstance(sample["id"], list):
|
||||
sample_ids = sample["id"].tolist()
|
||||
else:
|
||||
sample_ids = sample["id"]
|
||||
for i, sample_id in enumerate(sample_ids):
|
||||
# Remove padding
|
||||
src_tokens = utils.strip_pad(
|
||||
sample["net_input"]["src_tokens"][i, :], tgt_dict.pad()
|
||||
)
|
||||
|
||||
# Either retrieve the original sentences or regenerate them from tokens.
|
||||
if align_dict is not None:
|
||||
src_str = task.dataset(args.gen_subset).src.get_original_text(
|
||||
sample_id
|
||||
)
|
||||
else:
|
||||
if src_dict is not None:
|
||||
src_str = src_dict.string(src_tokens, args.post_process)
|
||||
else:
|
||||
src_str = ""
|
||||
|
||||
if not args.quiet:
|
||||
if src_dict is not None:
|
||||
print("S-{}\t{}".format(sample_id, src_str))
|
||||
|
||||
source_sentences.append(f"{sample_id}\t{src_str}")
|
||||
|
||||
num_sentences += sample["nsentences"]
|
||||
if all_avg_pool.shape[0] >= 1000000:
|
||||
with open(
|
||||
f"{args.encoder_save_dir}/all_avg_pool.{args.source_lang}.{shard_id}",
|
||||
"w",
|
||||
) as avg_pool_file:
|
||||
all_avg_pool.tofile(avg_pool_file)
|
||||
with open(
|
||||
f"{args.encoder_save_dir}/sentences.{args.source_lang}.{shard_id}",
|
||||
"w",
|
||||
) as sentence_file:
|
||||
sentence_file.writelines(f"{line}\n" for line in source_sentences)
|
||||
all_avg_pool = None
|
||||
source_sentences = []
|
||||
shard_id += 1
|
||||
|
||||
if all_avg_pool is not None:
|
||||
with open(
|
||||
f"{args.encoder_save_dir}/all_avg_pool.{args.source_lang}.{shard_id}", "w"
|
||||
) as avg_pool_file:
|
||||
all_avg_pool.tofile(avg_pool_file)
|
||||
with open(
|
||||
f"{args.encoder_save_dir}/sentences.{args.source_lang}.{shard_id}", "w"
|
||||
) as sentence_file:
|
||||
sentence_file.writelines(f"{line}\n" for line in source_sentences)
|
||||
return None
|
||||
|
||||
|
||||
def cli_main():
|
||||
parser = options.get_generation_parser()
|
||||
parser.add_argument(
|
||||
"--encoder-save-dir",
|
||||
default="",
|
||||
type=str,
|
||||
metavar="N",
|
||||
help="directory to save encoder outputs",
|
||||
)
|
||||
args = options.parse_args_and_arch(parser)
|
||||
main(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli_main()
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3 -u
|
||||
# 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 argparse
|
||||
import glob
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
DIM = 1024
|
||||
|
||||
|
||||
def compute_dist(source_embs, target_embs, k=5, return_sim_mat=False):
|
||||
target_ids = [tid for tid in target_embs]
|
||||
source_mat = np.stack(source_embs.values(), axis=0)
|
||||
normalized_source_mat = source_mat / np.linalg.norm(
|
||||
source_mat, axis=1, keepdims=True
|
||||
)
|
||||
target_mat = np.stack(target_embs.values(), axis=0)
|
||||
normalized_target_mat = target_mat / np.linalg.norm(
|
||||
target_mat, axis=1, keepdims=True
|
||||
)
|
||||
sim_mat = normalized_source_mat.dot(normalized_target_mat.T)
|
||||
if return_sim_mat:
|
||||
return sim_mat
|
||||
neighbors_map = {}
|
||||
for i, sentence_id in enumerate(source_embs):
|
||||
idx = np.argsort(sim_mat[i, :])[::-1][:k]
|
||||
neighbors_map[sentence_id] = [target_ids[tid] for tid in idx]
|
||||
return neighbors_map
|
||||
|
||||
|
||||
def load_embeddings(directory, LANGS):
|
||||
sentence_embeddings = {}
|
||||
sentence_texts = {}
|
||||
for lang in LANGS:
|
||||
sentence_embeddings[lang] = {}
|
||||
sentence_texts[lang] = {}
|
||||
lang_dir = f"{directory}/{lang}"
|
||||
embedding_files = glob.glob(f"{lang_dir}/all_avg_pool.{lang}.*")
|
||||
for embed_file in embedding_files:
|
||||
shard_id = embed_file.split(".")[-1]
|
||||
embeddings = np.fromfile(embed_file, dtype=np.float32)
|
||||
num_rows = embeddings.shape[0] // DIM
|
||||
embeddings = embeddings.reshape((num_rows, DIM))
|
||||
|
||||
with open(f"{lang_dir}/sentences.{lang}.{shard_id}") as sentence_file:
|
||||
for idx, line in enumerate(sentence_file):
|
||||
sentence_id, sentence = line.strip().split("\t")
|
||||
sentence_texts[lang][sentence_id] = sentence
|
||||
sentence_embeddings[lang][sentence_id] = embeddings[idx, :]
|
||||
|
||||
return sentence_embeddings, sentence_texts
|
||||
|
||||
|
||||
def compute_accuracy(directory, LANGS):
|
||||
sentence_embeddings, sentence_texts = load_embeddings(directory, LANGS)
|
||||
|
||||
top_1_accuracy = {}
|
||||
|
||||
top1_str = " ".join(LANGS) + "\n"
|
||||
for source_lang in LANGS:
|
||||
top_1_accuracy[source_lang] = {}
|
||||
top1_str += f"{source_lang} "
|
||||
for target_lang in LANGS:
|
||||
top1 = 0
|
||||
top5 = 0
|
||||
neighbors_map = compute_dist(
|
||||
sentence_embeddings[source_lang], sentence_embeddings[target_lang]
|
||||
)
|
||||
for sentence_id, neighbors in neighbors_map.items():
|
||||
if sentence_id == neighbors[0]:
|
||||
top1 += 1
|
||||
if sentence_id in neighbors[:5]:
|
||||
top5 += 1
|
||||
n = len(sentence_embeddings[target_lang])
|
||||
top1_str += f"{top1/n} "
|
||||
top1_str += "\n"
|
||||
|
||||
print(top1_str)
|
||||
print(top1_str, file=open(f"{directory}/accuracy", "w"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Analyze encoder outputs")
|
||||
parser.add_argument("directory", help="Source language corpus")
|
||||
parser.add_argument("--langs", help="List of langs")
|
||||
args = parser.parse_args()
|
||||
langs = args.langs.split(",")
|
||||
compute_accuracy(args.directory, langs)
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
source_lang=kk_KZ
|
||||
target_lang=en_XX
|
||||
MODEL=criss_checkpoints/criss.3rd.pt
|
||||
SPM=criss_checkpoints/sentence.bpe.model
|
||||
SPLIT=test
|
||||
LANG_DICT=criss_checkpoints/lang_dict.txt
|
||||
ENCODER_ANALYSIS=sentence_retrieval/encoder_analysis.py
|
||||
SAVE_ENCODER=save_encoder.py
|
||||
ENCODER_SAVE_ROOT=sentence_embeddings/$MODEL
|
||||
|
||||
|
||||
|
||||
DATA_DIR=data_tmp
|
||||
INPUT_DIR=$DATA_DIR/${source_lang}-${target_lang}-tatoeba
|
||||
ENCODER_SAVE_DIR=${ENCODER_SAVE_ROOT}/${source_lang}-${target_lang}
|
||||
mkdir -p $ENCODER_SAVE_DIR/${target_lang}
|
||||
mkdir -p $ENCODER_SAVE_DIR/${source_lang}
|
||||
|
||||
# Save encoder outputs for source sentences
|
||||
python $SAVE_ENCODER \
|
||||
${INPUT_DIR} \
|
||||
--path ${MODEL} \
|
||||
--task translation_multi_simple_epoch \
|
||||
--lang-dict ${LANG_DICT} \
|
||||
--gen-subset ${SPLIT} \
|
||||
--bpe 'sentencepiece' \
|
||||
--lang-pairs ${source_lang}-${target_lang} \
|
||||
-s ${source_lang} -t ${target_lang} \
|
||||
--sentencepiece-model ${SPM} \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--beam 1 \
|
||||
--lang-tok-style mbart \
|
||||
--encoder-save-dir ${ENCODER_SAVE_DIR}/${source_lang}
|
||||
|
||||
# Save encoder outputs for target sentences
|
||||
python $SAVE_ENCODER \
|
||||
${INPUT_DIR} \
|
||||
--path ${MODEL} \
|
||||
--lang-dict ${LANG_DICT} \
|
||||
--task translation_multi_simple_epoch \
|
||||
--gen-subset ${SPLIT} \
|
||||
--bpe 'sentencepiece' \
|
||||
--lang-pairs ${target_lang}-${source_lang} \
|
||||
-t ${source_lang} -s ${target_lang} \
|
||||
--sentencepiece-model ${SPM} \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--beam 1 \
|
||||
--lang-tok-style mbart \
|
||||
--encoder-save-dir ${ENCODER_SAVE_DIR}/${target_lang}
|
||||
|
||||
# Analyze sentence retrieval accuracy
|
||||
python $ENCODER_ANALYSIS --langs "${source_lang},${target_lang}" ${ENCODER_SAVE_DIR}
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
SRC=si_LK
|
||||
TGT=en_XX
|
||||
MODEL=criss_checkpoints/criss.3rd.pt
|
||||
|
||||
MULTIBLEU=mosesdecoder/scripts/generic/multi-bleu.perl
|
||||
MOSES=mosesdecoder
|
||||
REPLACE_UNICODE_PUNCT=$MOSES/scripts/tokenizer/replace-unicode-punctuation.perl
|
||||
NORM_PUNC=$MOSES/scripts/tokenizer/normalize-punctuation.perl
|
||||
REM_NON_PRINT_CHAR=$MOSES/scripts/tokenizer/remove-non-printing-char.perl
|
||||
TOKENIZER=$MOSES/scripts/tokenizer/tokenizer.perl
|
||||
GEN_TMP_DIR=gen_tmp
|
||||
LANG_DICT=criss_checkpoints/lang_dict.txt
|
||||
|
||||
if [ ! -d "mosesdecoder" ]; then
|
||||
git clone https://github.com/moses-smt/mosesdecoder
|
||||
fi
|
||||
mkdir -p $GEN_TMP_DIR
|
||||
fairseq-generate data_tmp/${SRC}-${TGT}-flores \
|
||||
--task translation_multi_simple_epoch \
|
||||
--max-tokens 2000 \
|
||||
--path ${MODEL} \
|
||||
--skip-invalid-size-inputs-valid-test \
|
||||
--beam 5 --lenpen 1.0 --gen-subset test \
|
||||
--remove-bpe=sentencepiece \
|
||||
--source-lang ${SRC} --target-lang ${TGT} \
|
||||
--decoder-langtok --lang-pairs 'en_XX-ar_AR,en_XX-de_DE,en_XX-es_XX,en_XX-fr_XX,en_XX-hi_IN,en_XX-it_IT,en_XX-ja_XX,en_XX-ko_KR,en_XX-nl_XX,en_XX-ru_RU,en_XX-zh_CN,en_XX-tr_TR,en_XX-vi_VN,en_XX-ro_RO,en_XX-my_MM,en_XX-ne_NP,en_XX-si_LK,en_XX-cs_CZ,en_XX-lt_LT,en_XX-kk_KZ,en_XX-gu_IN,en_XX-fi_FI,en_XX-et_EE,en_XX-lv_LV,ar_AR-en_XX,cs_CZ-en_XX,de_DE-en_XX,es_XX-en_XX,et_EE-en_XX,fi_FI-en_XX,fr_XX-en_XX,gu_IN-en_XX,hi_IN-en_XX,it_IT-en_XX,ja_XX-en_XX,kk_KZ-en_XX,ko_KR-en_XX,lt_LT-en_XX,lv_LV-en_XX,my_MM-en_XX,ne_NP-en_XX,nl_XX-en_XX,ro_RO-en_XX,ru_RU-en_XX,si_LK-en_XX,tr_TR-en_XX,vi_VN-en_XX,zh_CN-en_XX,ar_AR-es_XX,es_XX-ar_AR,ar_AR-hi_IN,hi_IN-ar_AR,ar_AR-zh_CN,zh_CN-ar_AR,cs_CZ-es_XX,es_XX-cs_CZ,cs_CZ-hi_IN,hi_IN-cs_CZ,cs_CZ-zh_CN,zh_CN-cs_CZ,de_DE-es_XX,es_XX-de_DE,de_DE-hi_IN,hi_IN-de_DE,de_DE-zh_CN,zh_CN-de_DE,es_XX-hi_IN,hi_IN-es_XX,es_XX-zh_CN,zh_CN-es_XX,et_EE-es_XX,es_XX-et_EE,et_EE-hi_IN,hi_IN-et_EE,et_EE-zh_CN,zh_CN-et_EE,fi_FI-es_XX,es_XX-fi_FI,fi_FI-hi_IN,hi_IN-fi_FI,fi_FI-zh_CN,zh_CN-fi_FI,fr_XX-es_XX,es_XX-fr_XX,fr_XX-hi_IN,hi_IN-fr_XX,fr_XX-zh_CN,zh_CN-fr_XX,gu_IN-es_XX,es_XX-gu_IN,gu_IN-hi_IN,hi_IN-gu_IN,gu_IN-zh_CN,zh_CN-gu_IN,hi_IN-zh_CN,zh_CN-hi_IN,it_IT-es_XX,es_XX-it_IT,it_IT-hi_IN,hi_IN-it_IT,it_IT-zh_CN,zh_CN-it_IT,ja_XX-es_XX,es_XX-ja_XX,ja_XX-hi_IN,hi_IN-ja_XX,ja_XX-zh_CN,zh_CN-ja_XX,kk_KZ-es_XX,es_XX-kk_KZ,kk_KZ-hi_IN,hi_IN-kk_KZ,kk_KZ-zh_CN,zh_CN-kk_KZ,ko_KR-es_XX,es_XX-ko_KR,ko_KR-hi_IN,hi_IN-ko_KR,ko_KR-zh_CN,zh_CN-ko_KR,lt_LT-es_XX,es_XX-lt_LT,lt_LT-hi_IN,hi_IN-lt_LT,lt_LT-zh_CN,zh_CN-lt_LT,lv_LV-es_XX,es_XX-lv_LV,lv_LV-hi_IN,hi_IN-lv_LV,lv_LV-zh_CN,zh_CN-lv_LV,my_MM-es_XX,es_XX-my_MM,my_MM-hi_IN,hi_IN-my_MM,my_MM-zh_CN,zh_CN-my_MM,ne_NP-es_XX,es_XX-ne_NP,ne_NP-hi_IN,hi_IN-ne_NP,ne_NP-zh_CN,zh_CN-ne_NP,nl_XX-es_XX,es_XX-nl_XX,nl_XX-hi_IN,hi_IN-nl_XX,nl_XX-zh_CN,zh_CN-nl_XX,ro_RO-es_XX,es_XX-ro_RO,ro_RO-hi_IN,hi_IN-ro_RO,ro_RO-zh_CN,zh_CN-ro_RO,ru_RU-es_XX,es_XX-ru_RU,ru_RU-hi_IN,hi_IN-ru_RU,ru_RU-zh_CN,zh_CN-ru_RU,si_LK-es_XX,es_XX-si_LK,si_LK-hi_IN,hi_IN-si_LK,si_LK-zh_CN,zh_CN-si_LK,tr_TR-es_XX,es_XX-tr_TR,tr_TR-hi_IN,hi_IN-tr_TR,tr_TR-zh_CN,zh_CN-tr_TR,vi_VN-es_XX,es_XX-vi_VN,vi_VN-hi_IN,hi_IN-vi_VN,vi_VN-zh_CN,zh_CN-vi_VN' \
|
||||
--lang-dict ${LANG_DICT} --lang-tok-style 'mbart' --sampling-method 'temperature' --sampling-temperature '1.0' > $GEN_TMP_DIR/${SRC}_${TGT}.gen
|
||||
cat $GEN_TMP_DIR/${SRC}_${TGT}.gen | grep -P "^T-" | cut -f2 | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l ${TGT:0:2} | $REM_NON_PRINT_CHAR | $TOKENIZER -no-escape ${TGT:0:2} > $GEN_TMP_DIR/${SRC}_${TGT}.hyp
|
||||
cat $GEN_TMP_DIR/${SRC}_${TGT}.gen | grep -P "^H-" | cut -f3 | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l ${TGT:0:2} | $REM_NON_PRINT_CHAR | $TOKENIZER -no-escape ${TGT:0:2} > $GEN_TMP_DIR/${SRC}_${TGT}.ref
|
||||
${MULTIBLEU} $GEN_TMP_DIR/${SRC}_${TGT}.ref < $GEN_TMP_DIR/${SRC}_${TGT}.hyp
|
||||
@@ -0,0 +1,77 @@
|
||||
# Cross-Lingual Language Model Pre-training
|
||||
|
||||
Below are some details for training Cross-Lingual Language Models (XLM) - similar to the ones presented in [Lample & Conneau, 2019](https://arxiv.org/pdf/1901.07291.pdf) - in Fairseq. The current implementation only supports the Masked Language Model (MLM) from the paper above.
|
||||
|
||||
## Downloading and Tokenizing Monolingual Data
|
||||
|
||||
Pointers to the monolingual data from wikipedia, used for training the XLM-style MLM model as well as details on processing (tokenization and BPE) it can be found in the [XLM Github Repository](https://github.com/facebookresearch/XLM#download--preprocess-monolingual-data).
|
||||
|
||||
Let's assume the following for the code snippets in later sections to work
|
||||
- Processed data is in the folder: monolingual_data/processed
|
||||
- Each language has 3 files for train, test and validation. For example we have the following files for English:
|
||||
train.en, valid.en
|
||||
- We are training a model for 5 languages: Arabic (ar), German (de), English (en), Hindi (hi) and French (fr)
|
||||
- The vocabulary file is monolingual_data/processed/vocab_mlm
|
||||
|
||||
|
||||
## Fairseq Pre-processing and Binarization
|
||||
|
||||
Pre-process and binarize the data with the MaskedLMDictionary and cross_lingual_lm task
|
||||
|
||||
```bash
|
||||
# Ensure the output directory exists
|
||||
DATA_DIR=monolingual_data/fairseq_processed
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
for lg in ar de en hi fr
|
||||
do
|
||||
|
||||
fairseq-preprocess \
|
||||
--task cross_lingual_lm \
|
||||
--srcdict monolingual_data/processed/vocab_mlm \
|
||||
--only-source \
|
||||
--trainpref monolingual_data/processed/train \
|
||||
--validpref monolingual_data/processed/valid \
|
||||
--testpref monolingual_data/processed/test \
|
||||
--destdir monolingual_data/fairseq_processed \
|
||||
--workers 20 \
|
||||
--source-lang $lg
|
||||
|
||||
# Since we only have a source language, the output file has a None for the
|
||||
# target language. Remove this
|
||||
|
||||
for stage in train test valid
|
||||
|
||||
sudo mv "$DATA_DIR/$stage.$lg-None.$lg.bin" "$stage.$lg.bin"
|
||||
sudo mv "$DATA_DIR/$stage.$lg-None.$lg.idx" "$stage.$lg.idx"
|
||||
|
||||
done
|
||||
|
||||
done
|
||||
```
|
||||
|
||||
## Train a Cross-lingual Language Model similar to the XLM MLM model
|
||||
|
||||
Use the following command to train the model on 5 languages.
|
||||
|
||||
```
|
||||
fairseq-train \
|
||||
--task cross_lingual_lm monolingual_data/fairseq_processed \
|
||||
--save-dir checkpoints/mlm \
|
||||
--max-update 2400000 --save-interval 1 --no-epoch-checkpoints \
|
||||
--arch xlm_base \
|
||||
--optimizer adam --lr-scheduler reduce_lr_on_plateau \
|
||||
--lr-shrink 0.5 --lr 0.0001 --stop-min-lr 1e-09 \
|
||||
--dropout 0.1 \
|
||||
--criterion legacy_masked_lm_loss \
|
||||
--max-tokens 2048 --tokens-per-sample 256 --attention-dropout 0.1 \
|
||||
--dataset-impl lazy --seed 0 \
|
||||
--masked-lm-only \
|
||||
--monolingual-langs 'ar,de,en,hi,fr' --num-segment 5 \
|
||||
--ddp-backend=no_c10d
|
||||
```
|
||||
|
||||
Some Notes:
|
||||
- Using tokens_per_sample greater than 256 can cause OOM (out-of-memory) issues. Usually since MLM packs in streams of text, this parameter doesn't need much tuning.
|
||||
- The Evaluation workflow for computing MLM Perplexity on test data is in progress.
|
||||
- Finetuning this model on a downstream task is something which is not currently available.
|
||||
@@ -0,0 +1,345 @@
|
||||
# Language Models not just for Pre-training: Fast Online Neural Noisy Channel Modeling
|
||||
|
||||
## Introduction
|
||||
- [Yee et al. (2019)](https://www.aclweb.org/anthology/D19-1571.pdf) introduce a simple and effective noisy channel modeling approach for neural machine translation. However, the noisy channel online decoding approach introduced in this paper is too slow to be practical.
|
||||
- To address this, [Bhosale et al. (2020)](http://www.statmt.org/wmt20/pdf/2020.wmt-1.68.pdf) introduces 3 simple approximations to make this approach very fast and practical without much loss in accuracy.
|
||||
- This README provides intructions on how to run online decoding or generation with the noisy channel modeling approach, including ways to make it very fast without much loss in accuracy.
|
||||
|
||||
## Noisy Channel Modeling
|
||||
|
||||
[Yee et al. (2019)](https://www.aclweb.org/anthology/D19-1571.pdf) applies the Bayes Rule to predict `P(y|x)`, the probability of the target `y` given the source `x`.
|
||||
```P(y|x) = P(x|y) * P(y) / P(x)```
|
||||
- `P(x|y)` predicts the source `x` given the target `y` and is referred to as the **channel model**
|
||||
- `P(y)` is a **language model** over the target `y`
|
||||
- `P(x)` is generally not modeled since it is constant for all `y`.
|
||||
|
||||
We use Transformer models to parameterize the direct model `P(y|x)`, the channel model `P(x|y)` and the language model `P(y)`.
|
||||
|
||||
During online decoding with beam search, we generate the top `K2` candidates per beam and score them with the following linear combination of the channel model, the language model as well as the direct model scores.
|
||||
|
||||
```(1 / t) * log(P(y|x) + (1 / s) * ( λ1 * log(P(x|y)) + λ2 * log(P(y) ) )```
|
||||
- `t` - Target Prefix Length
|
||||
- `s` - Source Length
|
||||
- `λ1` - Channel Model Weight
|
||||
- `λ2` - Language Model Weight
|
||||
|
||||
The top `beam_size` candidates based on the above combined scores are chosen to continue the beams in beam search. In beam search with a direct model alone, the scores from the direct model `P(y|x)` are used to choose the top candidates in beam search.
|
||||
|
||||
This framework provides a great way to utlize strong target language models trained on large amounts of unlabeled data. Language models can prefer targets unrelated to the source, so we also need a channel model whose role is to ensure that the target preferred by the language model also translates back to the source.
|
||||
|
||||
### Training Translation Models and Language Models
|
||||
|
||||
For training Transformer models in fairseq for machine translation, refer to instructions [here](https://github.com/pytorch/fairseq/tree/master/examples/translation)
|
||||
|
||||
For training Transformer models in fairseq for language modeling, refer to instructions [here](https://github.com/pytorch/fairseq/tree/master/examples/language_model)
|
||||
|
||||
### Generation with Language Model for German-English translation with fairseq
|
||||
|
||||
Here are instructions to generate using a direct model and a target-side language model.
|
||||
|
||||
Note:
|
||||
- Download and install fairseq as per instructions [here](https://github.com/pytorch/fairseq)
|
||||
- Preprocess and binarize the dataset as per instructions in section [Test Data Preprocessing](#test-data-preprocessing)
|
||||
|
||||
```sh
|
||||
binarized_data=data_dir/binarized
|
||||
direct_model=de_en_seed4.pt
|
||||
lm_model=en_lm.pt
|
||||
lm_data=lm_data
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed4.pt -O ${direct_model}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/transformer_lm.pt -O ${lm_model}
|
||||
mkdir -p ${lm_data}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/lm_dict/dict.txt -O ${lm_data}/dict.txt
|
||||
|
||||
k2=10
|
||||
lenpen=0.16
|
||||
lm_wt=0.14
|
||||
fairseq-generate ${binarized_data} \
|
||||
--user-dir examples/fast_noisy_channel \
|
||||
--beam 5 \
|
||||
--path ${direct_model} \
|
||||
--lm-model ${lm_model} \
|
||||
--lm-data ${lm_data} \
|
||||
--k2 ${k2} \
|
||||
--combine-method lm_only \
|
||||
--task noisy_channel_translation \
|
||||
--lenpen ${lenpen} \
|
||||
--lm-wt ${lm_wt} \
|
||||
--gen-subset valid \
|
||||
--remove-bpe \
|
||||
--fp16 \
|
||||
--batch-size 10
|
||||
```
|
||||
### Noisy Channel Generation for German-English translation with fairseq
|
||||
|
||||
Here are instructions for noisy channel generation with a direct model, channel model and language model as explained in section [Noisy Channel Modeling](#noisy-channel-modeling).
|
||||
|
||||
Note:
|
||||
- Download and install fairseq as per instructions [here](https://github.com/pytorch/fairseq)
|
||||
- Preprocess and binarize the dataset as per instructions in section [Test Data Preprocessing](#test-data-preprocessing)
|
||||
|
||||
```sh
|
||||
binarized_data=data_dir/binarized
|
||||
direct_model=de_en_seed4.pt
|
||||
lm_model=en_lm.pt
|
||||
lm_data=lm_data
|
||||
ch_model=en_de.big.seed4.pt
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed4.pt -O ${direct_model}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/transformer_lm.pt -O ${lm_model}
|
||||
mkdir -p ${lm_data}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/lm_dict/dict.txt -O ${lm_data}/dict.txt
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big.seed4.pt -O ${ch_model}
|
||||
|
||||
k2=10
|
||||
lenpen=0.21
|
||||
lm_wt=0.50
|
||||
bw_wt=0.30
|
||||
fairseq-generate ${binarized_data} \
|
||||
--user-dir examples/fast_noisy_channel \
|
||||
--beam 5 \
|
||||
--path ${direct_model} \
|
||||
--lm-model ${lm_model} \
|
||||
--lm-data ${lm_data} \
|
||||
--channel-model ${ch_model} \
|
||||
--k2 ${k2} \
|
||||
--combine-method noisy_channel \
|
||||
--task noisy_channel_translation \
|
||||
--lenpen ${lenpen} \
|
||||
--lm-wt ${lm_wt} \
|
||||
--ch-wt ${bw_wt} \
|
||||
--gen-subset test \
|
||||
--remove-bpe \
|
||||
--fp16 \
|
||||
--batch-size 1
|
||||
```
|
||||
## Fast Noisy Channel Modeling
|
||||
|
||||
[Bhosale et al. (2020)](http://www.statmt.org/wmt20/pdf/2020.wmt-1.68.pdf) introduces 3 approximations that speed up online noisy channel decoding -
|
||||
- Smaller channel models (`Tranformer Base` with 1 encoder and decoder layer each vs. `Transformer Big`)
|
||||
- This involves training a channel model that is possibly smaller and less accurate in terms of BLEU than a channel model of the same size as the direct model.
|
||||
- Since the role of the channel model is mainly to assign low scores to generations from the language model if they don't translate back to the source, we may not need the most accurate channel model for this purpose.
|
||||
- Smaller output vocabulary size for the channel model (~30,000 -> ~1000)
|
||||
- The channel model doesn't need to score the full output vocabulary, it just needs to score the source tokens, which are completely known.
|
||||
- This is specified using the arguments `--channel-scoring-type src_vocab --top-k-vocab 500`
|
||||
- This means that the output vocabulary for the channel model will be the source tokens for all examples in the batch and the top-K most frequent tokens in the vocabulary
|
||||
- This reduces the memory consumption needed to store channel model scores significantly
|
||||
- Smaller number of candidates (`k2`) scored per beam
|
||||
- This is specified by reducing the argument `--k2`
|
||||
|
||||
|
||||
### Fast Noisy Channel Generation for German-English translation with fairseq
|
||||
|
||||
Here are instructions for **fast** noisy channel generation with a direct model, channel model and language model as explained in section [Fast Noisy Channel Modeling](#fast-noisy-channel-modeling). The main differences are that we use a smaller channel model, reduce `--k2`, set `--channel-scoring-type src_vocab --top-k-vocab 500` and increase the `--batch-size`.
|
||||
|
||||
Note:
|
||||
- Download and install fairseq as per instructions [here](https://github.com/pytorch/fairseq)
|
||||
- Preprocess and binarize the dataset as per instructions in section [Test Data Preprocessing](#test-data-preprocessing)
|
||||
|
||||
```sh
|
||||
binarized_data=data_dir/binarized
|
||||
direct_model=de_en_seed4.pt
|
||||
lm_model=en_lm.pt
|
||||
lm_data=lm_data
|
||||
small_ch_model=en_de.base_1_1.seed4.pt
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed4.pt -O ${direct_model}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/transformer_lm.pt -O ${lm_model}
|
||||
mkdir -p ${lm_data}
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/lm_dict/dict.txt -O ${lm_data}/dict.txt
|
||||
wget https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base_1_1.seed4.pt -O ${small_ch_model}
|
||||
|
||||
k2=3
|
||||
lenpen=0.23
|
||||
lm_wt=0.58
|
||||
bw_wt=0.26
|
||||
fairseq-generate ${binarized_data} \
|
||||
--user-dir examples/fast_noisy_channel \
|
||||
--beam 5 \
|
||||
--path ${direct_model} \
|
||||
--lm-model ${lm_model} \
|
||||
--lm-data ${lm_data} \
|
||||
--channel-model ${small_ch_model} \
|
||||
--k2 ${k2} \
|
||||
--combine-method noisy_channel \
|
||||
--task noisy_channel_translation \
|
||||
--lenpen ${lenpen} \
|
||||
--lm-wt ${lm_wt} \
|
||||
--ch-wt ${bw_wt} \
|
||||
--gen-subset test \
|
||||
--remove-bpe \
|
||||
--fp16 \
|
||||
--batch-size 50 \
|
||||
--channel-scoring-type src_vocab --top-k-vocab 500
|
||||
```
|
||||
|
||||
## Test Data Preprocessing
|
||||
|
||||
For preprocessing and binarizing the test sets for Romanian-English and German-English translation, we use the following script -
|
||||
|
||||
```sh
|
||||
FAIRSEQ=/path/to/fairseq
|
||||
cd $FAIRSEQ
|
||||
SCRIPTS=$FAIRSEQ/mosesdecoder/scripts
|
||||
if [ ! -d "${SCRIPTS}" ]; then
|
||||
echo 'Cloning Moses github repository (for tokenization scripts)...'
|
||||
git clone https://github.com/moses-smt/mosesdecoder.git
|
||||
fi
|
||||
TOKENIZER=$SCRIPTS/tokenizer/tokenizer.perl
|
||||
NORMALIZE=$SCRIPTS/tokenizer/normalize-punctuation.perl
|
||||
|
||||
s=de
|
||||
t=en
|
||||
test=wmt18
|
||||
|
||||
mkdir -p data_dir
|
||||
|
||||
# Tokenization
|
||||
if [ $s == "ro" ] ; then
|
||||
# Note: Get normalise-romanian.py and remove-diacritics.py from
|
||||
# https://github.com/rsennrich/wmt16-scripts/tree/master/preprocess
|
||||
sacrebleu -t $test -l $s-$t --echo src | \
|
||||
$NORMALIZE -l $s | \
|
||||
python normalise-romanian.py | \
|
||||
python remove-diacritics.py | \
|
||||
$TOKENIZER -l $s -a -q > data_dir/$test.$s-$t.$s
|
||||
else
|
||||
sacrebleu -t $test -l $s-$t --echo src | perl $NORMALIZE -l $s | perl $TOKENIZER -threads 8 -a -l $s > data_dir/$test.$s-$t.$s
|
||||
fi
|
||||
|
||||
sacrebleu -t $test -l $s-$t --echo ref | perl $NORMALIZE -l $t | perl $TOKENIZER -threads 8 -a -l $t > data_dir/$test.$s-$t.$t
|
||||
|
||||
|
||||
# Applying BPE
|
||||
src_bpe_code=/path/to/source/language/bpe/code
|
||||
tgt_bpe_code=/path/to/target/language/bpe/code
|
||||
src_dict=/path/to/source/language/dict
|
||||
tgt_dict=/path/to/target/language/dict
|
||||
|
||||
FASTBPE=$FAIRSEQ/fastBPE
|
||||
if [ ! -d "${FASTBPE}" ] ; then
|
||||
git clone https://github.com/glample/fastBPE.git
|
||||
# Follow compilation instructions at https://github.com/glample/fastBPE
|
||||
g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast
|
||||
fi
|
||||
|
||||
${FASTBPE}/fast applybpe data_dir/bpe.$test.$s-$t.$s data_dir/$test.$s-$t.$s ${src_bpe_code}
|
||||
${FASTBPE}/fast applybpe data_dir/bpe.$test.$s-$t.$s data_dir/$test.$s-$t.$s ${tgt_bpe_code}
|
||||
|
||||
fairseq-preprocess -s $s -t $t \
|
||||
--testpref data_dir/bpe.$test.$s-$t \
|
||||
--destdir data_dir/binarized \
|
||||
--srcdict ${src_dict} \
|
||||
--tgtdict ${tgt_dict}
|
||||
```
|
||||
|
||||
## Calculating BLEU
|
||||
|
||||
```sh
|
||||
DETOKENIZER=$SCRIPTS/tokenizer/detokenizer.perl
|
||||
cat ${generation_output} | grep -P "^H" | sort -V | cut -f 3- | $DETOKENIZER -l $t -q -a | sacrebleu -t $test -l $s-$t
|
||||
```
|
||||
|
||||
|
||||
## Romanian-English Translation
|
||||
|
||||
The direct and channel models are trained using bitext data (WMT16) combined with backtranslated data (The monolingual data used for backtranslation comes from http://data.statmt.org/rsennrich/wmt16_backtranslations/ (Sennrich et al., 2016c))
|
||||
|
||||
The backtranslated data is generated using an ensemble of 3 English-Romanian models trained on bitext training data (WMT16) with unrestricted sampling.
|
||||
|
||||
### BPE Codes and Dictionary
|
||||
|
||||
We learn a joint BPE vocabulary of 18K types on the bitext training data which is used for both the source and target.
|
||||
||Path|
|
||||
|----------|------|
|
||||
| BPE Code | [joint_bpe_18k](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/bpe_18k) |
|
||||
| Dictionary | [dict](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/dict) |
|
||||
|
||||
### Direct Models
|
||||
For Ro-En with backtranslation, the direct and channel models use a Transformer-Big architecture.
|
||||
|
||||
| Seed | Model |
|
||||
|----|----|
|
||||
| 2 | [ro_en_seed2.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/direct_models/seed2.pt)
|
||||
| 4 | [ro_en_seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/direct_models/seed4.pt)
|
||||
| 6 | [ro_en_seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/direct_models/seed6.pt)
|
||||
|
||||
### Channel Models
|
||||
For channel models, we follow the same steps as for the direct models. But backtranslated data is generated in the opposite direction using [this Romanian monolingual data](http://data.statmt.org/rsennrich/wmt16_backtranslations/).
|
||||
The best lenpen, LM weight and CH weight are obtained by sweeping over the validation set (wmt16/dev) using beam 5.
|
||||
| Model Size | Lenpen | LM Weight | CH Weight | Seed 2 | Seed 4 | Seed 6 |
|
||||
|----|----|----|----|----|----|----|
|
||||
| `big` | 0.84 | 0.64 | 0.56 | [big.seed2.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/big.seed2.pt) | [big.seed2.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/big.seed2.pt) | [big.seed2.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/big.seed2.pt) |
|
||||
| `base_1_1` | 0.63 | 0.40 | 0.37 | [base_1_1.seed2.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/base_1_1.seed2.pt) | [base_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/base_1_1.seed4.pt) | [base_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/channel_models/base_1_1.seed6.pt) |
|
||||
|
||||
### Language Model
|
||||
The model is trained on de-duplicated English Newscrawl data from 2007-2018 comprising 186 million sentences or 4.5B words after normalization and tokenization.
|
||||
| | Path |
|
||||
|----|----|
|
||||
| `--lm-model` | [transformer_en_lm](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/lm_model/transformer_lm.pt) |
|
||||
| `--lm-data` | [lm_data](https://dl.fbaipublicfiles.com/fast_noisy_channel/ro_en/lm_model/lm_dict)
|
||||
|
||||
## German-English Translation
|
||||
|
||||
### BPE Codes and Dictionaries
|
||||
|
||||
| | Path|
|
||||
|----------|------|
|
||||
| Source BPE Code | [de_bpe_code_24K](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/de_bpe_code_24K) |
|
||||
| Target BPE Code | [en_bpe_code_24K](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/en_bpe_code_24K)
|
||||
| Source Dictionary | [de_dict](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/de_dict) |
|
||||
| Target Dictionary | [en_dict](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/en_dict) |
|
||||
|
||||
### Direct Models
|
||||
We train on WMT’19 training data. Following [Ng et al., 2019](http://statmt.org/wmt19/pdf/53/WMT33.pdf), we apply language identification filtering and remove sentences longer than 250 tokens as well as sentence pairs with a source/target length ratio exceeding 1.5. This results in 26.8M sentence pairs.
|
||||
We use the Transformer-Big architecture for the direct model.
|
||||
|
||||
| Seed | Model |
|
||||
|:----:|----|
|
||||
| 4 | [de_en_seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed4.pt)
|
||||
| 5 | [de_en_seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed5.pt)
|
||||
| 6 | [de_en_seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/direct_models/seed6.pt)
|
||||
|
||||
### Channel Models
|
||||
|
||||
We train on WMT’19 training data. Following [Ng et al., 2019](http://statmt.org/wmt19/pdf/53/WMT33.pdf), we apply language identification filtering and remove sentences longer than 250 tokens as well as sentence pairs with a source/target length ratio exceeding 1.5. This results in 26.8M sentence pairs.
|
||||
|
||||
| Model Size | Seed 4 | Seed 5 | Seed 6 |
|
||||
|----|----|----|----|
|
||||
| `big` | [big.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big.seed4.pt) | [big.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big.seed5.pt) | [big.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big.seed6.pt) |
|
||||
| `big_1_1` | [big_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big_1_1.seed4.pt) | [big_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big_1_1.seed5.pt) | [big_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/big_1_1.seed6.pt) |
|
||||
| `base` | [base.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base.seed4.pt) | [base.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base.seed5.pt) | [base.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base.seed6.pt) |
|
||||
| `base_1_1` | [base_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base_1_1.seed4.pt) | [base_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base_1_1.seed5.pt) | [base_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/base_1_1.seed6.pt) |
|
||||
| `half` | [half.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half.seed4.pt) | [half.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half.seed5.pt) | [half.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half.seed6.pt) |
|
||||
| `half_1_1` | [half_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half_1_1.seed4.pt) | [half_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half_1_1.seed5.pt) | [half_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/half_1_1.seed6.pt) |
|
||||
| `quarter` | [quarter.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter.seed4.pt) | [quarter.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter.seed5.pt) | [quarter.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter.seed6.pt) |
|
||||
| `quarter_1_1` | [quarter_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter_1_1.seed4.pt) | [quarter_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter_1_1.seed5.pt) | [quarter_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/quarter_1_1.seed6.pt) |
|
||||
| `8th` | [8th.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th.seed4.pt) | [8th.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th.seed5.pt) | [8th.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th.seed6.pt) |
|
||||
| `8th_1_1` | [8th_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th_1_1.seed4.pt) | [8th_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th_1_1.seed5.pt) | [8th_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/8th_1_1.seed6.pt) |
|
||||
| `16th` | [16th.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th.seed4.pt) | [16th.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th.seed5.pt) | [16th.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th.seed6.pt) |
|
||||
| `16th_1_1` | [16th_1_1.seed4.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th_1_1.seed4.pt) | [16th_1_1.seed5.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th_1_1.seed5.pt) | [16th_1_1.seed6.pt](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/channel_models/16th_1_1.seed6.pt) |
|
||||
|
||||
### Language Model
|
||||
The model is trained on de-duplicated English Newscrawl data from 2007-2018 comprising 186 million sentences or 4.5B words after normalization and tokenization.
|
||||
| | Path |
|
||||
|----|----|
|
||||
| `--lm-model` | [transformer_en_lm](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/transformer_lm.pt) |
|
||||
| `--lm-data` | [lm_data](https://dl.fbaipublicfiles.com/fast_noisy_channel/de_en/lm_model/lm_dict/)
|
||||
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@inproceedings{bhosale2020language,
|
||||
title={Language Models not just for Pre-training: Fast Online Neural Noisy Channel Modeling},
|
||||
author={Shruti Bhosale and Kyra Yee and Sergey Edunov and Michael Auli},
|
||||
booktitle={Proceedings of the Fifth Conference on Machine Translation (WMT)},
|
||||
year={2020},
|
||||
}
|
||||
|
||||
@inproceedings{yee2019simple,
|
||||
title={Simple and Effective Noisy Channel Modeling for Neural Machine Translation},
|
||||
author={Yee, Kyra and Dauphin, Yann and Auli, Michael},
|
||||
booktitle={Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP)},
|
||||
pages={5700--5705},
|
||||
year={2019}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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 . import noisy_channel_translation # noqa
|
||||
from . import noisy_channel_sequence_generator # noqa
|
||||
from . import noisy_channel_beam_search # noqa
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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.search import Search
|
||||
|
||||
|
||||
class NoisyChannelBeamSearch(Search):
|
||||
|
||||
def __init__(self, tgt_dict):
|
||||
super().__init__(tgt_dict)
|
||||
self.fw_scores_buf = None
|
||||
self.lm_scores_buf = None
|
||||
|
||||
def _init_buffers(self, t):
|
||||
# super()._init_buffers(t)
|
||||
if self.fw_scores_buf is None:
|
||||
self.scores_buf = t.new()
|
||||
self.indices_buf = torch.LongTensor().to(device=t.device)
|
||||
self.beams_buf = torch.LongTensor().to(device=t.device)
|
||||
self.fw_scores_buf = t.new()
|
||||
self.lm_scores_buf = t.new()
|
||||
|
||||
def combine_fw_bw(self, combine_method, fw_cum, bw, step):
|
||||
if combine_method == "noisy_channel":
|
||||
fw_norm = fw_cum.div(step + 1)
|
||||
lprobs = bw + fw_norm
|
||||
elif combine_method == "lm_only":
|
||||
lprobs = bw + fw_cum
|
||||
|
||||
return lprobs
|
||||
|
||||
def step(self, step, fw_lprobs, scores, bw_lprobs, lm_lprobs, combine_method):
|
||||
self._init_buffers(fw_lprobs)
|
||||
bsz, beam_size, vocab_size = fw_lprobs.size()
|
||||
|
||||
if step == 0:
|
||||
# at the first step all hypotheses are equally likely, so use
|
||||
# only the first beam
|
||||
fw_lprobs = fw_lprobs[:, ::beam_size, :].contiguous()
|
||||
bw_lprobs = bw_lprobs[:, ::beam_size, :].contiguous()
|
||||
# nothing to add since we are at the first step
|
||||
fw_lprobs_cum = fw_lprobs
|
||||
|
||||
else:
|
||||
# make probs contain cumulative scores for each hypothesis
|
||||
raw_scores = (scores[:, :, step - 1].unsqueeze(-1))
|
||||
fw_lprobs_cum = (fw_lprobs.add(raw_scores))
|
||||
|
||||
combined_lprobs = self.combine_fw_bw(combine_method, fw_lprobs_cum, bw_lprobs, step)
|
||||
|
||||
# choose the top k according to the combined noisy channel model score
|
||||
torch.topk(
|
||||
combined_lprobs.view(bsz, -1),
|
||||
k=min(
|
||||
# Take the best 2 x beam_size predictions. We'll choose the first
|
||||
# beam_size of these which don't predict eos to continue with.
|
||||
beam_size * 2,
|
||||
combined_lprobs.view(bsz, -1).size(1) - 1, # -1 so we never select pad
|
||||
),
|
||||
out=(self.scores_buf, self.indices_buf),
|
||||
)
|
||||
# save corresponding fw and lm scores
|
||||
self.fw_scores_buf = torch.gather(fw_lprobs_cum.view(bsz, -1), 1, self.indices_buf)
|
||||
self.lm_scores_buf = torch.gather(lm_lprobs.view(bsz, -1), 1, self.indices_buf)
|
||||
# Project back into relative indices and beams
|
||||
self.beams_buf = self.indices_buf // vocab_size
|
||||
self.indices_buf.fmod_(vocab_size)
|
||||
return self.scores_buf, self.fw_scores_buf, self.lm_scores_buf, self.indices_buf, self.beams_buf
|
||||
@@ -0,0 +1,842 @@
|
||||
# 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 typing import Dict, List, Optional
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import Tensor
|
||||
|
||||
from .noisy_channel_beam_search import NoisyChannelBeamSearch
|
||||
from fairseq.sequence_generator import EnsembleModel
|
||||
|
||||
|
||||
class NoisyChannelSequenceGenerator(object):
|
||||
def __init__(
|
||||
self,
|
||||
combine_method,
|
||||
tgt_dict,
|
||||
src_dict=None,
|
||||
beam_size=1,
|
||||
max_len_a=0,
|
||||
max_len_b=200,
|
||||
min_len=1,
|
||||
len_penalty=1.0,
|
||||
unk_penalty=0.0,
|
||||
retain_dropout=False,
|
||||
temperature=1.0,
|
||||
match_source_len=False,
|
||||
no_repeat_ngram_size=0,
|
||||
normalize_scores=True,
|
||||
channel_models=None,
|
||||
k2=10,
|
||||
ch_weight=1.0,
|
||||
channel_scoring_type='log_norm',
|
||||
top_k_vocab=0,
|
||||
lm_models=None,
|
||||
lm_dict=None,
|
||||
lm_weight=1.0,
|
||||
normalize_lm_scores_by_tgt_len=False,
|
||||
):
|
||||
"""Generates translations of a given source sentence,
|
||||
using beam search with noisy channel decoding.
|
||||
|
||||
Args:
|
||||
combine_method (string, optional): Method to combine direct, LM and
|
||||
channel model scores (default: None)
|
||||
tgt_dict (~fairseq.data.Dictionary): target dictionary
|
||||
src_dict (~fairseq.data.Dictionary): source dictionary
|
||||
beam_size (int, optional): beam width (default: 1)
|
||||
max_len_a/b (int, optional): generate sequences of maximum length
|
||||
ax + b, where x is the source length
|
||||
min_len (int, optional): the minimum length of the generated output
|
||||
(not including end-of-sentence)
|
||||
len_penalty (float, optional): length penalty, where <1.0 favors
|
||||
shorter, >1.0 favors longer sentences (default: 1.0)
|
||||
unk_penalty (float, optional): unknown word penalty, where <0
|
||||
produces more unks, >0 produces fewer (default: 0.0)
|
||||
retain_dropout (bool, optional): use dropout when generating
|
||||
(default: False)
|
||||
temperature (float, optional): temperature, where values
|
||||
>1.0 produce more uniform samples and values <1.0 produce
|
||||
sharper samples (default: 1.0)
|
||||
match_source_len (bool, optional): outputs should match the source
|
||||
length (default: False)
|
||||
no_repeat_ngram_size (int, optional): Size of n-grams that we avoid
|
||||
repeating in the generation (default: 0)
|
||||
normalize_scores (bool, optional): normalize scores by the length
|
||||
of the output (default: True)
|
||||
channel_models (List[~fairseq.models.FairseqModel]): ensemble of models
|
||||
translating from the target to the source
|
||||
k2 (int, optional): Top K2 candidates to score per beam at each step (default:10)
|
||||
ch_weight (int, optional): Weight associated with the channel model score
|
||||
assuming that the direct model score has weight 1.0 (default: 1.0)
|
||||
channel_scoring_type (str, optional): String specifying how to score
|
||||
the channel model (default: 'log_norm')
|
||||
top_k_vocab (int, optional): If `channel_scoring_type` is `'src_vocab'` or
|
||||
`'src_vocab_batched'`, then this parameter specifies the number of
|
||||
most frequent tokens to include in the channel model output vocabulary,
|
||||
in addition to the source tokens in the input batch (default: 0)
|
||||
lm_models (List[~fairseq.models.FairseqModel]): ensemble of models
|
||||
generating text in the target language
|
||||
lm_dict (~fairseq.data.Dictionary): LM Model dictionary
|
||||
lm_weight (int, optional): Weight associated with the LM model score
|
||||
assuming that the direct model score has weight 1.0 (default: 1.0)
|
||||
normalize_lm_scores_by_tgt_len (bool, optional): Should we normalize LM scores
|
||||
by the target length? By default, we normalize the combination of
|
||||
LM and channel model scores by the source length
|
||||
"""
|
||||
self.pad = tgt_dict.pad()
|
||||
self.unk = tgt_dict.unk()
|
||||
self.eos = tgt_dict.eos()
|
||||
self.vocab_size = len(tgt_dict)
|
||||
self.beam_size = beam_size
|
||||
# the max beam size is the dictionary size - 1, since we never select pad
|
||||
self.beam_size = min(beam_size, self.vocab_size - 1)
|
||||
self.max_len_a = max_len_a
|
||||
self.max_len_b = max_len_b
|
||||
self.min_len = min_len
|
||||
self.normalize_scores = normalize_scores
|
||||
self.len_penalty = len_penalty
|
||||
self.unk_penalty = unk_penalty
|
||||
self.retain_dropout = retain_dropout
|
||||
self.temperature = temperature
|
||||
self.match_source_len = match_source_len
|
||||
self.no_repeat_ngram_size = no_repeat_ngram_size
|
||||
self.channel_models = channel_models
|
||||
self.src_dict = src_dict
|
||||
self.tgt_dict = tgt_dict
|
||||
self.combine_method = combine_method
|
||||
self.k2 = k2
|
||||
self.ch_weight = ch_weight
|
||||
self.channel_scoring_type = channel_scoring_type
|
||||
self.top_k_vocab = top_k_vocab
|
||||
self.lm_models = lm_models
|
||||
self.lm_dict = lm_dict
|
||||
self.lm_weight = lm_weight
|
||||
self.log_softmax_fn = torch.nn.LogSoftmax(dim=1)
|
||||
self.normalize_lm_scores_by_tgt_len = normalize_lm_scores_by_tgt_len
|
||||
|
||||
self.share_tgt_dict = (self.lm_dict == self.tgt_dict)
|
||||
self.tgt_to_lm = make_dict2dict(tgt_dict, lm_dict)
|
||||
|
||||
self.ch_scoring_bsz = 3072
|
||||
|
||||
assert temperature > 0, '--temperature must be greater than 0'
|
||||
|
||||
self.search = NoisyChannelBeamSearch(tgt_dict)
|
||||
|
||||
@torch.no_grad()
|
||||
def generate(
|
||||
self,
|
||||
models,
|
||||
sample,
|
||||
prefix_tokens=None,
|
||||
bos_token=None,
|
||||
**kwargs
|
||||
):
|
||||
"""Generate a batch of translations.
|
||||
Args:
|
||||
models (List[~fairseq.models.FairseqModel]): ensemble of models
|
||||
sample (dict): batch
|
||||
prefix_tokens (torch.LongTensor, optional): force decoder to begin
|
||||
with these tokens
|
||||
"""
|
||||
model = EnsembleModel(models)
|
||||
incremental_states = torch.jit.annotate(
|
||||
List[Dict[str, Dict[str, Optional[Tensor]]]],
|
||||
[
|
||||
torch.jit.annotate(Dict[str, Dict[str, Optional[Tensor]]], {})
|
||||
for i in range(model.models_size)
|
||||
],
|
||||
)
|
||||
if not self.retain_dropout:
|
||||
model.eval()
|
||||
|
||||
# model.forward normally channels prev_output_tokens into the decoder
|
||||
# separately, but SequenceGenerator directly calls model.encoder
|
||||
encoder_input = {
|
||||
k: v for k, v in sample['net_input'].items()
|
||||
if k != 'prev_output_tokens'
|
||||
}
|
||||
src_tokens = encoder_input['src_tokens']
|
||||
src_lengths_no_eos = (src_tokens.ne(self.eos) & src_tokens.ne(self.pad)).long().sum(dim=1)
|
||||
input_size = src_tokens.size()
|
||||
# batch dimension goes first followed by source lengths
|
||||
bsz = input_size[0]
|
||||
src_len = input_size[1]
|
||||
beam_size = self.beam_size
|
||||
|
||||
if self.match_source_len:
|
||||
max_len = src_lengths_no_eos.max().item()
|
||||
else:
|
||||
max_len = min(
|
||||
int(self.max_len_a * src_len + self.max_len_b),
|
||||
# exclude the EOS marker
|
||||
model.max_decoder_positions() - 1,
|
||||
)
|
||||
|
||||
# compute the encoder output for each beam
|
||||
encoder_outs = model.forward_encoder(encoder_input)
|
||||
new_order = torch.arange(bsz).view(-1, 1).repeat(1, beam_size).view(-1)
|
||||
new_order = new_order.to(src_tokens.device).long()
|
||||
encoder_outs = model.reorder_encoder_out(encoder_outs, new_order)
|
||||
|
||||
src_lengths = encoder_input['src_lengths']
|
||||
# initialize buffers
|
||||
scores = src_tokens.new(bsz * beam_size, max_len + 1).float().fill_(0)
|
||||
lm_prefix_scores = src_tokens.new(bsz * beam_size).float().fill_(0)
|
||||
|
||||
scores_buf = scores.clone()
|
||||
tokens = src_tokens.new(bsz * beam_size, max_len + 2).long().fill_(self.pad)
|
||||
tokens_buf = tokens.clone()
|
||||
tokens[:, 0] = self.eos if bos_token is None else bos_token
|
||||
|
||||
# reorder source tokens so they may be used as a reference in generating P(S|T)
|
||||
src_tokens = reorder_all_tokens(src_tokens, src_lengths, self.src_dict.eos_index)
|
||||
|
||||
src_tokens = src_tokens.repeat(1, beam_size).view(-1, src_len)
|
||||
src_lengths = src_lengths.view(bsz, -1).repeat(1, beam_size).view(bsz*beam_size, -1)
|
||||
|
||||
attn, attn_buf = None, None
|
||||
nonpad_idxs = None
|
||||
|
||||
# The cands_to_ignore indicates candidates that should be ignored.
|
||||
# For example, suppose we're sampling and have already finalized 2/5
|
||||
# samples. Then the cands_to_ignore would mark 2 positions as being ignored,
|
||||
# so that we only finalize the remaining 3 samples.
|
||||
cands_to_ignore = src_tokens.new_zeros(bsz, beam_size).eq(-1) # forward and backward-compatible False mask
|
||||
|
||||
# list of completed sentences
|
||||
finalized = [[] for i in range(bsz)]
|
||||
finished = [False for i in range(bsz)]
|
||||
num_remaining_sent = bsz
|
||||
|
||||
# number of candidate hypos per step
|
||||
cand_size = 2 * beam_size # 2 x beam size in case half are EOS
|
||||
|
||||
# offset arrays for converting between different indexing schemes
|
||||
bbsz_offsets = (torch.arange(0, bsz) * beam_size).unsqueeze(1).type_as(tokens)
|
||||
cand_offsets = torch.arange(0, cand_size).type_as(tokens)
|
||||
|
||||
# helper function for allocating buffers on the fly
|
||||
buffers = {}
|
||||
|
||||
def buffer(name, type_of=tokens): # noqa
|
||||
if name not in buffers:
|
||||
buffers[name] = type_of.new()
|
||||
return buffers[name]
|
||||
|
||||
def is_finished(sent, step, unfin_idx):
|
||||
"""
|
||||
Check whether we've finished generation for a given sentence, by
|
||||
comparing the worst score among finalized hypotheses to the best
|
||||
possible score among unfinalized hypotheses.
|
||||
"""
|
||||
assert len(finalized[sent]) <= beam_size
|
||||
if len(finalized[sent]) == beam_size:
|
||||
return True
|
||||
return False
|
||||
|
||||
def finalize_hypos(step, bbsz_idx, eos_scores, combined_noisy_channel_eos_scores):
|
||||
"""
|
||||
Finalize the given hypotheses at this step, while keeping the total
|
||||
number of finalized hypotheses per sentence <= beam_size.
|
||||
|
||||
Note: the input must be in the desired finalization order, so that
|
||||
hypotheses that appear earlier in the input are preferred to those
|
||||
that appear later.
|
||||
|
||||
Args:
|
||||
step: current time step
|
||||
bbsz_idx: A vector of indices in the range [0, bsz*beam_size),
|
||||
indicating which hypotheses to finalize
|
||||
eos_scores: A vector of the same size as bbsz_idx containing
|
||||
fw scores for each hypothesis
|
||||
combined_noisy_channel_eos_scores: A vector of the same size as bbsz_idx containing
|
||||
combined noisy channel scores for each hypothesis
|
||||
"""
|
||||
assert bbsz_idx.numel() == eos_scores.numel()
|
||||
|
||||
# clone relevant token and attention tensors
|
||||
tokens_clone = tokens.index_select(0, bbsz_idx)
|
||||
tokens_clone = tokens_clone[:, 1:step + 2] # skip the first index, which is EOS
|
||||
assert not tokens_clone.eq(self.eos).any()
|
||||
tokens_clone[:, step] = self.eos
|
||||
attn_clone = attn.index_select(0, bbsz_idx)[:, :, 1:step+2] if attn is not None else None
|
||||
|
||||
# compute scores per token position
|
||||
pos_scores = scores.index_select(0, bbsz_idx)[:, :step+1]
|
||||
pos_scores[:, step] = eos_scores
|
||||
# convert from cumulative to per-position scores
|
||||
pos_scores[:, 1:] = pos_scores[:, 1:] - pos_scores[:, :-1]
|
||||
|
||||
# normalize sentence-level scores
|
||||
if self.normalize_scores:
|
||||
combined_noisy_channel_eos_scores /= (step + 1) ** self.len_penalty
|
||||
|
||||
cum_unfin = []
|
||||
prev = 0
|
||||
for f in finished:
|
||||
if f:
|
||||
prev += 1
|
||||
else:
|
||||
cum_unfin.append(prev)
|
||||
|
||||
sents_seen = set()
|
||||
for i, (idx, score) in enumerate(zip(bbsz_idx.tolist(), combined_noisy_channel_eos_scores.tolist())):
|
||||
unfin_idx = idx // beam_size
|
||||
sent = unfin_idx + cum_unfin[unfin_idx]
|
||||
|
||||
sents_seen.add((sent, unfin_idx))
|
||||
|
||||
if self.match_source_len and step > src_lengths_no_eos[unfin_idx]:
|
||||
score = -math.inf
|
||||
|
||||
def get_hypo():
|
||||
|
||||
if attn_clone is not None:
|
||||
# remove padding tokens from attn scores
|
||||
hypo_attn = attn_clone[i][nonpad_idxs[sent]]
|
||||
_, alignment = hypo_attn.max(dim=0)
|
||||
else:
|
||||
hypo_attn = None
|
||||
alignment = None
|
||||
|
||||
return {
|
||||
'tokens': tokens_clone[i],
|
||||
'score': score,
|
||||
'attention': hypo_attn, # src_len x tgt_len
|
||||
'alignment': alignment,
|
||||
'positional_scores': pos_scores[i],
|
||||
}
|
||||
|
||||
if len(finalized[sent]) < beam_size:
|
||||
finalized[sent].append(get_hypo())
|
||||
|
||||
newly_finished = []
|
||||
for sent, unfin_idx in sents_seen:
|
||||
# check termination conditions for this sentence
|
||||
if not finished[sent] and is_finished(sent, step, unfin_idx):
|
||||
finished[sent] = True
|
||||
newly_finished.append(unfin_idx)
|
||||
return newly_finished
|
||||
|
||||
def noisy_channel_rescoring(lprobs, beam_size, bsz, src_tokens, tokens, k):
|
||||
"""Rescore the top k hypothesis from each beam using noisy channel modeling
|
||||
Returns:
|
||||
new_fw_lprobs: the direct model probabilities after pruning the top k
|
||||
new_ch_lm_lprobs: the combined channel and language model probabilities
|
||||
new_lm_lprobs: the language model probabilities after pruning the top k
|
||||
"""
|
||||
with torch.no_grad():
|
||||
lprobs_size = lprobs.size()
|
||||
if prefix_tokens is not None and step < prefix_tokens.size(1):
|
||||
probs_slice = lprobs.view(bsz, -1, lprobs.size(-1))[:, 0, :]
|
||||
cand_scores = torch.gather(
|
||||
probs_slice, dim=1,
|
||||
index=prefix_tokens[:, step].view(-1, 1).data
|
||||
).expand(-1, beam_size).contiguous().view(bsz*beam_size, 1)
|
||||
cand_indices = prefix_tokens[:, step].view(-1, 1).expand(bsz, beam_size).data.contiguous().view(bsz*beam_size, 1)
|
||||
|
||||
# need to calculate and save fw and lm probs for prefix tokens
|
||||
fw_top_k = cand_scores
|
||||
fw_top_k_idx = cand_indices
|
||||
k = 1
|
||||
else:
|
||||
# take the top k best words for every sentence in batch*beam
|
||||
fw_top_k, fw_top_k_idx = torch.topk(lprobs.view(beam_size*bsz, -1), k=k)
|
||||
eos_idx = torch.nonzero(fw_top_k_idx.view(bsz*beam_size*k, -1) == self.eos)[:, 0]
|
||||
ch_scores = fw_top_k.new_full((beam_size*bsz*k, ), 0)
|
||||
src_size = torch.sum(src_tokens[:, :] != self.src_dict.pad_index, dim=1, keepdim=True, dtype=fw_top_k.dtype)
|
||||
|
||||
if self.combine_method != "lm_only":
|
||||
temp_src_tokens_full = src_tokens[:, :].repeat(1, k).view(bsz*beam_size*k, -1)
|
||||
not_padding = temp_src_tokens_full[:, 1:] != self.src_dict.pad_index
|
||||
cur_tgt_size = step+2
|
||||
|
||||
# add eos to all candidate sentences except those that already end in eos
|
||||
eos_tokens = tokens[:, 0].repeat(1, k).view(-1, 1)
|
||||
eos_tokens[eos_idx] = self.tgt_dict.pad_index
|
||||
|
||||
if step == 0:
|
||||
channel_input = torch.cat((fw_top_k_idx.view(-1, 1), eos_tokens), 1)
|
||||
else:
|
||||
# move eos from beginning to end of target sentence
|
||||
channel_input = torch.cat((tokens[:, 1:step + 1].repeat(1, k).view(-1, step), fw_top_k_idx.view(-1, 1), eos_tokens), 1)
|
||||
|
||||
ch_input_lengths = torch.tensor(np.full(channel_input.size(0), cur_tgt_size))
|
||||
ch_input_lengths[eos_idx] = cur_tgt_size-1
|
||||
if self.channel_scoring_type == "unnormalized":
|
||||
ch_encoder_output = channel_model.encoder(channel_input, src_lengths=ch_input_lengths)
|
||||
ch_decoder_output, _ = channel_model.decoder(temp_src_tokens_full, encoder_out=ch_encoder_output, features_only=True)
|
||||
del ch_encoder_output
|
||||
ch_intermed_scores = channel_model.decoder.unnormalized_scores_given_target(ch_decoder_output, target_ids=temp_src_tokens_full[:, 1:])
|
||||
ch_intermed_scores = ch_intermed_scores.float()
|
||||
ch_intermed_scores *= not_padding.float()
|
||||
ch_scores = torch.sum(ch_intermed_scores, dim=1)
|
||||
elif self.channel_scoring_type == "k2_separate":
|
||||
for k_idx in range(k):
|
||||
k_eos_tokens = eos_tokens[k_idx::k, :]
|
||||
if step == 0:
|
||||
k_ch_input = torch.cat((fw_top_k_idx[:, k_idx:k_idx+1], k_eos_tokens), 1)
|
||||
else:
|
||||
# move eos from beginning to end of target sentence
|
||||
k_ch_input = torch.cat((tokens[:, 1:step + 1], fw_top_k_idx[:, k_idx:k_idx+1], k_eos_tokens), 1)
|
||||
k_ch_input_lengths = ch_input_lengths[k_idx::k]
|
||||
k_ch_output = channel_model(k_ch_input, k_ch_input_lengths, src_tokens)
|
||||
k_ch_lprobs = channel_model.get_normalized_probs(k_ch_output, log_probs=True)
|
||||
k_ch_intermed_scores = torch.gather(k_ch_lprobs[:, :-1, :], 2, src_tokens[:, 1:].unsqueeze(2)).squeeze(2)
|
||||
k_ch_intermed_scores *= not_padding.float()
|
||||
ch_scores[k_idx::k] = torch.sum(k_ch_intermed_scores, dim=1)
|
||||
elif self.channel_scoring_type == "src_vocab":
|
||||
ch_encoder_output = channel_model.encoder(channel_input, src_lengths=ch_input_lengths)
|
||||
ch_decoder_output, _ = channel_model.decoder(temp_src_tokens_full, encoder_out=ch_encoder_output, features_only=True)
|
||||
|
||||
del ch_encoder_output
|
||||
ch_lprobs = normalized_scores_with_batch_vocab(
|
||||
channel_model.decoder,
|
||||
ch_decoder_output, src_tokens, k, bsz, beam_size,
|
||||
self.src_dict.pad_index, top_k=self.top_k_vocab)
|
||||
ch_scores = torch.sum(ch_lprobs, dim=1)
|
||||
elif self.channel_scoring_type == "src_vocab_batched":
|
||||
ch_bsz_size = temp_src_tokens_full.shape[0]
|
||||
ch_lprobs_list = [None] * len(range(0, ch_bsz_size, self.ch_scoring_bsz))
|
||||
for i, start_idx in enumerate(range(0, ch_bsz_size, self.ch_scoring_bsz)):
|
||||
end_idx = min(start_idx + self.ch_scoring_bsz, ch_bsz_size)
|
||||
temp_src_tokens_full_batch = temp_src_tokens_full[start_idx:end_idx, :]
|
||||
channel_input_batch = channel_input[start_idx:end_idx, :]
|
||||
ch_input_lengths_batch = ch_input_lengths[start_idx:end_idx]
|
||||
ch_encoder_output_batch = channel_model.encoder(channel_input_batch, src_lengths=ch_input_lengths_batch)
|
||||
ch_decoder_output_batch, _ = channel_model.decoder(temp_src_tokens_full_batch, encoder_out=ch_encoder_output_batch, features_only=True)
|
||||
ch_lprobs_list[i] = normalized_scores_with_batch_vocab(
|
||||
channel_model.decoder,
|
||||
ch_decoder_output_batch, src_tokens, k, bsz, beam_size,
|
||||
self.src_dict.pad_index, top_k=self.top_k_vocab,
|
||||
start_idx=start_idx, end_idx=end_idx)
|
||||
ch_lprobs = torch.cat(ch_lprobs_list, dim=0)
|
||||
ch_scores = torch.sum(ch_lprobs, dim=1)
|
||||
else:
|
||||
ch_output = channel_model(channel_input, ch_input_lengths, temp_src_tokens_full)
|
||||
ch_lprobs = channel_model.get_normalized_probs(ch_output, log_probs=True)
|
||||
ch_intermed_scores = torch.gather(ch_lprobs[:, :-1, :], 2, temp_src_tokens_full[:, 1:].unsqueeze(2)).squeeze().view(bsz*beam_size*k, -1)
|
||||
ch_intermed_scores *= not_padding.float()
|
||||
ch_scores = torch.sum(ch_intermed_scores, dim=1)
|
||||
|
||||
else:
|
||||
cur_tgt_size = 0
|
||||
ch_scores = ch_scores.view(bsz*beam_size, k)
|
||||
expanded_lm_prefix_scores = lm_prefix_scores.unsqueeze(1).expand(-1, k).flatten()
|
||||
|
||||
if self.share_tgt_dict:
|
||||
lm_scores = get_lm_scores(lm, tokens[:, :step + 1].view(-1, step+1), lm_incremental_states, fw_top_k_idx.view(-1, 1), torch.tensor(np.full(tokens.size(0), step+1)), k)
|
||||
else:
|
||||
new_lm_input = dict2dict(tokens[:, :step + 1].view(-1, step+1), self.tgt_to_lm)
|
||||
new_cands = dict2dict(fw_top_k_idx.view(-1, 1), self.tgt_to_lm)
|
||||
lm_scores = get_lm_scores(lm, new_lm_input, lm_incremental_states, new_cands, torch.tensor(np.full(tokens.size(0), step+1)), k)
|
||||
|
||||
lm_scores.add_(expanded_lm_prefix_scores)
|
||||
ch_lm_scores = combine_ch_lm(self.combine_method, ch_scores, lm_scores, src_size, cur_tgt_size)
|
||||
# initialize all as min value
|
||||
new_fw_lprobs = ch_scores.new(lprobs_size).fill_(-1e17).view(bsz*beam_size, -1)
|
||||
new_ch_lm_lprobs = ch_scores.new(lprobs_size).fill_(-1e17).view(bsz*beam_size, -1)
|
||||
new_lm_lprobs = ch_scores.new(lprobs_size).fill_(-1e17).view(bsz*beam_size, -1)
|
||||
new_fw_lprobs[:, self.pad] = -math.inf
|
||||
new_ch_lm_lprobs[:, self.pad] = -math.inf
|
||||
new_lm_lprobs[:, self.pad] = -math.inf
|
||||
|
||||
new_fw_lprobs.scatter_(1, fw_top_k_idx, fw_top_k)
|
||||
new_ch_lm_lprobs.scatter_(1, fw_top_k_idx, ch_lm_scores)
|
||||
new_lm_lprobs.scatter_(1, fw_top_k_idx, lm_scores.view(-1, k))
|
||||
return new_fw_lprobs, new_ch_lm_lprobs, new_lm_lprobs
|
||||
|
||||
def combine_ch_lm(combine_type, ch_scores, lm_scores1, src_size, tgt_size):
|
||||
if self.channel_scoring_type == "unnormalized":
|
||||
ch_scores = self.log_softmax_fn(
|
||||
ch_scores.view(-1, self.beam_size * self.k2)
|
||||
).view(ch_scores.shape)
|
||||
ch_scores = ch_scores * self.ch_weight
|
||||
lm_scores1 = lm_scores1 * self.lm_weight
|
||||
|
||||
if combine_type == "lm_only":
|
||||
# log P(T|S) + log P(T)
|
||||
ch_scores = lm_scores1.view(ch_scores.size())
|
||||
elif combine_type == "noisy_channel":
|
||||
# 1/t log P(T|S) + 1/s log P(S|T) + 1/t log P(T)
|
||||
if self.normalize_lm_scores_by_tgt_len:
|
||||
ch_scores.div_(src_size)
|
||||
lm_scores_norm = lm_scores1.view(ch_scores.size()).div(tgt_size)
|
||||
ch_scores.add_(lm_scores_norm)
|
||||
# 1/t log P(T|S) + 1/s log P(S|T) + 1/s log P(T)
|
||||
else:
|
||||
ch_scores.add_(lm_scores1.view(ch_scores.size()))
|
||||
ch_scores.div_(src_size)
|
||||
|
||||
return ch_scores
|
||||
|
||||
if self.channel_models is not None:
|
||||
channel_model = self.channel_models[0] # assume only one channel_model model
|
||||
else:
|
||||
channel_model = None
|
||||
|
||||
lm = EnsembleModel(self.lm_models)
|
||||
lm_incremental_states = torch.jit.annotate(
|
||||
List[Dict[str, Dict[str, Optional[Tensor]]]],
|
||||
[
|
||||
torch.jit.annotate(Dict[str, Dict[str, Optional[Tensor]]], {})
|
||||
for i in range(lm.models_size)
|
||||
],
|
||||
)
|
||||
|
||||
reorder_state = None
|
||||
batch_idxs = None
|
||||
for step in range(max_len + 1): # one extra step for EOS marker
|
||||
# reorder decoder internal states based on the prev choice of beams
|
||||
if reorder_state is not None:
|
||||
if batch_idxs is not None:
|
||||
# update beam indices to take into account removed sentences
|
||||
corr = batch_idxs - torch.arange(batch_idxs.numel()).type_as(batch_idxs)
|
||||
reorder_state.view(-1, beam_size).add_(corr.unsqueeze(-1) * beam_size)
|
||||
model.reorder_incremental_state(incremental_states, reorder_state)
|
||||
encoder_outs = model.reorder_encoder_out(encoder_outs, reorder_state)
|
||||
|
||||
lm.reorder_incremental_state(lm_incremental_states, reorder_state)
|
||||
|
||||
fw_lprobs, avg_attn_scores = model.forward_decoder(
|
||||
tokens[:, :step + 1], encoder_outs, incremental_states, temperature=self.temperature,
|
||||
)
|
||||
|
||||
fw_lprobs[:, self.pad] = -math.inf # never select pad
|
||||
fw_lprobs[:, self.unk] -= self.unk_penalty # apply unk penalty
|
||||
fw_lprobs, ch_lm_lprobs, lm_lprobs = noisy_channel_rescoring(fw_lprobs, beam_size, bsz, src_tokens, tokens, self.k2)
|
||||
|
||||
# handle min and max length constraints
|
||||
if step >= max_len:
|
||||
fw_lprobs[:, :self.eos] = -math.inf
|
||||
fw_lprobs[:, self.eos + 1:] = -math.inf
|
||||
elif step < self.min_len:
|
||||
fw_lprobs[:, self.eos] = -math.inf
|
||||
|
||||
# handle prefix tokens (possibly with different lengths)
|
||||
if prefix_tokens is not None and step < prefix_tokens.size(1):
|
||||
prefix_toks = prefix_tokens[:, step].unsqueeze(-1).repeat(1, beam_size).view(-1)
|
||||
prefix_mask = prefix_toks.ne(self.pad)
|
||||
|
||||
prefix_fw_lprobs = fw_lprobs.gather(-1, prefix_toks.unsqueeze(-1))
|
||||
fw_lprobs[prefix_mask] = -math.inf
|
||||
fw_lprobs[prefix_mask] = fw_lprobs[prefix_mask].scatter_(
|
||||
-1, prefix_toks[prefix_mask].unsqueeze(-1), prefix_fw_lprobs
|
||||
)
|
||||
|
||||
prefix_ch_lm_lprobs = ch_lm_lprobs.gather(-1, prefix_toks.unsqueeze(-1))
|
||||
ch_lm_lprobs[prefix_mask] = -math.inf
|
||||
ch_lm_lprobs[prefix_mask] = ch_lm_lprobs[prefix_mask].scatter_(
|
||||
-1, prefix_toks[prefix_mask].unsqueeze(-1), prefix_ch_lm_lprobs
|
||||
)
|
||||
|
||||
prefix_lm_lprobs = lm_lprobs.gather(-1, prefix_toks.unsqueeze(-1))
|
||||
lm_lprobs[prefix_mask] = -math.inf
|
||||
lm_lprobs[prefix_mask] = lm_lprobs[prefix_mask].scatter_(
|
||||
-1, prefix_toks[prefix_mask].unsqueeze(-1), prefix_lm_lprobs
|
||||
)
|
||||
|
||||
# if prefix includes eos, then we should make sure tokens and
|
||||
# scores are the same across all beams
|
||||
eos_mask = prefix_toks.eq(self.eos)
|
||||
if eos_mask.any():
|
||||
# validate that the first beam matches the prefix
|
||||
first_beam = tokens[eos_mask].view(-1, beam_size, tokens.size(-1))[:, 0, 1:step + 1]
|
||||
eos_mask_batch_dim = eos_mask.view(-1, beam_size)[:, 0]
|
||||
target_prefix = prefix_tokens[eos_mask_batch_dim][:, :step]
|
||||
assert (first_beam == target_prefix).all()
|
||||
|
||||
def replicate_first_beam(tensor, mask):
|
||||
tensor = tensor.view(-1, beam_size, tensor.size(-1))
|
||||
tensor[mask] = tensor[mask][:, :1, :]
|
||||
return tensor.view(-1, tensor.size(-1))
|
||||
|
||||
# copy tokens, scores and lprobs from the first beam to all beams
|
||||
tokens = replicate_first_beam(tokens, eos_mask_batch_dim)
|
||||
scores = replicate_first_beam(scores, eos_mask_batch_dim)
|
||||
|
||||
fw_lprobs = replicate_first_beam(fw_lprobs, eos_mask_batch_dim)
|
||||
ch_lm_lprobs = replicate_first_beam(ch_lm_lprobs, eos_mask_batch_dim)
|
||||
lm_lprobs = replicate_first_beam(lm_lprobs, eos_mask_batch_dim)
|
||||
|
||||
if self.no_repeat_ngram_size > 0:
|
||||
# for each beam and batch sentence, generate a list of previous ngrams
|
||||
gen_ngrams = [{} for bbsz_idx in range(bsz * beam_size)]
|
||||
for bbsz_idx in range(bsz * beam_size):
|
||||
gen_tokens = tokens[bbsz_idx].tolist()
|
||||
for ngram in zip(*[gen_tokens[i:] for i in range(self.no_repeat_ngram_size)]):
|
||||
gen_ngrams[bbsz_idx][tuple(ngram[:-1])] = \
|
||||
gen_ngrams[bbsz_idx].get(tuple(ngram[:-1]), []) + [ngram[-1]]
|
||||
|
||||
# Record attention scores
|
||||
if avg_attn_scores is not None:
|
||||
if attn is None:
|
||||
attn = scores.new(bsz * beam_size, src_tokens.size(1), max_len + 2)
|
||||
attn_buf = attn.clone()
|
||||
nonpad_idxs = src_tokens.ne(self.pad)
|
||||
attn[:, :, step + 1].copy_(avg_attn_scores)
|
||||
|
||||
scores = scores.type_as(fw_lprobs)
|
||||
scores_buf = scores_buf.type_as(fw_lprobs)
|
||||
|
||||
self.search.set_src_lengths(src_lengths_no_eos)
|
||||
|
||||
if self.no_repeat_ngram_size > 0:
|
||||
def calculate_banned_tokens(bbsz_idx):
|
||||
# before decoding the next token, prevent decoding of ngrams that have already appeared
|
||||
ngram_index = tuple(tokens[bbsz_idx, step + 2 - self.no_repeat_ngram_size:step + 1].tolist())
|
||||
return gen_ngrams[bbsz_idx].get(ngram_index, [])
|
||||
|
||||
if step + 2 - self.no_repeat_ngram_size >= 0:
|
||||
# no banned tokens if we haven't generated no_repeat_ngram_size tokens yet
|
||||
banned_tokens = [calculate_banned_tokens(bbsz_idx) for bbsz_idx in range(bsz * beam_size)]
|
||||
else:
|
||||
banned_tokens = [[] for bbsz_idx in range(bsz * beam_size)]
|
||||
|
||||
for bbsz_idx in range(bsz * beam_size):
|
||||
fw_lprobs[bbsz_idx, banned_tokens[bbsz_idx]] = -math.inf
|
||||
|
||||
combined_noisy_channel_scores, fw_lprobs_top_k, lm_lprobs_top_k, cand_indices, cand_beams = self.search.step(
|
||||
step,
|
||||
fw_lprobs.view(bsz, -1, self.vocab_size),
|
||||
scores.view(bsz, beam_size, -1)[:, :, :step], ch_lm_lprobs.view(bsz, -1, self.vocab_size),
|
||||
lm_lprobs.view(bsz, -1, self.vocab_size), self.combine_method
|
||||
)
|
||||
|
||||
# cand_bbsz_idx contains beam indices for the top candidate
|
||||
# hypotheses, with a range of values: [0, bsz*beam_size),
|
||||
# and dimensions: [bsz, cand_size]
|
||||
cand_bbsz_idx = cand_beams.add(bbsz_offsets)
|
||||
|
||||
# finalize hypotheses that end in eos (except for candidates to be ignored)
|
||||
eos_mask = cand_indices.eq(self.eos)
|
||||
eos_mask[:, :beam_size] &= ~cands_to_ignore
|
||||
|
||||
# only consider eos when it's among the top beam_size indices
|
||||
eos_bbsz_idx = torch.masked_select(
|
||||
cand_bbsz_idx[:, :beam_size], mask=eos_mask[:, :beam_size]
|
||||
)
|
||||
|
||||
finalized_sents = set()
|
||||
if eos_bbsz_idx.numel() > 0:
|
||||
eos_scores = torch.masked_select(
|
||||
fw_lprobs_top_k[:, :beam_size], mask=eos_mask[:, :beam_size]
|
||||
)
|
||||
combined_noisy_channel_eos_scores = torch.masked_select(
|
||||
combined_noisy_channel_scores[:, :beam_size],
|
||||
mask=eos_mask[:, :beam_size],
|
||||
)
|
||||
|
||||
# finalize hypo using channel model score
|
||||
finalized_sents = finalize_hypos(
|
||||
step, eos_bbsz_idx, eos_scores, combined_noisy_channel_eos_scores)
|
||||
|
||||
num_remaining_sent -= len(finalized_sents)
|
||||
|
||||
assert num_remaining_sent >= 0
|
||||
if num_remaining_sent == 0:
|
||||
break
|
||||
|
||||
if len(finalized_sents) > 0:
|
||||
new_bsz = bsz - len(finalized_sents)
|
||||
|
||||
# construct batch_idxs which holds indices of batches to keep for the next pass
|
||||
batch_mask = cand_indices.new_ones(bsz)
|
||||
batch_mask[cand_indices.new(finalized_sents)] = 0
|
||||
batch_idxs = torch.nonzero(batch_mask).squeeze(-1)
|
||||
|
||||
eos_mask = eos_mask[batch_idxs]
|
||||
cand_beams = cand_beams[batch_idxs]
|
||||
bbsz_offsets.resize_(new_bsz, 1)
|
||||
cand_bbsz_idx = cand_beams.add(bbsz_offsets)
|
||||
|
||||
lm_lprobs_top_k = lm_lprobs_top_k[batch_idxs]
|
||||
|
||||
fw_lprobs_top_k = fw_lprobs_top_k[batch_idxs]
|
||||
cand_indices = cand_indices[batch_idxs]
|
||||
if prefix_tokens is not None:
|
||||
prefix_tokens = prefix_tokens[batch_idxs]
|
||||
src_lengths_no_eos = src_lengths_no_eos[batch_idxs]
|
||||
cands_to_ignore = cands_to_ignore[batch_idxs]
|
||||
|
||||
scores = scores.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, -1)
|
||||
scores_buf.resize_as_(scores)
|
||||
tokens = tokens.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, -1)
|
||||
tokens_buf.resize_as_(tokens)
|
||||
src_tokens = src_tokens.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, -1)
|
||||
src_lengths = src_lengths.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, -1)
|
||||
lm_prefix_scores = lm_prefix_scores.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, -1).squeeze()
|
||||
|
||||
if attn is not None:
|
||||
attn = attn.view(bsz, -1)[batch_idxs].view(new_bsz * beam_size, attn.size(1), -1)
|
||||
attn_buf.resize_as_(attn)
|
||||
bsz = new_bsz
|
||||
else:
|
||||
batch_idxs = None
|
||||
|
||||
# Set active_mask so that values > cand_size indicate eos or
|
||||
# ignored hypos and values < cand_size indicate candidate
|
||||
# active hypos. After this, the min values per row are the top
|
||||
# candidate active hypos.
|
||||
eos_mask[:, :beam_size] |= cands_to_ignore
|
||||
active_mask = torch.add(
|
||||
eos_mask.type_as(cand_offsets) * cand_size,
|
||||
cand_offsets[: eos_mask.size(1)],
|
||||
)
|
||||
|
||||
# get the top beam_size active hypotheses, which are just the hypos
|
||||
# with the smallest values in active_mask
|
||||
active_hypos, new_cands_to_ignore = buffer('active_hypos'), buffer('new_cands_to_ignore')
|
||||
torch.topk(
|
||||
active_mask, k=beam_size, dim=1, largest=False,
|
||||
out=(new_cands_to_ignore, active_hypos)
|
||||
)
|
||||
|
||||
# update cands_to_ignore to ignore any finalized hypos
|
||||
cands_to_ignore = new_cands_to_ignore.ge(cand_size)[:, :beam_size]
|
||||
assert (~cands_to_ignore).any(dim=1).all()
|
||||
|
||||
active_bbsz_idx = buffer('active_bbsz_idx')
|
||||
torch.gather(
|
||||
cand_bbsz_idx, dim=1, index=active_hypos,
|
||||
out=active_bbsz_idx,
|
||||
)
|
||||
active_scores = torch.gather(
|
||||
fw_lprobs_top_k, dim=1, index=active_hypos,
|
||||
out=scores[:, step].view(bsz, beam_size),
|
||||
)
|
||||
|
||||
active_bbsz_idx = active_bbsz_idx.view(-1)
|
||||
active_scores = active_scores.view(-1)
|
||||
|
||||
# copy tokens and scores for active hypotheses
|
||||
torch.index_select(
|
||||
tokens[:, :step + 1], dim=0, index=active_bbsz_idx,
|
||||
out=tokens_buf[:, :step + 1],
|
||||
)
|
||||
torch.gather(
|
||||
cand_indices, dim=1, index=active_hypos,
|
||||
out=tokens_buf.view(bsz, beam_size, -1)[:, :, step + 1],
|
||||
)
|
||||
if step > 0:
|
||||
torch.index_select(
|
||||
scores[:, :step], dim=0, index=active_bbsz_idx,
|
||||
out=scores_buf[:, :step],
|
||||
)
|
||||
torch.gather(
|
||||
fw_lprobs_top_k, dim=1, index=active_hypos,
|
||||
out=scores_buf.view(bsz, beam_size, -1)[:, :, step],
|
||||
)
|
||||
torch.gather(
|
||||
lm_lprobs_top_k, dim=1, index=active_hypos,
|
||||
out=lm_prefix_scores.view(bsz, beam_size)
|
||||
)
|
||||
|
||||
# copy attention for active hypotheses
|
||||
if attn is not None:
|
||||
torch.index_select(
|
||||
attn[:, :, :step + 2], dim=0, index=active_bbsz_idx,
|
||||
out=attn_buf[:, :, :step + 2],
|
||||
)
|
||||
|
||||
# swap buffers
|
||||
tokens, tokens_buf = tokens_buf, tokens
|
||||
scores, scores_buf = scores_buf, scores
|
||||
if attn is not None:
|
||||
attn, attn_buf = attn_buf, attn
|
||||
|
||||
# reorder incremental state in decoder
|
||||
reorder_state = active_bbsz_idx
|
||||
|
||||
# sort by score descending
|
||||
for sent in range(len(finalized)):
|
||||
finalized[sent] = sorted(finalized[sent], key=lambda r: r['score'], reverse=True)
|
||||
|
||||
return finalized
|
||||
|
||||
|
||||
def get_lm_scores(model, input_tokens, incremental_states, cand_tokens, input_len, k):
|
||||
with torch.no_grad():
|
||||
lm_lprobs, avg_attn_scores = model.forward_decoder(
|
||||
input_tokens, encoder_outs=None, incremental_states=incremental_states,
|
||||
)
|
||||
|
||||
lm_lprobs_size = lm_lprobs.size(0)
|
||||
probs_next_wrd = torch.gather(lm_lprobs.repeat(1, k).view(lm_lprobs_size*k, -1), 1, cand_tokens).squeeze().view(-1)
|
||||
|
||||
return probs_next_wrd
|
||||
|
||||
|
||||
def make_dict2dict(old_dict, new_dict):
|
||||
dict2dict_map = {}
|
||||
for sym in old_dict.symbols:
|
||||
dict2dict_map[old_dict.index(sym)] = new_dict.index(sym)
|
||||
return dict2dict_map
|
||||
|
||||
|
||||
def dict2dict(tokens, dict2dict_map):
|
||||
if tokens.device == torch.device('cpu'):
|
||||
tokens_tmp = tokens
|
||||
else:
|
||||
tokens_tmp = tokens.cpu()
|
||||
return tokens_tmp.map_(
|
||||
tokens_tmp,
|
||||
lambda _, val, dict2dict_map=dict2dict_map : dict2dict_map[float(val)]
|
||||
).to(tokens.device)
|
||||
|
||||
|
||||
def reorder_tokens(tokens, lengths, eos):
|
||||
# reorder source tokens so they may be used as reference for P(S|T)
|
||||
return torch.cat((tokens.new([eos]), tokens[-lengths:-1], tokens[:-lengths]), 0)
|
||||
|
||||
|
||||
def reorder_all_tokens(tokens, lengths, eos):
|
||||
# used to reorder src tokens from [<pad> <w1> <w2> .. <eos>] to [<eos> <w1> <w2>...<pad>]
|
||||
# so source tokens can be used to predict P(S|T)
|
||||
return torch.stack([reorder_tokens(token, length, eos) for token, length in zip(tokens, lengths)])
|
||||
|
||||
|
||||
def normalized_scores_with_batch_vocab(
|
||||
model_decoder, features, target_ids, k, bsz, beam_size,
|
||||
pad_idx, top_k=0, vocab_size_meter=None, start_idx=None,
|
||||
end_idx=None, **kwargs):
|
||||
"""
|
||||
Get normalized probabilities (or log probs) from a net's output
|
||||
w.r.t. vocab consisting of target IDs in the batch
|
||||
"""
|
||||
if model_decoder.adaptive_softmax is None:
|
||||
weight = model_decoder.output_projection.weight
|
||||
vocab_ids = torch.unique(
|
||||
torch.cat(
|
||||
(torch.unique(target_ids), torch.arange(top_k, device=target_ids.device))
|
||||
)
|
||||
)
|
||||
id_map = dict(zip(vocab_ids.tolist(), range(len(vocab_ids))))
|
||||
mapped_target_ids = target_ids.cpu().apply_(
|
||||
lambda x, id_map=id_map: id_map[x]
|
||||
).to(target_ids.device)
|
||||
expanded_target_ids = mapped_target_ids[:, :].repeat(1, k).view(bsz*beam_size*k, -1)
|
||||
if start_idx is not None and end_idx is not None:
|
||||
expanded_target_ids = expanded_target_ids[start_idx:end_idx, :]
|
||||
logits = F.linear(features, weight[vocab_ids, :])
|
||||
log_softmax = F.log_softmax(logits, dim=-1, dtype=torch.float32)
|
||||
intermed_scores = torch.gather(
|
||||
log_softmax[:, :-1, :],
|
||||
2,
|
||||
expanded_target_ids[:, 1:].unsqueeze(2),
|
||||
).squeeze()
|
||||
not_padding = expanded_target_ids[:, 1:] != pad_idx
|
||||
intermed_scores *= not_padding.float()
|
||||
return intermed_scores
|
||||
else:
|
||||
raise ValueError("adaptive softmax doesn't work with " +
|
||||
"`normalized_scores_with_batch_vocab()`")
|
||||
@@ -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.
|
||||
|
||||
from fairseq.tasks.translation import TranslationTask
|
||||
from fairseq.tasks.language_modeling import LanguageModelingTask
|
||||
from fairseq import checkpoint_utils
|
||||
import argparse
|
||||
from fairseq.tasks import register_task
|
||||
import torch
|
||||
|
||||
|
||||
@register_task("noisy_channel_translation")
|
||||
class NoisyChannelTranslation(TranslationTask):
|
||||
"""
|
||||
Rescore the top k candidates from each beam using noisy channel modeling
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
TranslationTask.add_args(parser)
|
||||
# fmt: off
|
||||
parser.add_argument('--channel-model', metavar='FILE',
|
||||
help='path to P(S|T) model. P(S|T) and P(T|S) must share source and target dictionaries.')
|
||||
parser.add_argument('--combine-method', default='lm_only',
|
||||
choices=['lm_only', 'noisy_channel'],
|
||||
help="""method for combining direct and channel model scores.
|
||||
lm_only: decode with P(T|S)P(T)
|
||||
noisy_channel: decode with 1/t P(T|S) + 1/s(P(S|T)P(T))""")
|
||||
parser.add_argument('--normalize-lm-scores-by-tgt-len', action='store_true', default=False,
|
||||
help='normalize lm score by target length instead of source length')
|
||||
parser.add_argument('--channel-scoring-type', default='log_norm', choices=['unnormalized', 'log_norm', 'k2_separate', 'src_vocab', 'src_vocab_batched'],
|
||||
help="Normalize bw scores with log softmax or return bw scores without log softmax")
|
||||
parser.add_argument('--top-k-vocab', default=0, type=int,
|
||||
help='top k vocab IDs to use with `src_vocab` in channel model scoring')
|
||||
parser.add_argument('--k2', default=50, type=int,
|
||||
help='the top k2 candidates to rescore with the noisy channel model for each beam')
|
||||
parser.add_argument('--ch-wt', default=1, type=float,
|
||||
help='weight for the channel model')
|
||||
parser.add_argument('--lm-model', metavar='FILE',
|
||||
help='path to lm model file, to model P(T). P(T) must share the same vocab as the direct model on the target side')
|
||||
parser.add_argument('--lm-data', metavar='FILE',
|
||||
help='path to lm model training data for target language, used to properly load LM with correct dictionary')
|
||||
parser.add_argument('--lm-wt', default=1, type=float,
|
||||
help='the weight of the lm in joint decoding')
|
||||
# fmt: on
|
||||
|
||||
def build_generator(
|
||||
self, models, args, seq_gen_cls=None, extra_gen_cls_kwargs=None
|
||||
):
|
||||
if getattr(args, "score_reference", False):
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
from .noisy_channel_sequence_generator import NoisyChannelSequenceGenerator
|
||||
use_cuda = torch.cuda.is_available() and not self.args.cpu
|
||||
assert self.args.lm_model is not None, '--lm-model required for noisy channel generation!'
|
||||
assert self.args.lm_data is not None, '--lm-data required for noisy channel generation to map between LM and bitext vocabs'
|
||||
if self.args.channel_model is not None:
|
||||
import copy
|
||||
ch_args_task = copy.deepcopy(self.args)
|
||||
tmp = ch_args_task.source_lang
|
||||
ch_args_task.source_lang = ch_args_task.target_lang
|
||||
ch_args_task.target_lang = tmp
|
||||
ch_args_task._name = 'translation'
|
||||
channel_task = TranslationTask.setup_task(ch_args_task)
|
||||
|
||||
arg_dict = {}
|
||||
arg_dict['task'] = 'language_modeling'
|
||||
arg_dict['sample_break_mode'] = 'eos'
|
||||
arg_dict['data'] = self.args.lm_data
|
||||
arg_dict['output_dictionary_size'] = -1
|
||||
lm_args = argparse.Namespace(**arg_dict)
|
||||
lm_task = LanguageModelingTask.setup_task(lm_args)
|
||||
lm_dict = lm_task.output_dictionary
|
||||
|
||||
if self.args.channel_model is not None:
|
||||
channel_models, _ = checkpoint_utils.load_model_ensemble(self.args.channel_model.split(':'), task=channel_task)
|
||||
|
||||
for model in channel_models:
|
||||
model.make_generation_fast_(
|
||||
beamable_mm_beam_size=None if args.no_beamable_mm else args.beam,
|
||||
need_attn=args.print_alignment,
|
||||
)
|
||||
if self.args.fp16:
|
||||
model.half()
|
||||
if use_cuda:
|
||||
model.cuda()
|
||||
else:
|
||||
channel_models = None
|
||||
|
||||
lm_models, _ = checkpoint_utils.load_model_ensemble(self.args.lm_model.split(':'), task=lm_task)
|
||||
|
||||
for model in lm_models:
|
||||
model.make_generation_fast_(
|
||||
beamable_mm_beam_size=None if args.no_beamable_mm else args.beam,
|
||||
need_attn=args.print_alignment,
|
||||
)
|
||||
if self.args.fp16:
|
||||
model.half()
|
||||
if use_cuda:
|
||||
model.cuda()
|
||||
return NoisyChannelSequenceGenerator(
|
||||
combine_method=self.args.combine_method,
|
||||
tgt_dict=self.target_dictionary,
|
||||
src_dict=self.source_dictionary,
|
||||
beam_size=getattr(args, 'beam', 5),
|
||||
max_len_a=getattr(args, 'max_len_a', 0),
|
||||
max_len_b=getattr(args, 'max_len_b', 200),
|
||||
min_len=getattr(args, 'min_len', 1),
|
||||
len_penalty=getattr(args, 'lenpen', 1),
|
||||
unk_penalty=getattr(args, 'unkpen', 0),
|
||||
temperature=getattr(args, 'temperature', 1.),
|
||||
match_source_len=getattr(args, 'match_source_len', False),
|
||||
no_repeat_ngram_size=getattr(args, 'no_repeat_ngram_size', 0),
|
||||
normalize_scores=(not getattr(args, 'unnormalized', False)),
|
||||
channel_models=channel_models,
|
||||
k2=getattr(self.args, 'k2', 50),
|
||||
ch_weight=getattr(self.args, 'ch_wt', 1),
|
||||
channel_scoring_type=self.args.channel_scoring_type,
|
||||
top_k_vocab=self.args.top_k_vocab,
|
||||
lm_models=lm_models,
|
||||
lm_dict=lm_dict,
|
||||
lm_weight=getattr(self.args, 'lm_wt', 1),
|
||||
normalize_lm_scores_by_tgt_len=getattr(self.args, 'normalize_lm_scores_by_tgt_len', False),
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
# GottBERT: a pure German language model
|
||||
|
||||
## Introduction
|
||||
|
||||
[GottBERT](http://arxiv.org/abs/2012.02110) is a pretrained language model trained on 145GB of German text based on RoBERTa.
|
||||
|
||||
## Example usage
|
||||
|
||||
### fairseq
|
||||
##### Load GottBERT from torch.hub (PyTorch >= 1.1):
|
||||
```python
|
||||
import torch
|
||||
gottbert = torch.hub.load('pytorch/fairseq', 'gottbert-base')
|
||||
gottbert.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Load GottBERT (for PyTorch 1.0 or custom models):
|
||||
```python
|
||||
# Download gottbert model
|
||||
wget https://dl.gottbert.de/fairseq/models/gottbert-base.tar.gz
|
||||
tar -xzvf gottbert.tar.gz
|
||||
|
||||
# Load the model in fairseq
|
||||
from fairseq.models.roberta import GottbertModel
|
||||
gottbert = GottbertModel.from_pretrained('/path/to/gottbert')
|
||||
gottbert.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Filling masks:
|
||||
```python
|
||||
masked_line = 'Gott ist <mask> ! :)'
|
||||
gottbert.fill_mask(masked_line, topk=3)
|
||||
# [('Gott ist gut ! :)', 0.3642110526561737, ' gut'),
|
||||
# ('Gott ist überall ! :)', 0.06009674072265625, ' überall'),
|
||||
# ('Gott ist großartig ! :)', 0.0370681993663311, ' großartig')]
|
||||
```
|
||||
|
||||
##### Extract features from GottBERT
|
||||
|
||||
```python
|
||||
# Extract the last layer's features
|
||||
line = "Der erste Schluck aus dem Becher der Naturwissenschaft macht atheistisch , aber auf dem Grunde des Bechers wartet Gott !"
|
||||
tokens = gottbert.encode(line)
|
||||
last_layer_features = gottbert.extract_features(tokens)
|
||||
assert last_layer_features.size() == torch.Size([1, 27, 768])
|
||||
|
||||
# Extract all layer's features (layer 0 is the embedding layer)
|
||||
all_layers = gottbert.extract_features(tokens, return_all_hiddens=True)
|
||||
assert len(all_layers) == 13
|
||||
assert torch.all(all_layers[-1] == last_layer_features)
|
||||
```
|
||||
## Citation
|
||||
If you use our work, please cite:
|
||||
|
||||
```bibtex
|
||||
@misc{scheible2020gottbert,
|
||||
title={GottBERT: a pure German Language Model},
|
||||
author={Raphael Scheible and Fabian Thomczyk and Patric Tippmann and Victor Jaravine and Martin Boeker},
|
||||
year={2020},
|
||||
eprint={2012.02110},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CL}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
# Jointly Learning to Align and Translate with Transformer Models (Garg et al., 2019)
|
||||
|
||||
This page includes instructions for training models described in [Jointly Learning to Align and Translate with Transformer Models (Garg et al., 2019)](https://arxiv.org/abs/1909.02074).
|
||||
|
||||
## Training a joint alignment-translation model on WMT'18 En-De
|
||||
|
||||
##### 1. Extract and preprocess the WMT'18 En-De data
|
||||
```bash
|
||||
./prepare-wmt18en2de_no_norm_no_escape_no_agressive.sh
|
||||
```
|
||||
|
||||
##### 2. Generate alignments from statistical alignment toolkits e.g. Giza++/FastAlign.
|
||||
In this example, we use FastAlign.
|
||||
```bash
|
||||
git clone git@github.com:clab/fast_align.git
|
||||
pushd fast_align
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
popd
|
||||
ALIGN=fast_align/build/fast_align
|
||||
paste bpe.32k/train.en bpe.32k/train.de | awk -F '\t' '{print $1 " ||| " $2}' > bpe.32k/train.en-de
|
||||
$ALIGN -i bpe.32k/train.en-de -d -o -v > bpe.32k/train.align
|
||||
```
|
||||
|
||||
##### 3. Preprocess the dataset with the above generated alignments.
|
||||
```bash
|
||||
fairseq-preprocess \
|
||||
--source-lang en --target-lang de \
|
||||
--trainpref bpe.32k/train \
|
||||
--validpref bpe.32k/valid \
|
||||
--testpref bpe.32k/test \
|
||||
--align-suffix align \
|
||||
--destdir binarized/ \
|
||||
--joined-dictionary \
|
||||
--workers 32
|
||||
```
|
||||
|
||||
##### 4. Train a model
|
||||
```bash
|
||||
fairseq-train \
|
||||
binarized \
|
||||
--arch transformer_wmt_en_de_big_align --share-all-embeddings \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --clip-norm 0.0 --activation-fn relu\
|
||||
--lr 0.0002 --lr-scheduler inverse_sqrt --warmup-updates 4000 --warmup-init-lr 1e-07 \
|
||||
--dropout 0.3 --attention-dropout 0.1 --weight-decay 0.0 \
|
||||
--max-tokens 3500 --label-smoothing 0.1 \
|
||||
--save-dir ./checkpoints --log-interval 1000 --max-update 60000 \
|
||||
--keep-interval-updates -1 --save-interval-updates 0 \
|
||||
--load-alignments --criterion label_smoothed_cross_entropy_with_alignment \
|
||||
--fp16
|
||||
```
|
||||
|
||||
Note that the `--fp16` flag requires you have CUDA 9.1 or greater and a Volta GPU or newer.
|
||||
|
||||
If you want to train the above model with big batches (assuming your machine has 8 GPUs):
|
||||
- add `--update-freq 8` to simulate training on 8x8=64 GPUs
|
||||
- increase the learning rate; 0.0007 works well for big batches
|
||||
|
||||
##### 5. Evaluate and generate the alignments (BPE level)
|
||||
```bash
|
||||
fairseq-generate \
|
||||
binarized --gen-subset test --print-alignment \
|
||||
--source-lang en --target-lang de \
|
||||
--path checkpoints/checkpoint_best.pt --beam 5 --nbest 1
|
||||
```
|
||||
|
||||
##### 6. Other resources.
|
||||
The code for:
|
||||
1. preparing alignment test sets
|
||||
2. converting BPE level alignments to token level alignments
|
||||
3. symmetrizing bidirectional alignments
|
||||
4. evaluating alignments using AER metric
|
||||
can be found [here](https://github.com/lilt/alignment-scripts)
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@inproceedings{garg2019jointly,
|
||||
title = {Jointly Learning to Align and Translate with Transformer Models},
|
||||
author = {Garg, Sarthak and Peitz, Stephan and Nallasamy, Udhyakumar and Paulik, Matthias},
|
||||
booktitle = {Conference on Empirical Methods in Natural Language Processing (EMNLP)},
|
||||
address = {Hong Kong},
|
||||
month = {November},
|
||||
url = {https://arxiv.org/abs/1909.02074},
|
||||
year = {2019},
|
||||
}
|
||||
```
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
echo 'Cloning Moses github repository (for tokenization scripts)...'
|
||||
git clone https://github.com/moses-smt/mosesdecoder.git
|
||||
|
||||
SCRIPTS=mosesdecoder/scripts
|
||||
TOKENIZER=$SCRIPTS/tokenizer/tokenizer.perl
|
||||
CLEAN=$SCRIPTS/training/clean-corpus-n.perl
|
||||
REM_NON_PRINT_CHAR=$SCRIPTS/tokenizer/remove-non-printing-char.perl
|
||||
|
||||
URLS=(
|
||||
"http://statmt.org/wmt13/training-parallel-europarl-v7.tgz"
|
||||
"http://statmt.org/wmt13/training-parallel-commoncrawl.tgz"
|
||||
"http://data.statmt.org/wmt18/translation-task/training-parallel-nc-v13.tgz"
|
||||
"http://data.statmt.org/wmt18/translation-task/rapid2016.tgz"
|
||||
"http://data.statmt.org/wmt17/translation-task/dev.tgz"
|
||||
"http://statmt.org/wmt14/test-full.tgz"
|
||||
)
|
||||
CORPORA=(
|
||||
"training/europarl-v7.de-en"
|
||||
"commoncrawl.de-en"
|
||||
"training-parallel-nc-v13/news-commentary-v13.de-en"
|
||||
"rapid2016.de-en"
|
||||
)
|
||||
|
||||
if [ ! -d "$SCRIPTS" ]; then
|
||||
echo "Please set SCRIPTS variable correctly to point to Moses scripts."
|
||||
exit
|
||||
fi
|
||||
|
||||
src=en
|
||||
tgt=de
|
||||
lang=en-de
|
||||
prep=wmt18_en_de
|
||||
tmp=$prep/tmp
|
||||
orig=orig
|
||||
dev=dev/newstest2012
|
||||
codes=32000
|
||||
bpe=bpe.32k
|
||||
|
||||
mkdir -p $orig $tmp $prep $bpe
|
||||
|
||||
cd $orig
|
||||
|
||||
for ((i=0;i<${#URLS[@]};++i)); do
|
||||
url=${URLS[i]}
|
||||
file=$(basename $url)
|
||||
if [ -f $file ]; then
|
||||
echo "$file already exists, skipping download"
|
||||
else
|
||||
wget "$url"
|
||||
if [ -f $file ]; then
|
||||
echo "$url successfully downloaded."
|
||||
else
|
||||
echo "$url not successfully downloaded."
|
||||
exit 1
|
||||
fi
|
||||
if [ ${file: -4} == ".tgz" ]; then
|
||||
tar zxvf $file
|
||||
elif [ ${file: -4} == ".tar" ]; then
|
||||
tar xvf $file
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
|
||||
echo "pre-processing train data..."
|
||||
for l in $src $tgt; do
|
||||
rm -rf $tmp/train.tags.$lang.tok.$l
|
||||
for f in "${CORPORA[@]}"; do
|
||||
cat $orig/$f.$l | \
|
||||
perl $REM_NON_PRINT_CHAR | \
|
||||
perl $TOKENIZER -threads 8 -l $l -no-escape >> $tmp/train.tags.$lang.tok.$l
|
||||
done
|
||||
done
|
||||
|
||||
echo "pre-processing test data..."
|
||||
for l in $src $tgt; do
|
||||
if [ "$l" == "$src" ]; then
|
||||
t="src"
|
||||
else
|
||||
t="ref"
|
||||
fi
|
||||
grep '<seg id' $orig/test-full/newstest2014-deen-$t.$l.sgm | \
|
||||
sed -e 's/<seg id="[0-9]*">\s*//g' | \
|
||||
sed -e 's/\s*<\/seg>\s*//g' | \
|
||||
sed -e "s/\’/\'/g" | \
|
||||
perl $TOKENIZER -threads 8 -l $l -no-escape > $tmp/test.$l
|
||||
echo ""
|
||||
done
|
||||
|
||||
# apply length filtering before BPE
|
||||
perl $CLEAN -ratio 1.5 $tmp/train.tags.$lang.tok $src $tgt $tmp/train 1 100
|
||||
|
||||
# use newstest2012 for valid
|
||||
echo "pre-processing valid data..."
|
||||
for l in $src $tgt; do
|
||||
rm -rf $tmp/valid.$l
|
||||
cat $orig/$dev.$l | \
|
||||
perl $REM_NON_PRINT_CHAR | \
|
||||
perl $TOKENIZER -threads 8 -l $l -no-escape >> $tmp/valid.$l
|
||||
done
|
||||
|
||||
mkdir output
|
||||
mv $tmp/{train,valid,test}.{$src,$tgt} output
|
||||
|
||||
#BPE
|
||||
git clone https://github.com/glample/fastBPE.git
|
||||
pushd fastBPE
|
||||
g++ -std=c++11 -pthread -O3 fastBPE/main.cc -IfastBPE -o fast
|
||||
popd
|
||||
fastBPE/fast learnbpe $codes output/train.$src output/train.$tgt > $bpe/codes
|
||||
for split in {train,valid,test}; do for lang in {en,de}; do fastBPE/fast applybpe $bpe/$split.$lang output/$split.$lang $bpe/codes; done; done
|
||||
@@ -0,0 +1,39 @@
|
||||
# Adaptive Input Representations for Neural Language Modeling (Baevski and Auli, 2018)
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Description | Parameters | Dataset | Model and Test set(s)
|
||||
---|---:|---|---
|
||||
Adaptive Inputs <br> ([Baevski and Auli, 2018](https://arxiv.org/abs/1809.10853)) | 1026M | [Google Billion Words](https://github.com/ciprian-chelba/1-billion-word-language-modeling-benchmark) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/lm/adaptive_lm_gbw_huge.tar.bz2)
|
||||
Adaptive Inputs <br> ([Baevski and Auli, 2018](https://arxiv.org/abs/1809.10853)) | 247M | [WikiText-103](https://blog.einstein.ai/the-wikitext-long-term-dependency-language-modeling-dataset/) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/lm/adaptive_lm_wiki103.v2.tar.bz2)
|
||||
|
||||
## Training an LM with adaptive inputs
|
||||
|
||||
First, see the general [language modeling README](README.md) for instructions on
|
||||
preprocessing the WikiText-103 data.
|
||||
|
||||
Then use the following training command to train a model with adaptive inputs
|
||||
using the `transformer_lm_wiki103` model architecture:
|
||||
```bash
|
||||
fairseq-train --task language_modeling \
|
||||
data-bin/wikitext-103 \
|
||||
--save-dir checkpoints/transformer_wikitext-103 \
|
||||
--arch transformer_lm_wiki103 \
|
||||
--max-update 286000 --lr 1.0 --t-mult 2 --lr-period-updates 270000 --lr-scheduler cosine --lr-shrink 0.75 \
|
||||
--warmup-updates 16000 --warmup-init-lr 1e-07 --stop-min-lr 1e-09 --optimizer nag --min-lr 0.0001 --clip-norm 0.1 \
|
||||
--criterion adaptive_loss --max-tokens 3072 --update-freq 3 --tokens-per-sample 3072 --seed 1 \
|
||||
--sample-break-mode none --skip-invalid-size-inputs-valid-test --ddp-backend=no_c10d
|
||||
```
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@inproceedings{
|
||||
baevski2018adaptive,
|
||||
title={Adaptive Input Representations for Neural Language Modeling},
|
||||
author={Alexei Baevski and Michael Auli},
|
||||
booktitle={International Conference on Learning Representations},
|
||||
year={2019},
|
||||
url={https://openreview.net/forum?id=ByxZX20qFQ},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Language Modeling with Gated Convolutional Networks (Dauphin et al., 2017)
|
||||
|
||||
## Example usage
|
||||
|
||||
First download and preprocess the data following the main [language modeling README](README.md).
|
||||
|
||||
Then to train a convolutional LM using the `fconv_lm_dauphin_wikitext103`
|
||||
architecture:
|
||||
```bash
|
||||
fairseq-train --task language_modeling \
|
||||
data-bin/wikitext-103 \
|
||||
--save-dir checkpoints/fconv_wikitext-103 \
|
||||
--arch fconv_lm_dauphin_wikitext103 \
|
||||
--adaptive-softmax-cutoff 10000,20000,200000 \
|
||||
--dropout 0.2 \
|
||||
--criterion adaptive_loss \
|
||||
--optimizer nag --clip-norm 0.1 --weight-decay 5e-06 \
|
||||
--lr 1.0 --lr-scheduler reduce_lr_on_plateau --lr-shrink 0.5 \
|
||||
--max-tokens 1024 --tokens-per-sample 1024 \
|
||||
--ddp-backend no_c10d \
|
||||
--max-epoch 35
|
||||
```
|
||||
|
||||
And evaluate with:
|
||||
```bash
|
||||
fairseq-eval-lm data-bin/wikitext-103 --path checkpoints/fconv_wiki103/checkpoint_best.pt
|
||||
```
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@inproceedings{dauphin2017language,
|
||||
title={Language Modeling with Gated Convolutional Networks},
|
||||
author={Dauphin, Yann N and Fan, Angela and Auli, Michael and Grangier, David},
|
||||
booktitle={Proceedings of the 34th International Conference on Machine Learning-Volume 70},
|
||||
pages={933--941},
|
||||
year={2017},
|
||||
organization={JMLR}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
# Neural Language Modeling
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | Dataset | Download
|
||||
---|---|---|---
|
||||
`transformer_lm.gbw.adaptive_huge` | Adaptive Inputs <br> ([Baevski and Auli, 2018](https://arxiv.org/abs/1809.10853)) <br> 1026M params | [Google Billion Words](https://github.com/ciprian-chelba/1-billion-word-language-modeling-benchmark) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/lm/adaptive_lm_gbw_huge.tar.bz2)
|
||||
`transformer_lm.wiki103.adaptive` | Adaptive Inputs <br> ([Baevski and Auli, 2018](https://arxiv.org/abs/1809.10853)) <br> 247M params | [WikiText-103](https://blog.einstein.ai/the-wikitext-long-term-dependency-language-modeling-dataset) | [download (.tar.bz2)](https://dl.fbaipublicfiles.com/fairseq/models/lm/adaptive_lm_wiki103.v2.tar.bz2)
|
||||
`transformer_lm.wmt19.en` | English LM <br> ([Ng et al., 2019](https://arxiv.org/abs/1907.06616)) | [WMT News Crawl](http://data.statmt.org/news-crawl/) | [download (.tar.gz)](https://dl.fbaipublicfiles.com/fairseq/models/lm/wmt19.en.tar.gz)
|
||||
`transformer_lm.wmt19.de` | German LM <br> ([Ng et al., 2019](https://arxiv.org/abs/1907.06616)) | [WMT News Crawl](http://data.statmt.org/news-crawl/) | [download (.tar.gz)](https://dl.fbaipublicfiles.com/fairseq/models/lm/wmt19.de.tar.gz)
|
||||
`transformer_lm.wmt19.ru` | Russian LM <br> ([Ng et al., 2019](https://arxiv.org/abs/1907.06616)) | [WMT News Crawl](http://data.statmt.org/news-crawl/) | [download (.tar.gz)](https://dl.fbaipublicfiles.com/fairseq/models/lm/wmt19.ru.tar.gz)
|
||||
|
||||
## Example usage
|
||||
|
||||
We require a few additional Python dependencies for preprocessing:
|
||||
```bash
|
||||
pip install fastBPE sacremoses
|
||||
```
|
||||
|
||||
To sample from a language model using PyTorch Hub:
|
||||
```python
|
||||
import torch
|
||||
|
||||
# List available models
|
||||
torch.hub.list('pytorch/fairseq') # [..., 'transformer_lm.wmt19.en', ...]
|
||||
|
||||
# Load an English LM trained on WMT'19 News Crawl data
|
||||
en_lm = torch.hub.load('pytorch/fairseq', 'transformer_lm.wmt19.en', tokenizer='moses', bpe='fastbpe')
|
||||
en_lm.eval() # disable dropout
|
||||
|
||||
# Move model to GPU
|
||||
en_lm.cuda()
|
||||
|
||||
# Sample from the language model
|
||||
en_lm.sample('Barack Obama', beam=1, sampling=True, sampling_topk=10, temperature=0.8)
|
||||
# "Barack Obama is coming to Sydney and New Zealand (...)"
|
||||
|
||||
# Compute perplexity for a sequence
|
||||
en_lm.score('Barack Obama is coming to Sydney and New Zealand')['positional_scores'].mean().neg().exp()
|
||||
# tensor(15.1474)
|
||||
|
||||
# The same interface can be used with custom models as well
|
||||
from fairseq.models.transformer_lm import TransformerLanguageModel
|
||||
custom_lm = TransformerLanguageModel.from_pretrained('/path/to/model/dir', 'checkpoint100.pt', tokenizer='moses', bpe='fastbpe')
|
||||
custom_lm.sample('Barack Obama', beam=5)
|
||||
# "Barack Obama (...)"
|
||||
```
|
||||
|
||||
## Training a transformer language model with the CLI tools
|
||||
|
||||
### 1) Preprocess the data
|
||||
|
||||
First download and prepare the [WikiText-103 dataset](https://www.salesforce.com/products/einstein/ai-research/the-wikitext-dependency-language-modeling-dataset/):
|
||||
```bash
|
||||
cd examples/language_model/
|
||||
bash prepare-wikitext-103.sh
|
||||
cd ../..
|
||||
```
|
||||
|
||||
Next preprocess/binarize the data:
|
||||
```bash
|
||||
TEXT=examples/language_model/wikitext-103
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref $TEXT/wiki.train.tokens \
|
||||
--validpref $TEXT/wiki.valid.tokens \
|
||||
--testpref $TEXT/wiki.test.tokens \
|
||||
--destdir data-bin/wikitext-103 \
|
||||
--workers 20
|
||||
```
|
||||
|
||||
### 2) Train a language model
|
||||
|
||||
Next we'll train a basic transformer language model on wikitext-103. For more
|
||||
advanced usage, see the [adaptive inputs README](README.adaptive_inputs.md).
|
||||
|
||||
To train a basic LM (assumes 2 GPUs):
|
||||
```
|
||||
$ fairseq-train --task language_modeling \
|
||||
data-bin/wikitext-103 \
|
||||
--save-dir checkpoints/transformer_wikitext-103 \
|
||||
--arch transformer_lm --share-decoder-input-output-embed \
|
||||
--dropout 0.1 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --weight-decay 0.01 --clip-norm 0.0 \
|
||||
--lr 0.0005 --lr-scheduler inverse_sqrt --warmup-updates 4000 --warmup-init-lr 1e-07 \
|
||||
--tokens-per-sample 512 --sample-break-mode none \
|
||||
--max-tokens 2048 --update-freq 16 \
|
||||
--fp16 \
|
||||
--max-update 50000
|
||||
```
|
||||
|
||||
If you run out of memory, try reducing `--max-tokens` (max number of tokens per
|
||||
batch) or `--tokens-per-sample` (max sequence length). You can also adjust
|
||||
`--update-freq` to accumulate gradients and simulate training on a different
|
||||
number of GPUs.
|
||||
|
||||
### 3) Evaluate
|
||||
|
||||
```bash
|
||||
fairseq-eval-lm data-bin/wikitext-103 \
|
||||
--path checkpoints/transformer_wiki103/checkpoint_best.pt \
|
||||
--batch-size 2 \
|
||||
--tokens-per-sample 512 \
|
||||
--context-window 400
|
||||
# | Evaluated 245569 tokens in 56.1s (4379.02 tokens/s)
|
||||
# | Loss: 3.4164, Perplexity: 30.46
|
||||
```
|
||||
|
||||
*Note:* The `--context-window` option controls how much context is provided to
|
||||
each token when computing perplexity. When the window size is 0, the dataset is
|
||||
chunked into segments of length 512 and perplexity is computed over each segment
|
||||
normally. However, this results in worse (higher) perplexity since tokens that
|
||||
appear earlier in each segment have less conditioning. When the maximum window
|
||||
size is used (511 in this case), then we compute perplexity for each token
|
||||
fully conditioned on 511 tokens of context. This slows down evaluation
|
||||
significantly, since we must run a separate forward pass for every token in the
|
||||
dataset, but results in better (lower) perplexity.
|
||||
|
||||
|
||||
## Convolutional language models
|
||||
|
||||
Please see the [convolutional LM README](README.conv.md) for instructions on
|
||||
training convolutional language models.
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Adapted from https://github.com/facebookresearch/MIXER/blob/master/prepareData.sh
|
||||
|
||||
URLS=(
|
||||
"https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-v1.zip"
|
||||
)
|
||||
FILES=(
|
||||
"wikitext-103-v1.zip"
|
||||
)
|
||||
|
||||
for ((i=0;i<${#URLS[@]};++i)); do
|
||||
file=${FILES[i]}
|
||||
if [ -f $file ]; then
|
||||
echo "$file already exists, skipping download"
|
||||
else
|
||||
url=${URLS[i]}
|
||||
wget "$url"
|
||||
if [ -f $file ]; then
|
||||
echo "$url successfully downloaded."
|
||||
else
|
||||
echo "$url not successfully downloaded."
|
||||
exit -1
|
||||
fi
|
||||
if [ ${file: -4} == ".tgz" ]; then
|
||||
tar zxvf $file
|
||||
elif [ ${file: -4} == ".tar" ]; then
|
||||
tar xvf $file
|
||||
elif [ ${file: -4} == ".zip" ]; then
|
||||
unzip $file
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
@@ -0,0 +1,77 @@
|
||||
# Deep Transformers with Latent Depth (Li et al., 2020)
|
||||
|
||||
[https://arxiv.org/abs/2009.13102](https://arxiv.org/abs/2009.13102).
|
||||
|
||||
## Introduction
|
||||
|
||||
We present a probabilistic framework to automatically learn which layer(s) to use by learning the posterior distributions of layer selection. As an extension of this framework, we propose a novel method to train one shared Transformer network for multilingual machine translation with different layer selection posteriors for each language pair.
|
||||
|
||||
## Training a multilingual model with latent depth
|
||||
|
||||
Below is an example of training with latent depth in decoder for one-to-many (O2M) related languages. We use the same preprocessed (numberized and binarized) TED8 dataset as in [Balancing Training for Multilingual Neural Machine Translation (Wang et al., 2020)](https://github.com/cindyxinyiwang/multiDDS), which could be generated by [the script](https://github.com/cindyxinyiwang/multiDDS/blob/multiDDS/util_scripts/prepare_multilingual_data.sh) the author provided.
|
||||
```bash
|
||||
lang_pairs_str="eng-aze,eng-bel,eng-ces,eng-glg,eng-por,eng-rus,eng-slk,eng-tur"
|
||||
databin_dir=<path to binarized data>
|
||||
|
||||
fairseq-train ${databin_dir} \
|
||||
--user-dir examples/latent_depth/latent_depth_src \
|
||||
--lang-pairs "${lang_pairs_str}" \
|
||||
--arch multilingual_transformer_iwslt_de_en \
|
||||
--task multilingual_translation_latent_depth \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.1 \
|
||||
--share-encoders \
|
||||
--share-decoders \
|
||||
--decoder-langtok \
|
||||
--share-decoder-input-output-embed \
|
||||
--dropout 0.3 --attention-dropout 0.3 \
|
||||
--optimizer adam --adam-eps 1e-06 --adam-betas '(0.9, 0.98)' \
|
||||
--lr-scheduler inverse_sqrt --stop-min-lr 1e-9 --warmup-init-lr 1e-7 --warmup-updates 8000 \
|
||||
--max-tokens 4096 --update-freq 1 \
|
||||
--lr 0.0015 \
|
||||
--clip-norm 1.0 \
|
||||
--seed 2 \
|
||||
--ddp-backend=no_c10d \
|
||||
--encoder-layers 12 \
|
||||
--decoder-layers 24 \
|
||||
--decoder-latent-layer \
|
||||
--sparsity-weight 0.1 \
|
||||
--anneal-updates 5000 \
|
||||
--soft-update 500 \
|
||||
--target-layers 12 \
|
||||
--share-weight 0.1
|
||||
```
|
||||
## Inference command
|
||||
|
||||
```bash
|
||||
lang_pairs_str="eng-aze,eng-bel,eng-ces,eng-glg,eng-por,eng-rus,eng-slk,eng-tur"
|
||||
databin_dir=<path to binarized data>
|
||||
model_path=<path to checkpoint>
|
||||
src_lang=<source language to translate from>
|
||||
tgt_lang=<target language to translate to>
|
||||
gen_data=<name of data split, e.g. valid, test, etc>
|
||||
|
||||
fairseq-generate ${databin_dir} \
|
||||
--path ${model_path} \
|
||||
--task multilingual_translation_latent_depth \
|
||||
--decoder-latent-layer \
|
||||
--lang-pairs "${lang_pairs_str}" \
|
||||
-s ${src_lang} -t ${tgt_lang} \
|
||||
--gen-subset $gen_data \
|
||||
--scoring sacrebleu \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--lenpen 1.0 \
|
||||
--beam 5 \
|
||||
--decoder-langtok \
|
||||
--max-tokens 4096
|
||||
```
|
||||
|
||||
|
||||
## Citation
|
||||
```bibtex
|
||||
@article{li2020deep,
|
||||
title={Deep Transformers with Latent Depth},
|
||||
author={Li, Xian and Stickland, Asa Cooper and Tang, Yuqing and Kong, Xiang},
|
||||
journal={arXiv preprint arXiv:2009.13102},
|
||||
year={2020}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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 . import multilingual_translation_latent_depth # noqa
|
||||
from .loss import latent_depth # noqa
|
||||
from .models import latent_multilingual_transformer # noqa
|
||||
from .modules import latent_layers # noqa
|
||||
@@ -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
|
||||
from torch.nn.modules.loss import _Loss
|
||||
|
||||
|
||||
class LatentLayersKLLoss(_Loss):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
|
||||
def forward(self, layer_samples, lang_idx, update_num, sample_size):
|
||||
prior = self.args.prior
|
||||
samples = layer_samples[lang_idx]
|
||||
eps = 1e-7
|
||||
if prior == "uniform":
|
||||
# uniform prior
|
||||
kl_loss = (samples * (torch.log(samples + eps) - math.log(0.5))).sum(-1)
|
||||
elif prior == "agged_posterior":
|
||||
# aggregated posterior
|
||||
y_t = torch.stack([x.detach() for x in layer_samples], dim=0)
|
||||
agged_q = torch.sum(y_t, dim=0)
|
||||
row_norm = agged_q.sum(-1)
|
||||
normed_agg_q = agged_q / row_norm
|
||||
kl_loss = (
|
||||
samples * (torch.log(samples + eps) - torch.log(normed_agg_q + eps))
|
||||
).sum(-1)
|
||||
else:
|
||||
raise NotImplementedError("The specified prior is not implemented.")
|
||||
|
||||
# normalized by number of layers
|
||||
kl_loss /= layer_samples[0].size()[0]
|
||||
kl_weight = min(
|
||||
self.args.sparsity_weight,
|
||||
(update_num - self.args.soft_update)
|
||||
* self.args.sparsity_weight
|
||||
/ self.args.anneal_updates,
|
||||
)
|
||||
kl_loss *= kl_weight * sample_size
|
||||
return kl_loss
|
||||
|
||||
|
||||
class LatentLayersSparsityLoss(_Loss):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.args = args
|
||||
|
||||
def is_valid(self, update_num):
|
||||
if self.args.target_layers <= 0:
|
||||
return False
|
||||
return update_num > (self.args.soft_update + self.args.anneal_updates)
|
||||
|
||||
def forward(self, layer_samples_list, update_num, sample_size):
|
||||
batch_loss = 0
|
||||
share_loss = 0
|
||||
global_sparsity_loss = 0
|
||||
layer_samples = torch.stack(layer_samples_list, dim=0)
|
||||
if (
|
||||
self.args.target_layers > 0 or self.args.share_weight > 0
|
||||
) and update_num > (self.args.soft_update + self.args.anneal_updates):
|
||||
# anneal sparsity weight
|
||||
if update_num < (self.args.anneal_updates + self.args.soft_update):
|
||||
weight_anneal = 0
|
||||
elif update_num < (2 * self.args.anneal_updates + self.args.soft_update):
|
||||
weight_anneal = (
|
||||
(update_num - self.args.soft_update - self.args.anneal_updates)
|
||||
* self.args.share_weight
|
||||
/ self.args.anneal_updates
|
||||
)
|
||||
else:
|
||||
weight_anneal = 1
|
||||
# compute ratio among languages
|
||||
layer_utilization = torch.sum(layer_samples, dim=0)
|
||||
layer_utilization /= layer_samples.size()[0]
|
||||
if self.args.share_weight > 0:
|
||||
# encouraging sharing across languages
|
||||
share_loss = sum(
|
||||
-1.0 * v * math.log(v) for v in layer_utilization if v > 0
|
||||
)
|
||||
batch_loss += (
|
||||
weight_anneal * self.args.share_weight * sample_size * share_loss
|
||||
)
|
||||
if self.args.target_layers > 0:
|
||||
# computed expected number of layers selected
|
||||
expeted_layers = sum(layer_utilization)
|
||||
# compute l2 loss wrt target number of layers
|
||||
global_sparsity_loss = (expeted_layers - self.args.target_layers) ** 2
|
||||
batch_loss += (
|
||||
weight_anneal
|
||||
* self.args.share_weight
|
||||
* sample_size
|
||||
* global_sparsity_loss
|
||||
)
|
||||
return batch_loss
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
# 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.models import register_model, register_model_architecture
|
||||
from fairseq.models.multilingual_transformer import MultilingualTransformerModel
|
||||
from fairseq.models.transformer import (
|
||||
TransformerDecoder,
|
||||
TransformerEncoder,
|
||||
base_architecture,
|
||||
)
|
||||
|
||||
from .latent_transformer import LatentTransformerDecoder, LatentTransformerEncoder
|
||||
|
||||
|
||||
@register_model("latent_multilingual_transformer")
|
||||
class LatentMultilingualTransformerModel(MultilingualTransformerModel):
|
||||
"""A variant of standard multilingual Transformer models which encoder and/or
|
||||
decoders supports latent depth, as is in "Deep Transformer with Latent Depth"
|
||||
(https://arxiv.org/abs/2009.13102).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add model-specific arguments to the parser."""
|
||||
MultilingualTransformerModel.add_args(parser)
|
||||
parser.add_argument(
|
||||
'--soft-select',
|
||||
action='store_true',
|
||||
help='use soft samples in training an inference',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sampling-tau',
|
||||
type=float,
|
||||
default=5.,
|
||||
help='sampling temperature',
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_module_class(cls, is_encoder, args, lang_dict, embed_tokens, langs):
|
||||
if is_encoder:
|
||||
if hasattr(args, "encoder_latent_layer") and args.encoder_latent_layer:
|
||||
return LatentTransformerEncoder(
|
||||
args, lang_dict, embed_tokens, num_logits=len(langs)
|
||||
)
|
||||
else:
|
||||
return TransformerEncoder(args, lang_dict, embed_tokens)
|
||||
else:
|
||||
if hasattr(args, "decoder_latent_layer") and args.decoder_latent_layer:
|
||||
return LatentTransformerDecoder(
|
||||
args, lang_dict, embed_tokens, num_logits=len(langs)
|
||||
)
|
||||
else:
|
||||
return TransformerDecoder(args, lang_dict, embed_tokens)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"latent_multilingual_transformer", "latent_multilingual_transformer"
|
||||
)
|
||||
def latent_multilingual_architecture(args):
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 512)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", 1024)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", 4)
|
||||
args.encoder_layers = getattr(args, "encoder_layers", 12)
|
||||
args.decoder_embed_dim = getattr(args, "decoder_embed_dim", 512)
|
||||
args.decoder_ffn_embed_dim = getattr(args, "decoder_ffn_embed_dim", 1024)
|
||||
args.decoder_attention_heads = getattr(args, "decoder_attention_heads", 4)
|
||||
args.decoder_layers = getattr(args, "decoder_layers", 24)
|
||||
args.share_encoders = getattr(args, "share_encoders", True)
|
||||
args.share_decoders = getattr(args, "share_decoders", True)
|
||||
args.share_encoder_embeddings = getattr(args, "share_encoder_embeddings", True)
|
||||
args.share_decoder_embeddings = getattr(args, "share_decoder_embeddings", True)
|
||||
|
||||
base_architecture(args)
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
import torch.nn as nn
|
||||
from fairseq.models.fairseq_encoder import EncoderOut
|
||||
from fairseq.models.transformer import TransformerDecoder, TransformerEncoder
|
||||
from fairseq.modules import TransformerDecoderLayer, TransformerEncoderLayer
|
||||
from torch import Tensor
|
||||
|
||||
from ..modules.latent_layers import LayerSelect
|
||||
|
||||
|
||||
class LatentTransformerEncoder(TransformerEncoder):
|
||||
"""Latent depth (https://arxiv.org/abs/2009.13102) implemented in
|
||||
TransformerEncoder.
|
||||
"""
|
||||
|
||||
def __init__(self, args, dictionary, embed_tokens, num_logits=1):
|
||||
self.num_logits = num_logits
|
||||
self.num_layers = args.encoder_layers
|
||||
super().__init__(args, dictionary, embed_tokens)
|
||||
self.layer_select = LayerSelect(
|
||||
num_layers=self.num_layers,
|
||||
num_logits=self.num_logits,
|
||||
soft_select=getattr(args, "soft_select", False),
|
||||
sampling_tau=getattr(args, "sampling_tau", 5.),
|
||||
)
|
||||
self.lang_idx = None
|
||||
self.layers = nn.ModuleList(
|
||||
[self._build_encoder_layer(args, idx) for idx in range(args.encoder_layers)]
|
||||
)
|
||||
|
||||
def set_lang_idx(self, lang_idx):
|
||||
self.lang_idx = lang_idx
|
||||
|
||||
def _build_encoder_layer(self, args, idx=None):
|
||||
return LatentTransformerEncoderLayer(args, idx, layer_select=self.layer_select)
|
||||
|
||||
def forward(self, src_tokens, src_lengths, return_all_hiddens: bool = False):
|
||||
self.layer_select.sample(self.lang_idx)
|
||||
return super().forward(src_tokens, src_lengths, return_all_hiddens)
|
||||
|
||||
|
||||
class LatentTransformerEncoderLayer(TransformerEncoderLayer):
|
||||
"""Encoder layer with each (non_residual) block weighted by samples of Bernouli
|
||||
or Gumbel Signmoid samples.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments from standard
|
||||
TransformerEncoderLayer.
|
||||
idx (int): layer index (used to retrieve samples).
|
||||
layer_select (LayerSelect, optional): instance of LayerSelect module with logits
|
||||
parameters and sampling method.
|
||||
"""
|
||||
|
||||
def __init__(self, args, idx, layer_select=None):
|
||||
super().__init__(args)
|
||||
self.idx = idx
|
||||
self.layer_select = layer_select
|
||||
|
||||
def residual_connection(self, x, residual):
|
||||
return residual + x * self.layer_select(self.idx)
|
||||
|
||||
|
||||
class LatentTransformerDecoder(TransformerDecoder):
|
||||
"""Latent depth (https://arxiv.org/abs/2009.13102) implemented in
|
||||
TransformerDecoder.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, args, dictionary, embed_tokens, no_encoder_attn=False, num_logits=1
|
||||
):
|
||||
self.num_logits = num_logits
|
||||
self.num_layers = args.decoder_layers
|
||||
super().__init__(
|
||||
args, dictionary, embed_tokens, no_encoder_attn=no_encoder_attn
|
||||
)
|
||||
self.layer_select = LayerSelect(
|
||||
num_layers=self.num_layers,
|
||||
num_logits=self.num_logits,
|
||||
soft_select=getattr(args, "soft_select", False),
|
||||
sampling_tau=getattr(args, "sampling_tau", 5.),
|
||||
)
|
||||
self.lang_idx = None
|
||||
self.layers = nn.ModuleList(
|
||||
[
|
||||
self._build_decoder_layer(args, no_encoder_attn, idx)
|
||||
for idx in range(args.decoder_layers)
|
||||
]
|
||||
)
|
||||
|
||||
def set_lang_idx(self, lang_idx):
|
||||
self.lang_idx = lang_idx
|
||||
|
||||
def _build_decoder_layer(self, args, no_encoder_attn=False, idx=None):
|
||||
return LatentTransformerDecoderLayer(
|
||||
args, idx, layer_select=self.layer_select, no_encoder_attn=no_encoder_attn
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
prev_output_tokens,
|
||||
encoder_out: Optional[EncoderOut] = None,
|
||||
incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]] = None,
|
||||
features_only: bool = False,
|
||||
alignment_layer: Optional[int] = None,
|
||||
alignment_heads: Optional[int] = None,
|
||||
src_lengths: Optional[Any] = None,
|
||||
return_all_hiddens: bool = False,
|
||||
):
|
||||
self.layer_select.sample(self.lang_idx)
|
||||
return super().forward(
|
||||
prev_output_tokens=prev_output_tokens,
|
||||
encoder_out=encoder_out,
|
||||
incremental_state=incremental_state,
|
||||
features_only=features_only,
|
||||
alignment_layer=alignment_layer,
|
||||
src_lengths=src_lengths,
|
||||
return_all_hiddens=return_all_hiddens,
|
||||
)
|
||||
|
||||
|
||||
class LatentTransformerDecoderLayer(TransformerDecoderLayer):
|
||||
"""Decoder layer with each (non_residual) block weighted by samples of Bernouli
|
||||
or Gumbel Signmoid samples.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments from standard
|
||||
TransformerDecoderLayer.
|
||||
idx (int): layer index (used to retrieve samples).
|
||||
layer_select (LayerSelect, optional): instance of LayerSelect module with logits
|
||||
parameters and sampling method.
|
||||
no_encoder_attn (bool, optional): whether to attend to encoder outputs
|
||||
(default: False).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args,
|
||||
idx,
|
||||
layer_select=None,
|
||||
no_encoder_attn=False,
|
||||
add_bias_kv=False,
|
||||
add_zero_attn=False,
|
||||
):
|
||||
super().__init__(args, no_encoder_attn, add_bias_kv, add_zero_attn)
|
||||
self.idx = idx
|
||||
self.layer_select = layer_select
|
||||
|
||||
def residual_connection(self, x, residual):
|
||||
return residual + x * self.layer_select(self.idx)
|
||||
@@ -0,0 +1,75 @@
|
||||
# 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
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class LayerSelect(nn.Module):
|
||||
"""Compute samples (from a Gumbel-Sigmoid distribution) which is used as
|
||||
either (soft) weighting or (hard) selection of residual connection.
|
||||
https://arxiv.org/abs/2009.13102
|
||||
"""
|
||||
def __init__(self, num_layers, num_logits, soft_select=False, sampling_tau=5.):
|
||||
super(LayerSelect, self).__init__()
|
||||
self.layer_logits = torch.nn.Parameter(
|
||||
torch.Tensor(num_logits, num_layers),
|
||||
requires_grad=True,
|
||||
)
|
||||
self.hard_select = not soft_select
|
||||
self.tau = sampling_tau
|
||||
self.detach_grad = False
|
||||
self.layer_samples = [None] * num_logits
|
||||
|
||||
def sample(self, logit_idx):
|
||||
"""To leverage the efficiency of distributed training, samples for all
|
||||
layers are computed at once for each logit_idx. Logits are parameters
|
||||
learnt independent of each other.
|
||||
|
||||
Args:
|
||||
logit_idx: The index of logit parameters used for sampling.
|
||||
"""
|
||||
assert logit_idx is not None
|
||||
self.samples = self._gumbel_sigmoid(
|
||||
self.layer_logits[logit_idx, :].detach()
|
||||
if self.detach_grad
|
||||
else self.layer_logits[logit_idx, :],
|
||||
dim=-1,
|
||||
tau=self.tau,
|
||||
hard=self.hard_select,
|
||||
)
|
||||
self.layer_samples[logit_idx] = self.samples
|
||||
|
||||
def forward(self, i):
|
||||
sample = self.samples[i]
|
||||
return sample
|
||||
|
||||
def _gumbel_sigmoid(
|
||||
self, logits, tau=1, hard=False, eps=1e-10, dim=-1, threshold=0.5
|
||||
):
|
||||
# ~Gumbel(0,1)
|
||||
gumbels1 = (
|
||||
-torch.empty_like(logits, memory_format=torch.legacy_contiguous_format)
|
||||
.exponential_()
|
||||
.log()
|
||||
)
|
||||
gumbels2 = (
|
||||
-torch.empty_like(logits, memory_format=torch.legacy_contiguous_format)
|
||||
.exponential_()
|
||||
.log()
|
||||
)
|
||||
# Difference of two gumbels because we apply a sigmoid
|
||||
gumbels1 = (logits + gumbels1 - gumbels2) / tau
|
||||
y_soft = gumbels1.sigmoid()
|
||||
if hard:
|
||||
# Straight through.
|
||||
y_hard = torch.zeros_like(
|
||||
logits, memory_format=torch.legacy_contiguous_format
|
||||
).masked_fill(y_soft > threshold, 1.0)
|
||||
ret = y_hard - y_soft.detach() + y_soft
|
||||
else:
|
||||
# Reparametrization trick.
|
||||
ret = y_soft
|
||||
return ret
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
# 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.tasks import register_task
|
||||
from fairseq.tasks.multilingual_translation import MultilingualTranslationTask
|
||||
|
||||
from .loss.latent_depth import LatentLayersKLLoss, LatentLayersSparsityLoss
|
||||
|
||||
|
||||
@register_task("multilingual_translation_latent_depth")
|
||||
class MultilingualTranslationTaskLatentDepth(MultilingualTranslationTask):
|
||||
"""A task for multiple translation with latent depth.
|
||||
|
||||
See `"Deep Transformer with Latent Depth"
|
||||
(Li et al., 2020) <https://arxiv.org/pdf/2009.13102.pdf>`_.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
MultilingualTranslationTask.add_args(parser)
|
||||
parser.add_argument('--encoder-latent-layer', action='store_true', help='latent layer selection in encoder')
|
||||
parser.add_argument('--decoder-latent-layer', action='store_true', help='latent layer selection in decoder')
|
||||
parser.add_argument('--target-layers', default=-1, type=int,
|
||||
help='number of effective layers to learn; -1 means no constraint')
|
||||
parser.add_argument('--sparsity-weight', default=0.0, type=float,
|
||||
help='weight for sparsity loss')
|
||||
parser.add_argument('--share-weight', default=0.0, type=float,
|
||||
help='weight for sharing loss')
|
||||
parser.add_argument('--soft-update', default=1, type=int,
|
||||
help='number of updates with soft sampling')
|
||||
parser.add_argument('--anneal-updates', default=1, type=int,
|
||||
help='number of updates to anneal the KL loss weight')
|
||||
parser.add_argument('--prior', default="uniform", type=str,
|
||||
help='prior used for computing KL loss')
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, args, dicts, training):
|
||||
super().__init__(args, dicts, training)
|
||||
self.src_langs, self.tgt_langs = zip(
|
||||
*[(lang.split("-")[0], lang.split("-")[1]) for lang in args.lang_pairs]
|
||||
)
|
||||
if self.training and self.encoder_latent_layer:
|
||||
assert self.args.share_encoders
|
||||
if self.training and self.decoder_latent_layer:
|
||||
assert self.args.share_decoders
|
||||
if training or self.encoder_latent_layer or self.decoder_latent_layer:
|
||||
self.lang_pairs = args.lang_pairs
|
||||
else:
|
||||
self.lang_pairs = ["{}-{}".format(args.source_lang, args.target_lang)]
|
||||
self.eval_lang_pairs = self.lang_pairs
|
||||
self.model_lang_pairs = self.lang_pairs
|
||||
if self.training and (self.encoder_latent_layer or self.decoder_latent_layer):
|
||||
self.kl_loss = LatentLayersKLLoss(self.args)
|
||||
self.sparsity_loss = LatentLayersSparsityLoss(self.args)
|
||||
|
||||
def _per_lang_pair_train_loss(
|
||||
self, lang_pair, model, update_num, criterion, sample, optimizer, ignore_grad
|
||||
):
|
||||
src, tgt = lang_pair.split("-")
|
||||
if self.encoder_latent_layer:
|
||||
src_lang_idx = self.src_lang_idx_dict[src]
|
||||
model.models[lang_pair].encoder.set_lang_idx(src_lang_idx)
|
||||
model.models[lang_pair].encoder.layer_select.hard_select = (
|
||||
update_num > self.args.soft_update
|
||||
)
|
||||
if self.decoder_latent_layer:
|
||||
tgt_lang_idx = self.tgt_lang_idx_dict[tgt]
|
||||
model.models[lang_pair].decoder.set_lang_idx(tgt_lang_idx)
|
||||
model.models[lang_pair].decoder.layer_select.hard_select = (
|
||||
update_num > self.args.soft_update
|
||||
)
|
||||
|
||||
loss, sample_size, logging_output = criterion(
|
||||
model.models[lang_pair], sample[lang_pair]
|
||||
)
|
||||
if self.encoder_latent_layer:
|
||||
none_samples = sum(
|
||||
1 if x is None else 0
|
||||
for x in model.models[lang_pair].encoder.layer_select.layer_samples
|
||||
)
|
||||
if none_samples == 0 or self.args.prior != "agged_posterior":
|
||||
loss += self.kl_loss(
|
||||
model.models[lang_pair].encoder.layer_select.layer_samples,
|
||||
src_lang_idx,
|
||||
update_num,
|
||||
sample_size,
|
||||
)
|
||||
if self.decoder_latent_layer:
|
||||
none_samples = sum(
|
||||
1 if x is None else 0
|
||||
for x in model.models[lang_pair].decoder.layer_select.layer_samples
|
||||
)
|
||||
if none_samples == 0 or self.args.prior != "agged_posterior":
|
||||
loss += self.kl_loss(
|
||||
model.models[lang_pair].decoder.layer_select.layer_samples,
|
||||
tgt_lang_idx,
|
||||
update_num,
|
||||
sample_size,
|
||||
)
|
||||
if ignore_grad:
|
||||
loss *= 0
|
||||
|
||||
if hasattr(self, "sparsity_loss") and self.sparsity_loss.is_valid(update_num):
|
||||
# need to retain the graph if sparsity loss needs to be added
|
||||
loss.backward(retain_graph=True)
|
||||
else:
|
||||
optimizer.backward(loss)
|
||||
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def train_step(
|
||||
self, sample, model, criterion, optimizer, update_num, ignore_grad=False
|
||||
):
|
||||
agg_loss, agg_sample_size, agg_logging_output = super().train_step(
|
||||
sample, model, criterion, optimizer, update_num, ignore_grad
|
||||
)
|
||||
# compute auxiliary loss from layere sparsity, based on all samples from all languages
|
||||
if hasattr(self, "sparsity_loss") and self.sparsity_loss.is_valid(update_num):
|
||||
sparsity_loss = 0
|
||||
if self.encoder_latent_layer:
|
||||
sparsity_loss += self.sparsity_loss(
|
||||
next(
|
||||
iter(model.models.values())
|
||||
).encoder.layer_select.layer_samples,
|
||||
update_num,
|
||||
agg_sample_size,
|
||||
)
|
||||
if self.decoder_latent_layer:
|
||||
sparsity_loss += self.sparsity_loss(
|
||||
next(
|
||||
iter(model.models.values())
|
||||
).decoder.layer_select.layer_samples,
|
||||
update_num,
|
||||
agg_sample_size,
|
||||
)
|
||||
if sparsity_loss > 0:
|
||||
optimizer.backward(sparsity_loss)
|
||||
return agg_loss, agg_sample_size, agg_logging_output
|
||||
|
||||
def _per_lang_pair_valid_loss(self, lang_pair, model, criterion, sample):
|
||||
src, tgt = lang_pair.split("-")
|
||||
if self.encoder_latent_layer:
|
||||
src_lang_idx = self.src_lang_idx_dict[src]
|
||||
model.models[lang_pair].encoder.set_lang_idx(src_lang_idx)
|
||||
if self.decoder_latent_layer:
|
||||
tgt_lang_idx = self.tgt_lang_idx_dict[tgt]
|
||||
model.models[lang_pair].decoder.set_lang_idx(tgt_lang_idx)
|
||||
loss, sample_size, logging_output = criterion(
|
||||
model.models[lang_pair], sample[lang_pair]
|
||||
)
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
def inference_step(
|
||||
self, generator, models, sample, prefix_tokens=None, constraints=None
|
||||
):
|
||||
if self.encoder_latent_layer or self.decoder_latent_layer:
|
||||
for model in models:
|
||||
if self.encoder_latent_layer:
|
||||
assert model.encoder.layer_select is not None
|
||||
src_lang_idx = self.src_lang_idx_dict[self.args.source_lang]
|
||||
model.encoder.set_lang_idx(src_lang_idx)
|
||||
if self.decoder_latent_layer:
|
||||
assert model.decoder.layer_select is not None
|
||||
tgt_lang_idx = self.tgt_lang_idx_dict[self.args.target_lang]
|
||||
model.decoder.set_lang_idx(tgt_lang_idx)
|
||||
return super().inference_step(
|
||||
generator, models, sample, prefix_tokens, constraints
|
||||
)
|
||||
|
||||
@property
|
||||
def encoder_latent_layer(self):
|
||||
return (
|
||||
hasattr(self.args, "encoder_latent_layer")
|
||||
and self.args.encoder_latent_layer
|
||||
)
|
||||
|
||||
@property
|
||||
def decoder_latent_layer(self):
|
||||
return (
|
||||
hasattr(self.args, "decoder_latent_layer")
|
||||
and self.args.decoder_latent_layer
|
||||
)
|
||||
|
||||
@property
|
||||
def src_lang_idx_dict(self):
|
||||
return {lang: lang_idx for lang_idx, lang in enumerate(self.src_langs)}
|
||||
|
||||
@property
|
||||
def tgt_lang_idx_dict(self):
|
||||
return {lang: lang_idx for lang_idx, lang in enumerate(self.tgt_langs)}
|
||||
@@ -0,0 +1,154 @@
|
||||
# Reducing Transformer Depth on Demand with Structured Dropout (Fan et al., 2019)
|
||||
This page contains information for how to train models with LayerDrop, based on this [paper](https://arxiv.org/abs/1909.11556).
|
||||
|
||||
## Citation:
|
||||
If you found this technique useful, please cite our paper:
|
||||
```bibtex
|
||||
@article{fan2019reducing,
|
||||
title={Reducing Transformer Depth on Demand with Structured Dropout},
|
||||
author={Fan, Angela and Grave, Edouard and Joulin, Armand},
|
||||
journal={arXiv preprint arXiv:1909.11556},
|
||||
year={2019}
|
||||
}
|
||||
```
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | Download
|
||||
---|---|---
|
||||
`layerdrop_wmt_en_de_12_6` | Transformer + LayerDrop 0.2 trained on WMT16 en-de with 12 encoder and 6 decoder layers | [layerdrop_wmt_en_de_12_6.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/layerdrop_wmt_en_de_12_6.tar.gz)
|
||||
`roberta_layerdrop.base` | RoBERTa Base + LayerDrop 0.2 | [roberta_layerdrop.base.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta_layerdrop.base.qnli.tar.gz)
|
||||
`roberta_layerdrop.large` | RoBERTa Large + LayerDrop 0.2 | [roberta_layerdrop.large.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta_layerdrop.large.tar.gz)
|
||||
`roberta_layerdrop.large.mnli` | `roberta_layerdrop.large` finetuned on [MNLI](http://www.nyu.edu/projects/bowman/multinli) | [roberta_layerdrop.large.mnli.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta_layerdrop.large.mnli.tar.gz)
|
||||
`roberta_layerdrop.large.qnli` | `roberta_layerdrop.large` finetuned on [QNLI](https://arxiv.org/abs/1804.07461) | [roberta_layerdrop.large.mnli.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta_layerdrop.large.qnli.tar.gz)
|
||||
|
||||
|
||||
Evaluate performance of these pre-trained models:
|
||||
```bash
|
||||
# Example for Machine Translation
|
||||
fairseq-generate /path/to/bped/wmt/data --path nmt_checkpoint.pt \
|
||||
--beam 8 --lenpen 0.4 \
|
||||
--batch-size 64 \
|
||||
--remove-bpe \
|
||||
--gen-subset test > wmt16_gen.txt
|
||||
bash scripts/compound_split_bleu.sh wmt16_gen.txt
|
||||
# prints BLEU4 = 30.17
|
||||
```
|
||||
|
||||
```python
|
||||
# Example for RoBERTa + LayerDrop finetuned on MNLI:
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
|
||||
roberta_layerdrop = RobertaModel.from_pretrained(
|
||||
'/path/to/MNLI/model',
|
||||
checkpoint_file='mnli_checkpoint.pt',
|
||||
data_name_or_path='/path/to/MNLI/data/MNLI-bin'
|
||||
)
|
||||
label_map = {0: 'contradiction', 2: 'neutral', 1: 'entailment'}
|
||||
ncorrect, nsamples = 0, 0
|
||||
roberta_layerdrop.cuda()
|
||||
roberta_layerdrop.eval()
|
||||
with open('/path/to/MNLI/data/dev_matched.tsv') as fin:
|
||||
fin.readline()
|
||||
for index, line in enumerate(fin):
|
||||
tokens = line.strip().split('\t')
|
||||
sent1, sent2, target = tokens[8], tokens[9], tokens[-1]
|
||||
tokens = roberta_layerdrop.encode(sent1, sent2)
|
||||
prediction = roberta_layerdrop.predict('sentence_classification_head', tokens).argmax().item()
|
||||
prediction_label = label_map[prediction]
|
||||
ncorrect += int(prediction_label == target)
|
||||
nsamples += 1
|
||||
print('| Accuracy: ', float(ncorrect)/float(nsamples))
|
||||
# prints | Accuracy: 0.9026999490575649
|
||||
|
||||
|
||||
# Example for RoBERTa + LayerDrop finetuned on QNLI:
|
||||
roberta = RobertaModel.from_pretrained(
|
||||
'/path/to/QNLI/model',
|
||||
checkpoint_file='qnli_checkpoint.pt',
|
||||
data_name_or_path='/path/to/QNLI/data/QNLI-bin'
|
||||
)
|
||||
|
||||
label_fn = lambda label: roberta.task.label_dictionary.string(
|
||||
[label + roberta.task.target_dictionary.nspecial]
|
||||
)
|
||||
ncorrect, nsamples = 0, 0
|
||||
roberta.cuda()
|
||||
roberta.eval()
|
||||
with open('/path/to/QNLI/data/dev.tsv') as fin:
|
||||
fin.readline()
|
||||
for index, line in enumerate(fin):
|
||||
tokens = line.strip().split('\t')
|
||||
sent1, sent2, target = tokens[1], tokens[2], tokens[3]
|
||||
tokens = roberta.encode(sent1, sent2)
|
||||
prediction = roberta.predict('sentence_classification_head', tokens).argmax().item()
|
||||
prediction_label = label_fn(prediction)
|
||||
ncorrect += int(prediction_label == target)
|
||||
nsamples += 1
|
||||
print('| Accuracy: ', float(ncorrect)/float(nsamples))
|
||||
# prints | Accuracy: 0.9480139117700896
|
||||
```
|
||||
|
||||
|
||||
## Example usage
|
||||
|
||||
To train a model with LayerDrop, add the following flags. We recommend 0.2, a value that worked well in our experiments. For Language Models that are decoder-only, you need only the decoder flag. For RoBERTa, an encoder, you need only the encoder flag. The encoder and decoder LayerDrop values can be set differently.
|
||||
```
|
||||
--encoder-layerdrop 0.2 --decoder-layerdrop 0.2
|
||||
```
|
||||
|
||||
To prune a model that has been trained with LayerDrop, add the following flags followed by a comma separated list of which layers you would like to keep.
|
||||
```
|
||||
--encoder-layers-to-keep 0,2,4,6,8,10,12,14 --decoder-layers-to-keep 0,2,4,6,8,10,12,14
|
||||
```
|
||||
Setting these flags should print a message such as:
|
||||
```
|
||||
| Pruning model to specified layer configuration
|
||||
```
|
||||
You should also see a smaller number of parameters in the model, for example the 16-Layer Transformer Language Model prints:
|
||||
```
|
||||
num. model params: 246933504
|
||||
```
|
||||
while a model pruned to 8 Layers prints:
|
||||
```
|
||||
num. model params: 146163712
|
||||
```
|
||||
|
||||
If you would like to pick up training with a model that has been pruned, simply adding these flags is sufficient. If you would like to use a script that only does evaluation (no training), you may need to pass an override command. A specific example would be for language modeling:
|
||||
```bash
|
||||
fairseq-eval-lm /path/to/wikitext-103 \
|
||||
--path /path/to/model/checkpoint.pt \
|
||||
--model-overrides "{'decoder_layers_to_keep':'0,2,4,6,8,10,12,14'}"
|
||||
```
|
||||
This model override command overrides the training parameters and updates the model arguments so that the pruned model is run instead of the full model.
|
||||
|
||||
## Reproduce Paper Results
|
||||
|
||||
Looking to reproduce the results in the paper?
|
||||
|
||||
1. For Translation on WMT16 en-de, we followed this setting [here](https://github.com/pytorch/fairseq/blob/master/examples/scaling_nmt/README.md)
|
||||
2. To train RoBERTa, we followed this setting [here](https://github.com/pytorch/fairseq/tree/master/examples/roberta)
|
||||
3. To train Language Models on Wikitext-103, we followed this setting [here](https://github.com/pytorch/fairseq/tree/master/examples/language_model)
|
||||
|
||||
|
||||
## Tips
|
||||
|
||||
1. If you would like to train large models with better performance, LayerDrop should be set to a smaller value such as 0.1 or 0.2. Too much LayerDrop will mean the model has too much regularization, so may not reach the best performance. Since LayerDrop adds regularization, you may achieve the best performance by slightly reducing the amount of standard dropout (for example, reduce by 0.1).
|
||||
|
||||
2. If you would like to train large models to be pruned and made smaller, LayerDrop should be set to a larger value such as 0.5 if you want to prune very aggressively (such as removing half the network or more). If you would like to prune fewer layers away, LayerDrop can be set to a smaller value such as 0.2. Our experiments were conducted with low values of LayerDrop (such as 0.1 and 0.2), for reference.
|
||||
|
||||
3. When pruning layers at inference time, it is best to spread out the layers remaining so they are evenly spaced throughout the network. For example, if you want to remove 50% of the network, keeping every other layer is good.
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
1. How did the sharing layers experiment work? In an appendix (https://openreview.net/pdf?id=SylO2yStDr) we added an experiment on Wikitext-103 language modeling that combined LayerDrop with Weight Sharing. We shared chunks of 2 layers such that every other layer had shared weights. For example, if our network has layers 1 through 6, then layer 1 and 2 are shared, layer 3 and 4 are shared, and layer 5 and 6 are shared.
|
||||
|
||||
2. LayerDrop hasn't been helping in my setting? During training time, LayerDrop can help regularize your network. This is most important if your network is already overfitting - if your network is underfitting, it is possible LayerDrop is adding too much regularization. We recommend using smaller values (such as 0.1 or 0.2) and also decreasing the quantity of standard dropout (for example, reduce by 0.1).
|
||||
|
||||
3. Can you train a model without LayerDrop and finetune with LayerDrop (e.g. for BERT)? In our experiments, we did not see great performance. Models such as RoBERTa have trained for a long time in the pre-training setting, so only finetuning with LayerDrop for a few epochs on a downstream task such as MNLI does not achieve the robustness required for successful pruning.
|
||||
|
||||
|
||||
## Having an issue or have a question?
|
||||
|
||||
Please open an issue in this repository with the details of your question. Thanks!
|
||||
@@ -0,0 +1,22 @@
|
||||
# Linformer: Self-Attention with Linear Complexity (Wang et al., 2020)
|
||||
|
||||
This example contains code to train Linformer models as described in our paper
|
||||
[Linformer: Self-Attention with Linear Complexity](https://arxiv.org/abs/2006.04768).
|
||||
|
||||
## Training a new Linformer RoBERTa model
|
||||
|
||||
You can mostly follow the [RoBERTa pretraining README](/examples/roberta/README.pretraining.md),
|
||||
updating your training command with `--user-dir examples/linformer/linformer_src --arch linformer_roberta_base`.
|
||||
|
||||
## Citation
|
||||
|
||||
If you use our work, please cite:
|
||||
|
||||
```bibtex
|
||||
@article{wang2020linformer,
|
||||
title={Linformer: Self-Attention with Linear Complexity},
|
||||
author={Wang, Sinong and Li, Belinda and Khabsa, Madian and Fang, Han and Ma, Hao},
|
||||
journal={arXiv preprint arXiv:2006.04768},
|
||||
year={2020}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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 .models import linformer_roberta # noqa
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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.
|
||||
"""
|
||||
Linformer: Self-Attention with Linear Complexity
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fairseq.models import register_model, register_model_architecture
|
||||
from fairseq.models.roberta import RobertaEncoder, RobertaModel
|
||||
|
||||
from ..modules.linformer_sentence_encoder import LinformerSentenceEncoder
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_model("linformer_roberta")
|
||||
class LinformerModel(RobertaModel):
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
RobertaModel.add_args(parser)
|
||||
|
||||
# add args for Linformer
|
||||
parser.add_argument(
|
||||
"--compressed", type=int, help="compressed ratio of sequence length"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shared-kv-compressed",
|
||||
type=int,
|
||||
help="share compressed matrix between k and v, in each layer",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--shared-layer-kv-compressed",
|
||||
type=int,
|
||||
help="share compressed matrix between k and v and across all layers",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--freeze-compress",
|
||||
type=int,
|
||||
help="freeze the parameters in compressed layer",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_model(cls, args, task):
|
||||
"""Build a new model instance."""
|
||||
|
||||
# make sure all arguments are present
|
||||
base_architecture(args)
|
||||
|
||||
if not hasattr(args, "max_positions"):
|
||||
args.max_positions = args.tokens_per_sample
|
||||
|
||||
encoder = LinformerEncoder(args, task.source_dictionary)
|
||||
return cls(args, encoder)
|
||||
|
||||
|
||||
class LinformerEncoder(RobertaEncoder):
|
||||
"""Linformer encoder."""
|
||||
|
||||
def __init__(self, args, dictionary):
|
||||
super().__init__(args, dictionary)
|
||||
|
||||
self.sentence_encoder = LinformerSentenceEncoder(
|
||||
padding_idx=dictionary.pad(),
|
||||
vocab_size=len(dictionary),
|
||||
num_encoder_layers=args.encoder_layers,
|
||||
embedding_dim=args.encoder_embed_dim,
|
||||
ffn_embedding_dim=args.encoder_ffn_embed_dim,
|
||||
num_attention_heads=args.encoder_attention_heads,
|
||||
dropout=args.dropout,
|
||||
attention_dropout=args.attention_dropout,
|
||||
activation_dropout=args.activation_dropout,
|
||||
layerdrop=args.encoder_layerdrop,
|
||||
max_seq_len=args.max_positions,
|
||||
num_segments=0,
|
||||
encoder_normalize_before=True,
|
||||
apply_bert_init=True,
|
||||
activation_fn=args.activation_fn,
|
||||
q_noise=args.quant_noise_pq,
|
||||
qn_block_size=args.quant_noise_pq_block_size,
|
||||
compressed=args.compressed,
|
||||
shared_kv_compressed=args.shared_kv_compressed,
|
||||
shared_layer_kv_compressed=args.shared_layer_kv_compressed,
|
||||
freeze_compress=args.freeze_compress,
|
||||
)
|
||||
|
||||
|
||||
@register_model_architecture("linformer_roberta", "linformer_roberta")
|
||||
def base_architecture(args):
|
||||
args.encoder_layers = getattr(args, "encoder_layers", 12)
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 768)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", 3072)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", 12)
|
||||
|
||||
args.activation_fn = getattr(args, "activation_fn", "gelu")
|
||||
args.pooler_activation_fn = getattr(args, "pooler_activation_fn", "tanh")
|
||||
|
||||
args.dropout = getattr(args, "dropout", 0.1)
|
||||
args.attention_dropout = getattr(args, "attention_dropout", 0.1)
|
||||
args.activation_dropout = getattr(args, "activation_dropout", 0.0)
|
||||
args.pooler_dropout = getattr(args, "pooler_dropout", 0.0)
|
||||
args.encoder_layers_to_keep = getattr(args, "encoder_layers_to_keep", None)
|
||||
args.encoder_layerdrop = getattr(args, "encoder_layerdrop", 0.0)
|
||||
args.compressed = getattr(args, "compressed", 4)
|
||||
args.shared_kv_compressed = getattr(args, "shared_kv_compressed", 0)
|
||||
args.shared_layer_kv_compressed = getattr(args, "shared_layer_kv_compressed", 0)
|
||||
args.freeze_compress = getattr(args, "freeze_compress", 0)
|
||||
|
||||
|
||||
@register_model_architecture("linformer_roberta", "linformer_roberta_base")
|
||||
def linformer_roberta_base_architecture(args):
|
||||
base_architecture(args)
|
||||
|
||||
|
||||
@register_model_architecture("linformer_roberta", "linformer_roberta_large")
|
||||
def linformer_roberta_large_architecture(args):
|
||||
args.encoder_layers = getattr(args, "encoder_layers", 24)
|
||||
args.encoder_embed_dim = getattr(args, "encoder_embed_dim", 1024)
|
||||
args.encoder_ffn_embed_dim = getattr(args, "encoder_ffn_embed_dim", 4096)
|
||||
args.encoder_attention_heads = getattr(args, "encoder_attention_heads", 16)
|
||||
|
||||
args.activation_fn = getattr(args, "activation_fn", "gelu")
|
||||
args.pooler_activation_fn = getattr(args, "pooler_activation_fn", "tanh")
|
||||
|
||||
args.dropout = getattr(args, "dropout", 0.1)
|
||||
args.attention_dropout = getattr(args, "attention_dropout", 0.1)
|
||||
args.activation_dropout = getattr(args, "activation_dropout", 0.0)
|
||||
args.pooler_dropout = getattr(args, "pooler_dropout", 0.0)
|
||||
args.compressed = getattr(args, "compressed", 4)
|
||||
args.shared_kv_compressed = getattr(args, "shared_kv_compressed", 0)
|
||||
args.shared_layer_kv_compressed = getattr(args, "shared_layer_kv_compressed", 0)
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# 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.nn as nn
|
||||
from fairseq.modules import TransformerSentenceEncoder
|
||||
|
||||
from .linformer_sentence_encoder_layer import LinformerSentenceEncoderLayer
|
||||
|
||||
|
||||
class LinformerSentenceEncoder(TransformerSentenceEncoder):
|
||||
"""
|
||||
Implementation for a Bi-directional Linformer based Sentence Encoder used
|
||||
in BERT/XLM style pre-trained models.
|
||||
|
||||
This first computes the token embedding using the token embedding matrix,
|
||||
position embeddings (if specified) and segment embeddings
|
||||
(if specified). After applying the specified number of
|
||||
LinformerEncoderLayers, it outputs all the internal states of the
|
||||
encoder as well as the final representation associated with the first
|
||||
token (usually CLS token).
|
||||
|
||||
Input:
|
||||
- tokens: B x T matrix representing sentences
|
||||
- segment_labels: B x T matrix representing segment label for tokens
|
||||
|
||||
Output:
|
||||
- a tuple of the following:
|
||||
- a list of internal model states used to compute the
|
||||
predictions where each tensor has shape T x B x C
|
||||
- sentence representation associated with first input token
|
||||
in format B x C.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
padding_idx: int,
|
||||
vocab_size: int,
|
||||
num_encoder_layers: int = 6,
|
||||
embedding_dim: int = 768,
|
||||
ffn_embedding_dim: int = 3072,
|
||||
num_attention_heads: int = 8,
|
||||
dropout: float = 0.1,
|
||||
attention_dropout: float = 0.1,
|
||||
activation_dropout: float = 0.1,
|
||||
layerdrop: float = 0.0,
|
||||
max_seq_len: int = 256,
|
||||
num_segments: int = 2,
|
||||
use_position_embeddings: bool = True,
|
||||
offset_positions_by_padding: bool = True,
|
||||
encoder_normalize_before: bool = False,
|
||||
apply_bert_init: bool = False,
|
||||
activation_fn: str = "relu",
|
||||
learned_pos_embedding: bool = True,
|
||||
embed_scale: float = None,
|
||||
freeze_embeddings: bool = False,
|
||||
n_trans_layers_to_freeze: int = 0,
|
||||
export: bool = False,
|
||||
traceable: bool = False,
|
||||
q_noise: float = 0.0,
|
||||
qn_block_size: int = 8,
|
||||
compressed: int = 4,
|
||||
shared_kv_compressed: int = 0,
|
||||
shared_layer_kv_compressed: int = 0,
|
||||
freeze_compress: int = 0,
|
||||
) -> None:
|
||||
|
||||
# Initialize linformer parameters
|
||||
self.compressed = compressed
|
||||
self.shared_kv_compressed = shared_kv_compressed
|
||||
self.shared_layer_kv_compressed = shared_layer_kv_compressed
|
||||
self.compress_layer = None
|
||||
self.freeze_compress = freeze_compress
|
||||
|
||||
super().__init__(
|
||||
padding_idx=padding_idx,
|
||||
vocab_size=vocab_size,
|
||||
num_encoder_layers=num_encoder_layers,
|
||||
embedding_dim=embedding_dim,
|
||||
ffn_embedding_dim=ffn_embedding_dim,
|
||||
num_attention_heads=num_attention_heads,
|
||||
dropout=dropout,
|
||||
attention_dropout=attention_dropout,
|
||||
activation_dropout=activation_dropout,
|
||||
layerdrop=layerdrop,
|
||||
max_seq_len=max_seq_len,
|
||||
num_segments=num_segments,
|
||||
use_position_embeddings=use_position_embeddings,
|
||||
offset_positions_by_padding=offset_positions_by_padding,
|
||||
encoder_normalize_before=encoder_normalize_before,
|
||||
apply_bert_init=apply_bert_init,
|
||||
activation_fn=activation_fn,
|
||||
learned_pos_embedding=learned_pos_embedding,
|
||||
embed_scale=embed_scale,
|
||||
freeze_embeddings=freeze_embeddings,
|
||||
n_trans_layers_to_freeze=n_trans_layers_to_freeze,
|
||||
export=export,
|
||||
traceable=traceable,
|
||||
q_noise=q_noise,
|
||||
qn_block_size=qn_block_size,
|
||||
)
|
||||
|
||||
def build_transformer_sentence_encoder_layer(
|
||||
self,
|
||||
embedding_dim,
|
||||
ffn_embedding_dim,
|
||||
num_attention_heads,
|
||||
dropout,
|
||||
attention_dropout,
|
||||
activation_dropout,
|
||||
activation_fn,
|
||||
export,
|
||||
q_noise,
|
||||
qn_block_size,
|
||||
):
|
||||
if self.shared_layer_kv_compressed == 1:
|
||||
compress_layer = nn.Linear(
|
||||
self.max_seq_len, self.max_seq_len // self.compressed
|
||||
)
|
||||
# intialize parameters for compressed layer
|
||||
nn.init.xavier_uniform_(compress_layer.weight, gain=1 / math.sqrt(2))
|
||||
if self.freeze_compress == 1:
|
||||
compress_layer.weight.requires_grad = False
|
||||
self.compress_layer = compress_layer
|
||||
|
||||
return LinformerSentenceEncoderLayer(
|
||||
embedding_dim=embedding_dim,
|
||||
ffn_embedding_dim=ffn_embedding_dim,
|
||||
num_attention_heads=num_attention_heads,
|
||||
dropout=dropout,
|
||||
attention_dropout=attention_dropout,
|
||||
activation_dropout=activation_dropout,
|
||||
activation_fn=activation_fn,
|
||||
export=export,
|
||||
q_noise=q_noise,
|
||||
qn_block_size=qn_block_size,
|
||||
compressed=self.compressed,
|
||||
max_seq_len=self.max_seq_len,
|
||||
shared_kv_compressed=self.shared_kv_compressed,
|
||||
shared_compress_layer=(
|
||||
None if self.shared_layer_kv_compressed == 0 else self.compress_layer
|
||||
),
|
||||
freeze_compress=self.freeze_compress,
|
||||
)
|
||||
|
||||
def upgrade_state_dict_named(self, state_dict, name):
|
||||
prefix = name + "." if name != "" else ""
|
||||
items_to_add = {}
|
||||
keys_to_remove = []
|
||||
|
||||
# update key name for shared layer in new version of code
|
||||
for k in state_dict.keys():
|
||||
if k.startswith(prefix + "compress_layer"):
|
||||
if self.shared_layer_kv_compressed:
|
||||
for layer_idx in range(len(self.layers)):
|
||||
new_k = prefix + "layers.{0}.shared_compress_layer.{1}".format(
|
||||
layer_idx,
|
||||
k[len(prefix + "compress_layer.") :],
|
||||
)
|
||||
items_to_add[new_k] = state_dict[k]
|
||||
|
||||
for k in keys_to_remove:
|
||||
del state_dict[k]
|
||||
|
||||
for key, value in items_to_add.items():
|
||||
state_dict[key] = value
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# 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 typing import Callable
|
||||
|
||||
from fairseq.modules import TransformerSentenceEncoderLayer
|
||||
|
||||
from .multihead_linear_attention import MultiheadLinearAttention
|
||||
|
||||
|
||||
class LinformerSentenceEncoderLayer(TransformerSentenceEncoderLayer):
|
||||
"""
|
||||
Implements a Linformer Encoder Layer used in BERT/XLM style pre-trained
|
||||
models.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_dim: int = 768,
|
||||
ffn_embedding_dim: int = 3072,
|
||||
num_attention_heads: int = 8,
|
||||
dropout: float = 0.1,
|
||||
attention_dropout: float = 0.1,
|
||||
activation_dropout: float = 0.1,
|
||||
activation_fn: str = "relu",
|
||||
export: bool = False,
|
||||
q_noise: float = 0.0,
|
||||
qn_block_size: int = 8,
|
||||
init_fn: Callable = None,
|
||||
compressed: int = 1,
|
||||
max_seq_len: int = 256,
|
||||
shared_kv_compressed: int = 0,
|
||||
shared_compress_layer: any = None,
|
||||
freeze_compress: int = 0,
|
||||
) -> None:
|
||||
|
||||
# Initialize linformer parameters
|
||||
self.compressed = compressed
|
||||
self.max_seq_len = max_seq_len
|
||||
self.shared_kv_compressed = shared_kv_compressed
|
||||
self.freeze_compress = freeze_compress
|
||||
|
||||
def init_fn():
|
||||
# This needs to be set after nn.Module.__init__ is called
|
||||
self.shared_compress_layer = shared_compress_layer
|
||||
|
||||
super().__init__(
|
||||
embedding_dim=embedding_dim,
|
||||
ffn_embedding_dim=ffn_embedding_dim,
|
||||
num_attention_heads=num_attention_heads,
|
||||
dropout=dropout,
|
||||
attention_dropout=attention_dropout,
|
||||
activation_dropout=activation_dropout,
|
||||
activation_fn=activation_fn,
|
||||
export=export,
|
||||
q_noise=q_noise,
|
||||
qn_block_size=qn_block_size,
|
||||
init_fn=init_fn,
|
||||
)
|
||||
|
||||
def build_self_attention(
|
||||
self,
|
||||
embed_dim,
|
||||
num_attention_heads,
|
||||
dropout,
|
||||
self_attention,
|
||||
q_noise,
|
||||
qn_block_size,
|
||||
):
|
||||
return MultiheadLinearAttention(
|
||||
embed_dim,
|
||||
num_attention_heads,
|
||||
dropout=dropout,
|
||||
self_attention=True,
|
||||
q_noise=q_noise,
|
||||
qn_block_size=qn_block_size,
|
||||
compressed=self.compressed,
|
||||
max_seq_len=self.max_seq_len,
|
||||
shared_kv_compressed=self.shared_kv_compressed,
|
||||
shared_compress_layer=self.shared_compress_layer,
|
||||
freeze_compress=self.freeze_compress,
|
||||
)
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
# 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 typing import Dict, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import utils
|
||||
from fairseq.incremental_decoding_utils import with_incremental_state
|
||||
from fairseq.modules.quant_noise import quant_noise
|
||||
from torch import Tensor, nn
|
||||
from torch.nn import Parameter
|
||||
|
||||
|
||||
@with_incremental_state
|
||||
class MultiheadLinearAttention(nn.Module):
|
||||
"""Multi-headed linformer attention.
|
||||
|
||||
Projects the key and values down to the compressed dimension, before computing self-attention.
|
||||
|
||||
See "Linformer: Self-Attention with Linear Complexity" for more details.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embed_dim,
|
||||
num_heads,
|
||||
kdim=None,
|
||||
vdim=None,
|
||||
dropout=0.0,
|
||||
bias=True,
|
||||
add_bias_kv=False,
|
||||
add_zero_attn=False,
|
||||
self_attention=False,
|
||||
encoder_decoder_attention=False,
|
||||
q_noise=0.0,
|
||||
qn_block_size=8,
|
||||
compressed=1,
|
||||
max_seq_len=256,
|
||||
shared_kv_compressed=0,
|
||||
shared_compress_layer=None,
|
||||
freeze_compress=0,
|
||||
):
|
||||
super().__init__()
|
||||
self.embed_dim = embed_dim
|
||||
self.kdim = kdim if kdim is not None else embed_dim
|
||||
self.vdim = vdim if vdim is not None else embed_dim
|
||||
self.qkv_same_dim = self.kdim == embed_dim and self.vdim == embed_dim
|
||||
|
||||
self.num_heads = num_heads
|
||||
self.dropout = dropout
|
||||
self.head_dim = embed_dim // num_heads
|
||||
assert (
|
||||
self.head_dim * num_heads == self.embed_dim
|
||||
), "embed_dim must be divisible by num_heads"
|
||||
self.scaling = self.head_dim ** -0.5
|
||||
|
||||
self.self_attention = self_attention
|
||||
self.encoder_decoder_attention = encoder_decoder_attention
|
||||
|
||||
assert not self.self_attention or self.qkv_same_dim, (
|
||||
"Self-attention requires query, key and " "value to be of the same size"
|
||||
)
|
||||
|
||||
self.k_proj = quant_noise(
|
||||
nn.Linear(self.kdim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
self.v_proj = quant_noise(
|
||||
nn.Linear(self.vdim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
self.q_proj = quant_noise(
|
||||
nn.Linear(embed_dim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
|
||||
# used for compress sequence to subsequence
|
||||
if shared_compress_layer is None:
|
||||
self.compress_seq_len = max_seq_len // compressed
|
||||
self.compress_k = nn.Linear(max_seq_len, self.compress_seq_len, bias=False)
|
||||
if shared_kv_compressed == 0:
|
||||
self.compress_v = nn.Linear(
|
||||
max_seq_len, self.compress_seq_len, bias=False
|
||||
)
|
||||
self.layerwise_sharing = False
|
||||
else:
|
||||
self.compress_k = shared_compress_layer
|
||||
if shared_kv_compressed == 0:
|
||||
self.compress_v = shared_compress_layer
|
||||
self.layerwise_sharing = True
|
||||
self.shared_kv_compressed = shared_kv_compressed
|
||||
|
||||
self.out_proj = quant_noise(
|
||||
nn.Linear(embed_dim, embed_dim, bias=bias), q_noise, qn_block_size
|
||||
)
|
||||
|
||||
if add_bias_kv:
|
||||
self.bias_k = Parameter(torch.Tensor(1, 1, embed_dim))
|
||||
self.bias_v = Parameter(torch.Tensor(1, 1, embed_dim))
|
||||
else:
|
||||
self.bias_k = self.bias_v = None
|
||||
|
||||
self.add_zero_attn = add_zero_attn
|
||||
|
||||
self.reset_parameters()
|
||||
|
||||
if freeze_compress == 1:
|
||||
self.compress_k.weight.requires_grad = False
|
||||
if shared_kv_compressed == 0:
|
||||
self.compress_v.weight.requires_grad = False
|
||||
|
||||
self.onnx_trace = False
|
||||
|
||||
def prepare_for_onnx_export_(self):
|
||||
self.onnx_trace = True
|
||||
|
||||
def reset_parameters(self):
|
||||
if self.qkv_same_dim:
|
||||
# Empirically observed the convergence to be much better with
|
||||
# the scaled initialization
|
||||
nn.init.xavier_uniform_(self.k_proj.weight, gain=1 / math.sqrt(2))
|
||||
nn.init.xavier_uniform_(self.v_proj.weight, gain=1 / math.sqrt(2))
|
||||
nn.init.xavier_uniform_(self.q_proj.weight, gain=1 / math.sqrt(2))
|
||||
if (
|
||||
not self.layerwise_sharing
|
||||
): # otherwise, we already initialize the parameters
|
||||
nn.init.xavier_uniform_(self.compress_k.weight, gain=1 / math.sqrt(2))
|
||||
if self.shared_kv_compressed == 0:
|
||||
nn.init.xavier_uniform_(
|
||||
self.compress_v.weight, gain=1 / math.sqrt(2)
|
||||
)
|
||||
else:
|
||||
nn.init.xavier_uniform_(self.k_proj.weight)
|
||||
nn.init.xavier_uniform_(self.v_proj.weight)
|
||||
nn.init.xavier_uniform_(self.q_proj.weight)
|
||||
if (
|
||||
not self.layerwise_sharing
|
||||
): # otherwise, we already initialize the parameters
|
||||
nn.init.xavier_uniform_(self.compress_k.weight)
|
||||
if self.shared_kv_compressed == 0:
|
||||
nn.init.xavier_uniform_(self.compress_v.weight)
|
||||
|
||||
nn.init.xavier_uniform_(self.out_proj.weight)
|
||||
if self.out_proj.bias is not None:
|
||||
nn.init.constant_(self.out_proj.bias, 0.0)
|
||||
if self.bias_k is not None:
|
||||
nn.init.xavier_normal_(self.bias_k)
|
||||
if self.bias_v is not None:
|
||||
nn.init.xavier_normal_(self.bias_v)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query,
|
||||
key: Optional[Tensor],
|
||||
value: Optional[Tensor],
|
||||
key_padding_mask: Optional[Tensor] = None,
|
||||
incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]] = None,
|
||||
need_weights: bool = True,
|
||||
static_kv: bool = False,
|
||||
attn_mask: Optional[Tensor] = None,
|
||||
before_softmax: bool = False,
|
||||
need_head_weights: bool = False,
|
||||
) -> Tuple[Tensor, Optional[Tensor]]:
|
||||
"""Input shape: Time x Batch x Channel
|
||||
|
||||
Args:
|
||||
key_padding_mask (ByteTensor, optional): mask to exclude
|
||||
keys that are pads, of shape `(batch, src_len)`, where
|
||||
padding elements are indicated by 1s.
|
||||
need_weights (bool, optional): return the attention weights,
|
||||
averaged over heads (default: False).
|
||||
attn_mask (ByteTensor, optional): typically used to
|
||||
implement causal attention, where the mask prevents the
|
||||
attention from looking forward in time (default: None).
|
||||
before_softmax (bool, optional): return the raw attention
|
||||
weights and values before the attention softmax.
|
||||
need_head_weights (bool, optional): return the attention
|
||||
weights for each head. Implies *need_weights*. Default:
|
||||
return the average attention weights over all heads.
|
||||
"""
|
||||
if need_head_weights:
|
||||
need_weights = True
|
||||
|
||||
tgt_len, bsz, embed_dim = query.size()
|
||||
assert embed_dim == self.embed_dim
|
||||
assert list(query.size()) == [tgt_len, bsz, embed_dim]
|
||||
|
||||
if incremental_state is not None:
|
||||
saved_state = self._get_input_buffer(incremental_state)
|
||||
if saved_state is not None and "prev_key" in saved_state:
|
||||
# previous time steps are cached - no need to recompute
|
||||
# key and value if they are static
|
||||
if static_kv:
|
||||
assert self.encoder_decoder_attention and not self.self_attention
|
||||
key = value = None
|
||||
else:
|
||||
saved_state = None
|
||||
|
||||
if self.self_attention:
|
||||
q = self.q_proj(query)
|
||||
|
||||
k_input = query.permute(1, 2, 0).contiguous() # B * C * T
|
||||
k_input = (
|
||||
F.linear(k_input, self.compress_k.weight[:, 0:tgt_len])
|
||||
.permute(2, 0, 1)
|
||||
.contiguous()
|
||||
)
|
||||
k = self.k_proj(k_input)
|
||||
|
||||
v_input = query.permute(1, 2, 0).contiguous() # B * C * T
|
||||
if self.shared_kv_compressed == 0:
|
||||
v_input = (
|
||||
F.linear(v_input, self.compress_v.weight[:, 0:tgt_len])
|
||||
.permute(2, 0, 1)
|
||||
.contiguous()
|
||||
)
|
||||
if self.shared_kv_compressed == 1: # use shared kv compressed linear layer
|
||||
v_input = (
|
||||
F.linear(v_input, self.compress_k.weight[:, 0:tgt_len])
|
||||
.permute(2, 0, 1)
|
||||
.contiguous()
|
||||
)
|
||||
v = self.v_proj(v_input)
|
||||
elif self.encoder_decoder_attention:
|
||||
# encoder-decoder attention
|
||||
q = self.q_proj(query)
|
||||
if key is None:
|
||||
assert value is None
|
||||
k = v = None
|
||||
else:
|
||||
k = self.k_proj(key)
|
||||
v = self.v_proj(key)
|
||||
|
||||
else:
|
||||
assert key is not None and value is not None
|
||||
q = self.q_proj(query)
|
||||
k = self.k_proj(key)
|
||||
v = self.v_proj(value)
|
||||
q *= self.scaling
|
||||
|
||||
if self.bias_k is not None:
|
||||
assert self.bias_v is not None
|
||||
k = torch.cat([k, self.bias_k.repeat(1, bsz, 1)])
|
||||
v = torch.cat([v, self.bias_v.repeat(1, bsz, 1)])
|
||||
if attn_mask is not None:
|
||||
attn_mask = torch.cat(
|
||||
[attn_mask, attn_mask.new_zeros(attn_mask.size(0), 1)], dim=1
|
||||
)
|
||||
if key_padding_mask is not None:
|
||||
key_padding_mask = torch.cat(
|
||||
[
|
||||
key_padding_mask,
|
||||
key_padding_mask.new_zeros(key_padding_mask.size(0), 1),
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
q = (
|
||||
q.contiguous()
|
||||
.view(tgt_len, bsz * self.num_heads, self.head_dim)
|
||||
.transpose(0, 1)
|
||||
)
|
||||
if k is not None:
|
||||
k = (
|
||||
k.contiguous()
|
||||
.view(-1, bsz * self.num_heads, self.head_dim)
|
||||
.transpose(0, 1)
|
||||
)
|
||||
if v is not None:
|
||||
v = (
|
||||
v.contiguous()
|
||||
.view(-1, bsz * self.num_heads, self.head_dim)
|
||||
.transpose(0, 1)
|
||||
)
|
||||
|
||||
if saved_state is not None:
|
||||
# saved states are stored with shape (bsz, num_heads, seq_len, head_dim)
|
||||
if "prev_key" in saved_state:
|
||||
_prev_key = saved_state["prev_key"]
|
||||
assert _prev_key is not None
|
||||
prev_key = _prev_key.view(bsz * self.num_heads, -1, self.head_dim)
|
||||
if static_kv:
|
||||
k = prev_key
|
||||
else:
|
||||
assert k is not None
|
||||
k = torch.cat([prev_key, k], dim=1)
|
||||
if "prev_value" in saved_state:
|
||||
_prev_value = saved_state["prev_value"]
|
||||
assert _prev_value is not None
|
||||
prev_value = _prev_value.view(bsz * self.num_heads, -1, self.head_dim)
|
||||
if static_kv:
|
||||
v = prev_value
|
||||
else:
|
||||
assert v is not None
|
||||
v = torch.cat([prev_value, v], dim=1)
|
||||
prev_key_padding_mask: Optional[Tensor] = None
|
||||
if "prev_key_padding_mask" in saved_state:
|
||||
prev_key_padding_mask = saved_state["prev_key_padding_mask"]
|
||||
assert k is not None and v is not None
|
||||
key_padding_mask = MultiheadLinearAttention._append_prev_key_padding_mask(
|
||||
key_padding_mask=key_padding_mask,
|
||||
prev_key_padding_mask=prev_key_padding_mask,
|
||||
batch_size=bsz,
|
||||
src_len=k.size(1),
|
||||
static_kv=static_kv,
|
||||
)
|
||||
|
||||
saved_state["prev_key"] = k.view(bsz, self.num_heads, -1, self.head_dim)
|
||||
saved_state["prev_value"] = v.view(bsz, self.num_heads, -1, self.head_dim)
|
||||
saved_state["prev_key_padding_mask"] = key_padding_mask
|
||||
# In this branch incremental_state is never None
|
||||
assert incremental_state is not None
|
||||
incremental_state = self._set_input_buffer(incremental_state, saved_state)
|
||||
assert k is not None
|
||||
src_len = k.size(1)
|
||||
|
||||
if self.add_zero_attn:
|
||||
assert v is not None
|
||||
src_len += 1
|
||||
k = torch.cat([k, k.new_zeros((k.size(0), 1) + k.size()[2:])], dim=1)
|
||||
v = torch.cat([v, v.new_zeros((v.size(0), 1) + v.size()[2:])], dim=1)
|
||||
if attn_mask is not None:
|
||||
attn_mask = torch.cat(
|
||||
[attn_mask, attn_mask.new_zeros(attn_mask.size(0), 1)], dim=1
|
||||
)
|
||||
|
||||
attn_weights = torch.bmm(q, k.transpose(1, 2))
|
||||
attn_weights = MultiheadLinearAttention.apply_sparse_mask(
|
||||
attn_weights, tgt_len, src_len, bsz
|
||||
)
|
||||
|
||||
assert list(attn_weights.size()) == [bsz * self.num_heads, tgt_len, src_len]
|
||||
|
||||
if attn_mask is not None:
|
||||
attn_mask = attn_mask.unsqueeze(0)
|
||||
if self.onnx_trace:
|
||||
attn_mask = attn_mask.repeat(attn_weights.size(0), 1, 1)
|
||||
attn_weights += attn_mask
|
||||
|
||||
if before_softmax:
|
||||
return attn_weights, v
|
||||
|
||||
attn_weights_float = utils.softmax(
|
||||
attn_weights, dim=-1, onnx_trace=self.onnx_trace
|
||||
)
|
||||
attn_weights = attn_weights_float.type_as(attn_weights)
|
||||
attn_probs = F.dropout(
|
||||
attn_weights,
|
||||
p=self.dropout,
|
||||
training=self.training,
|
||||
)
|
||||
assert v is not None
|
||||
attn = torch.bmm(attn_probs, v)
|
||||
assert list(attn.size()) == [bsz * self.num_heads, tgt_len, self.head_dim]
|
||||
if self.onnx_trace and attn.size(1) == 1:
|
||||
# when ONNX tracing a single decoder step (sequence length == 1)
|
||||
# the transpose is a no-op copy before view, thus unnecessary
|
||||
attn = attn.contiguous().view(tgt_len, bsz, embed_dim)
|
||||
else:
|
||||
attn = attn.transpose(0, 1).contiguous().view(tgt_len, bsz, embed_dim)
|
||||
attn = self.out_proj(attn)
|
||||
attn_weights: Optional[Tensor] = None
|
||||
if need_weights:
|
||||
attn_weights = attn_weights_float.view(
|
||||
bsz, self.num_heads, tgt_len, src_len
|
||||
).transpose(1, 0)
|
||||
if not need_head_weights:
|
||||
# average attention weights over heads
|
||||
attn_weights = attn_weights.mean(dim=0)
|
||||
|
||||
return attn, attn_weights
|
||||
|
||||
@staticmethod
|
||||
def _append_prev_key_padding_mask(
|
||||
key_padding_mask: Optional[Tensor],
|
||||
prev_key_padding_mask: Optional[Tensor],
|
||||
batch_size: int,
|
||||
src_len: int,
|
||||
static_kv: bool,
|
||||
) -> Optional[Tensor]:
|
||||
# saved key padding masks have shape (bsz, seq_len)
|
||||
if prev_key_padding_mask is not None and static_kv:
|
||||
new_key_padding_mask = prev_key_padding_mask
|
||||
elif prev_key_padding_mask is not None and key_padding_mask is not None:
|
||||
new_key_padding_mask = torch.cat(
|
||||
[prev_key_padding_mask.float(), key_padding_mask.float()], dim=1
|
||||
)
|
||||
# During incremental decoding, as the padding token enters and
|
||||
# leaves the frame, there will be a time when prev or current
|
||||
# is None
|
||||
elif prev_key_padding_mask is not None:
|
||||
filler = torch.zeros(
|
||||
(batch_size, src_len - prev_key_padding_mask.size(1)),
|
||||
device=prev_key_padding_mask.device,
|
||||
)
|
||||
new_key_padding_mask = torch.cat(
|
||||
[prev_key_padding_mask.float(), filler.float()], dim=1
|
||||
)
|
||||
elif key_padding_mask is not None:
|
||||
filler = torch.zeros(
|
||||
(batch_size, src_len - key_padding_mask.size(1)),
|
||||
device=key_padding_mask.device,
|
||||
)
|
||||
new_key_padding_mask = torch.cat(
|
||||
[filler.float(), key_padding_mask.float()], dim=1
|
||||
)
|
||||
else:
|
||||
new_key_padding_mask = prev_key_padding_mask
|
||||
return new_key_padding_mask
|
||||
|
||||
@torch.jit.export
|
||||
def reorder_incremental_state(
|
||||
self,
|
||||
incremental_state: Dict[str, Dict[str, Optional[Tensor]]],
|
||||
new_order: Tensor,
|
||||
):
|
||||
"""Reorder buffered internal state (for incremental generation)."""
|
||||
input_buffer = self._get_input_buffer(incremental_state)
|
||||
if input_buffer is not None:
|
||||
for k in input_buffer.keys():
|
||||
input_buffer_k = input_buffer[k]
|
||||
if input_buffer_k is not None:
|
||||
if self.encoder_decoder_attention and input_buffer_k.size(
|
||||
0
|
||||
) == new_order.size(0):
|
||||
break
|
||||
input_buffer[k] = input_buffer_k.index_select(0, new_order)
|
||||
incremental_state = self._set_input_buffer(incremental_state, input_buffer)
|
||||
return incremental_state
|
||||
|
||||
def _get_input_buffer(
|
||||
self, incremental_state: Optional[Dict[str, Dict[str, Optional[Tensor]]]]
|
||||
) -> Dict[str, Optional[Tensor]]:
|
||||
result = self.get_incremental_state(incremental_state, "attn_state")
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
empty_result: Dict[str, Optional[Tensor]] = {}
|
||||
return empty_result
|
||||
|
||||
def _set_input_buffer(
|
||||
self,
|
||||
incremental_state: Dict[str, Dict[str, Optional[Tensor]]],
|
||||
buffer: Dict[str, Optional[Tensor]],
|
||||
):
|
||||
return self.set_incremental_state(incremental_state, "attn_state", buffer)
|
||||
|
||||
def apply_sparse_mask(attn_weights, tgt_len: int, src_len: int, bsz: int):
|
||||
return attn_weights
|
||||
|
||||
def upgrade_state_dict_named(self, state_dict, name):
|
||||
prefix = name + "." if name != "" else ""
|
||||
items_to_add = {}
|
||||
keys_to_remove = []
|
||||
for k in state_dict.keys():
|
||||
if k.endswith(prefix + "in_proj_weight"):
|
||||
# in_proj_weight used to be q + k + v with same dimensions
|
||||
dim = int(state_dict[k].shape[0] / 3)
|
||||
items_to_add[prefix + "q_proj.weight"] = state_dict[k][:dim]
|
||||
items_to_add[prefix + "k_proj.weight"] = state_dict[k][dim : 2 * dim]
|
||||
items_to_add[prefix + "v_proj.weight"] = state_dict[k][2 * dim :]
|
||||
|
||||
keys_to_remove.append(k)
|
||||
|
||||
k_bias = prefix + "in_proj_bias"
|
||||
if k_bias in state_dict.keys():
|
||||
dim = int(state_dict[k].shape[0] / 3)
|
||||
items_to_add[prefix + "q_proj.bias"] = state_dict[k_bias][:dim]
|
||||
items_to_add[prefix + "k_proj.bias"] = state_dict[k_bias][
|
||||
dim : 2 * dim
|
||||
]
|
||||
items_to_add[prefix + "v_proj.bias"] = state_dict[k_bias][2 * dim :]
|
||||
|
||||
keys_to_remove.append(prefix + "in_proj_bias")
|
||||
|
||||
for k in keys_to_remove:
|
||||
del state_dict[k]
|
||||
|
||||
for key, value in items_to_add.items():
|
||||
state_dict[key] = value
|
||||
@@ -0,0 +1,241 @@
|
||||
# Beyond English-Centric Multilingual Machine Translation
|
||||
|
||||
## Introduction
|
||||
In this work, we create a true Many-to-Many multilingual translation model that can translate directly between any pair of 100 languages. Our focus on non-English-Centric models brings gains of more than 10 BLEU when directly translating between non-English directions while performing competitively with the best single systems of WMT.
|
||||
|
||||
If you are new to using fairseq, read the following walkthrough. Otherwise, skip to the sections below.
|
||||
|
||||
0. **Generation Data**
|
||||
|
||||
To download the generation data, follow the below commands. Note that all datasets need to be detokenized *before* applying SPM in the data preprocessing step. If you use these evaluation datasets, please cite their associated papers.
|
||||
```bash
|
||||
# WMT - use sacrebleu, example here:
|
||||
sacrebleu -t wmt14 -l fr-en --echo src > wmt.test.fr-en.fr
|
||||
sacrebleu -t wmt14 -l fr-en --echo ref > wmt.test.fr-en.en
|
||||
|
||||
# WAT
|
||||
wget http://lotus.kuee.kyoto-u.ac.jp/WAT/my-en-data/wat2020.my-en.zip
|
||||
unzip wat2020.my-en.zip
|
||||
|
||||
# FLORES
|
||||
# download from: https://github.com/facebookresearch/flores
|
||||
|
||||
# TED - need to detokenize with Moses!
|
||||
# from: https://github.com/neulab/word-embeddings-for-nmt
|
||||
wget http://phontron.com/data/ted_talks.tar.gz
|
||||
|
||||
# Autshumato
|
||||
# request to download: https://repo.sadilar.org/handle/20.500.12185/397
|
||||
|
||||
# Tatoeba Challenge
|
||||
# available here: https://github.com/Helsinki-NLP/Tatoeba-Challenge
|
||||
```
|
||||
|
||||
1. **Training Data**
|
||||
|
||||
To produce the training data, we use a combination of [CCMatrix](https://arxiv.org/abs/1911.04944) and [CCAligned](https://arxiv.org/abs/1911.06154). Check out the instructions [here](https://github.com/facebookresearch/LASER/tree/master/tasks/CCMatrix) to download the raw data.
|
||||
|
||||
2. **Preprocess Data**
|
||||
|
||||
After downloading raw data, you will need to postprocess the data, then apply SPM, then binarize. Note that it is very important you run the postprocessing script, because this removes any instance of the evaluation data in the mined training data.
|
||||
|
||||
```bash
|
||||
# preprocess data
|
||||
|
||||
# remove sentences with more than 50% punctuation
|
||||
python /path/to/fairseq/examples/m2m_100/process_data/remove_too_much_punc.py
|
||||
|
||||
# deduplicate training data
|
||||
paste /path/to/datadir/train.$src /path/to/datadir/train.$tgt | awk '!x[$0]++' > /path/to/datadir/train.dedup
|
||||
echo "keeping $(wc -l /path/to/datadir/train.dedup) bitext out of $(wc -l /path/to/datadir/train.$src)"
|
||||
cut -f1 /path/to/datadir/train.dedup > /path/to/datadir/train.$src
|
||||
cut -f2 /path/to/datadir/train.dedup > /path/to/datadir/train.$tgt
|
||||
|
||||
# remove all instances of evaluation data from the training data
|
||||
python /path/to/fairseq/examples/m2m_100/process_data/dedup_data.py
|
||||
|
||||
# frequency cleaning
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/histograms.tar.gz
|
||||
tar -xvzf histograms.tar.gz
|
||||
python /path/to/fairseq/examples/m2m_100/process_data/clean_histogram.py --src $src --tgt $tgt --src-file /path/to/source/file --tgt-file /path/to/output/file --src-output-file source_output.$src --tgt-output-file target_output.$tgt --histograms /path/to/histograms
|
||||
|
||||
# apply SPM
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/spm.128k.model
|
||||
python /path/to/fairseq/scripts/spm_encode.py \
|
||||
--model spm.128k.model \
|
||||
--output_format=piece \
|
||||
--inputs=/path/to/input/file/here \
|
||||
--outputs=/path/to/output/file/here
|
||||
|
||||
# length ratio cleaning
|
||||
perl mosesdecoder/scripts/training/clean-corpus-n.perl --ratio 3 /path/to/training/data/train.spm.$src-$tgt $src $tgt /path/to/output/directory/train.spm.$src-$tgt 1 250
|
||||
|
||||
# binarize data
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/data_dict.128k.txt
|
||||
fairseq-preprocess \
|
||||
--source-lang $src --target-lang $tgt \
|
||||
--testpref spm.$src.$tgt \
|
||||
--thresholdsrc 0 --thresholdtgt 0 \
|
||||
--destdir data_bin \
|
||||
--srcdict data_dict.128k.txt --tgtdict data_dict.128k.txt
|
||||
```
|
||||
|
||||
3. **Training Scripts**
|
||||
|
||||
To reproduce the training of our models, we train with fairseq-py's multilingual translation [task](https://github.com/pytorch/fairseq/tree/master/examples/multilingual). If you are interested in model parallel training, also check out [fairscale](https://github.com/facebookresearch/fairscale).
|
||||
|
||||
4. **Generation**
|
||||
|
||||
To generate from our models, follow the the commands in the generation section below.
|
||||
|
||||
|
||||
If you use any of the resources listed here, please cite:
|
||||
```bibtex
|
||||
@article{fan2020beyond,
|
||||
title={Beyond English-Centric Multilingual Machine Translation},
|
||||
author={Fan, Angela and Bhosale, Shruti and Schwenk, Holger and Ma, Zhiyi and El-Kishky, Ahmed and Goyal, Siddharth and Baines, Mandeep and Celebi, Onur and Wenzek, Guillaume and Chaudhary, Vishrav and Goyal, Naman and Birch, Tom and Liptchinsky, Vitaliy and Edunov, Sergey and Grave, Edouard and Auli, Michael and Joulin, Armand},
|
||||
journal={arXiv preprint},
|
||||
year={2020}
|
||||
}
|
||||
|
||||
@article{schwenk2019ccmatrix,
|
||||
title={Ccmatrix: Mining billions of high-quality parallel sentences on the web},
|
||||
author={Schwenk, Holger and Wenzek, Guillaume and Edunov, Sergey and Grave, Edouard and Joulin, Armand},
|
||||
journal={arXiv preprint arXiv:1911.04944},
|
||||
year={2019}
|
||||
}
|
||||
|
||||
@article{el2019massive,
|
||||
title={A Massive Collection of Cross-Lingual Web-Document Pairs},
|
||||
author={El-Kishky, Ahmed and Chaudhary, Vishrav and Guzman, Francisco and Koehn, Philipp},
|
||||
journal={arXiv preprint arXiv:1911.06154},
|
||||
year={2019}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Trained Models
|
||||
|
||||
### 418M and 1.2B Model
|
||||
We include the last checkpoint for both of these models.
|
||||
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/model_dict.128k.txt
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/language_pairs_small_models.txt
|
||||
|
||||
# 418M parameter model
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/418M_last_checkpoint.pt
|
||||
|
||||
# 1.2B parameter model
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/1.2B_last_checkpoint.pt
|
||||
|
||||
# Generation:
|
||||
fairseq-generate $binarized_data_path --batch-size 32 --path $path_to_model -s en -t fr --remove-bpe 'sentencepiece' --beam 5 --task translation_multi_simple_epoch --lang-pairs language_pairs_small_models --decoder-langtok --encoder-langtok src --gen-subset test > gen_out
|
||||
```
|
||||
|
||||
### 12B Model
|
||||
12B parameter model trained on many-to-many training data for 100 languages. We include the last checkpoint, average of last 5 checkpoints, average of last 10 checkpoints. There isn't a universally best choice out of these three, but all three versions are pretty close in accuracy. You can either sweep over the 3 checkpoints on a dev test and use the best performing checkpoint for final testing. Or the last checkpoint can be a good default choice.
|
||||
|
||||
**Model Download Links**
|
||||
Configuration | 2 32GB GPUs | 4 16GB GPUs | 6 12GB GPUs | 8 8GB GPUs
|
||||
:--|:--|:--|:--|:--
|
||||
Last Checkpoint | [12b_last_chk_2_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_last_chk_2_gpus.pt) | [12b_last_chk_4_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_last_chk_4_gpus.pt) | [12b_last_chk_6_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_last_chk_6_gpus.pt) | [12b_last_chk_8_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_last_chk_8_gpus.pt)
|
||||
Average of last 5 checkpoints | [12b_avg5_chk_2_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg5_chk_2_gpus.pt) | [12b_avg5_chk_4_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg5_chk_4_gpus.pt) | [12b_avg5_chk_6_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg5_chk_6_gpus.pt) | [12b_avg5_chk_8_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg5_chk_8_gpus.pt)
|
||||
Average of last 10 checkpoints | [12b_avg10_chk_2_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg10_chk_2_gpus.pt) | [12b_avg10_chk_4_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg10_chk_4_gpus.pt) | [12b_avg10_chk_6_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg10_chk_6_gpus.pt) | [12b_avg10_chk_8_gpus.pt](https://dl.fbaipublicfiles.com/m2m_100/12b_avg10_chk_8_gpus.pt)
|
||||
|
||||
**Generation Arguments**
|
||||
Configuration | 2 32GB GPUs | 4 16GB GPUs | 6 12GB GPUs | 8 8GB GPUs
|
||||
:--|:--|:--|:--|:--
|
||||
`--pipeline-encoder-balance` | `[26]` | `[1,15,10]` | `[1,9,9,7]` | `[1,6,6,6,7]`
|
||||
`--pipeline-encoder-devices` | `[0]` | `[0,1,0]` | `[0,1,2,0]` | `[0,4,5,1,0]`
|
||||
`--pipeline-decoder-balance` | `[3,22,1]` | `[3,11,11,1]` | `[3,7,7,8,1]` | `[1,6,6,6,6,1]`
|
||||
`--pipeline-decoder-devices` | `[0,1,0]` | `[0,2,3,0]` | `[0,3,4,5,0]` | `[0,2,6,7,3,0]`
|
||||
|
||||
|
||||
## SentencePiece Model
|
||||
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/spm.128k.model
|
||||
```
|
||||
|
||||
## Generation with M2M-100
|
||||
|
||||
### Encode using our SentencePiece Model
|
||||
|
||||
Note: Install SentencePiece from [here](https://github.com/google/sentencepiece)
|
||||
|
||||
```bash
|
||||
fairseq=/path/to/fairseq
|
||||
cd $fairseq
|
||||
sacrebleu --echo src -l de-fr -t wmt19 | head -n 20 > raw_input.de-fr.de
|
||||
sacrebleu --echo ref -l de-fr -t wmt19 | head -n 20 > raw_input.de-fr.fr
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/spm.128k.model
|
||||
for lang in de fr ; do
|
||||
python scripts/spm_encode.py \
|
||||
--model spm.128k.model \
|
||||
--output_format=piece \
|
||||
--inputs=raw_input.de-fr.${lang} \
|
||||
--outputs=spm.de-fr.${lang}
|
||||
done
|
||||
```
|
||||
|
||||
### Binarization
|
||||
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/data_dict.128k.txt
|
||||
fairseq-preprocess \
|
||||
--source-lang de --target-lang fr \
|
||||
--testpref spm.de-fr \
|
||||
--thresholdsrc 0 --thresholdtgt 0 \
|
||||
--destdir data_bin \
|
||||
--srcdict data_dict.128k.txt --tgtdict data_dict.128k.txt
|
||||
```
|
||||
|
||||
### Generation for the 12B model
|
||||
|
||||
Note that generation can currently be run using 2 32GB / 4 16GB / 6 12GB / 8 8GB GPUs, and the corresponding model checkpoints and pipeline arguments can be found in the [12B Model Section](#12b-model).
|
||||
Generation on CPUs will be added in the future.
|
||||
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/model_dict.128k.txt
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/language_pairs.txt
|
||||
wget https://dl.fbaipublicfiles.com/m2m_100/12b_last_chk_4_gpus.pt
|
||||
fairseq-generate \
|
||||
data_bin \
|
||||
--batch-size 1 \
|
||||
--path 12b_last_chk_4_gpus.pt \
|
||||
--fixed-dictionary model_dict.128k.txt \
|
||||
-s de -t fr \
|
||||
--remove-bpe 'sentencepiece' \
|
||||
--beam 5 \
|
||||
--task translation_multi_simple_epoch \
|
||||
--lang-pairs language_pairs.txt \
|
||||
--decoder-langtok --encoder-langtok src \
|
||||
--gen-subset test \
|
||||
--fp16 \
|
||||
--dataset-impl mmap \
|
||||
--distributed-world-size 1 --distributed-no-spawn \
|
||||
--pipeline-model-parallel \
|
||||
--pipeline-chunks 1 \
|
||||
--pipeline-encoder-balance '[1,15,10]' \
|
||||
--pipeline-encoder-devices '[0,1,0]' \
|
||||
--pipeline-decoder-balance '[3,11,11,1]' \
|
||||
--pipeline-decoder-devices '[0,2,3,0]' > gen_out
|
||||
```
|
||||
## Evaluation with M2M-100
|
||||
|
||||
### Tokenization
|
||||
|
||||
Note: Refer to tokenizers/README.md for more details on tokenization.
|
||||
|
||||
```bash
|
||||
cd ${fairseq}/examples/m2m_100
|
||||
cat ${fairseq}/gen_out | grep -P "^H" | sort -V | cut -f 3- | sh tok.sh fr > hyp
|
||||
cat ${fairseq}/raw_input.de-fr.fr | sh tok.sh fr > ref
|
||||
```
|
||||
|
||||
### BLEU
|
||||
|
||||
```bash
|
||||
sacrebleu -tok 'none' ref < hyp
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
|
||||
|
||||
CWD=`pwd`
|
||||
INSTALL_PATH=$CWD/tokenizers/thirdparty
|
||||
|
||||
MOSES=$INSTALL_PATH/mosesdecoder
|
||||
if [ ! -d $MOSES ]; then
|
||||
echo 'Cloning Moses github repository (for tokenization scripts)...'
|
||||
git clone https://github.com/moses-smt/mosesdecoder.git $MOSES
|
||||
cd $MOSES
|
||||
# To deal with differences in handling ' vs "
|
||||
git checkout 03578921cc1a03402
|
||||
cd -
|
||||
fi
|
||||
|
||||
WMT16_SCRIPTS=$INSTALL_PATH/wmt16-scripts
|
||||
if [ ! -d $WMT16_SCRIPTS ]; then
|
||||
echo 'Cloning Romanian tokenization scripts'
|
||||
git clone https://github.com/rsennrich/wmt16-scripts.git $WMT16_SCRIPTS
|
||||
fi
|
||||
|
||||
KYTEA=$INSTALL_PATH/kytea
|
||||
if [ ! -f $KYTEA/bin/kytea ]; then
|
||||
git clone https://github.com/neubig/kytea.git $KYTEA
|
||||
cd $KYTEA
|
||||
autoreconf -i
|
||||
./configure --prefix=`pwd`
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
fi
|
||||
|
||||
export MECAB=$INSTALL_PATH/mecab-0.996-ko-0.9.2
|
||||
if [ ! -f $MECAB/bin/mecab ]; then
|
||||
cd $INSTALL_PATH
|
||||
curl -LO https://bitbucket.org/eunjeon/mecab-ko/downloads/mecab-0.996-ko-0.9.2.tar.gz
|
||||
tar zxfv mecab-0.996-ko-0.9.2.tar.gz
|
||||
cd mecab-0.996-ko-0.9.2/
|
||||
./configure --prefix=`pwd`
|
||||
make
|
||||
make install
|
||||
|
||||
cd ..
|
||||
curl -LO https://bitbucket.org/eunjeon/mecab-ko-dic/downloads/mecab-ko-dic-2.1.1-20180720.tar.gz
|
||||
tar zxfv mecab-ko-dic-2.1.1-20180720.tar.gz
|
||||
cd mecab-ko-dic-2.1.1-20180720/
|
||||
./autogen.sh
|
||||
./configure --prefix=`pwd` --with-dicdir=$MECAB/lib/mecab/dic/mecab-ko-dic --with-mecab-config=$MECAB/bin/mecab-config
|
||||
make
|
||||
sh -c 'echo "dicdir=$MECAB/lib/mecab/dic/mecab-ko-dic" > $MECAB/etc/mecabrc'
|
||||
make install
|
||||
cd $CWD
|
||||
fi
|
||||
|
||||
INDIC_RESOURCES_PATH=$INSTALL_PATH/indic_nlp_resources
|
||||
if [ ! -d $INDIC_RESOURCES_PATH ]; then
|
||||
echo 'Cloning indic_nlp_resources'
|
||||
git clone https://github.com/anoopkunchukuttan/indic_nlp_resources.git $INDIC_RESOURCES_PATH
|
||||
fi
|
||||
|
||||
|
||||
if [ ! -f $INSTALL_PATH/seg_my.py ]; then
|
||||
cd $INSTALL_PATH
|
||||
wget http://lotus.kuee.kyoto-u.ac.jp/WAT/my-en-data/wat2020.my-en.zip
|
||||
unzip wat2020.my-en.zip
|
||||
# switch to python3
|
||||
cat wat2020.my-en/myseg.py |sed 's/^sys.std/###sys.std/g' | sed 's/### sys/sys/g' | sed 's/unichr/chr/g' > seg_my.py
|
||||
cd $CWD
|
||||
fi
|
||||
|
||||
|
||||
pip install pythainlp sacrebleu indic-nlp-library
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--src', type=str, help='Source language')
|
||||
parser.add_argument('--tgt', type=str, help='Target language')
|
||||
parser.add_argument('--src-file', type=str, help='Input source file')
|
||||
parser.add_argument('--tgt-file', type=str, help='Input target file')
|
||||
parser.add_argument('--src-output-file', type=str, help='Output source file')
|
||||
parser.add_argument('--tgt-output-file', type=str, help='Output target file')
|
||||
parser.add_argument('--threshold', type=float, default=0.5, help='Threshold')
|
||||
parser.add_argument('--threshold-character', type=str, default=']', help='Threshold character')
|
||||
parser.add_argument('--histograms', type=str, help='Path to histograms')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def read_hist(f):
|
||||
ch = []
|
||||
for line in f:
|
||||
c = line[0]
|
||||
if c == args.threshold_character:
|
||||
break
|
||||
ch.append(c)
|
||||
return ch
|
||||
|
||||
|
||||
with(open("{}/{}".format(args.histograms, args.src), 'r', encoding='utf8')) as f:
|
||||
ch1 = read_hist(f)
|
||||
|
||||
with(open("{}/{}".format(args.histograms, args.tgt), 'r', encoding='utf8')) as f:
|
||||
ch2 = read_hist(f)
|
||||
|
||||
print("Accepted characters for {}: {}".format(args.src, ch1))
|
||||
print("Accepted characters for {}: {}".format(args.tgt, ch2))
|
||||
|
||||
with open(args.src_file, 'r', encoding='utf8') as fs1, open(args.tgt_file, 'r', encoding='utf8') as fs2, open(args.src_output_file, 'w', encoding='utf8') as fos1, open(args.tgt_output_file, 'w', encoding='utf8') as fos2:
|
||||
ls1 = fs1.readline()
|
||||
ls2 = fs2.readline()
|
||||
|
||||
while ls1 or ls2:
|
||||
cnt1 = len([c for c in ls1.strip() if c in ch1])
|
||||
cnt2 = len([c for c in ls2.strip() if c in ch2])
|
||||
|
||||
if cnt1 / len(ls1) > args.threshold and cnt2 / len(ls2) > args.threshold:
|
||||
fos1.write(ls1)
|
||||
fos2.write(ls2)
|
||||
else:
|
||||
print("{} {} {} \n{} {} {}".format(args.src, cnt1 / len(ls1), ls1.strip(), args.tgt, cnt2 / len(ls2), ls2.strip()))
|
||||
|
||||
ls1 = fs1.readline()
|
||||
ls2 = fs2.readline()
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
import os
|
||||
|
||||
DATADIR = "/path/to/train_data"
|
||||
DEDUP_FROM_DIR = "/path/to/eval/data"
|
||||
OUTPUT_DIR = "/path/to/output/data"
|
||||
|
||||
|
||||
def main(args):
|
||||
languages = set()
|
||||
for language_directory in os.listdir(DATADIR):
|
||||
if "_" in language_directory:
|
||||
src, tgt = language_directory.split("_")
|
||||
languages.add(LanguagePair(src=src, tgt=tgt))
|
||||
|
||||
data = existing_data()
|
||||
train_languages = sorted(languages)
|
||||
for language_pair in train_languages[args.start_index:args.start_index + args.size]:
|
||||
print(language_pair)
|
||||
dedup(language_pair, data)
|
||||
|
||||
|
||||
LanguagePair = namedtuple("LanguagePair", ["src", "tgt"])
|
||||
|
||||
|
||||
def existing_data():
|
||||
data = set()
|
||||
for file in os.listdir(DEDUP_FROM_DIR):
|
||||
with open(os.path.join(DEDUP_FROM_DIR, file)) as f:
|
||||
data |= set(f.readlines())
|
||||
return data
|
||||
|
||||
def dedup(language_pair, data, verbose=True, output=True):
|
||||
train_filenames = LanguagePair(
|
||||
src=f"{DATADIR}/{language_pair.src}_{language_pair.tgt}/train.{language_pair.src}",
|
||||
tgt=f"{DATADIR}/{language_pair.src}_{language_pair.tgt}/train.{language_pair.tgt}",
|
||||
)
|
||||
|
||||
output_filenames = LanguagePair(
|
||||
src=f"{OUTPUT_DIR}/train.dedup.{language_pair.src}-{language_pair.tgt}.{language_pair.src}",
|
||||
tgt=f"{OUTPUT_DIR}/train.dedup.{language_pair.src}-{language_pair.tgt}.{language_pair.tgt}"
|
||||
)
|
||||
|
||||
# If output exists, skip this pair. It has already been done.
|
||||
if (os.path.exists(output_filenames.src) and
|
||||
os.path.exists(output_filenames.tgt)):
|
||||
if verbose:
|
||||
print(f"{language_pair.src}-{language_pair.tgt} already done.")
|
||||
return
|
||||
|
||||
if verbose:
|
||||
print(f"{language_pair.src}-{language_pair.tgt} ready, will check dups.")
|
||||
|
||||
# If there is no output, no need to actually do the loop.
|
||||
if not output:
|
||||
return
|
||||
|
||||
if os.path.exists(train_filenames.src) and os.path.exists(train_filenames.tgt):
|
||||
with open(train_filenames.src) as f:
|
||||
train_source = f.readlines()
|
||||
|
||||
with open(train_filenames.tgt) as f:
|
||||
train_target = f.readlines()
|
||||
|
||||
# do dedup
|
||||
new_train_source = []
|
||||
new_train_target = []
|
||||
for i, train_line in enumerate(train_source):
|
||||
if train_line not in data and train_target[i] not in data:
|
||||
new_train_source.append(train_line)
|
||||
new_train_target.append(train_target[i])
|
||||
|
||||
assert len(train_source) == len(train_target)
|
||||
assert len(new_train_source) == len(new_train_target)
|
||||
assert len(new_train_source) <= len(train_source)
|
||||
|
||||
with open(output_filenames.src, "w") as o:
|
||||
for line in new_train_source:
|
||||
o.write(line)
|
||||
|
||||
with open(output_filenames.tgt, "w") as o:
|
||||
for line in new_train_target:
|
||||
o.write(line)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-s", "--start-index", required=True, type=int)
|
||||
parser.add_argument("-n", "--size", required=True, type=int)
|
||||
main(parser.parse_args())
|
||||
@@ -0,0 +1,36 @@
|
||||
import gzip
|
||||
import argparse
|
||||
from string import punctuation
|
||||
|
||||
def len_no_punc(s, punc):
|
||||
return len([ch for ch in s if ch in punc])
|
||||
|
||||
def filter_overpunc(len_npunc, len_sen):
|
||||
return len_npunc < 0.5*len_sen
|
||||
|
||||
def main(args):
|
||||
punc = punctuation + "—|–"
|
||||
print('Processing file {}'.format(args.input))
|
||||
with gzip.open(args.input, 'rt', encoding=args.encoding) as tsv:
|
||||
with open(args.bitext + '.' + args.src_lang, 'wt', encoding=args.encoding) as fsrc:
|
||||
with open(args.bitext + '.' + args.tgt_lang, 'wt', encoding=args.encoding) as ftgt:
|
||||
line = tsv.readline()
|
||||
fields = line.split('\t')
|
||||
|
||||
src, tgt = fields[1], fields[2]
|
||||
|
||||
nchar_npunc_src = len_no_punc(src, punc)
|
||||
nchar_npunc_tgt = len_no_punc(tgt, punc)
|
||||
|
||||
if filter_overpunc(nchar_npunc_src, len(src)) and filter_overpunc(nchar_npunc_tgt, len(tgt)):
|
||||
fsrc.write(src.strip() + '\n')
|
||||
ftgt.write(tgt.strip() + '\n')
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", required=True, type=str)
|
||||
parser.add_argument('--encoding', default='utf-8', help='character encoding for input/output')
|
||||
parser.add_argument('--bitext', type=str, required=True, help='language direction')
|
||||
parser.add_argument('--src-lang', type=str, required=True, help='Source language')
|
||||
parser.add_argument('--tgt-lang', type=str, required=True, help='Target language')
|
||||
main(parser.parse_args())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2019-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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
TOKENIZERS_SCRIPTS=tokenizers
|
||||
INSTALL_PATH=$TOKENIZERS_SCRIPTS/thirdparty
|
||||
|
||||
N_THREADS=8
|
||||
|
||||
lg=$1
|
||||
|
||||
MOSES=$INSTALL_PATH/mosesdecoder
|
||||
REPLACE_UNICODE_PUNCT=$MOSES/scripts/tokenizer/replace-unicode-punctuation.perl
|
||||
NORM_PUNC=$MOSES/scripts/tokenizer/normalize-punctuation.perl
|
||||
REM_NON_PRINT_CHAR=$MOSES/scripts/tokenizer/remove-non-printing-char.perl
|
||||
TOKENIZER=$MOSES/scripts/tokenizer/tokenizer.perl
|
||||
|
||||
# special tokenization for Romanian
|
||||
WMT16_SCRIPTS=$INSTALL_PATH/wmt16-scripts
|
||||
|
||||
NORMALIZE_ROMANIAN=$WMT16_SCRIPTS/preprocess/normalise-romanian.py
|
||||
REMOVE_DIACRITICS=$WMT16_SCRIPTS/preprocess/remove-diacritics.py
|
||||
|
||||
# Burmese
|
||||
MY_SEGMENT=$INSTALL_PATH/seg_my.py
|
||||
|
||||
# Arabic
|
||||
AR_TOKENIZER=$TOKENIZERS_SCRIPTS/tokenizer_ar.sh
|
||||
|
||||
# Korean
|
||||
KO_SEGMENT=$TOKENIZERS_SCRIPTS/seg_ko.sh
|
||||
|
||||
# Japanese
|
||||
JA_SEGMENT=$TOKENIZERS_SCRIPTS/seg_ja.sh
|
||||
|
||||
# Indic
|
||||
IN_TOKENIZER=$TOKENIZERS_SCRIPTS/tokenize_indic.py
|
||||
INDIC_RESOURCES_PATH=$INSTALL_PATH/indic_nlp_resources
|
||||
|
||||
# Thai
|
||||
THAI_TOKENIZER=$TOKENIZERS_SCRIPTS/tokenize_thai.py
|
||||
|
||||
# Chinese
|
||||
CHINESE_TOKENIZER=$TOKENIZERS_SCRIPTS/tokenize_zh.py
|
||||
|
||||
# Chinese
|
||||
if [ "$lg" = "zh" ]; then
|
||||
cat - | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l $lg | $REM_NON_PRINT_CHAR | python $CHINESE_TOKENIZER
|
||||
# Thai
|
||||
elif [ "$lg" = "th" ]; then
|
||||
cat - | python $THAI_TOKENIZER
|
||||
# Japanese
|
||||
elif [ "$lg" = "ja" ]; then
|
||||
cat - | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l $lg | $REM_NON_PRINT_CHAR | ${JA_SEGMENT}
|
||||
# Korean
|
||||
elif [ "$lg" = "ko" ]; then
|
||||
cat - | $REM_NON_PRINT_CHAR | ${KO_SEGMENT}
|
||||
# Romanian
|
||||
elif [ "$lg" = "ro" ]; then
|
||||
cat - | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l $lg | $REM_NON_PRINT_CHAR | $NORMALIZE_ROMANIAN | $REMOVE_DIACRITICS | $TOKENIZER -no-escape -threads $N_THREADS -l $lg
|
||||
# Burmese
|
||||
elif [ "$lg" = "my" ]; then
|
||||
cat - | python ${MY_SEGMENT}
|
||||
# Arabic
|
||||
elif [ "$lg" = "ar" ]; then
|
||||
cat - | ${AR_TOKENIZER}
|
||||
# Indic
|
||||
elif [ "$lg" = "ne" ]; then
|
||||
cat - | python ${IN_TOKENIZER} $lg
|
||||
elif [ "$lg" = "si" ]; then
|
||||
cat - | python ${IN_TOKENIZER} $lg
|
||||
elif [ "$lg" = "hi" ]; then
|
||||
cat - | python ${IN_TOKENIZER} $lg
|
||||
# other languages
|
||||
else
|
||||
cat - | $REPLACE_UNICODE_PUNCT | $NORM_PUNC -l $lg | $REM_NON_PRINT_CHAR | $TOKENIZER -no-escape -threads $N_THREADS -l $lg
|
||||
fi
|
||||
@@ -0,0 +1,18 @@
|
||||
# M2M-100 Tokenization
|
||||
|
||||
We apply different tokenization strategies for different languages following the existing literature. Here we provide tok.sh a tokenizer that can be used to reproduce our results.
|
||||
|
||||
To reproduce the results, follow these steps:
|
||||
|
||||
```
|
||||
tgt_lang=...
|
||||
reference_translation=...
|
||||
cat generation_output | grep -P "^H" | sort -V | cut -f 3- | sh tok.sh $tgt_lang > hyp
|
||||
cat $reference_translation |sh tok.sh $tgt_lang > ref
|
||||
sacrebleu -tok 'none' ref < hyp
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Tools needed for all the languages except Arabic can be installed by running install_dependencies.sh
|
||||
If you want to evaluate Arabic models, please follow the instructions provided here: http://alt.qcri.org/tools/arabic-normalizer/ to install
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
SCRIPT=`realpath $0`
|
||||
KYTEA=`dirname $SCRIPT`/thirdparty/kytea
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$KYTEA/lib:/usr/local/lib
|
||||
export PATH=$PATH:"$KYTEA/bin"
|
||||
|
||||
cat - | tr -d "[:blank:]" | kytea -notags
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
SCRIPT=`realpath $0`
|
||||
MECAB=`dirname $SCRIPT`/thirdparty/mecab-0.996-ko-0.9.2
|
||||
|
||||
export PATH=$PATH:"$MECAB/bin":"$MECAB/lib"
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$MECAB/lib"
|
||||
|
||||
cat - | mecab -O wakati
|
||||
@@ -0,0 +1,12 @@
|
||||
seg_my.py
|
||||
indic_nlp_library/
|
||||
indic_nlp_resources/
|
||||
kytea/
|
||||
mecab-0.996-ko-0.9.2.tar.gz
|
||||
mecab-0.996-ko-0.9.2/
|
||||
mosesdecoder/
|
||||
wat2020.my-en.zip
|
||||
wat2020.my-en/
|
||||
wmt16-scripts/
|
||||
mecab-ko-dic-2.1.1-20180720/
|
||||
mecab-ko-dic-2.1.1-20180720.tar.gz
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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.
|
||||
|
||||
# Use: echo {text} | python tokenize_indic.py {language}
|
||||
|
||||
import sys
|
||||
|
||||
from indicnlp.normalize.indic_normalize import IndicNormalizerFactory
|
||||
from indicnlp.tokenize.indic_tokenize import trivial_tokenize
|
||||
|
||||
|
||||
factory = IndicNormalizerFactory()
|
||||
normalizer = factory.get_normalizer(
|
||||
sys.argv[1], remove_nuktas=False, nasals_mode="do_nothing"
|
||||
)
|
||||
|
||||
for line in sys.stdin:
|
||||
normalized_line = normalizer.normalize(line.strip())
|
||||
tokenized_line = " ".join(trivial_tokenize(normalized_line, sys.argv[1]))
|
||||
print(tokenized_line)
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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 sys
|
||||
|
||||
from pythainlp import word_tokenize
|
||||
|
||||
|
||||
for line in sys.stdin:
|
||||
print(" ".join(word_tokenize(line.strip())))
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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 fileinput
|
||||
|
||||
import sacrebleu
|
||||
|
||||
|
||||
for line in fileinput.input():
|
||||
print(sacrebleu.tokenize_zh(line))
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env sh
|
||||
# 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.
|
||||
#
|
||||
# Please follow the instructions here http://alt.qcri.org/tools/arabic-normalizer/
|
||||
# to install tools needed for Arabic
|
||||
|
||||
echo "Please install Arabic tools: http://alt.qcri.org/tools/arabic-normalizer/"
|
||||
echo "Then update environment variables in tokenizer_ar.sh"
|
||||
exit 1
|
||||
|
||||
SVMTOOL=...
|
||||
GOMOSESGO=...
|
||||
QCRI_ARABIC_NORMALIZER=...
|
||||
|
||||
export PERL5LIB="$SVMTOOL/lib":"$GOMOSESGO/bin/MADA-3.2":$PERL5LIB
|
||||
|
||||
|
||||
tempfile=$(mktemp)
|
||||
cat - > $tempfile
|
||||
|
||||
cd $QCRI_ARABIC_NORMALIZER
|
||||
|
||||
bash qcri_normalizer_mada3.2_aramorph1.2.1.sh $tempfile
|
||||
cat $tempfile.mada_norm-aramorph.europarl_tok
|
||||
@@ -0,0 +1,123 @@
|
||||
# MBART: Multilingual Denoising Pre-training for Neural Machine Translation
|
||||
[https://arxiv.org/abs/2001.08210]
|
||||
|
||||
## Introduction
|
||||
|
||||
MBART is a sequence-to-sequence denoising auto-encoder pre-trained on large-scale monolingual corpora in many languages using the BART objective. mBART is one of the first methods for pre-training a complete sequence-to-sequence model by denoising full texts in multiple languages, while previous approaches have focused only on the encoder, decoder, or reconstructing parts of the text.
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | # params | Download
|
||||
---|---|---|---
|
||||
`mbart.CC25` | mBART model with 12 encoder and decoder layers trained on 25 languages' monolingual corpus | 610M | [mbart.CC25.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/mbart/mbart.cc25.v2.tar.gz)
|
||||
`mbart.ft.ro_en` | finetune mBART cc25 model on ro-en language pairs | 610M | [mbart.cc25.ft.enro.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/mbart/mbart.cc25.ft.enro.tar.gz)
|
||||
|
||||
## Results
|
||||
|
||||
**[WMT16 EN-RO](https://www.statmt.org/wmt16/translation-task.html)**
|
||||
|
||||
_(test set, no additional data used)_
|
||||
|
||||
Model | en-ro | ro-en
|
||||
---|---|---
|
||||
`Random` | 34.3 | 34.0
|
||||
`mbart.cc25` | 37.7 | 37.8
|
||||
`mbart.enro.bilingual` | 38.5 | 38.5
|
||||
|
||||
## BPE data
|
||||
# download model
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/mbart/mbart.cc25.v2.tar.gz
|
||||
tar -xzvf mbart.CC25.tar.gz
|
||||
# bpe data
|
||||
install SPM [here](https://github.com/google/sentencepiece)
|
||||
```bash
|
||||
SPM=/path/to/sentencepiece/build/src/spm_encode
|
||||
MODEL=sentence.bpe.model
|
||||
${SPM} --model=${MODEL} < ${DATA}/${TRAIN}.${SRC} > ${DATA}/${TRAIN}.spm.${SRC} &
|
||||
${SPM} --model=${MODEL} < ${DATA}/${TRAIN}.${TGT} > ${DATA}/${TRAIN}.spm.${TGT} &
|
||||
${SPM} --model=${MODEL} < ${DATA}/${VALID}.${SRC} > ${DATA}/${VALID}.spm.${SRC} &
|
||||
${SPM} --model=${MODEL} < ${DATA}/${VALID}.${TGT} > ${DATA}/${VALID}.spm.${TGT} &
|
||||
${SPM} --model=${MODEL} < ${DATA}/${TEST}.${SRC} > ${DATA}/${TEST}.spm.${SRC} &
|
||||
${SPM} --model=${MODEL} < ${DATA}/${TEST}.${TGT} > ${DATA}/${TEST}.spm.${TGT} &
|
||||
```
|
||||
|
||||
## Preprocess data
|
||||
|
||||
```bash
|
||||
DICT=dict.txt
|
||||
fairseq-preprocess \
|
||||
--source-lang ${SRC} \
|
||||
--target-lang ${TGT} \
|
||||
--trainpref ${DATA}/${TRAIN}.spm \
|
||||
--validpref ${DATA}/${VALID}.spm \
|
||||
--testpref ${DATA}/${TEST}.spm \
|
||||
--destdir ${DEST}/${NAME} \
|
||||
--thresholdtgt 0 \
|
||||
--thresholdsrc 0 \
|
||||
--srcdict ${DICT} \
|
||||
--tgtdict ${DICT} \
|
||||
--workers 70
|
||||
```
|
||||
|
||||
## Finetune on EN-RO
|
||||
Finetune on mbart CC25
|
||||
|
||||
```bash
|
||||
PRETRAIN=mbart.cc25 # fix if you moved the downloaded checkpoint
|
||||
langs=ar_AR,cs_CZ,de_DE,en_XX,es_XX,et_EE,fi_FI,fr_XX,gu_IN,hi_IN,it_IT,ja_XX,kk_KZ,ko_KR,lt_LT,lv_LV,my_MM,ne_NP,nl_XX,ro_RO,ru_RU,si_LK,tr_TR,vi_VN,zh_CN
|
||||
|
||||
fairseq-train path_2_data \
|
||||
--encoder-normalize-before --decoder-normalize-before \
|
||||
--arch mbart_large --layernorm-embedding \
|
||||
--task translation_from_pretrained_bart \
|
||||
--source-lang en_XX --target-lang ro_RO \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.2 \
|
||||
--optimizer adam --adam-eps 1e-06 --adam-betas '(0.9, 0.98)' \
|
||||
--lr-scheduler polynomial_decay --lr 3e-05 --warmup-updates 2500 --total-num-update 40000 \
|
||||
--dropout 0.3 --attention-dropout 0.1 --weight-decay 0.0 \
|
||||
--max-tokens 1024 --update-freq 2 \
|
||||
--save-interval 1 --save-interval-updates 5000 --keep-interval-updates 10 --no-epoch-checkpoints \
|
||||
--seed 222 --log-format simple --log-interval 2 \
|
||||
--restore-file $PRETRAIN \
|
||||
--reset-optimizer --reset-meters --reset-dataloader --reset-lr-scheduler \
|
||||
--langs $langs \
|
||||
--ddp-backend no_c10d
|
||||
```
|
||||
## Generate on EN-RO
|
||||
Get sacrebleu on finetuned en-ro model
|
||||
|
||||
get tokenizer [here](https://github.com/rsennrich/wmt16-scripts)
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/mbart/mbart.cc25.ft.enro.tar.gz
|
||||
tar -xzvf mbart.cc25.ft.enro.tar.gz
|
||||
```
|
||||
|
||||
```bash
|
||||
model_dir=MBART_finetuned_enro # fix if you moved the checkpoint
|
||||
|
||||
fairseq-generate path_2_data \
|
||||
--path $model_dir/model.pt \
|
||||
--task translation_from_pretrained_bart \
|
||||
--gen-subset test \
|
||||
-t ro_RO -s en_XX \
|
||||
--bpe 'sentencepiece' --sentencepiece-model $model_dir/sentence.bpe.model \
|
||||
--sacrebleu --remove-bpe 'sentencepiece' \
|
||||
--batch-size 32 --langs $langs > en_ro
|
||||
|
||||
cat en_ro | grep -P "^H" |sort -V |cut -f 3- | sed 's/\[ro_RO\]//g' |$TOKENIZER ro > en_ro.hyp
|
||||
cat en_ro | grep -P "^T" |sort -V |cut -f 2- | sed 's/\[ro_RO\]//g' |$TOKENIZER ro > en_ro.ref
|
||||
sacrebleu -tok 'none' -s 'none' en_ro.ref < en_ro.hyp
|
||||
```
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@article{liu2020multilingual,
|
||||
title={Multilingual Denoising Pre-training for Neural Machine Translation},
|
||||
author={Yinhan Liu and Jiatao Gu and Naman Goyal and Xian Li and Sergey Edunov and Marjan Ghazvininejad and Mike Lewis and Luke Zettlemoyer},
|
||||
year={2020},
|
||||
eprint={2001.08210},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CL}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,161 @@
|
||||
# Megatron-11b
|
||||
|
||||
Megatron-11b is a unidirectional language model with `11B` parameters based on [Megatron-LM](https://arxiv.org/pdf/1909.08053.pdf). Following the original Megatron work, we trained the model using intra-layer model parallelism with each layer's parameters split across 8 GPUs.
|
||||
|
||||
Megatron-11b is trained on the same data and uses the same byte-pair encoding (BPE) as [RoBERTa](https://arxiv.org/pdf/1907.11692.pdf).
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | # params | # filesize | Download
|
||||
---|---|---|---|---
|
||||
`megatron_11b` | megatron_11b unidirectional language model | 11B | 19Gb | [megatron_11b.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/model_parallel/megatron_11b.tar.gz)
|
||||
|
||||
#### Architecture:
|
||||
|
||||
Param | Value
|
||||
---|---
|
||||
embed_dim | 3072
|
||||
ffn_dim | 3072 * 6
|
||||
layers | 72
|
||||
attention heads | 32
|
||||
|
||||
#### Training details:
|
||||
|
||||
Param | value
|
||||
---|---
|
||||
bsz | 512
|
||||
num_updates | 300,000
|
||||
peak_lr | 1.5e-04
|
||||
lr scheduler | inverse_sqrt
|
||||
clip norm | 0.0
|
||||
|
||||
|
||||
## Example training command (model parallel)
|
||||
|
||||
Megatron-11b contains too many parameters to train on a single GPU. Following
|
||||
the original Megatron work, we adopt an intra-layer model parallel training
|
||||
approach in which each layer's parameters are split across multiple GPUs and
|
||||
activations and gradients are communicated during the forward/backward pass,
|
||||
respectively. We similarly split the loss computation using the
|
||||
`vocab_parallel_cross_entropy` criterion.
|
||||
|
||||
The following training command illustrates how to do model parallel training in
|
||||
fairseq. We assume that each machine (node) has 8 GPUs among which to split the
|
||||
model parameters (`--model-parallel-size 8`). If you have access to multiple
|
||||
nodes, you may combine this with data parallel training by increasing
|
||||
`--distributed-world-size`.
|
||||
|
||||
To train Megatron-11b on a single node:
|
||||
|
||||
|
||||
```bash
|
||||
fairseq-train <DATA_PATH> \
|
||||
--distributed-world-size 8 \
|
||||
--memory-efficient-fp16 \
|
||||
--num-workers 2 \
|
||||
--model-parallel-size 8 \
|
||||
--criterion vocab_parallel_cross_entropy \
|
||||
--task language_modeling \
|
||||
--sample-break-mode none \
|
||||
--tokens-per-sample 1024 \
|
||||
--arch transformer_lm_megatron_11b \
|
||||
--share-decoder-input-output-embed \
|
||||
--optimizer adam --adam-betas "(0.9, 0.98)" --adam-eps 1e-08 --clip-norm 0.0 \
|
||||
--lr-scheduler inverse_sqrt --lr 0.00015 \
|
||||
--warmup-updates 3000 --weight-decay 0.01 \
|
||||
--dropout 0.1 --attention-dropout 0.1 \
|
||||
--batch-size 2 \
|
||||
--max-update 300000;
|
||||
```
|
||||
|
||||
Note: Above was tested on `DGX-1` box, with `8xV100-32Gb` GPUs.
|
||||
|
||||
## Results
|
||||
|
||||
**[Wikitext103](https://blog.einstein.ai/the-wikitext-long-term-dependency-language-modeling-dataset/)**
|
||||
|
||||
Model | Valid perplexity | Test perplexity
|
||||
---|---|---
|
||||
`megatron_11b` | 10.64 | 10.54
|
||||
|
||||
|
||||
## Evaluating `megatron_11b` on Wikitext-103
|
||||
|
||||
#### 1. Downloading Megatron-11b
|
||||
```bash
|
||||
# WARNING: this file is 19GB
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/model_parallel/megatron_11b.tar.gz
|
||||
tar -xzvf megatron_11b.tar.gz
|
||||
```
|
||||
|
||||
#### 2. Download Wikitext-103
|
||||
```bash
|
||||
wget https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-raw-v1.zip
|
||||
unzip wikitext-103-raw-v1.zip
|
||||
```
|
||||
|
||||
#### 3. Detokenize test tokens
|
||||
Megatron-11b uses a byte-level BPE that expects raw (untokenized) input. Since
|
||||
the wikitext-103 dataset comes tokenized, we apply a simple detokenization
|
||||
process to restore the untokenized test set:
|
||||
|
||||
```bash
|
||||
python -m examples.megatron_11b.detok wikitext-103-raw/wiki.test.raw > wikitext-103-raw/wiki.test.detok
|
||||
```
|
||||
|
||||
#### 4. BPE encoding
|
||||
```bash
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json'
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe'
|
||||
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json encoder.json \
|
||||
--vocab-bpe vocab.bpe \
|
||||
--inputs "wikitext-103-raw/wiki.test.detok" \
|
||||
--outputs "wikitext-103-raw/wiki.test.bpe" \
|
||||
--workers 60;
|
||||
```
|
||||
|
||||
#### 5. Fairseq binarize
|
||||
```bash
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--testpref wikitext-103-raw/wiki.test.bpe \
|
||||
--srcdict megatron_11b/dict.txt \
|
||||
--destdir wikitext103-bin;
|
||||
```
|
||||
|
||||
#### 6. Evaluating perplexity.
|
||||
We can now evaluate perplexity on the test set. Note that because we've modified
|
||||
the test set (via detokenization and BPE), the perplexity reported by
|
||||
`fairseq-eval-lm` needs to be renormalized.
|
||||
|
||||
Compute unnormalized perplexity:
|
||||
|
||||
```bash
|
||||
DATA_PATH=wikitext103-bin/
|
||||
fairseq-eval-lm \
|
||||
$DATA_PATH \
|
||||
--path megatron_11b/model.pt \
|
||||
--task language_modeling \
|
||||
--gen-subset test \
|
||||
--batch-size 8 \
|
||||
--criterion cross_entropy \
|
||||
--context-window 992 \
|
||||
--distributed-world-size 8 \
|
||||
--model-parallel-size 8;
|
||||
# Expected PPL (unnormalized_ppl): [8.46]
|
||||
# Note: the eval command needs to run on 8 GPUs for the released model
|
||||
```
|
||||
Renormalizing formula: `2 ^ ( log_2(unnormalized_PPL) * (270847 / 245566))`.
|
||||
PPL After normalization: `10.54`
|
||||
|
||||
To renormalize the perplexity, we must account for the change in token count
|
||||
after detokenizing and appling BPE. The formula for this is:
|
||||
`2 ^ ( log_2(unnormalized_PPL) * (new_token_cnt / orig_token_cnt))`
|
||||
|
||||
For the wikitext-103 test set, the original token count is `245566` and the
|
||||
token count after detokenization and applying BPE is `270847`.
|
||||
|
||||
The perplexity after renormalization is:
|
||||
`2 ^ ( log_2(8.46) * (270847 / 245566)) = 10.54`
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3 -u
|
||||
# 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 argparse
|
||||
import fileinput
|
||||
|
||||
import sacremoses
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="")
|
||||
parser.add_argument("files", nargs="*", help="input files")
|
||||
args = parser.parse_args()
|
||||
|
||||
detok = sacremoses.MosesDetokenizer()
|
||||
|
||||
for line in fileinput.input(args.files, openhook=fileinput.hook_compressed):
|
||||
print(
|
||||
detok.detokenize(line.strip().split(" "))
|
||||
.replace(" @", "")
|
||||
.replace("@ ", "")
|
||||
.replace(" =", "=")
|
||||
.replace("= ", "=")
|
||||
.replace(" – ", "–")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
ar_AR
|
||||
cs_CZ
|
||||
de_DE
|
||||
en_XX
|
||||
es_XX
|
||||
et_EE
|
||||
fi_FI
|
||||
fr_XX
|
||||
gu_IN
|
||||
hi_IN
|
||||
it_IT
|
||||
ja_XX
|
||||
kk_KZ
|
||||
ko_KR
|
||||
lt_LT
|
||||
lv_LV
|
||||
my_MM
|
||||
ne_NP
|
||||
nl_XX
|
||||
ro_RO
|
||||
ru_RU
|
||||
si_LK
|
||||
tr_TR
|
||||
vi_VN
|
||||
zh_CN
|
||||
af_ZA
|
||||
az_AZ
|
||||
bn_IN
|
||||
fa_IR
|
||||
he_IL
|
||||
hr_HR
|
||||
id_ID
|
||||
ka_GE
|
||||
km_KH
|
||||
mk_MK
|
||||
ml_IN
|
||||
mn_MN
|
||||
mr_IN
|
||||
pl_PL
|
||||
ps_AF
|
||||
pt_XX
|
||||
sv_SE
|
||||
sw_KE
|
||||
ta_IN
|
||||
te_IN
|
||||
th_TH
|
||||
tl_XX
|
||||
uk_UA
|
||||
ur_PK
|
||||
xh_ZA
|
||||
gl_ES
|
||||
sl_SI
|
||||
@@ -0,0 +1,158 @@
|
||||
# Multilingual Translation
|
||||
|
||||
[[Multilingual Translation with Extensible Multilingual Pretraining and Finetuning, https://arxiv.org/abs/2008.00401]](https://arxiv.org/abs/2008.00401)
|
||||
|
||||
## Introduction
|
||||
|
||||
This work is for training multilingual translation models with multiple bitext datasets. This multilingual translation framework supports (see [[training section]](#Training) and [[finetuning section]](#Finetuning) for examples)
|
||||
|
||||
* temperature based sampling over unbalancing datasets of different translation directions
|
||||
- --sampling-method' with
|
||||
choices=['uniform', 'temperature', 'concat']
|
||||
- --sampling-temperature
|
||||
* configurable to automatically add source and/or target language tokens to source/target sentences using data which are prepared in the same way as bilignual training
|
||||
- --encoder-langtok with choices=['src', 'tgt', None] to specify whether to add source or target language tokens to the source sentences
|
||||
- --decoder-langtok (binary option) to specify whether to add target language tokens to the target sentences or not
|
||||
* finetuning mBART pretrained models for multilingual translation
|
||||
- --finetune-from-model to specify the path from which to load the pretrained model
|
||||
|
||||
## Preprocessing data
|
||||
Multilingual training requires a joint BPE vocab. Please follow [mBART's preprocessing steps](https://github.com/pytorch/fairseq/tree/master/examples/mbart#bpe-data) to reuse our pretrained sentence-piece model.
|
||||
|
||||
You can also train a joint BPE model on your own dataset and then follow the steps in [[link]](https://github.com/pytorch/fairseq/tree/master/examples/translation#multilingual-translation).
|
||||
|
||||
## Training
|
||||
|
||||
|
||||
```bash
|
||||
lang_pairs=<language pairs to be trained, e.g. "en-cs,cs-en">
|
||||
path_2_data=<set to data path>
|
||||
lang_list=<a file which contains a list of languages separated by new lines>
|
||||
|
||||
fairseq-train $path_2_data \
|
||||
--encoder-normalize-before --decoder-normalize-before \
|
||||
--arch transformer --layernorm-embedding \
|
||||
--task translation_multi_simple_epoch \
|
||||
--sampling-method "temperature" \
|
||||
--sampling-temperature 1.5 \
|
||||
--encoder-langtok "src" \
|
||||
--decoder-langtok \
|
||||
--lang-dict "$lang_list" \
|
||||
--lang-pairs "$lang_pairs" \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.2 \
|
||||
--optimizer adam --adam-eps 1e-06 --adam-betas '(0.9, 0.98)' \
|
||||
--lr-scheduler inverse_sqrt --lr 3e-05 --warmup-updates 2500 --max-update 40000 \
|
||||
--dropout 0.3 --attention-dropout 0.1 --weight-decay 0.0 \
|
||||
--max-tokens 1024 --update-freq 2 \
|
||||
--save-interval 1 --save-interval-updates 5000 --keep-interval-updates 10 --no-epoch-checkpoints \
|
||||
--seed 222 --log-format simple --log-interval 2
|
||||
```
|
||||
|
||||
## Finetuning
|
||||
We can also finetune multilingual models from a monolingual pretrained models, e.g. [mMBART](https://github.com/pytorch/fairseq/tree/master/examples/mbart).
|
||||
```bash
|
||||
lang_pairs=<language pairs to be trained, e.g. "en-cs,cs-en">
|
||||
path_2_data=<set to data path>
|
||||
lang_list=<a file which contains a list of languages separated by new lines>
|
||||
pretrained_model=<path to the pretrained model, e.g. mbart or another trained multilingual model>
|
||||
|
||||
fairseq-train $path_2_data \
|
||||
--finetune-from-model $pretrained_model \
|
||||
--encoder-normalize-before --decoder-normalize-before \
|
||||
--arch transformer --layernorm-embedding \
|
||||
--task translation_multi_simple_epoch \
|
||||
--sampling-method "temperature" \
|
||||
--sampling-temperature 1.5 \
|
||||
--encoder-langtok "src" \
|
||||
--decoder-langtok \
|
||||
--lang-dict "$lang_list" \
|
||||
--lang-pairs "$lang_pairs" \
|
||||
--criterion label_smoothed_cross_entropy --label-smoothing 0.2 \
|
||||
--optimizer adam --adam-eps 1e-06 --adam-betas '(0.9, 0.98)' \
|
||||
--lr-scheduler inverse_sqrt --lr 3e-05 --warmup-updates 2500 --max-update 40000 \
|
||||
--dropout 0.3 --attention-dropout 0.1 --weight-decay 0.0 \
|
||||
--max-tokens 1024 --update-freq 2 \
|
||||
--save-interval 1 --save-interval-updates 5000 --keep-interval-updates 10 --no-epoch-checkpoints \
|
||||
--seed 222 --log-format simple --log-interval 2
|
||||
```
|
||||
## Generate
|
||||
The following command uses the multilingual task (translation_multi_simple_epoch) to generate translation from $source_lang to $target_lang on the test dataset. During generaton, the source language tokens are added to source sentences and the target language tokens are added as the starting token to decode target sentences. Options --lang-dict and --lang-pairs are needed to tell the generation process the ordered list of languages and translation directions that the trained model are awared of; they will need to be consistent with the training.
|
||||
|
||||
```bash
|
||||
model=<multilingual model>
|
||||
source_lang=<source language>
|
||||
target_lang=<target language>
|
||||
|
||||
fairseq-generate $path_2_data \
|
||||
--path $model \
|
||||
--task translation_multi_simple_epoch \
|
||||
--gen-subset test \
|
||||
--source-lang $source_lang \
|
||||
--target-lang $target_lang
|
||||
--sacrebleu --remove-bpe 'sentencepiece'\
|
||||
--batch-size 32 \
|
||||
--encoder-langtok "src" \
|
||||
--decoder-langtok \
|
||||
--lang-dict "$lang_list" \
|
||||
--lang-pairs "$lang_pairs" > ${source_lang}_${target_lang}.txt
|
||||
```
|
||||
Fairseq will generate translation into a file {source_lang}_${target_lang}.txt with sacreblue at the end.
|
||||
|
||||
You can also use costomized tokenizer to compare the performance with the literature. For example, you get a tokenizer [here](https://github.com/rsennrich/wmt16-scripts) and do the following:
|
||||
```bash
|
||||
TOKENIZER=<path to a customized tokenizer for decoding evaluation>
|
||||
TOK_CMD=<"$TOKENIZER $target_lang" or cat for sacrebleu>
|
||||
|
||||
cat {source_lang}_${target_lang}.txt | grep -P "^H" |sort -V |cut -f 3- |$TOK_CMD > ${source_lang}_${target_lang}.hyp
|
||||
cat {source_lang}_${target_lang}.txt | grep -P "^T" |sort -V |cut -f 2- |$TOK_CMD > ${source_lang}_${target_lang}.ref
|
||||
sacrebleu -tok 'none' -s 'none' ${source_lang}_${target_lang}.ref < ${source_lang}_${target_lang}.hyp
|
||||
```
|
||||
|
||||
# mBART50 models
|
||||
|
||||
* [mMBART 50 pretrained model](https://dl.fbaipublicfiles.com/fairseq/models/mbart50/mbart50.pretrained.tar.gz).
|
||||
* [mMBART 50 finetuned many-to-one](https://dl.fbaipublicfiles.com/fairseq/models/mbart50/mbart50.ft.n1.tar.gz).
|
||||
* [mMBART 50 finetuned one-to-many](https://dl.fbaipublicfiles.com/fairseq/models/mbart50/mbart50.ft.1n.tar.gz).
|
||||
* [mMBART 50 finetuned many-to-many](https://dl.fbaipublicfiles.com/fairseq/models/mbart50/mbart50.ft.nn.tar.gz).
|
||||
|
||||
Please download and extract from the above tarballs. Each tarball contains
|
||||
* The fairseq model checkpoint: model.pt
|
||||
* The list of supported languages: ML50_langs.txt
|
||||
* Sentence piece model: sentence.bpe.model
|
||||
* Fairseq dictionary of each language: dict.{lang}.txt (please replace lang with a language specified in ML50_langs.txt)
|
||||
|
||||
To use the trained models,
|
||||
* use the tool [binarize.py](./data_scripts/binarize.py) to binarize your data using sentence.bpe.model and dict.{lang}.txt, and copy the dictionaries to your data path
|
||||
* then run the generation command:
|
||||
```bash
|
||||
path_2_data=<path to your binarized data with fairseq dictionaries>
|
||||
model=<path_to_extracted_folder>/model.pt
|
||||
lang_list=<path_to_extracted_folder>/ML50_langs.txt
|
||||
source_lang=<source language>
|
||||
target_lang=<target language>
|
||||
|
||||
fairseq-generate $path_2_data \
|
||||
--path $model \
|
||||
--task translation_multi_simple_epoch \
|
||||
--gen-subset test \
|
||||
--source-lang $source_lang \
|
||||
--target-lang $target_lang
|
||||
--sacrebleu --remove-bpe 'sentencepiece'\
|
||||
--batch-size 32 \
|
||||
--encoder-langtok "src" \
|
||||
--decoder-langtok \
|
||||
--lang-dict "$lang_list"
|
||||
```
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@article{tang2020multilingual,
|
||||
title={Multilingual Translation with Extensible Multilingual Pretraining and Finetuning},
|
||||
author={Yuqing Tang and Chau Tran and Xian Li and Peng-Jen Chen and Naman Goyal and Vishrav Chaudhary and Jiatao Gu and Angela Fan},
|
||||
year={2020},
|
||||
eprint={2008.00401},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.CL}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
# Install dependency
|
||||
```bash
|
||||
pip install -r requirement.txt
|
||||
```
|
||||
|
||||
# Download the data set
|
||||
```bash
|
||||
export WORKDIR_ROOT=<a directory which will hold all working files>
|
||||
|
||||
```
|
||||
The downloaded data will be at $WORKDIR_ROOT/ML50
|
||||
|
||||
# preprocess the data
|
||||
Install SPM [here](https://github.com/google/sentencepiece)
|
||||
```bash
|
||||
export WORKDIR_ROOT=<a directory which will hold all working files>
|
||||
export SPM_PATH=<a path pointing to sentencepice spm_encode.py>
|
||||
```
|
||||
* $WORKDIR_ROOT/ML50/raw: extracted raw data
|
||||
* $WORKDIR_ROOT/ML50/dedup: dedup data
|
||||
* $WORKDIR_ROOT/ML50/clean: data with valid and test sentences removed from the dedup data
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import shutil
|
||||
import os, sys
|
||||
from subprocess import check_call, check_output
|
||||
import glob
|
||||
import argparse
|
||||
import shutil
|
||||
import pathlib
|
||||
import itertools
|
||||
|
||||
def call_output(cmd):
|
||||
print(f"Executing: {cmd}")
|
||||
ret = check_output(cmd, shell=True)
|
||||
print(ret)
|
||||
return ret
|
||||
|
||||
def call(cmd):
|
||||
print(cmd)
|
||||
check_call(cmd, shell=True)
|
||||
|
||||
|
||||
WORKDIR_ROOT = os.environ.get('WORKDIR_ROOT', None)
|
||||
|
||||
if WORKDIR_ROOT is None or not WORKDIR_ROOT.strip():
|
||||
print('please specify your working directory root in OS environment variable WORKDIR_ROOT. Exitting..."')
|
||||
sys.exit(-1)
|
||||
|
||||
SPM_PATH = os.environ.get('SPM_PATH', None)
|
||||
|
||||
if SPM_PATH is None or not SPM_PATH.strip():
|
||||
print("Please install sentence piecence from https://github.com/google/sentencepiece and set SPM_PATH pointing to the installed spm_encode.py. Exitting...")
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
SPM_MODEL = f'{WORKDIR_ROOT}/sentence.bpe.model'
|
||||
SPM_VOCAB = f'{WORKDIR_ROOT}/dict_250k.txt'
|
||||
|
||||
SPM_ENCODE = f'{SPM_PATH}'
|
||||
|
||||
if not os.path.exists(SPM_MODEL):
|
||||
call(f"wget https://dl.fbaipublicfiles.com/fairseq/models/mbart50/sentence.bpe.model -O {SPM_MODEL}")
|
||||
|
||||
|
||||
if not os.path.exists(SPM_VOCAB):
|
||||
call(f"wget https://dl.fbaipublicfiles.com/fairseq/models/mbart50/dict_250k.txt -O {SPM_VOCAB}")
|
||||
|
||||
|
||||
|
||||
def get_data_size(raw):
|
||||
cmd = f'wc -l {raw}'
|
||||
ret = call_output(cmd)
|
||||
return int(ret.split()[0])
|
||||
|
||||
def encode_spm(model, direction, prefix='', splits=['train', 'test', 'valid'], pairs_per_shard=None):
|
||||
src, tgt = direction.split('-')
|
||||
|
||||
for split in splits:
|
||||
src_raw, tgt_raw = f'{RAW_DIR}/{split}{prefix}.{direction}.{src}', f'{RAW_DIR}/{split}{prefix}.{direction}.{tgt}'
|
||||
if os.path.exists(src_raw) and os.path.exists(tgt_raw):
|
||||
cmd = f"""python {SPM_ENCODE} \
|
||||
--model {model}\
|
||||
--output_format=piece \
|
||||
--inputs {src_raw} {tgt_raw} \
|
||||
--outputs {BPE_DIR}/{direction}{prefix}/{split}.bpe.{src} {BPE_DIR}/{direction}{prefix}/{split}.bpe.{tgt} """
|
||||
print(cmd)
|
||||
call(cmd)
|
||||
|
||||
|
||||
def binarize_(
|
||||
bpe_dir,
|
||||
databin_dir,
|
||||
direction, spm_vocab=SPM_VOCAB,
|
||||
splits=['train', 'test', 'valid'],
|
||||
):
|
||||
src, tgt = direction.split('-')
|
||||
|
||||
try:
|
||||
shutil.rmtree(f'{databin_dir}', ignore_errors=True)
|
||||
os.mkdir(f'{databin_dir}')
|
||||
except OSError as error:
|
||||
print(error)
|
||||
cmds = [
|
||||
"fairseq-preprocess",
|
||||
f"--source-lang {src} --target-lang {tgt}",
|
||||
f"--destdir {databin_dir}/",
|
||||
f"--workers 8",
|
||||
]
|
||||
if isinstance(spm_vocab, tuple):
|
||||
src_vocab, tgt_vocab = spm_vocab
|
||||
cmds.extend(
|
||||
[
|
||||
f"--srcdict {src_vocab}",
|
||||
f"--tgtdict {tgt_vocab}",
|
||||
]
|
||||
)
|
||||
else:
|
||||
cmds.extend(
|
||||
[
|
||||
f"--joined-dictionary",
|
||||
f"--srcdict {spm_vocab}",
|
||||
]
|
||||
)
|
||||
input_options = []
|
||||
if 'train' in splits and glob.glob(f"{bpe_dir}/train.bpe*"):
|
||||
input_options.append(
|
||||
f"--trainpref {bpe_dir}/train.bpe",
|
||||
)
|
||||
if 'valid' in splits and glob.glob(f"{bpe_dir}/valid.bpe*"):
|
||||
input_options.append(f"--validpref {bpe_dir}/valid.bpe")
|
||||
if 'test' in splits and glob.glob(f"{bpe_dir}/test.bpe*"):
|
||||
input_options.append(f"--testpref {bpe_dir}/test.bpe")
|
||||
if len(input_options) > 0:
|
||||
cmd = " ".join(cmds + input_options)
|
||||
print(cmd)
|
||||
call(cmd)
|
||||
|
||||
|
||||
def binarize(
|
||||
databin_dir,
|
||||
direction, spm_vocab=SPM_VOCAB, prefix='',
|
||||
splits=['train', 'test', 'valid'],
|
||||
pairs_per_shard=None,
|
||||
):
|
||||
def move_databin_files(from_folder, to_folder):
|
||||
for bin_file in glob.glob(f"{from_folder}/*.bin") \
|
||||
+ glob.glob(f"{from_folder}/*.idx") \
|
||||
+ glob.glob(f"{from_folder}/dict*"):
|
||||
try:
|
||||
shutil.move(bin_file, to_folder)
|
||||
except OSError as error:
|
||||
print(error)
|
||||
bpe_databin_dir = f"{BPE_DIR}/{direction}{prefix}_databin"
|
||||
bpe_dir = f"{BPE_DIR}/{direction}{prefix}"
|
||||
if pairs_per_shard is None:
|
||||
binarize_(bpe_dir, bpe_databin_dir, direction, spm_vocab=spm_vocab, splits=splits)
|
||||
move_databin_files(bpe_databin_dir, databin_dir)
|
||||
else:
|
||||
# binarize valid and test which will not be sharded
|
||||
binarize_(
|
||||
bpe_dir, bpe_databin_dir, direction,
|
||||
spm_vocab=spm_vocab, splits=[s for s in splits if s != "train"])
|
||||
for shard_bpe_dir in glob.glob(f"{bpe_dir}/shard*"):
|
||||
path_strs = os.path.split(shard_bpe_dir)
|
||||
shard_str = path_strs[-1]
|
||||
shard_folder = f"{bpe_databin_dir}/{shard_str}"
|
||||
databin_shard_folder = f"{databin_dir}/{shard_str}"
|
||||
print(f'working from {shard_folder} to {databin_shard_folder}')
|
||||
os.makedirs(databin_shard_folder, exist_ok=True)
|
||||
binarize_(
|
||||
shard_bpe_dir, shard_folder, direction,
|
||||
spm_vocab=spm_vocab, splits=["train"])
|
||||
|
||||
for test_data in glob.glob(f"{bpe_databin_dir}/valid.*") + glob.glob(f"{bpe_databin_dir}/test.*"):
|
||||
filename = os.path.split(test_data)[-1]
|
||||
try:
|
||||
os.symlink(test_data, f"{databin_shard_folder}/{filename}")
|
||||
except OSError as error:
|
||||
print(error)
|
||||
move_databin_files(shard_folder, databin_shard_folder)
|
||||
|
||||
|
||||
def load_langs(path):
|
||||
with open(path) as fr:
|
||||
langs = [l.strip() for l in fr]
|
||||
return langs
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--data_root", default=f"{WORKDIR_ROOT}/ML50")
|
||||
parser.add_argument("--raw-folder", default='raw')
|
||||
parser.add_argument("--bpe-folder", default='bpe')
|
||||
parser.add_argument("--databin-folder", default='databin')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
DATA_PATH = args.data_root #'/private/home/yuqtang/public_data/ML50'
|
||||
RAW_DIR = f'{DATA_PATH}/{args.raw_folder}'
|
||||
BPE_DIR = f'{DATA_PATH}/{args.bpe_folder}'
|
||||
DATABIN_DIR = f'{DATA_PATH}/{args.databin_folder}'
|
||||
os.makedirs(BPE_DIR, exist_ok=True)
|
||||
|
||||
raw_files = itertools.chain(
|
||||
glob.glob(f'{RAW_DIR}/train*'),
|
||||
glob.glob(f'{RAW_DIR}/valid*'),
|
||||
glob.glob(f'{RAW_DIR}/test*'),
|
||||
)
|
||||
|
||||
directions = [os.path.split(file_path)[-1].split('.')[1] for file_path in raw_files]
|
||||
|
||||
for direction in directions:
|
||||
prefix = ""
|
||||
splits = ['train', 'valid', 'test']
|
||||
try:
|
||||
shutil.rmtree(f'{BPE_DIR}/{direction}{prefix}', ignore_errors=True)
|
||||
os.mkdir(f'{BPE_DIR}/{direction}{prefix}')
|
||||
os.makedirs(DATABIN_DIR, exist_ok=True)
|
||||
except OSError as error:
|
||||
print(error)
|
||||
spm_model, spm_vocab = SPM_MODEL, SPM_VOCAB
|
||||
encode_spm(spm_model, direction=direction, splits=splits)
|
||||
binarize(DATABIN_DIR, direction, spm_vocab=spm_vocab, splits=splits)
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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, sys
|
||||
import subprocess
|
||||
import re
|
||||
from subprocess import check_call, check_output
|
||||
|
||||
WORKDIR_ROOT = os.environ.get('WORKDIR_ROOT', None)
|
||||
|
||||
if WORKDIR_ROOT is None or not WORKDIR_ROOT.strip():
|
||||
print('please specify your working directory root in OS environment variable WORKDIR_ROOT. Exitting..."')
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
BLEU_REGEX = re.compile("^BLEU\\S* = (\\S+) ")
|
||||
def run_eval_bleu(cmd):
|
||||
output = check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode("utf-8").strip()
|
||||
print(output)
|
||||
bleu = -1.0
|
||||
for line in output.strip().split('\n'):
|
||||
m = BLEU_REGEX.search(line)
|
||||
if m is not None:
|
||||
bleu = m.groups()[0]
|
||||
bleu = float(bleu)
|
||||
break
|
||||
return bleu
|
||||
|
||||
def check_data_test_bleu(raw_folder, data_lang_pairs):
|
||||
not_matchings = []
|
||||
for sacrebleu_set, src_tgts in data_lang_pairs:
|
||||
for src_tgt in src_tgts:
|
||||
print(f'checking test bleus for: {src_tgt} at {sacrebleu_set}')
|
||||
src, tgt = src_tgt.split('-')
|
||||
ssrc, stgt = src[:2], tgt[:2]
|
||||
if os.path.exists(f'{raw_folder}/test.{tgt}-{src}.{src}'):
|
||||
# reversed direction may have different test set
|
||||
test_src = f'{raw_folder}/test.{tgt}-{src}.{src}'
|
||||
else:
|
||||
test_src = f'{raw_folder}/test.{src}-{tgt}.{src}'
|
||||
cmd1 = f'cat {test_src} | sacrebleu -t "{sacrebleu_set}" -l {stgt}-{ssrc}; [ $? -eq 0 ] || echo ""'
|
||||
test_tgt = f'{raw_folder}/test.{src}-{tgt}.{tgt}'
|
||||
cmd2 = f'cat {test_tgt} | sacrebleu -t "{sacrebleu_set}" -l {ssrc}-{stgt}; [ $? -eq 0 ] || echo ""'
|
||||
bleu1 = run_eval_bleu(cmd1)
|
||||
if bleu1 != 100.0:
|
||||
not_matchings.append(f'{sacrebleu_set}:{src_tgt} source side not matching: {test_src}')
|
||||
bleu2 = run_eval_bleu(cmd2)
|
||||
if bleu2 != 100.0:
|
||||
not_matchings.append(f'{sacrebleu_set}:{src_tgt} target side not matching: {test_tgt}')
|
||||
return not_matchings
|
||||
|
||||
if __name__ == "__main__":
|
||||
to_data_path = f'{WORKDIR_ROOT}/iwsltv2'
|
||||
not_matching = check_data_test_bleu(
|
||||
f'{to_data_path}/raw',
|
||||
[
|
||||
('iwslt17', ['en_XX-ar_AR', 'en_XX-ko_KR', 'ar_AR-en_XX', 'ko_KR-en_XX']),
|
||||
('iwslt17', ['en_XX-it_IT', 'en_XX-nl_XX', 'it_IT-en_XX', 'nl_XX-en_XX']),
|
||||
('iwslt17/tst2015', ['en_XX-vi_VN', "vi_VN-en_XX"]),
|
||||
]
|
||||
)
|
||||
if len(not_matching) > 0:
|
||||
print('the following datasets do not have matching test datasets:\n\t', '\n\t'.join(not_matching))
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# 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 glob
|
||||
import argparse
|
||||
from utils.dedup import deup
|
||||
import sys
|
||||
|
||||
WORKDIR_ROOT = os.environ.get('WORKDIR_ROOT', None)
|
||||
|
||||
if WORKDIR_ROOT is None or not WORKDIR_ROOT.strip():
|
||||
print('please specify your working directory root in OS environment variable WORKDIR_ROOT. Exitting..."')
|
||||
sys.exit(-1)
|
||||
|
||||
def get_directions(folder):
|
||||
raw_files = glob.glob(f'{folder}/train*')
|
||||
directions = [os.path.split(file_path)[-1].split('.')[1] for file_path in raw_files]
|
||||
return directions
|
||||
|
||||
def diff_list(lhs, rhs):
|
||||
return set(lhs).difference(set(rhs))
|
||||
|
||||
def check_diff(
|
||||
from_src_file, from_tgt_file,
|
||||
to_src_file, to_tgt_file,
|
||||
):
|
||||
seen_in_from = set()
|
||||
seen_src_in_from = set()
|
||||
seen_tgt_in_from = set()
|
||||
from_count = 0
|
||||
with open(from_src_file, encoding='utf-8') as fsrc, \
|
||||
open(from_tgt_file, encoding='utf-8') as ftgt:
|
||||
for s, t in zip(fsrc, ftgt):
|
||||
seen_in_from.add((s, t))
|
||||
seen_src_in_from.add(s)
|
||||
seen_tgt_in_from.add(t)
|
||||
from_count += 1
|
||||
common = 0
|
||||
common_src = 0
|
||||
common_tgt = 0
|
||||
to_count = 0
|
||||
seen = set()
|
||||
|
||||
with open(to_src_file, encoding='utf-8') as fsrc, \
|
||||
open(to_tgt_file, encoding='utf-8') as ftgt:
|
||||
for s, t in zip(fsrc, ftgt):
|
||||
to_count += 1
|
||||
if (s, t) not in seen:
|
||||
if (s, t) in seen_in_from:
|
||||
common += 1
|
||||
if s in seen_src_in_from:
|
||||
common_src += 1
|
||||
seen_src_in_from.remove(s)
|
||||
if t in seen_tgt_in_from:
|
||||
common_tgt += 1
|
||||
seen_tgt_in_from.remove(t)
|
||||
seen.add((s, t))
|
||||
return common, common_src, common_tgt, from_count, to_count
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--folder", type=str, required=True,
|
||||
help="the data folder ")
|
||||
parser.add_argument("--split", type=str, default='test',
|
||||
help="split (valid, test) to check against training data")
|
||||
parser.add_argument('--directions', type=str, default=None, required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.directions is None:
|
||||
directions = set(get_directions(args.folder))
|
||||
directions = sorted(directions)
|
||||
else:
|
||||
directions = args.directions.split(',')
|
||||
directions = sorted(set(directions))
|
||||
|
||||
results = []
|
||||
print(f'checking where {args.split} split data are in training')
|
||||
print(f'direction\tcommon_count\tsrc common\ttgt common\tfrom_size\tto_size')
|
||||
|
||||
for direction in directions:
|
||||
src, tgt = direction.split('-')
|
||||
from_src_file = f'{args.folder}/{args.split}.{src}-{tgt}.{src}'
|
||||
from_tgt_file = f'{args.folder}/{args.split}.{src}-{tgt}.{tgt}'
|
||||
if not os.path.exists(from_src_file):
|
||||
# some test/valid data might in reverse directinos:
|
||||
from_src_file = f'{args.folder}/{args.split}.{tgt}-{src}.{src}'
|
||||
from_tgt_file = f'{args.folder}/{args.split}.{tgt}-{src}.{tgt}'
|
||||
to_src_file = f'{args.folder}/train.{src}-{tgt}.{src}'
|
||||
to_tgt_file = f'{args.folder}/train.{src}-{tgt}.{tgt}'
|
||||
if not os.path.exists(to_src_file) or not os.path.exists(from_src_file):
|
||||
continue
|
||||
r = check_diff(from_src_file, from_tgt_file, to_src_file, to_tgt_file)
|
||||
results.append(r)
|
||||
print(f'{direction}\t', '\t'.join(map(str, r)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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 os
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import sys
|
||||
|
||||
|
||||
WORKDIR_ROOT = os.environ.get('WORKDIR_ROOT', None)
|
||||
|
||||
if WORKDIR_ROOT is None or not WORKDIR_ROOT.strip():
|
||||
print('please specify your working directory root in OS environment variable WORKDIR_ROOT. Exitting..."')
|
||||
sys.exit(-1)
|
||||
|
||||
def load_langs(path):
|
||||
with open(path) as fr:
|
||||
langs = [l.strip() for l in fr]
|
||||
return langs
|
||||
|
||||
|
||||
|
||||
def load_sentences(raw_data, split, direction):
|
||||
src, tgt = direction.split('-')
|
||||
src_path = f"{raw_data}/{split}.{direction}.{src}"
|
||||
tgt_path = f"{raw_data}/{split}.{direction}.{tgt}"
|
||||
if os.path.exists(src_path) and os.path.exists(tgt_path):
|
||||
return [(src, open(src_path).read().splitlines()), (tgt, open(tgt_path).read().splitlines())]
|
||||
else:
|
||||
return []
|
||||
|
||||
def swap_direction(d):
|
||||
src, tgt = d.split('-')
|
||||
return f'{tgt}-{src}'
|
||||
|
||||
def get_all_test_data(raw_data, directions, split='test'):
|
||||
test_data = [
|
||||
x
|
||||
for dd in directions
|
||||
for d in [dd, swap_direction(dd)]
|
||||
for x in load_sentences(raw_data, split, d)
|
||||
]
|
||||
# all_test_data = {s for _, d in test_data for s in d}
|
||||
all_test_data = {}
|
||||
for lang, d in test_data:
|
||||
for s in d:
|
||||
s = s.strip()
|
||||
lgs = all_test_data.get(s, set())
|
||||
lgs.add(lang)
|
||||
all_test_data[s] = lgs
|
||||
return all_test_data, test_data
|
||||
|
||||
|
||||
def check_train_sentences(src_path, tgt_path, direction, all_test_data, mess_up_train={}):
|
||||
# src, tgt = direction.split('-')
|
||||
print(f'check training data for {direction} in {src_path} and {tgt_path}')
|
||||
size = 0
|
||||
overlapped_size_counted_dup = 0
|
||||
if not os.path.exists(tgt_path) or not os.path.exists(src_path):
|
||||
return mess_up_train, size, overlapped_size_counted_dup
|
||||
|
||||
with open(src_path) as f, open(tgt_path) as g:
|
||||
for src_line, tgt_line in zip(f, g):
|
||||
s = src_line.strip()
|
||||
t = tgt_line.strip()
|
||||
size += 1
|
||||
if s in all_test_data:
|
||||
langs = mess_up_train.get(s, set())
|
||||
langs.add(direction)
|
||||
mess_up_train[s] = langs
|
||||
overlapped_size_counted_dup += 1
|
||||
if t in all_test_data:
|
||||
langs = mess_up_train.get(t, set())
|
||||
langs.add(direction)
|
||||
mess_up_train[t] = langs
|
||||
overlapped_size_counted_dup += 1
|
||||
print(f'{direction}: size={size}, overlapped={overlapped_size_counted_dup}')
|
||||
return mess_up_train, size, overlapped_size_counted_dup
|
||||
|
||||
def check_train_all(raw_data, directions, all_test_data):
|
||||
mess_up_train = {}
|
||||
data_sizes = {}
|
||||
# raw_data = '~chau/data-bin/MineBART/multilingual_mined_100M/en_XX/et_EE-en_XX/all.{en_XX, et_EE}'
|
||||
print(f'checking training data againsts # {len(all_test_data)} sentences')
|
||||
print(f'example test data: ', [s for i, s in enumerate(all_test_data.keys()) if i < 10])
|
||||
for direction in directions:
|
||||
src, tgt = direction.split('-')
|
||||
path = f'{raw_data}/en_XX/{direction}/all'
|
||||
src_path = f'{path}.{src}'
|
||||
tgt_path = f'{path}.{tgt}'
|
||||
print(f'checking {src_path} {tgt_path}')
|
||||
_, size, overlapped_size_counted_dup = check_train_sentences(src_path, tgt_path, direction, all_test_data, mess_up_train)
|
||||
data_sizes[direction] = (size, overlapped_size_counted_dup)
|
||||
return mess_up_train, data_sizes
|
||||
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--folder", type=str, required=True,
|
||||
help="the data folder ")
|
||||
parser.add_argument("--test-data", type=str, required=True,
|
||||
help="the test data folder ")
|
||||
parser.add_argument('--directions', type=str, default=None, required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
directions = args.directions.split(',')
|
||||
directions = sorted(set(directions))
|
||||
|
||||
results = []
|
||||
# print(f'checking where {args.split} split data are in training')
|
||||
# print(f'direction\tcommon_count\tsrc common\ttgt common\tfrom_size\tto_size')
|
||||
raw_data = args.folder
|
||||
all_test_data, test_data = get_all_test_data(args.test_data, directions, split='test')
|
||||
mess_up_train, data_sizes = check_train_all(raw_data, directions, all_test_data)
|
||||
print(data_sizes)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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 glob
|
||||
import argparse
|
||||
from utils.dedup import deup
|
||||
|
||||
import sys
|
||||
WORKDIR_ROOT = os.environ.get('WORKDIR_ROOT', None)
|
||||
|
||||
if WORKDIR_ROOT is None or not WORKDIR_ROOT.strip():
|
||||
print('please specify your working directory root in OS environment variable WORKDIR_ROOT. Exitting..."')
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--from-folder", type=str, required=True,
|
||||
help="the data folder to be dedup")
|
||||
parser.add_argument("--to-folder", type=str, required=True,
|
||||
help="the data folder to save deduped data")
|
||||
parser.add_argument('--directions', type=str, default=None, required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.directions is None:
|
||||
raw_files = glob.glob(f'{args.from_folder}/train*')
|
||||
|
||||
directions = [os.path.split(file_path)[-1].split('.')[1] for file_path in raw_files]
|
||||
else:
|
||||
directions = args.directions.split(',')
|
||||
directions = sorted(set(directions))
|
||||
|
||||
for direction in directions:
|
||||
src, tgt = direction.split('-')
|
||||
src_file = f'{args.from_folder}/train.{src}-{tgt}.{src}'
|
||||
tgt_file = f'{args.from_folder}/train.{src}-{tgt}.{tgt}'
|
||||
src_file_out = f'{args.to_folder}/train.{src}-{tgt}.{src}'
|
||||
tgt_file_out = f'{args.to_folder}/train.{src}-{tgt}.{tgt}'
|
||||
assert src_file != src_file_out
|
||||
assert tgt_file != tgt_file_out
|
||||
print(f'deduping {src_file}, {tgt_file}')
|
||||
deup(src_file, tgt_file, src_file_out, tgt_file_out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
if [ -z $WORKDIR_ROOT ] ;
|
||||
then
|
||||
echo "please specify your working directory root in environment variable WORKDIR_ROOT. Exitting..."
|
||||
exit
|
||||
fi
|
||||
|
||||
# first run download_wmt20.sh; it will install a few useful tools for other scripts
|
||||
# TODO: need to print out instructions on downloading a few files which requires manually authentication from the websites
|
||||
bash ./download_wmt20.sh
|
||||
|
||||
python ./download_wmt19_and_before.py
|
||||
bash ./download_wat19_my.sh
|
||||
python ./download_ted_and_extract.py
|
||||
bash ./download_lotus.sh
|
||||
bash ./download_iitb.sh
|
||||
bash ./download_af_xh.sh
|
||||
|
||||
|
||||
# IWSLT downloading URLs have changed in between; TODO: fix them:
|
||||
bash ./download_iwslt_and_extract.sh
|
||||
|
||||
# TODO: globalvoices URLs changed; need to be fixed
|
||||
bash ./download_flores_data.sh
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
# set -x -e
|
||||
|
||||
if [ -z $WORKDIR_ROOT ] ;
|
||||
then
|
||||
echo "please specify your working directory root in environment variable WORKDIR_ROOT. Exitting..."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
# put intermediate files
|
||||
TMP_DIR=$WORKDIR_ROOT/temp/af_xhv2
|
||||
# output {train,valid,test} files to dest
|
||||
DEST=${WORKDIR_ROOT}/ML50/raw
|
||||
|
||||
|
||||
|
||||
ROOT=${WORKDIR_ROOT}
|
||||
UTILS=$PWD/utils
|
||||
TMX2CORPUS="${UTILS}/tmx2corpus"
|
||||
TMX_TOOL="python ${TMX2CORPUS}/tmx2corpus.py"
|
||||
|
||||
mkdir -p $TMP_DIR
|
||||
mkdir -p $DEST
|
||||
mkdir -p $UTILS
|
||||
|
||||
function download_opus(){
|
||||
src=$1
|
||||
tgt=$2
|
||||
subset=$3
|
||||
ulr=$4
|
||||
|
||||
mkdir extract_$subset.$src-$tgt
|
||||
pushd extract_$subset.$src-$tgt
|
||||
if [ ! -f "$subset.$src-$tgt.tmx.gz" ]; then
|
||||
wget $url -O "$subset.$src-$tgt.tmx.gz"
|
||||
gzip -d "$subset.$src-$tgt.tmx.gz"
|
||||
f=$subset.$src-$tgt.tmx
|
||||
$TMX_TOOL $f
|
||||
mv bitext.$src ../$subset.$src-$tgt.$src
|
||||
mv bitext.$tgt ../$subset.$src-$tgt.$tgt
|
||||
fi
|
||||
popd
|
||||
}
|
||||
|
||||
function concat_subsets(){
|
||||
src=$1
|
||||
tgt=$2
|
||||
subsets=$3
|
||||
src_train=raw_train.$src-$tgt.$src
|
||||
tgt_train=raw_train.$src-$tgt.$tgt
|
||||
> $src_train
|
||||
> $tgt_train
|
||||
for subset in $subsets; do
|
||||
cat $subset.$src-$tgt.$src >> $src_train
|
||||
cat $subset.$src-$tgt.$tgt >> $tgt_train
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
|
||||
function get_seeded_random()
|
||||
{
|
||||
seed="$1"
|
||||
openssl enc -aes-256-ctr -pass pass:"$seed" -nosalt \
|
||||
</dev/zero 2>/dev/null
|
||||
}
|
||||
|
||||
function split_train_valid(){
|
||||
src=$1
|
||||
tgt=$2
|
||||
raw_src_train=raw_train.$src-$tgt.$src
|
||||
raw_tgt_train=raw_train.$src-$tgt.$tgt
|
||||
|
||||
shuf --random-source=<(get_seeded_random 43) $raw_src_train > shuffled.$src-$tgt.$src
|
||||
shuf --random-source=<(get_seeded_random 43) $raw_tgt_train > shuffled.$src-$tgt.$tgt
|
||||
|
||||
head -n 1500 shuffled.$src-$tgt.$src > valid.$src-$tgt.$src
|
||||
head -n 1500 shuffled.$src-$tgt.$tgt > valid.$src-$tgt.$tgt
|
||||
|
||||
tail +1501 shuffled.$src-$tgt.$src > train.$src-$tgt.$src
|
||||
tail +1501 shuffled.$src-$tgt.$tgt > train.$src-$tgt.$tgt
|
||||
}
|
||||
|
||||
function copy2dst(){
|
||||
lsrc=$1
|
||||
ltgt=$2
|
||||
src=${lsrc:0:2}
|
||||
tgt=${ltgt:0:2}
|
||||
|
||||
|
||||
cp valid.$src-$tgt.$src $DEST/valid.$lsrc-$ltgt.$lsrc
|
||||
cp valid.$src-$tgt.$tgt $DEST/valid.$lsrc-$ltgt.$ltgt
|
||||
|
||||
cp train.$src-$tgt.$src $DEST/train.$lsrc-$ltgt.$lsrc
|
||||
cp train.$src-$tgt.$tgt $DEST/train.$lsrc-$ltgt.$ltgt
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#for xh-en
|
||||
declare -A xh_en_urls
|
||||
xh_en_urls=(
|
||||
[Tatoeba]=https://object.pouta.csc.fi/OPUS-Tatoeba/v20190709/tmx/en-xh.tmx.gz
|
||||
[wikimedia]=https://object.pouta.csc.fi/OPUS-wikimedia/v20190628/tmx/en-xh.tmx.gz
|
||||
[memat]=https://object.pouta.csc.fi/OPUS-memat/v1/tmx/en-xh.tmx.gz
|
||||
[uedin]=https://object.pouta.csc.fi/OPUS-bible-uedin/v1/tmx/en-xh.tmx.gz
|
||||
[GNOME]=https://object.pouta.csc.fi/OPUS-GNOME/v1/tmx/en-xh.tmx.gz
|
||||
[XhosaNavy]=https://object.pouta.csc.fi/OPUS-XhosaNavy/v1/tmx/en-xh.tmx.gz
|
||||
[KDE4]=https://object.pouta.csc.fi/OPUS-KDE4/v2/tmx/en-xh.tmx.gz
|
||||
[Ubuntu]=https://object.pouta.csc.fi/OPUS-Ubuntu/v14.10/tmx/en-xh.tmx.gz
|
||||
)
|
||||
|
||||
mkdir $TMP_DIR/xh-en
|
||||
pushd $TMP_DIR/xh-en
|
||||
for k in "${!xh_en_urls[@]}"
|
||||
do
|
||||
name=$k
|
||||
url=${xh_en_urls[$k]}
|
||||
echo "$name: $url"
|
||||
download_opus xh en $name $ulr
|
||||
done
|
||||
concat_subsets xh en "${!xh_en_urls[@]}"
|
||||
split_train_valid xh en
|
||||
copy2dst xh_ZA en_XX
|
||||
popd
|
||||
|
||||
|
||||
##
|
||||
#for af-en
|
||||
declare -A af_en_urls
|
||||
af_en_urls=(
|
||||
[Tatoeba]=https://object.pouta.csc.fi/OPUS-Tatoeba/v20190709/tmx/af-en.tmx.gz
|
||||
[uedin]=https://object.pouta.csc.fi/OPUS-bible-uedin/v1/tmx/af-en.tmx.gz
|
||||
[GNOME]=https://object.pouta.csc.fi/OPUS-GNOME/v1/tmx/af-en.tmx.gz
|
||||
[QED]=https://object.pouta.csc.fi/OPUS-QED/v2.0a/tmx/af-en.tmx.gz
|
||||
[KDE4]=https://object.pouta.csc.fi/OPUS-KDE4/v2/tmx/af-en.tmx.gz
|
||||
[OpenSubtitles]=https://object.pouta.csc.fi/OPUS-OpenSubtitles/v2018/tmx/af-en.tmx.gz
|
||||
[SPC]=https://object.pouta.csc.fi/OPUS-SPC/v1/tmx/af-en.tmx.gz
|
||||
[Ubuntu]=https://object.pouta.csc.fi/OPUS-Ubuntu/v14.10/tmx/af-en.tmx.gz
|
||||
)
|
||||
|
||||
mkdir $TMP_DIR/af-en
|
||||
pushd $TMP_DIR/af-en
|
||||
for k in "${!af_en_urls[@]}"
|
||||
do
|
||||
name=$k
|
||||
url=${af_en_urls[$k]}
|
||||
echo "$name: $url"
|
||||
download_opus af en $name $ulr
|
||||
done
|
||||
concat_subsets af en "${!af_en_urls[@]}"
|
||||
split_train_valid af en
|
||||
copy2dst af_ZA en_XX
|
||||
popd
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
if [ -z $WORKDIR_ROOT ] ;
|
||||
then
|
||||
echo "please specify your working directory root in environment variable WORKDIR_ROOT. Exitting..."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
SRC=en
|
||||
SI_TGT=si
|
||||
NE_TGT=ne
|
||||
|
||||
DESTDIR=${WORKDIR_ROOT}/ML50/raw/
|
||||
|
||||
ROOT=${WORKDIR_ROOT}/tmp
|
||||
mkdir -p $ROOT
|
||||
DATA=$ROOT/data
|
||||
NE_ROOT=$DATA/all-clean-ne
|
||||
SI_ROOT=$DATA/all-clean-si
|
||||
|
||||
mkdir -p $DATA $NE_ROOT $SI_ROOT
|
||||
|
||||
SI_OPUS_DATASETS=(
|
||||
"$SI_ROOT/GNOME.en-si"
|
||||
"$SI_ROOT/Ubuntu.en-si"
|
||||
"$SI_ROOT/KDE4.en-si"
|
||||
"$SI_ROOT/OpenSubtitles.en-si"
|
||||
)
|
||||
|
||||
SI_OPUS_URLS=(
|
||||
"https://object.pouta.csc.fi/OPUS-GNOME/v1/moses/en-si.txt.zip"
|
||||
"https://object.pouta.csc.fi/OPUS-Ubuntu/v14.10/moses/en-si.txt.zip"
|
||||
"https://object.pouta.csc.fi/OPUS-KDE4/v2/moses/en-si.txt.zip"
|
||||
"https://object.pouta.csc.fi/OPUS-OpenSubtitles/v2018/moses/en-si.txt.zip"
|
||||
)
|
||||
|
||||
NE_OPUS_DATASETS=(
|
||||
"$NE_ROOT/GNOME.en-ne"
|
||||
"$NE_ROOT/Ubuntu.en-ne"
|
||||
"$NE_ROOT/KDE4.en-ne"
|
||||
)
|
||||
|
||||
NE_OPUS_URLS=(
|
||||
"https://object.pouta.csc.fi/OPUS-GNOME/v1/moses/en-ne.txt.zip"
|
||||
"https://object.pouta.csc.fi/OPUS-Ubuntu/v14.10/moses/en-ne.txt.zip"
|
||||
"https://object.pouta.csc.fi/OPUS-KDE4/v2/moses/en-ne.txt.zip"
|
||||
)
|
||||
|
||||
REMOVE_FILE_PATHS=()
|
||||
|
||||
# Download data
|
||||
download_data() {
|
||||
CORPORA=$1
|
||||
URL=$2
|
||||
|
||||
if [ -f $CORPORA ]; then
|
||||
echo "$CORPORA already exists, skipping download"
|
||||
else
|
||||
echo "Downloading $URL"
|
||||
wget $URL -O $CORPORA --no-check-certificate || rm -f $CORPORA
|
||||
if [ -f $CORPORA ]; then
|
||||
echo "$URL successfully downloaded."
|
||||
else
|
||||
echo "$URL not successfully downloaded."
|
||||
rm -f $CORPORA
|
||||
exit -1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Example: download_opus_data $LANG_ROOT $TGT
|
||||
download_opus_data() {
|
||||
LANG_ROOT=$1
|
||||
TGT=$2
|
||||
|
||||
if [ "$TGT" = "si" ]; then
|
||||
URLS=("${SI_OPUS_URLS[@]}")
|
||||
DATASETS=("${SI_OPUS_DATASETS[@]}")
|
||||
else
|
||||
URLS=("${NE_OPUS_URLS[@]}")
|
||||
DATASETS=("${NE_OPUS_DATASETS[@]}")
|
||||
fi
|
||||
|
||||
# Download and extract data
|
||||
for ((i=0;i<${#URLS[@]};++i)); do
|
||||
URL=${URLS[i]}
|
||||
CORPORA=${DATASETS[i]}
|
||||
|
||||
download_data $CORPORA $URL
|
||||
unzip -o $CORPORA -d $LANG_ROOT
|
||||
REMOVE_FILE_PATHS+=( $CORPORA $CORPORA.xml $CORPORA.ids $LANG_ROOT/README $LANG_ROOT/LICENSE )
|
||||
done
|
||||
|
||||
cat ${DATASETS[0]}.$SRC ${DATASETS[1]}.$SRC ${DATASETS[2]}.$SRC > $LANG_ROOT/GNOMEKDEUbuntu.$SRC-$TGT.$SRC
|
||||
cat ${DATASETS[0]}.$TGT ${DATASETS[1]}.$TGT ${DATASETS[2]}.$TGT > $LANG_ROOT/GNOMEKDEUbuntu.$SRC-$TGT.$TGT
|
||||
|
||||
REMOVE_FILE_PATHS+=( ${DATASETS[0]}.$SRC ${DATASETS[1]}.$SRC ${DATASETS[2]}.$SRC )
|
||||
REMOVE_FILE_PATHS+=( ${DATASETS[0]}.$TGT ${DATASETS[1]}.$TGT ${DATASETS[2]}.$TGT )
|
||||
}
|
||||
|
||||
download_opus_data $SI_ROOT $SI_TGT
|
||||
cp ${SI_OPUS_DATASETS[3]}.$SRC $SI_ROOT/OpenSubtitles2018.$SRC-$SI_TGT.$SRC
|
||||
cp ${SI_OPUS_DATASETS[3]}.$SI_TGT $SI_ROOT/OpenSubtitles2018.$SRC-$SI_TGT.$SI_TGT
|
||||
REMOVE_FILE_PATHS+=( ${SI_OPUS_DATASETS[3]}.$SRC ${SI_OPUS_DATASETS[3]}.$SI_TGT )
|
||||
|
||||
download_opus_data $NE_ROOT $NE_TGT
|
||||
|
||||
|
||||
# Download and extract Global Voices data
|
||||
GLOBAL_VOICES="$NE_ROOT/globalvoices.2018q4.ne-en"
|
||||
GLOBAL_VOICES_URL="http://www.casmacat.eu/corpus/global-voices/globalvoices.ne-en.xliff.gz"
|
||||
|
||||
download_data $GLOBAL_VOICES.gz $GLOBAL_VOICES_URL
|
||||
gunzip -Nf $GLOBAL_VOICES.gz
|
||||
|
||||
sed -ne 's?.*<source>\(.*\)</source>.*?\1?p' $GLOBAL_VOICES > $GLOBAL_VOICES.$NE_TGT
|
||||
sed -ne 's?.*<target[^>]*>\(.*\)</target>.*?\1?p' $GLOBAL_VOICES > $GLOBAL_VOICES.$SRC
|
||||
|
||||
REMOVE_FILE_PATHS+=( $GLOBAL_VOICES )
|
||||
|
||||
# Download and extract the bible dataset
|
||||
BIBLE_TOOLS=bible-corpus-tools
|
||||
XML_BIBLES=XML_Bibles
|
||||
XML_BIBLES_DUP=XML_Bibles_dup
|
||||
|
||||
if [ ! -e $BIBLE_TOOLS ]; then
|
||||
echo "Cloning bible-corpus-tools repository..."
|
||||
git clone https://github.com/christos-c/bible-corpus-tools.git
|
||||
fi
|
||||
|
||||
mkdir -p $BIBLE_TOOLS/bin $XML_BIBLES $XML_BIBLES_DUP
|
||||
javac -cp "$BIBLE_TOOLS/lib/*" -d $BIBLE_TOOLS/bin $BIBLE_TOOLS/src/bible/readers/*.java $BIBLE_TOOLS/src/bible/*.java
|
||||
|
||||
download_data bible.tar.gz "https://github.com/christos-c/bible-corpus/archive/v1.2.1.tar.gz"
|
||||
tar xvzf bible.tar.gz
|
||||
|
||||
cp bible-corpus-1.2.1/bibles/{Greek.xml,English.xml,Nepali.xml} $XML_BIBLES/
|
||||
cp bible-corpus-1.2.1/bibles/{Greek.xml,English-WEB.xml,Nepali.xml} $XML_BIBLES_DUP/
|
||||
|
||||
java -cp $BIBLE_TOOLS/lib/*:$BIBLE_TOOLS/bin bible.CreateMLBooks $XML_BIBLES
|
||||
java -cp $BIBLE_TOOLS/lib/*:$BIBLE_TOOLS/bin bible.CreateMLBooks $XML_BIBLES_DUP
|
||||
java -cp $BIBLE_TOOLS/lib/*:$BIBLE_TOOLS/bin bible.CreateVerseAlignedBooks $XML_BIBLES
|
||||
java -cp $BIBLE_TOOLS/lib/*:$BIBLE_TOOLS/bin bible.CreateVerseAlignedBooks $XML_BIBLES_DUP
|
||||
|
||||
cat $XML_BIBLES/aligned/*/English.txt > $NE_ROOT/bible.$SRC-$NE_TGT.$SRC
|
||||
cat $XML_BIBLES/aligned/*/Nepali.txt > $NE_ROOT/bible.$SRC-$NE_TGT.$NE_TGT
|
||||
cat $XML_BIBLES_DUP/aligned/*/English-WEB.txt > $NE_ROOT/bible_dup.$SRC-$NE_TGT.$SRC
|
||||
cat $XML_BIBLES_DUP/aligned/*/Nepali.txt > $NE_ROOT/bible_dup.$SRC-$NE_TGT.$NE_TGT
|
||||
REMOVE_FILE_PATHS+=( bible-corpus-1.2.1 bible.tar.gz $BIBLE_TOOLS $XML_BIBLES $XML_BIBLES_DUP )
|
||||
|
||||
# Download and extract the Penn Treebank dataset
|
||||
NE_TAGGED=$ROOT/new_submissions_parallel_corpus_project_Nepal
|
||||
NE_TAGGED_URL="http://www.cle.org.pk/Downloads/ling_resources/parallelcorpus/NepaliTaggedCorpus.zip"
|
||||
EN_TAGGED_PATCH_URL="https://dl.fbaipublicfiles.com/fairseq/data/nepali-penn-treebank.en.patch"
|
||||
NE_TAGGED_PATCH_URL="https://dl.fbaipublicfiles.com/fairseq/data/nepali-penn-treebank.ne.patch"
|
||||
MOSES=mosesdecoder
|
||||
MOSES_TOK=$MOSES/scripts/tokenizer
|
||||
EN_PATCH_REGEX="{s:\\\/:\/:g;s/\*\T\*\-\n+//g;s/\-LCB\-/\{/g;s/\-RCB\-/\}/g; s/\-LSB\-/\[/g; s/\-RSB\-/\]/g;s/\-LRB\-/\(/g; s/\-RRB\-/\)/g; s/\'\'/\"/g; s/\`\`/\"/g; s/\ +\'s\ +/\'s /g; s/\ +\'re\ +/\'re /g; s/\"\ +/\"/g; s/\ +\"/\"/g; s/\ n't([\ \.\"])/n't\1/g; s/\r+(.)/\1/g;}"
|
||||
NE_PATCH_REGEX="{s:\p{Cf}::g;s:\\\/:\/:g;s/\*\T\*\-\n+//g;s/\-LCB\-/\{/g;s/\-RCB\-/\}/g; s/\-LSB\-/\[/g; s/\-RSB\-/\]/g;s/\-LRB\-/\(/g; s/\-RRB\-/\)/g; s/\'\'/\"/g; s/\`\`/\"/g; s/\ +\'s\ +/\'s /g; s/\ +\'re\ +/\'re /g; s/\"\ +/\"/g; s/\ +\"/\"/g; s/\ n't([\ \.\"])/n't\1/g; s/\r+(.)/\1/g;}"
|
||||
|
||||
download_data $DATA/nepali-penn-treebank.$SRC.patch $EN_TAGGED_PATCH_URL
|
||||
download_data $DATA/nepali-penn-treebank.$NE_TGT.patch $NE_TAGGED_PATCH_URL
|
||||
download_data original.zip $NE_TAGGED_URL
|
||||
unzip -o original.zip -d $ROOT
|
||||
|
||||
cat $NE_TAGGED/00.txt $NE_TAGGED/01.txt $NE_TAGGED/02.txt > $NE_TAGGED/nepali-penn-treebank.$SRC
|
||||
cat $NE_TAGGED/00ne_revised.txt $NE_TAGGED/01ne_revised.txt $NE_TAGGED/02ne_revised.txt > $NE_TAGGED/nepali-penn-treebank.$NE_TGT
|
||||
|
||||
patch $NE_TAGGED/nepali-penn-treebank.$SRC -i $DATA/nepali-penn-treebank.$SRC.patch -o $NE_TAGGED/nepali-penn-treebank-patched.$SRC
|
||||
patch $NE_TAGGED/nepali-penn-treebank.$NE_TGT -i $DATA/nepali-penn-treebank.$NE_TGT.patch -o $NE_TAGGED/nepali-penn-treebank-patched.$NE_TGT
|
||||
|
||||
if [ ! -e $MOSES ]; then
|
||||
echo "Cloning moses repository..."
|
||||
git clone https://github.com/moses-smt/mosesdecoder.git
|
||||
fi
|
||||
|
||||
cat $NE_TAGGED/nepali-penn-treebank-patched.$SRC | \
|
||||
perl -anpe "$EN_PATCH_REGEX" | \
|
||||
$MOSES_TOK/tokenizer.perl -l $SRC | \
|
||||
$MOSES_TOK/detokenizer.perl -l $SRC > $NE_ROOT/nepali-penn-treebank.$SRC
|
||||
|
||||
cat $NE_TAGGED/nepali-penn-treebank-patched.$NE_TGT | \
|
||||
perl -CIO -anpe "$NE_PATCH_REGEX" | \
|
||||
$MOSES_TOK/detokenizer.perl -l $SRC > $NE_ROOT/nepali-penn-treebank.$NE_TGT
|
||||
|
||||
|
||||
# Download nepali dictionary data
|
||||
NE_DICT=$NE_ROOT/dictionaries
|
||||
download_data $NE_DICT "http://www.seas.upenn.edu/~nlp/resources/TACL-data-release/dictionaries.tar.gz"
|
||||
tar xvzf $NE_DICT
|
||||
cp dictionaries/dict.ne $NE_ROOT/dictionary.$NE_TGT-$SRC
|
||||
REMOVE_FILE_PATHS+=( $NE_DICT dictionaries )
|
||||
|
||||
REMOVE_FILE_PATHS+=( $MOSES $NE_TAGGED original.zip $DATA/nepali-penn-treebank.$SRC.patch $DATA/nepali-penn-treebank.$NE_TGT.patch )
|
||||
|
||||
|
||||
# Remove the temporary files
|
||||
for ((i=0;i<${#REMOVE_FILE_PATHS[@]};++i)); do
|
||||
rm -rf ${REMOVE_FILE_PATHS[i]}
|
||||
done
|
||||
|
||||
# Copy the training data
|
||||
si=si_LK
|
||||
ne=ne_NP
|
||||
en=en_XX
|
||||
cat $SI_ROOT/GNOMEKDEUbuntu.en-si.si $SI_ROOT/OpenSubtitles2018.en-si.si > $DESTDIR/train.$si-$en.$si
|
||||
cat $SI_ROOT/GNOMEKDEUbuntu.en-si.en $SI_ROOT/OpenSubtitles2018.en-si.en > $DESTDIR/train.$si-$en.$en
|
||||
|
||||
cat $NE_ROOT/bible_dup.en-ne.ne $NE_ROOT/bible.en-ne.ne $NE_ROOT/globalvoices.2018q4.ne-en.ne $NE_ROOT/GNOMEKDEUbuntu.en-ne.ne $NE_ROOT/nepali-penn-treebank.ne > $DESTDIR/train.$ne-$en.$ne
|
||||
cat $NE_ROOT/bible_dup.en-ne.en $NE_ROOT/bible.en-ne.en $NE_ROOT/globalvoices.2018q4.ne-en.en $NE_ROOT/GNOMEKDEUbuntu.en-ne.en $NE_ROOT/nepali-penn-treebank.en > $DESTDIR/train.$ne-$en.$en
|
||||
|
||||
|
||||
#Download the test sets
|
||||
wget https://github.com/facebookresearch/flores/raw/master/data/wikipedia_en_ne_si_test_sets.tgz
|
||||
tar -xvzf wikipedia_en_ne_si_test_sets.tgz
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.dev.ne-en.ne $DESTDIR/valid.$ne-$en.$ne
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.dev.ne-en.en $DESTDIR/valid.$ne-$en.$en
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.dev.si-en.si $DESTDIR/valid.$si-$en.$si
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.dev.si-en.en $DESTDIR/valid.$si-$en.$en
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.devtest.ne-en.ne $DESTDIR/devtest.$ne-$en.$ne
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.devtest.ne-en.en $DESTDIR/devtest.$ne-$en.$en
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.devtest.si-en.si $DESTDIR/devtest.$si-$en.$si
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.devtest.si-en.en $DESTDIR/devtest.$si-$en.$en
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.test.ne-en.ne $DESTDIR/test.$ne-$en.$ne
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.test.ne-en.en $DESTDIR/test.$ne-$en.$en
|
||||
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.test.si-en.si $DESTDIR/test.$si-$en.$si
|
||||
cp wikipedia_en_ne_si_test_sets/wikipedia.test.si-en.en $DESTDIR/test.$si-$en.$en
|
||||
|
||||
rm -rf wikipedia_en_ne_si_test_sets.tgz wikipedia_en_ne_si_test_sets
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
if [ -z $WORKDIR_ROOT ] ;
|
||||
then
|
||||
echo "please specify your working directory root in environment variable WORKDIR_ROOT. Exitting..."
|
||||
exit
|
||||
fi
|
||||
|
||||
IITB=$WORKDIR_ROOT/IITB
|
||||
mkdir -p $IITB
|
||||
pushd $IITB
|
||||
|
||||
wget http://www.cfilt.iitb.ac.in/~moses/iitb_en_hi_parallel/iitb_corpus_download/parallel.tgz
|
||||
tar -xvzf parallel.tgz
|
||||
|
||||
wget http://www.cfilt.iitb.ac.in/~moses/iitb_en_hi_parallel/iitb_corpus_download/dev_test.tgz
|
||||
tar -xvzf dev_test.tgz
|
||||
|
||||
DESTDIR=${WORKDIR_ROOT}/ML50/raw/
|
||||
|
||||
cp parallel/IITB.en-hi.en $DESTDIR/train.hi_IN-en_XX.en_XX
|
||||
cp parallel/IITB.en-hi.hi $DESTDIR/train.hi_IN-en_XX.hi_IN
|
||||
|
||||
cp dev_test/dev.en $DESTDIR/valid.hi_IN-en_XX.en_XX
|
||||
cp dev_test/dev.hi $DESTDIR/valid.hi_IN-en_XX.hi_IN
|
||||
|
||||
cp dev_test/test.en $DESTDIR/test.hi_IN-en_XX.en_XX
|
||||
cp dev_test/test.hi $DESTDIR/test.hi_IN-en_XX.hi_IN
|
||||
popd
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
#echo 'Cloning Moses github repository (for tokenization scripts)...'
|
||||
#git clone https://github.com/moses-smt/mosesdecoder.git
|
||||
|
||||
if [ -z $WORKDIR_ROOT ] ;
|
||||
then
|
||||
echo "please specify your working directory root in environment variable WORKDIR_ROOT. Exitting..."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
|
||||
data_root=${WORKDIR_ROOT}/iwsltv2
|
||||
DESTDIR=${WORKDIR_ROOT}/ML50/raw
|
||||
|
||||
|
||||
langs="ar_AR it_IT nl_XX ko_KR vi_VN"
|
||||
echo "data_root: $data_root"
|
||||
|
||||
download_path=${data_root}/downloads
|
||||
raw=${DESTDIR}
|
||||
tmp=${data_root}/tmp
|
||||
orig=${data_root}/orig
|
||||
|
||||
mkdir -p $download_path $orig $raw $tmp
|
||||
#######################
|
||||
download_iwslt(){
|
||||
iwslt_key=$1
|
||||
src=$2
|
||||
tgt=$3
|
||||
save_prefix=$4
|
||||
pushd ${download_path}
|
||||
if [[ ! -f ${save_prefix}$src-$tgt.tgz ]]; then
|
||||
wget https://wit3.fbk.eu/archive/${iwslt_key}/texts/$src/$tgt/$src-$tgt.tgz -O ${save_prefix}$src-$tgt.tgz
|
||||
[ $? -eq 0 ] && return 0
|
||||
fi
|
||||
popd
|
||||
}
|
||||
|
||||
extract_iwslt(){
|
||||
src=$1
|
||||
tgt=$2
|
||||
prefix=$3
|
||||
pushd $orig
|
||||
tar zxvf ${download_path}/${prefix}$src-${tgt}.tgz
|
||||
popd
|
||||
}
|
||||
|
||||
generate_train(){
|
||||
lsrc=$1
|
||||
ltgt=$2
|
||||
src=${lsrc:0:2}
|
||||
tgt=${ltgt:0:2}
|
||||
for ll in $lsrc $ltgt; do
|
||||
l=${ll:0:2}
|
||||
f="$orig/*/train.tags.$src-$tgt.$l"
|
||||
f_raw=$raw/train.$lsrc-$ltgt.$ll
|
||||
cat $f \
|
||||
| grep -v '<url>' \
|
||||
| grep -v '<talkid>' \
|
||||
| grep -v '<keywords>' \
|
||||
| grep -v '<speaker>' \
|
||||
| grep -v '<reviewer' \
|
||||
| grep -v '<translator' \
|
||||
| grep -v '<doc' \
|
||||
| grep -v '</doc>' \
|
||||
| sed -e 's/<title>//g' \
|
||||
| sed -e 's/<\/title>//g' \
|
||||
| sed -e 's/<description>//g' \
|
||||
| sed -e 's/<\/description>//g' \
|
||||
| sed 's/^\s*//g' \
|
||||
| sed 's/\s*$//g' \
|
||||
> $f_raw
|
||||
[ $? -eq 0 ] && echo "extracted $f to $f_raw"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
convert_valid_test(){
|
||||
src=$1
|
||||
tgt=$2
|
||||
for l in $src $tgt; do
|
||||
echo "lang: ${l}"
|
||||
for o in `ls $orig/*/IWSLT*.TED*.$src-$tgt.$l.xml`; do
|
||||
fname=${o##*/}
|
||||
f=$tmp/${fname%.*}
|
||||
echo "$o => $f"
|
||||
grep '<seg id' $o \
|
||||
| sed -e 's/<seg id="[0-9]*">\s*//g' \
|
||||
| sed -e 's/\s*<\/seg>\s*//g' \
|
||||
| sed -e "s/\’/\'/g" \
|
||||
> $f
|
||||
echo ""
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
generate_subset(){
|
||||
lsrc=$1
|
||||
ltgt=$2
|
||||
src=${lsrc:0:2}
|
||||
tgt=${ltgt:0:2}
|
||||
subset=$3
|
||||
prefix=$4
|
||||
for ll in $lsrc $ltgt; do
|
||||
l=${ll:0:2}
|
||||
f=$tmp/$prefix.${src}-${tgt}.$l
|
||||
if [[ -f $f ]]; then
|
||||
cp $f $raw/$subset.${lsrc}-$ltgt.${ll}
|
||||
fi
|
||||
done
|
||||
}
|
||||
#################
|
||||
|
||||
echo "downloading iwslt training and dev data"
|
||||
# using multilingual for it, nl
|
||||
download_iwslt "2017-01-trnmted" DeEnItNlRo DeEnItNlRo
|
||||
download_iwslt "2017-01-trnted" ar en
|
||||
download_iwslt "2017-01-trnted" en ar
|
||||
download_iwslt "2017-01-trnted" ko en
|
||||
download_iwslt "2017-01-trnted" en ko
|
||||
download_iwslt "2015-01" vi en
|
||||
download_iwslt "2015-01" en vi
|
||||
|
||||
echo "donwloading iwslt test data"
|
||||
download_iwslt "2017-01-mted-test" it en "test."
|
||||
download_iwslt "2017-01-mted-test" en it "test."
|
||||
download_iwslt "2017-01-mted-test" nl en "test."
|
||||
download_iwslt "2017-01-mted-test" en nl "test."
|
||||
|
||||
download_iwslt "2017-01-ted-test" ar en "test."
|
||||
download_iwslt "2017-01-ted-test" en ar "test."
|
||||
download_iwslt "2017-01-ted-test" ko en "test."
|
||||
download_iwslt "2017-01-ted-test" en ko "test."
|
||||
download_iwslt "2015-01-test" vi en "test."
|
||||
download_iwslt "2015-01-test" en vi "test."
|
||||
|
||||
echo "extract training data tar balls"
|
||||
extract_iwslt DeEnItNlRo DeEnItNlRo
|
||||
extract_iwslt ar en
|
||||
extract_iwslt en ar
|
||||
extract_iwslt ko en
|
||||
extract_iwslt en ko
|
||||
extract_iwslt vi en
|
||||
extract_iwslt en vi
|
||||
|
||||
|
||||
echo "extracting iwslt test data"
|
||||
for lang in $langs; do
|
||||
l=${lang:0:2}
|
||||
extract_iwslt $l en "test."
|
||||
extract_iwslt en $l "test."
|
||||
done
|
||||
|
||||
echo "convert dev and test data"
|
||||
for lang in $langs; do
|
||||
s_lang=${lang:0:2}
|
||||
convert_valid_test $s_lang en
|
||||
convert_valid_test en $s_lang
|
||||
done
|
||||
|
||||
|
||||
|
||||
echo "creating training data into $raw"
|
||||
for lang in $langs; do
|
||||
generate_train $lang en_XX
|
||||
generate_train en_XX $lang
|
||||
done
|
||||
|
||||
echo "creating iwslt dev data into raw"
|
||||
generate_subset en_XX vi_VN valid "IWSLT15.TED.tst2013"
|
||||
generate_subset vi_VN en_XX valid "IWSLT15.TED.tst2013"
|
||||
|
||||
generate_subset en_XX ar_AR valid "IWSLT17.TED.tst2016"
|
||||
generate_subset ar_AR en_XX valid "IWSLT17.TED.tst2016"
|
||||
generate_subset en_XX ko_KR valid "IWSLT17.TED.tst2016"
|
||||
generate_subset ko_KR en_XX valid "IWSLT17.TED.tst2016"
|
||||
|
||||
|
||||
generate_subset en_XX it_IT valid "IWSLT17.TED.tst2010"
|
||||
generate_subset it_IT en_XX valid "IWSLT17.TED.tst2010"
|
||||
generate_subset en_XX nl_XX valid "IWSLT17.TED.tst2010"
|
||||
generate_subset nl_XX en_XX valid "IWSLT17.TED.tst2010"
|
||||
|
||||
echo "creating iswslt test data into raw"
|
||||
generate_subset en_XX vi_VN test "IWSLT15.TED.tst2015"
|
||||
generate_subset vi_VN en_XX test "IWSLT15.TED.tst2015"
|
||||
|
||||
generate_subset en_XX ar_AR test "IWSLT17.TED.tst2017"
|
||||
generate_subset ar_AR en_XX test "IWSLT17.TED.tst2017"
|
||||
generate_subset en_XX ko_KR test "IWSLT17.TED.tst2017"
|
||||
generate_subset ko_KR en_XX test "IWSLT17.TED.tst2017"
|
||||
|
||||
generate_subset en_XX it_IT test "IWSLT17.TED.tst2017.mltlng"
|
||||
generate_subset it_IT en_XX test "IWSLT17.TED.tst2017.mltlng"
|
||||
generate_subset en_XX nl_XX test "IWSLT17.TED.tst2017.mltlng"
|
||||
generate_subset nl_XX en_XX test "IWSLT17.TED.tst2017.mltlng"
|
||||
|
||||
# normalze iwslt directions into x-en
|
||||
pushd $raw
|
||||
for lang in $langs; do
|
||||
for split in test valid; do
|
||||
x_en_f1=$split.$lang-en_XX.en_XX
|
||||
x_en_f2=$split.$lang-en_XX.${lang}
|
||||
|
||||
en_x_f1=$split.en_XX-$lang.en_XX
|
||||
en_x_f2=$split.en_XX-$lang.${lang}
|
||||
|
||||
if [ -f $en_x_f1 ] && [ ! -f $x_en_f1 ]; then
|
||||
echo "cp $en_x_f1 $x_en_f1"
|
||||
cp $en_x_f1 $x_en_f1
|
||||
fi
|
||||
if [ -f $x_en_f2 ] && [ ! -f $x_en_f2 ]; then
|
||||
echo "cp $en_x_f2 $x_en_f2"
|
||||
cp $en_x_f2 $x_en_f2
|
||||
fi
|
||||
done
|
||||
done
|
||||
popd
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user