chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# Transformer with Pointer-Generator Network
|
||||
|
||||
This page describes the `transformer_pointer_generator` model that incorporates
|
||||
a pointing mechanism in the Transformer model that facilitates copying of input
|
||||
words to the output. This architecture is described in [Enarvi et al. (2020)](https://www.aclweb.org/anthology/2020.nlpmc-1.4/).
|
||||
|
||||
## Background
|
||||
|
||||
The pointer-generator network was introduced in [See et al. (2017)](https://arxiv.org/abs/1704.04368)
|
||||
for RNN encoder-decoder attention models. A similar mechanism can be
|
||||
incorporated in a Transformer model by reusing one of the many attention
|
||||
distributions for pointing. The attention distribution over the input words is
|
||||
interpolated with the normal output distribution over the vocabulary words. This
|
||||
allows the model to generate words that appear in the input, even if they don't
|
||||
appear in the vocabulary, helping especially with small vocabularies.
|
||||
|
||||
## Implementation
|
||||
|
||||
The mechanism for copying out-of-vocabulary words from the input has been
|
||||
implemented differently to See et al. In their [implementation](https://github.com/abisee/pointer-generator)
|
||||
they convey the word identities through the model in order to be able to produce
|
||||
words that appear in the input sequence but not in the vocabulary. A different
|
||||
approach was taken in the Fairseq implementation to keep it self-contained in
|
||||
the model file, avoiding any changes to the rest of the code base. Copying
|
||||
out-of-vocabulary words is possible by pre-processing the input and
|
||||
post-processing the output. This is described in detail in the next section.
|
||||
|
||||
## Usage
|
||||
|
||||
The training and evaluation procedure is outlined below. You can also find a
|
||||
more detailed example for the XSum dataset on [this page](README.xsum.md).
|
||||
|
||||
##### 1. Create a vocabulary and extend it with source position markers
|
||||
|
||||
The pointing mechanism is especially helpful with small vocabularies, if we are
|
||||
able to recover the identities of any out-of-vocabulary words that are copied
|
||||
from the input. For this purpose, the model allows extending the vocabulary with
|
||||
special tokens that can be used in place of `<unk>` tokens to identify different
|
||||
input positions. For example, the user may add `<unk-0>`, `<unk-1>`, `<unk-2>`,
|
||||
etc. to the end of the vocabulary, after the normal words. Below is an example
|
||||
of how to create a vocabulary of 10000 most common words and add 1000 input
|
||||
position markers.
|
||||
|
||||
```bash
|
||||
vocab_size=10000
|
||||
position_markers=1000
|
||||
export LC_ALL=C
|
||||
cat train.src train.tgt |
|
||||
tr -s '[:space:]' '\n' |
|
||||
sort |
|
||||
uniq -c |
|
||||
sort -k1,1bnr -k2 |
|
||||
head -n "$((vocab_size - 4))" |
|
||||
awk '{ print $2 " " $1 }' >dict.pg.txt
|
||||
python3 -c "[print('<unk-{}> 0'.format(n)) for n in range($position_markers)]" >>dict.pg.txt
|
||||
```
|
||||
|
||||
##### 2. Preprocess the text data
|
||||
|
||||
The idea is that any `<unk>` tokens in the text are replaced with `<unk-0>` if
|
||||
it appears in the first input position, `<unk-1>` if it appears in the second
|
||||
input position, and so on. This can be achieved using the `preprocess.py` script
|
||||
that is provided in this directory.
|
||||
|
||||
##### 3. Train a model
|
||||
|
||||
The number of these special tokens is given to the model with the
|
||||
`--source-position-markers` argument—the model simply maps all of these to the
|
||||
same word embedding as `<unk>`.
|
||||
|
||||
The attention distribution that is used for pointing is selected using the
|
||||
`--alignment-heads` and `--alignment-layer` command-line arguments in the same
|
||||
way as with the `transformer_align` model.
|
||||
|
||||
##### 4. Generate text and postprocess it
|
||||
|
||||
When using the model to generate text, you want to preprocess the input text in
|
||||
the same way that training data was processed, replacing out-of-vocabulary words
|
||||
with `<unk-N>` tokens. If any of these tokens are copied to the output, the
|
||||
actual words can be retrieved from the unprocessed input text. Any `<unk-N>`
|
||||
token should be replaced with the word at position N in the original input
|
||||
sequence. This can be achieved using the `postprocess.py` script.
|
||||
@@ -0,0 +1,180 @@
|
||||
## Training a pointer-generator model on the Extreme Summarization dataset
|
||||
|
||||
##### 1. Download the Extreme Summarization data and preprocess it
|
||||
|
||||
Follow the instructions [here](https://github.com/EdinburghNLP/XSum) to obtain
|
||||
the original Extreme Summarization dataset. You should have six files,
|
||||
{train,validation,test}.{document,summary}.
|
||||
|
||||
##### 2. Create a vocabulary and extend it with source position markers
|
||||
|
||||
```bash
|
||||
vocab_size=10000
|
||||
position_markers=1000
|
||||
export LC_ALL=C
|
||||
cat train.document train.summary |
|
||||
tr -s '[:space:]' '\n' |
|
||||
sort |
|
||||
uniq -c |
|
||||
sort -k1,1bnr -k2 |
|
||||
head -n "$((vocab_size - 4))" |
|
||||
awk '{ print $2 " " $1 }' >dict.pg.txt
|
||||
python3 -c "[print('<unk-{}> 0'.format(n)) for n in range($position_markers)]" >>dict.pg.txt
|
||||
```
|
||||
|
||||
This creates the file dict.pg.txt that contains the 10k most frequent words,
|
||||
followed by 1k source position markers:
|
||||
|
||||
```
|
||||
the 4954867
|
||||
. 4157552
|
||||
, 3439668
|
||||
to 2212159
|
||||
a 1916857
|
||||
of 1916820
|
||||
and 1823350
|
||||
...
|
||||
<unk-0> 0
|
||||
<unk-1> 0
|
||||
<unk-2> 0
|
||||
<unk-3> 0
|
||||
<unk-4> 0
|
||||
...
|
||||
```
|
||||
|
||||
##### 2. Preprocess the text data
|
||||
|
||||
```bash
|
||||
./preprocess.py --source train.document --target train.summary --vocab <(cut -d' ' -f1 dict.pg.txt) --source-out train.pg.src --target-out train.pg.tgt
|
||||
./preprocess.py --source validation.document --target validation.summary --vocab <(cut -d' ' -f1 dict.pg.txt) --source-out valid.pg.src --target-out valid.pg.tgt
|
||||
./preprocess.py --source test.document --vocab <(cut -d' ' -f1 dict.pg.txt) --source-out test.pg.src
|
||||
```
|
||||
|
||||
The data should now contain `<unk-N>` tokens in place of out-of-vocabulary words.
|
||||
|
||||
##### 3. Binarize the dataset:
|
||||
|
||||
```bash
|
||||
fairseq-preprocess \
|
||||
--source-lang src \
|
||||
--target-lang tgt \
|
||||
--trainpref train.pg \
|
||||
--validpref valid.pg \
|
||||
--destdir bin \
|
||||
--workers 60 \
|
||||
--srcdict dict.pg.txt \
|
||||
--joined-dictionary
|
||||
```
|
||||
|
||||
##### 3. Train a model
|
||||
|
||||
```bash
|
||||
total_updates=20000
|
||||
warmup_updates=500
|
||||
lr=0.001
|
||||
max_tokens=4096
|
||||
update_freq=4
|
||||
pointer_layer=-2
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 fairseq-train bin \
|
||||
--user-dir examples/pointer_generator/pointer_generator_src \
|
||||
--max-tokens "$max_tokens" \
|
||||
--task translation \
|
||||
--source-lang src --target-lang tgt \
|
||||
--truncate-source \
|
||||
--layernorm-embedding \
|
||||
--share-all-embeddings \
|
||||
--encoder-normalize-before \
|
||||
--decoder-normalize-before \
|
||||
--required-batch-size-multiple 1 \
|
||||
--arch transformer_pointer_generator \
|
||||
--alignment-layer "$pointer_layer" \
|
||||
--alignment-heads 1 \
|
||||
--source-position-markers 1000 \
|
||||
--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 inverse_sqrt --lr "$lr" --max-update "$total_updates" --warmup-updates "$warmup_updates" \
|
||||
--update-freq "$update_freq" \
|
||||
--skip-invalid-size-inputs-valid-test
|
||||
```
|
||||
|
||||
Above we specify that our dictionary contains 1000 source position markers, and
|
||||
that we want to use one attention head from the penultimate decoder layer for
|
||||
pointing. It should run in 5.5 hours on one node with eight 32GB V100 GPUs. The
|
||||
logged messages confirm that dictionary indices above 10000 will be mapped to
|
||||
the `<unk>` embedding:
|
||||
|
||||
```
|
||||
2020-09-24 20:43:53 | INFO | fairseq.tasks.translation | [src] dictionary: 11000 types
|
||||
2020-09-24 20:43:53 | INFO | fairseq.tasks.translation | [tgt] dictionary: 11000 types
|
||||
2020-09-24 20:43:53 | INFO | fairseq.data.data_utils | loaded 11332 examples from: bin/valid.src-tgt.src
|
||||
2020-09-24 20:43:53 | INFO | fairseq.data.data_utils | loaded 11332 examples from: bin/valid.src-tgt.tgt
|
||||
2020-09-24 20:43:53 | INFO | fairseq.tasks.translation | bin valid src-tgt 11332 examples
|
||||
2020-09-24 20:43:53 | INFO | fairseq.models.transformer_pg | dictionary indices from 10000 to 10999 will be mapped to 3
|
||||
```
|
||||
|
||||
##### 4. Summarize the test sequences
|
||||
|
||||
```bash
|
||||
batch_size=32
|
||||
beam_size=6
|
||||
max_length=60
|
||||
length_penalty=1.0
|
||||
|
||||
fairseq-interactive bin \
|
||||
--user-dir examples/pointer_generator/pointer_generator_src \
|
||||
--batch-size "$batch_size" \
|
||||
--task translation \
|
||||
--source-lang src --target-lang tgt \
|
||||
--path checkpoints/checkpoint_last.pt \
|
||||
--input test.pg.src \
|
||||
--buffer-size 200 \
|
||||
--max-len-a 0 \
|
||||
--max-len-b "$max_length" \
|
||||
--lenpen "$length_penalty" \
|
||||
--beam "$beam_size" \
|
||||
--skip-invalid-size-inputs-valid-test |
|
||||
tee generate.out
|
||||
grep ^H generate.out | cut -f 3- >generate.hyp
|
||||
```
|
||||
|
||||
Now you should have the generated sequences in `generate.hyp`. They contain
|
||||
`<unk-N>` tokens that the model has copied from the source sequence. In order to
|
||||
retrieve the original words, we need the unprocessed source sequences from
|
||||
`test.document`.
|
||||
|
||||
##### 5. Process the generated output
|
||||
|
||||
Since we skipped too long inputs when producing `generate.hyp`, we also have to
|
||||
skip too long sequences now that we read `test.document`.
|
||||
|
||||
```bash
|
||||
./postprocess.py \
|
||||
--source <(awk 'NF<1024' test.document) \
|
||||
--target generate.hyp \
|
||||
--target-out generate.hyp.processed
|
||||
```
|
||||
|
||||
Now you'll find the final sequences from `generate.hyp.processed`, with
|
||||
`<unk-N>` replaced with the original word from the source sequence.
|
||||
|
||||
##### An example of a summarized sequence
|
||||
|
||||
The original source document in `test.document`:
|
||||
|
||||
> de roon moved to teesside in june 2016 for an initial # 8.8 m fee and played 33 premier league games last term . the netherlands international , 26 , scored five goals in 36 league and cup games during his spell at boro . meanwhile , manager garry monk confirmed the championship club 's interest in signing chelsea midfielder lewis baker . `` he 's a target and one of many that we 've had throughout the summer months , '' said monk . find all the latest football transfers on our dedicated page .
|
||||
|
||||
The preprocessed source document in `test.src.pg`:
|
||||
|
||||
> de \<unk-1> moved to \<unk-4> in june 2016 for an initial # \<unk-12> m fee and played 33 premier league games last term . the netherlands international , 26 , scored five goals in 36 league and cup games during his spell at boro . meanwhile , manager garry monk confirmed the championship club 's interest in signing chelsea midfielder lewis baker . `` he 's a target and one of many that we 've had throughout the summer months , '' said monk . find all the latest football transfers on our dedicated page .
|
||||
|
||||
The generated summary in `generate.hyp`:
|
||||
|
||||
> middlesbrough striker \<unk> de \<unk-1> has joined spanish side \<unk> on a season-long loan .
|
||||
|
||||
The generated summary after postprocessing in `generate.hyp.processed`:
|
||||
|
||||
> middlesbrough striker \<unk> de roon has joined spanish side \<unk> on a season-long loan .
|
||||
@@ -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 . import transformer_pg # noqa
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
# 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 typing import Any, Dict, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from fairseq import metrics, utils
|
||||
from fairseq.models import register_model, register_model_architecture
|
||||
from fairseq.models.fairseq_encoder import EncoderOut
|
||||
from fairseq.models.transformer import (
|
||||
DEFAULT_MAX_SOURCE_POSITIONS,
|
||||
DEFAULT_MAX_TARGET_POSITIONS,
|
||||
TransformerDecoder,
|
||||
TransformerEncoder,
|
||||
TransformerModel,
|
||||
base_architecture,
|
||||
)
|
||||
from torch import Tensor
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register_model("transformer_pointer_generator")
|
||||
class TransformerPointerGeneratorModel(TransformerModel):
|
||||
"""
|
||||
Transformer model from `"Attention Is All You Need" (Vaswani et al, 2017)
|
||||
<https://arxiv.org/abs/1706.03762>`_, augmented with a pointer-generator
|
||||
network from `"Get To The Point: Summarization with Pointer-Generator
|
||||
Networks" (See et al, 2017) <https://arxiv.org/abs/1704.04368>`_.
|
||||
|
||||
Args:
|
||||
encoder (TransformerPointerGeneratorEncoder): the encoder
|
||||
decoder (TransformerPointerGeneratorDecoder): the decoder
|
||||
|
||||
The Transformer pointer-generator model provides the following named
|
||||
architectures and command-line arguments:
|
||||
|
||||
.. argparse::
|
||||
:ref: fairseq.models.transformer_pointer_generator_parser
|
||||
:prog:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add model-specific arguments to the parser."""
|
||||
# fmt: off
|
||||
TransformerModel.add_args(parser)
|
||||
parser.add_argument('--alignment-heads', type=int, metavar='N',
|
||||
help='number of attention heads to be used for '
|
||||
'pointing')
|
||||
parser.add_argument('--alignment-layer', type=int, metavar='I',
|
||||
help='layer number to be used for pointing (0 '
|
||||
'corresponding to the bottommost layer)')
|
||||
parser.add_argument('--source-position-markers', type=int, metavar='N',
|
||||
help='dictionary includes N additional items that '
|
||||
'represent an OOV token at a particular input '
|
||||
'position')
|
||||
parser.add_argument('--force-generation', type=float, metavar='P',
|
||||
default=None,
|
||||
help='set the vocabulary distribution weight to P, '
|
||||
'instead of predicting it from the input (1.0 '
|
||||
'corresponding to generation, 0.0 to pointing)')
|
||||
# fmt: on
|
||||
|
||||
@classmethod
|
||||
def build_model(cls, args, task):
|
||||
"""Build a new model instance."""
|
||||
|
||||
# make sure all arguments are present in older models
|
||||
base_architecture(args)
|
||||
|
||||
if args.encoder_layers_to_keep:
|
||||
args.encoder_layers = len(args.encoder_layers_to_keep.split(","))
|
||||
if args.decoder_layers_to_keep:
|
||||
args.decoder_layers = len(args.decoder_layers_to_keep.split(","))
|
||||
|
||||
if getattr(args, "max_source_positions", None) is None:
|
||||
args.max_source_positions = DEFAULT_MAX_SOURCE_POSITIONS
|
||||
if getattr(args, "max_target_positions", None) is None:
|
||||
args.max_target_positions = DEFAULT_MAX_TARGET_POSITIONS
|
||||
if getattr(args, "source_position_markers", None) is None:
|
||||
args.source_position_markers = args.max_source_positions
|
||||
|
||||
src_dict, tgt_dict = task.source_dictionary, task.target_dictionary
|
||||
if src_dict != tgt_dict:
|
||||
raise ValueError("Pointer-generator requires a joined dictionary")
|
||||
|
||||
def build_embedding(dictionary, embed_dim, path=None):
|
||||
# The dictionary may include additional items that can be used in
|
||||
# place of the normal OOV token and that all map to the same
|
||||
# embedding. Using a different token for each input position allows
|
||||
# one to restore the word identities from the original source text.
|
||||
num_embeddings = len(dictionary) - args.source_position_markers
|
||||
padding_idx = dictionary.pad()
|
||||
unk_idx = dictionary.unk()
|
||||
logger.info(
|
||||
"dictionary indices from {0} to {1} will be mapped to {2}".format(
|
||||
num_embeddings, len(dictionary) - 1, unk_idx
|
||||
)
|
||||
)
|
||||
emb = Embedding(num_embeddings, embed_dim, padding_idx, unk_idx)
|
||||
# if provided, load from preloaded dictionaries
|
||||
if path:
|
||||
embed_dict = utils.parse_embedding(path)
|
||||
utils.load_embedding(embed_dict, dictionary, emb)
|
||||
return emb
|
||||
|
||||
if args.share_all_embeddings:
|
||||
if args.encoder_embed_dim != args.decoder_embed_dim:
|
||||
raise ValueError(
|
||||
"--share-all-embeddings requires --encoder-embed-dim to match --decoder-embed-dim"
|
||||
)
|
||||
if args.decoder_embed_path and (
|
||||
args.decoder_embed_path != args.encoder_embed_path
|
||||
):
|
||||
raise ValueError(
|
||||
"--share-all-embeddings not compatible with --decoder-embed-path"
|
||||
)
|
||||
encoder_embed_tokens = build_embedding(
|
||||
src_dict, args.encoder_embed_dim, args.encoder_embed_path
|
||||
)
|
||||
decoder_embed_tokens = encoder_embed_tokens
|
||||
args.share_decoder_input_output_embed = True
|
||||
else:
|
||||
encoder_embed_tokens = build_embedding(
|
||||
src_dict, args.encoder_embed_dim, args.encoder_embed_path
|
||||
)
|
||||
decoder_embed_tokens = build_embedding(
|
||||
tgt_dict, args.decoder_embed_dim, args.decoder_embed_path
|
||||
)
|
||||
|
||||
encoder = cls.build_encoder(args, src_dict, encoder_embed_tokens)
|
||||
decoder = cls.build_decoder(args, tgt_dict, decoder_embed_tokens)
|
||||
return cls(args, encoder, decoder)
|
||||
|
||||
@classmethod
|
||||
def build_encoder(cls, args, src_dict, embed_tokens):
|
||||
return TransformerPointerGeneratorEncoder(args, src_dict, embed_tokens)
|
||||
|
||||
@classmethod
|
||||
def build_decoder(cls, args, tgt_dict, embed_tokens):
|
||||
return TransformerPointerGeneratorDecoder(args, tgt_dict, embed_tokens)
|
||||
|
||||
|
||||
class TransformerPointerGeneratorEncoder(TransformerEncoder):
|
||||
"""
|
||||
Transformer encoder consisting of *args.encoder_layers* layers. Each layer
|
||||
is a :class:`TransformerEncoderLayer`. The pointer-generator variant adds
|
||||
the source tokens to the encoder output as these are otherwise not passed
|
||||
to the decoder.
|
||||
"""
|
||||
|
||||
def forward(self, src_tokens, src_lengths, **kwargs):
|
||||
"""
|
||||
Runs the `forward()` method of the parent Transformer class. Then adds
|
||||
the source tokens into the encoder output tuple.
|
||||
|
||||
While it might be more elegant that the model would pass the source
|
||||
tokens to the `forward()` method of the decoder too, this would require
|
||||
changes to `SequenceGenerator`.
|
||||
|
||||
Args:
|
||||
src_tokens (torch.LongTensor): tokens in the source language of
|
||||
shape `(batch, src_len)`
|
||||
src_lengths (torch.LongTensor): lengths of each source sentence of
|
||||
shape `(batch)`
|
||||
|
||||
Returns:
|
||||
namedtuple:
|
||||
- **encoder_out** (Tensor): the last encoder layer's output of
|
||||
shape `(src_len, batch, embed_dim)`
|
||||
- **encoder_padding_mask** (ByteTensor): the positions of
|
||||
padding elements of shape `(batch, src_len)`
|
||||
- **encoder_embedding** (Tensor): the (scaled) embedding lookup
|
||||
of shape `(batch, src_len, embed_dim)`
|
||||
- **encoder_states** (List[Tensor]): all intermediate
|
||||
hidden states of shape `(src_len, batch, embed_dim)`.
|
||||
Only populated if *return_all_hiddens* is True.
|
||||
- **src_tokens** (Tensor): input token ids of shape
|
||||
`(batch, src_len)`
|
||||
"""
|
||||
encoder_out = super().forward(src_tokens, src_lengths, **kwargs)
|
||||
return {
|
||||
"encoder_out": encoder_out["encoder_out"], # T x B x C
|
||||
"encoder_padding_mask": encoder_out["encoder_padding_mask"], # B x T
|
||||
"encoder_embedding": encoder_out["encoder_embedding"], # B x T x C
|
||||
"encoder_states": encoder_out["encoder_states"], # List[T x B x C]
|
||||
"src_tokens": [src_tokens], # B x T
|
||||
"src_lengths": [],
|
||||
}
|
||||
|
||||
|
||||
class TransformerPointerGeneratorDecoder(TransformerDecoder):
|
||||
"""
|
||||
Transformer decoder consisting of *args.decoder_layers* layers. Each layer
|
||||
is a :class:`TransformerDecoderLayer`. The pointer-generator variant mixes
|
||||
the output probabilities with an attention distribution in the output layer.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): parsed command-line arguments
|
||||
dictionary (~fairseq.data.Dictionary): decoding dictionary
|
||||
embed_tokens (torch.nn.Embedding): output embedding
|
||||
"""
|
||||
|
||||
def __init__(self, args, dictionary, embed_tokens):
|
||||
super().__init__(args, dictionary, embed_tokens, no_encoder_attn=False)
|
||||
|
||||
# In the pointer-generator model these arguments define the decoder
|
||||
# layer and the number of attention heads that will be averaged to
|
||||
# create the alignment for pointing.
|
||||
self.alignment_heads = args.alignment_heads
|
||||
self.alignment_layer = args.alignment_layer
|
||||
|
||||
input_embed_dim = embed_tokens.embedding_dim
|
||||
|
||||
# Generation probabilities / interpolation coefficients are predicted
|
||||
# from the current decoder input embedding and the decoder output, which
|
||||
# is the size of output_embed_dim.
|
||||
p_gen_input_size = input_embed_dim + self.output_embed_dim
|
||||
self.project_p_gens = nn.Linear(p_gen_input_size, 1)
|
||||
nn.init.zeros_(self.project_p_gens.bias)
|
||||
|
||||
# The dictionary may include a separate entry for an OOV token in each
|
||||
# input position, so that their identity can be restored from the
|
||||
# original source text.
|
||||
self.num_types = len(dictionary)
|
||||
self.num_oov_types = args.source_position_markers
|
||||
self.num_embeddings = self.num_types - self.num_oov_types
|
||||
self.force_p_gen = args.force_generation
|
||||
|
||||
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] = 0,
|
||||
alignment_heads: Optional[int] = 1,
|
||||
src_lengths: Optional[Any] = None,
|
||||
return_all_hiddens: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
prev_output_tokens (LongTensor): previous decoder outputs of shape
|
||||
`(batch, tgt_len)`, for teacher forcing
|
||||
encoder_out (EncoderOut, optional): output from the encoder, used
|
||||
for encoder-side attention
|
||||
incremental_state (dict, optional): dictionary used for storing
|
||||
state during :ref:`Incremental decoding`
|
||||
features_only (bool, optional): only return features without
|
||||
applying output layer (default: False)
|
||||
alignment_layer (int, optional): 0-based index of the layer to be
|
||||
used for pointing (default: 0)
|
||||
alignment_heads (int, optional): number of attention heads to be
|
||||
used for pointing (default: 1)
|
||||
|
||||
Returns:
|
||||
tuple:
|
||||
- the decoder's output of shape `(batch, tgt_len, vocab)`
|
||||
- a dictionary with any model-specific outputs
|
||||
"""
|
||||
# The normal Transformer model doesn't pass the alignment_layer and
|
||||
# alignment_heads parameters correctly. We use our local variables.
|
||||
x, extra = self.extract_features(
|
||||
prev_output_tokens,
|
||||
encoder_out=encoder_out,
|
||||
incremental_state=incremental_state,
|
||||
alignment_layer=self.alignment_layer,
|
||||
alignment_heads=self.alignment_heads,
|
||||
)
|
||||
if not features_only:
|
||||
# Embedding the tokens again for generation probability prediction,
|
||||
# so that we don't have to reimplement the whole extract_features()
|
||||
# method.
|
||||
if incremental_state is not None:
|
||||
prev_output_tokens = prev_output_tokens[:, -1:]
|
||||
prev_output_embed = self.embed_tokens(prev_output_tokens)
|
||||
prev_output_embed *= self.embed_scale
|
||||
predictors = torch.cat((prev_output_embed, x), 2)
|
||||
p_gens = self.project_p_gens(predictors)
|
||||
p_gens = torch.sigmoid(p_gens)
|
||||
x = self.output_layer(x, extra["attn"][0], encoder_out["src_tokens"][0], p_gens)
|
||||
return x, extra
|
||||
|
||||
def output_layer(self, features, attn, src_tokens, p_gens, **kwargs):
|
||||
"""
|
||||
Project features to the vocabulary size and mix with the attention
|
||||
distributions.
|
||||
"""
|
||||
if self.force_p_gen is not None:
|
||||
p_gens = self.force_p_gen
|
||||
|
||||
# project back to size of vocabulary
|
||||
logits = super().output_layer(features, **kwargs)
|
||||
|
||||
batch_size = logits.shape[0]
|
||||
output_length = logits.shape[1]
|
||||
assert logits.shape[2] == self.num_embeddings
|
||||
assert src_tokens.shape[0] == batch_size
|
||||
src_length = src_tokens.shape[1]
|
||||
|
||||
# The final output distribution will be a mixture of the normal output
|
||||
# distribution (softmax of logits) and attention weights.
|
||||
gen_dists = super().get_normalized_probs(
|
||||
(logits, None), log_probs=False, sample=None
|
||||
)
|
||||
gen_dists = torch.mul(gen_dists, p_gens)
|
||||
padding_size = (batch_size, output_length, self.num_oov_types)
|
||||
padding = gen_dists.new_zeros(padding_size)
|
||||
gen_dists = torch.cat((gen_dists, padding), 2)
|
||||
assert gen_dists.shape[2] == self.num_types
|
||||
|
||||
# Scatter attention distributions to distributions over the extended
|
||||
# vocabulary in a tensor of shape [batch_size, output_length,
|
||||
# vocab_size]. Each attention weight will be written into a location
|
||||
# that is for other dimensions the same as in the index tensor, but for
|
||||
# the third dimension it's the value of the index tensor (the token ID).
|
||||
attn = torch.mul(attn, 1 - p_gens)
|
||||
index = src_tokens[:, None, :]
|
||||
index = index.expand(batch_size, output_length, src_length)
|
||||
attn_dists_size = (batch_size, output_length, self.num_types)
|
||||
attn_dists = attn.new_zeros(attn_dists_size)
|
||||
attn_dists.scatter_add_(2, index, attn)
|
||||
|
||||
# Final distributions, [batch_size, output_length, num_types].
|
||||
return gen_dists + attn_dists
|
||||
|
||||
def get_normalized_probs(self, net_output, log_probs, sample):
|
||||
"""
|
||||
Get normalized probabilities (or log probs) from a net's output.
|
||||
Pointer-generator network output is already normalized.
|
||||
"""
|
||||
probs = net_output[0]
|
||||
# Make sure the probabilities are greater than zero when returning log
|
||||
# probabilities.
|
||||
return probs.clamp(1e-10, 1.0).log() if log_probs else probs
|
||||
|
||||
|
||||
class Embedding(nn.Embedding):
|
||||
r"""A simple lookup table that stores embeddings of a fixed dictionary and size.
|
||||
This module is often used to store word embeddings and retrieve them using indices.
|
||||
The input to the module is a list of indices, and the output is the corresponding
|
||||
word embeddings. This subclass differs from the standard PyTorch Embedding class by
|
||||
allowing additional vocabulary entries that will be mapped to the unknown token
|
||||
embedding.
|
||||
Args:
|
||||
num_embeddings (int): size of the dictionary of embeddings
|
||||
embedding_dim (int): the size of each embedding vector
|
||||
padding_idx (int): Pads the output with the embedding vector at :attr:`padding_idx`
|
||||
(initialized to zeros) whenever it encounters the index.
|
||||
unk_idx (int): Maps all token indices that are greater than or equal to
|
||||
num_embeddings to this index.
|
||||
Attributes:
|
||||
weight (Tensor): the learnable weights of the module of shape (num_embeddings, embedding_dim)
|
||||
initialized from :math:`\mathcal{N}(0, 1)`
|
||||
Shape:
|
||||
- Input: :math:`(*)`, LongTensor of arbitrary shape containing the indices to extract
|
||||
- Output: :math:`(*, H)`, where `*` is the input shape and :math:`H=\text{embedding\_dim}`
|
||||
.. note::
|
||||
Keep in mind that only a limited number of optimizers support
|
||||
sparse gradients: currently it's :class:`optim.SGD` (`CUDA` and `CPU`),
|
||||
:class:`optim.SparseAdam` (`CUDA` and `CPU`) and :class:`optim.Adagrad` (`CPU`)
|
||||
.. note::
|
||||
With :attr:`padding_idx` set, the embedding vector at
|
||||
:attr:`padding_idx` is initialized to all zeros. However, note that this
|
||||
vector can be modified afterwards, e.g., using a customized
|
||||
initialization method, and thus changing the vector used to pad the
|
||||
output. The gradient for this vector from :class:`~torch.nn.Embedding`
|
||||
is always zero.
|
||||
"""
|
||||
__constants__ = ["unk_idx"]
|
||||
|
||||
def __init__(self, num_embeddings, embedding_dim, padding_idx, unk_idx):
|
||||
super().__init__(num_embeddings, embedding_dim, padding_idx=padding_idx)
|
||||
self.unk_idx = unk_idx
|
||||
nn.init.normal_(self.weight, mean=0, std=embedding_dim ** -0.5)
|
||||
nn.init.constant_(self.weight[padding_idx], 0)
|
||||
|
||||
def forward(self, input):
|
||||
input = torch.where(
|
||||
input >= self.num_embeddings, torch.ones_like(input) * self.unk_idx, input
|
||||
)
|
||||
return super().forward(input)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator", "transformer_pointer_generator"
|
||||
)
|
||||
def transformer_pointer_generator(args):
|
||||
args.alignment_heads = getattr(args, "alignment_heads", 1)
|
||||
args.alignment_layer = getattr(args, "alignment_layer", -1)
|
||||
base_architecture(args)
|
||||
if args.alignment_layer < 0:
|
||||
args.alignment_layer = args.decoder_layers + args.alignment_layer
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator", "transformer_pointer_generator_iwslt_de_en"
|
||||
)
|
||||
def transformer_pointer_generator_iwslt_de_en(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", 6)
|
||||
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", 6)
|
||||
transformer_pointer_generator(args)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator", "transformer_pointer_generator_wmt_en_de"
|
||||
)
|
||||
def transformer_pointer_generator_wmt_en_de(args):
|
||||
transformer_pointer_generator(args)
|
||||
|
||||
|
||||
# Transformer pointer-generator with the base Transformer parameters as used in
|
||||
# the "Attention Is All You Need" paper (Vaswani et al., 2017)
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator",
|
||||
"transformer_pointer_generator_vaswani_wmt_en_de_big",
|
||||
)
|
||||
def transformer_pointer_generator_vaswani_wmt_en_de_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)
|
||||
transformer_pointer_generator(args)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator",
|
||||
"transformer_pointer_generator_vaswani_wmt_en_fr_big",
|
||||
)
|
||||
def transformer_pointer_generator_vaswani_wmt_en_fr_big(args):
|
||||
args.dropout = getattr(args, "dropout", 0.1)
|
||||
transformer_pointer_generator_vaswani_wmt_en_de_big(args)
|
||||
|
||||
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator", "transformer_pointer_generator_wmt_en_de_big"
|
||||
)
|
||||
def transformer_pointer_generator_wmt_en_de_big(args):
|
||||
args.attention_dropout = getattr(args, "attention_dropout", 0.1)
|
||||
transformer_pointer_generator_vaswani_wmt_en_de_big(args)
|
||||
|
||||
|
||||
# default parameters used in tensor2tensor implementation
|
||||
@register_model_architecture(
|
||||
"transformer_pointer_generator", "transformer_pointer_generator_wmt_en_de_big_t2t"
|
||||
)
|
||||
def transformer_pointer_generator_wmt_en_de_big_t2t(args):
|
||||
args.encoder_normalize_before = getattr(args, "encoder_normalize_before", True)
|
||||
args.decoder_normalize_before = getattr(args, "decoder_normalize_before", True)
|
||||
args.attention_dropout = getattr(args, "attention_dropout", 0.1)
|
||||
args.activation_dropout = getattr(args, "activation_dropout", 0.1)
|
||||
transformer_pointer_generator_vaswani_wmt_en_de_big(args)
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/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 argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
class OOVIndexError(IndexError):
|
||||
def __init__(self, pos, source_seq, target_seq):
|
||||
super(OOVIndexError, self).__init__(
|
||||
"A <unk-N> tag in the target sequence refers to a position that is "
|
||||
"outside the source sequence. Most likely there was a mismatch in "
|
||||
"provided source and target sequences. Otherwise this would mean that "
|
||||
"the pointing mechanism somehow attended to a position that is past "
|
||||
"the actual sequence end."
|
||||
)
|
||||
self.source_pos = pos
|
||||
self.source_seq = source_seq
|
||||
self.target_seq = target_seq
|
||||
|
||||
|
||||
def replace_oovs(source_in, target_in, target_out):
|
||||
"""Replaces <unk-N> tokens in the target text with the corresponding word in
|
||||
the source text.
|
||||
"""
|
||||
|
||||
oov_re = re.compile("^<unk-([0-9]+)>$")
|
||||
|
||||
for source_seq, target_seq in zip(source_in, target_in):
|
||||
target_seq_out = []
|
||||
|
||||
pos_to_word = source_seq.strip().split()
|
||||
for token in target_seq.strip().split():
|
||||
m = oov_re.match(token)
|
||||
if m:
|
||||
pos = int(m.group(1))
|
||||
if pos >= len(pos_to_word):
|
||||
raise OOVIndexError(pos, source_seq, target_seq)
|
||||
token_out = pos_to_word[pos]
|
||||
else:
|
||||
token_out = token
|
||||
target_seq_out.append(token_out)
|
||||
target_out.write(" ".join(target_seq_out) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Replaces <unk-N> tokens in target sequences with words from "
|
||||
"the corresponding position in the source sequence."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source", type=str, help="text file with source sequences", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target", type=str, help="text file with target sequences", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-out",
|
||||
type=str,
|
||||
help="where to write target sequences without <unk-N> " "entries",
|
||||
required=True,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
target_in = (
|
||||
open(args.target, "r", encoding="utf-8") if args.target is not None else None
|
||||
)
|
||||
target_out = (
|
||||
open(args.target_out, "w", encoding="utf-8")
|
||||
if args.target_out is not None
|
||||
else None
|
||||
)
|
||||
with open(args.source, "r", encoding="utf-8") as source_in, open(
|
||||
args.target, "r", encoding="utf-8"
|
||||
) as target_in, open(args.target_out, "w", encoding="utf-8") as target_out:
|
||||
replace_oovs(source_in, target_in, target_out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except OOVIndexError as e:
|
||||
print(e, file=sys.stderr)
|
||||
print("Source sequence:", e.source_seq.strip(), file=sys.stderr)
|
||||
print("Target sequence:", e.target_seq.strip(), file=sys.stderr)
|
||||
print(
|
||||
"Source sequence length:",
|
||||
len(e.source_seq.strip().split()),
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("The offending tag points to:", e.source_pos)
|
||||
sys.exit(2)
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/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 argparse
|
||||
from itertools import zip_longest
|
||||
|
||||
|
||||
def replace_oovs(source_in, target_in, vocabulary, source_out, target_out):
|
||||
"""Replaces out-of-vocabulary words in source and target text with <unk-N>,
|
||||
where N in is the position of the word in the source sequence.
|
||||
"""
|
||||
|
||||
def format_unk(pos):
|
||||
return "<unk-{}>".format(pos)
|
||||
|
||||
if target_in is None:
|
||||
target_in = []
|
||||
|
||||
for seq_num, (source_seq, target_seq) in enumerate(
|
||||
zip_longest(source_in, target_in)
|
||||
):
|
||||
source_seq_out = []
|
||||
target_seq_out = []
|
||||
|
||||
word_to_pos = dict()
|
||||
for position, token in enumerate(source_seq.strip().split()):
|
||||
if token in vocabulary:
|
||||
token_out = token
|
||||
else:
|
||||
if token in word_to_pos:
|
||||
oov_pos = word_to_pos[token]
|
||||
else:
|
||||
word_to_pos[token] = position
|
||||
oov_pos = position
|
||||
token_out = format_unk(oov_pos)
|
||||
source_seq_out.append(token_out)
|
||||
source_out.write(" ".join(source_seq_out) + "\n")
|
||||
|
||||
if target_seq is not None:
|
||||
for token in target_seq.strip().split():
|
||||
if token in word_to_pos:
|
||||
token_out = format_unk(word_to_pos[token])
|
||||
else:
|
||||
token_out = token
|
||||
target_seq_out.append(token_out)
|
||||
if target_out is not None:
|
||||
target_out.write(" ".join(target_seq_out) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Replaces out-of-vocabulary words in both source and target "
|
||||
"sequences with tokens that indicate the position of the word "
|
||||
"in the source sequence."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source", type=str, help="text file with source sequences", required=True
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target", type=str, help="text file with target sequences", default=None
|
||||
)
|
||||
parser.add_argument("--vocab", type=str, help="vocabulary file", required=True)
|
||||
parser.add_argument(
|
||||
"--source-out",
|
||||
type=str,
|
||||
help="where to write source sequences with <unk-N> entries",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-out",
|
||||
type=str,
|
||||
help="where to write target sequences with <unk-N> entries",
|
||||
default=None,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.vocab, encoding="utf-8") as vocab:
|
||||
vocabulary = vocab.read().splitlines()
|
||||
|
||||
target_in = (
|
||||
open(args.target, "r", encoding="utf-8") if args.target is not None else None
|
||||
)
|
||||
target_out = (
|
||||
open(args.target_out, "w", encoding="utf-8")
|
||||
if args.target_out is not None
|
||||
else None
|
||||
)
|
||||
with open(args.source, "r", encoding="utf-8") as source_in, open(
|
||||
args.source_out, "w", encoding="utf-8"
|
||||
) as source_out:
|
||||
replace_oovs(source_in, target_in, vocabulary, source_out, target_out)
|
||||
if target_in is not None:
|
||||
target_in.close()
|
||||
if target_out is not None:
|
||||
target_out.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user