chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Finetuning RoBERTa on a custom classification task
|
||||
|
||||
This example shows how to finetune RoBERTa on the IMDB dataset, but should illustrate the process for most classification tasks.
|
||||
|
||||
### 1) Get the data
|
||||
|
||||
```bash
|
||||
wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
|
||||
tar zxvf aclImdb_v1.tar.gz
|
||||
```
|
||||
|
||||
|
||||
### 2) Format data
|
||||
|
||||
`IMDB` data has one data-sample in each file, below python code-snippet converts it one file for train and valid each for ease of processing.
|
||||
```python
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
from glob import glob
|
||||
|
||||
random.seed(0)
|
||||
|
||||
def main(args):
|
||||
for split in ['train', 'test']:
|
||||
samples = []
|
||||
for class_label in ['pos', 'neg']:
|
||||
fnames = glob(os.path.join(args.datadir, split, class_label) + '/*.txt')
|
||||
for fname in fnames:
|
||||
with open(fname) as fin:
|
||||
line = fin.readline()
|
||||
samples.append((line, 1 if class_label == 'pos' else 0))
|
||||
random.shuffle(samples)
|
||||
out_fname = 'train' if split == 'train' else 'dev'
|
||||
f1 = open(os.path.join(args.datadir, out_fname + '.input0'), 'w')
|
||||
f2 = open(os.path.join(args.datadir, out_fname + '.label'), 'w')
|
||||
for sample in samples:
|
||||
f1.write(sample[0] + '\n')
|
||||
f2.write(str(sample[1]) + '\n')
|
||||
f1.close()
|
||||
f2.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--datadir', default='aclImdb')
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
```
|
||||
|
||||
|
||||
### 3) BPE encode
|
||||
|
||||
Run `multiprocessing_bpe_encoder`, you can also do this in previous step for each sample but that might be slower.
|
||||
```bash
|
||||
# Download encoder.json and vocab.bpe
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json'
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe'
|
||||
|
||||
for SPLIT in train dev; do
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json encoder.json \
|
||||
--vocab-bpe vocab.bpe \
|
||||
--inputs "aclImdb/$SPLIT.input0" \
|
||||
--outputs "aclImdb/$SPLIT.input0.bpe" \
|
||||
--workers 60 \
|
||||
--keep-empty
|
||||
done
|
||||
```
|
||||
|
||||
|
||||
### 4) Preprocess data
|
||||
|
||||
```bash
|
||||
# Download fairseq dictionary.
|
||||
wget -N 'https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt'
|
||||
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref "aclImdb/train.input0.bpe" \
|
||||
--validpref "aclImdb/dev.input0.bpe" \
|
||||
--destdir "IMDB-bin/input0" \
|
||||
--workers 60 \
|
||||
--srcdict dict.txt
|
||||
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref "aclImdb/train.label" \
|
||||
--validpref "aclImdb/dev.label" \
|
||||
--destdir "IMDB-bin/label" \
|
||||
--workers 60
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 5) Run training
|
||||
|
||||
```bash
|
||||
TOTAL_NUM_UPDATES=7812 # 10 epochs through IMDB for bsz 32
|
||||
WARMUP_UPDATES=469 # 6 percent of the number of updates
|
||||
LR=1e-05 # Peak LR for polynomial LR scheduler.
|
||||
HEAD_NAME=imdb_head # Custom name for the classification head.
|
||||
NUM_CLASSES=2 # Number of classes for the classification task.
|
||||
MAX_SENTENCES=8 # Batch size.
|
||||
ROBERTA_PATH=/path/to/roberta.large/model.pt
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 fairseq-train IMDB-bin/ \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--max-positions 512 \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-tokens 4400 \
|
||||
--task sentence_prediction \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--required-batch-size-multiple 1 \
|
||||
--init-token 0 --separator-token 2 \
|
||||
--arch roberta_large \
|
||||
--criterion sentence_prediction \
|
||||
--classification-head-name $HEAD_NAME \
|
||||
--num-classes $NUM_CLASSES \
|
||||
--dropout 0.1 --attention-dropout 0.1 \
|
||||
--weight-decay 0.1 --optimizer adam --adam-betas "(0.9, 0.98)" --adam-eps 1e-06 \
|
||||
--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 \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \
|
||||
--shorten-method "truncate" \
|
||||
--find-unused-parameters \
|
||||
--update-freq 4
|
||||
```
|
||||
|
||||
The above command will finetune RoBERTa-large with an effective batch-size of 32
|
||||
sentences (`--batch-size=8 --update-freq=4`). The expected
|
||||
`best-validation-accuracy` after 10 epochs is ~96.5%.
|
||||
|
||||
If you run out of GPU memory, try decreasing `--batch-size` and increase
|
||||
`--update-freq` to compensate.
|
||||
|
||||
|
||||
### 6) Load model using hub interface
|
||||
|
||||
Now we can load the trained model checkpoint using the RoBERTa hub interface.
|
||||
|
||||
Assuming your checkpoints are stored in `checkpoints/`:
|
||||
```python
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
roberta = RobertaModel.from_pretrained(
|
||||
'checkpoints',
|
||||
checkpoint_file='checkpoint_best.pt',
|
||||
data_name_or_path='IMDB-bin'
|
||||
)
|
||||
roberta.eval() # disable dropout
|
||||
```
|
||||
|
||||
Finally you can make predictions using the `imdb_head` (or whatever you set
|
||||
`--classification-head-name` to during training):
|
||||
```python
|
||||
label_fn = lambda label: roberta.task.label_dictionary.string(
|
||||
[label + roberta.task.label_dictionary.nspecial]
|
||||
)
|
||||
|
||||
tokens = roberta.encode('Best movie this year')
|
||||
pred = label_fn(roberta.predict('imdb_head', tokens).argmax().item())
|
||||
assert pred == '1' # positive
|
||||
|
||||
tokens = roberta.encode('Worst movie ever')
|
||||
pred = label_fn(roberta.predict('imdb_head', tokens).argmax().item())
|
||||
assert pred == '0' # negative
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Finetuning RoBERTa 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:
|
||||
```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=122 # 6 percent of the number of updates
|
||||
LR=2e-05 # Peak LR for polynomial LR scheduler.
|
||||
NUM_CLASSES=2
|
||||
MAX_SENTENCES=16 # Batch size.
|
||||
ROBERTA_PATH=/path/to/roberta/model.pt
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 fairseq-train RTE-bin/ \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--max-positions 512 \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-tokens 4400 \
|
||||
--task sentence_prediction \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--required-batch-size-multiple 1 \
|
||||
--init-token 0 --separator-token 2 \
|
||||
--arch roberta_large \
|
||||
--criterion sentence_prediction \
|
||||
--num-classes $NUM_CLASSES \
|
||||
--dropout 0.1 --attention-dropout 0.1 \
|
||||
--weight-decay 0.1 --optimizer adam --adam-betas "(0.9, 0.98)" --adam-eps 1e-06 \
|
||||
--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` | 1e-5 | 1e-5 | 1e-5 | 2e-5 | 1e-5 | 1e-5 | 1e-5 | 2e-5
|
||||
`--batch-size` | 32 | 32 | 32 | 16 | 32 | 16 | 16 | 16
|
||||
`--total-num-update` | 123873 | 33112 | 113272 | 2036 | 20935 | 2296 | 5336 | 3598
|
||||
`--warmup-updates` | 7432 | 1986 | 28318 | 122 | 1256 | 137 | 320 | 214
|
||||
|
||||
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=16/32` depending on the task.
|
||||
|
||||
b) Above cmd-args and hyperparams are tested on one 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`.
|
||||
|
||||
c) All the settings in above table are suggested settings based on our hyperparam search within a fixed search space (for careful comparison across models). You might be able to find better metrics with wider hyperparam search.
|
||||
|
||||
### 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.roberta import RobertaModel
|
||||
|
||||
roberta = RobertaModel.from_pretrained(
|
||||
'checkpoints/',
|
||||
checkpoint_file='checkpoint_best.pt',
|
||||
data_name_or_path='RTE-bin'
|
||||
)
|
||||
|
||||
label_fn = lambda label: roberta.task.label_dictionary.string(
|
||||
[label + roberta.task.label_dictionary.nspecial]
|
||||
)
|
||||
ncorrect, nsamples = 0, 0
|
||||
roberta.cuda()
|
||||
roberta.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 = 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))
|
||||
|
||||
```
|
||||
@@ -0,0 +1,296 @@
|
||||
# RoBERTa: A Robustly Optimized BERT Pretraining Approach
|
||||
|
||||
https://arxiv.org/abs/1907.11692
|
||||
|
||||
## Introduction
|
||||
|
||||
RoBERTa iterates on BERT's pretraining procedure, including training the model longer, with bigger batches over more data; removing the next sentence prediction objective; training on longer sequences; and dynamically changing the masking pattern applied to the training data. See the associated paper for more details.
|
||||
|
||||
### What's New:
|
||||
|
||||
- December 2020: German model (GottBERT) is available: [GottBERT](https://github.com/pytorch/fairseq/tree/master/examples/gottbert).
|
||||
- January 2020: Italian model (UmBERTo) is available from Musixmatch Research: [UmBERTo](https://github.com/musixmatchresearch/umberto).
|
||||
- November 2019: French model (CamemBERT) is available: [CamemBERT](https://github.com/pytorch/fairseq/tree/master/examples/camembert).
|
||||
- November 2019: Multilingual encoder (XLM-RoBERTa) is available: [XLM-R](https://github.com/pytorch/fairseq/tree/master/examples/xlmr).
|
||||
- September 2019: TensorFlow and TPU support via the [transformers library](https://github.com/huggingface/transformers).
|
||||
- August 2019: RoBERTa is now supported in the [pytorch-transformers library](https://github.com/huggingface/pytorch-transformers).
|
||||
- August 2019: Added [tutorial for finetuning on WinoGrande](https://github.com/pytorch/fairseq/tree/master/examples/roberta/wsc#roberta-training-on-winogrande-dataset).
|
||||
- August 2019: Added [tutorial for pretraining RoBERTa using your own data](README.pretraining.md).
|
||||
|
||||
## Pre-trained models
|
||||
|
||||
Model | Description | # params | Download
|
||||
---|---|---|---
|
||||
`roberta.base` | RoBERTa using the BERT-base architecture | 125M | [roberta.base.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta.base.tar.gz)
|
||||
`roberta.large` | RoBERTa using the BERT-large architecture | 355M | [roberta.large.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta.large.tar.gz)
|
||||
`roberta.large.mnli` | `roberta.large` finetuned on [MNLI](http://www.nyu.edu/projects/bowman/multinli) | 355M | [roberta.large.mnli.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta.large.mnli.tar.gz)
|
||||
`roberta.large.wsc` | `roberta.large` finetuned on [WSC](wsc/README.md) | 355M | [roberta.large.wsc.tar.gz](https://dl.fbaipublicfiles.com/fairseq/models/roberta.large.wsc.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.base` | 87.6 | 92.8 | 91.9 | 78.7 | 94.8 | 90.2 | 63.6 | 91.2
|
||||
`roberta.large` | 90.2 | 94.7 | 92.2 | 86.6 | 96.4 | 90.9 | 68.0 | 92.4
|
||||
`roberta.large.mnli` | 90.2 | - | - | - | - | - | - | -
|
||||
|
||||
**[SuperGLUE (Wang et al., 2019)](https://super.gluebenchmark.com/)**
|
||||
_(dev set, single model, single-task finetuning)_
|
||||
|
||||
Model | BoolQ | CB | COPA | MultiRC | RTE | WiC | WSC
|
||||
---|---|---|---|---|---|---|---
|
||||
`roberta.large` | 86.9 | 98.2 | 94.0 | 85.7 | 89.5 | 75.6 | -
|
||||
`roberta.large.wsc` | - | - | - | - | - | - | 91.3
|
||||
|
||||
**[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
|
||||
|
||||
**[RACE (Lai et al., 2017)](http://www.qizhexie.com/data/RACE_leaderboard.html)**
|
||||
_(test set)_
|
||||
|
||||
Model | Accuracy | Middle | High
|
||||
---|---|---|---
|
||||
`roberta.large` | 83.2 | 86.5 | 81.3
|
||||
|
||||
**[HellaSwag (Zellers et al., 2019)](https://rowanzellers.com/hellaswag/)**
|
||||
_(test set)_
|
||||
|
||||
Model | Overall | In-domain | Zero-shot | ActivityNet | WikiHow
|
||||
---|---|---|---|---|---
|
||||
`roberta.large` | 85.2 | 87.3 | 83.1 | 74.6 | 90.9
|
||||
|
||||
**[Commonsense QA (Talmor et al., 2019)](https://www.tau-nlp.org/commonsenseqa)**
|
||||
_(test set)_
|
||||
|
||||
Model | Accuracy
|
||||
---|---
|
||||
`roberta.large` (single model) | 72.1
|
||||
`roberta.large` (ensemble) | 72.5
|
||||
|
||||
**[Winogrande (Sakaguchi et al., 2019)](https://arxiv.org/abs/1907.10641)**
|
||||
_(test set)_
|
||||
|
||||
Model | Accuracy
|
||||
---|---
|
||||
`roberta.large` | 78.1
|
||||
|
||||
**[XNLI (Conneau et al., 2018)](https://arxiv.org/abs/1809.05053)**
|
||||
_(TRANSLATE-TEST)_
|
||||
|
||||
Model | en | fr | es | de | el | bg | ru | tr | ar | vi | th | zh | hi | sw | ur
|
||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---
|
||||
`roberta.large.mnli` | 91.3 | 82.91 | 84.27 | 81.24 | 81.74 | 83.13 | 78.28 | 76.79 | 76.64 | 74.17 | 74.05 | 77.5 | 70.9 | 66.65 | 66.81
|
||||
|
||||
## Example usage
|
||||
|
||||
##### Load RoBERTa from torch.hub (PyTorch >= 1.1):
|
||||
```python
|
||||
import torch
|
||||
roberta = torch.hub.load('pytorch/fairseq', 'roberta.large')
|
||||
roberta.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Load RoBERTa (for PyTorch 1.0 or custom models):
|
||||
```python
|
||||
# Download roberta.large model
|
||||
wget https://dl.fbaipublicfiles.com/fairseq/models/roberta.large.tar.gz
|
||||
tar -xzvf roberta.large.tar.gz
|
||||
|
||||
# Load the model in fairseq
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
roberta = RobertaModel.from_pretrained('/path/to/roberta.large', checkpoint_file='model.pt')
|
||||
roberta.eval() # disable dropout (or leave in train mode to finetune)
|
||||
```
|
||||
|
||||
##### Apply Byte-Pair Encoding (BPE) to input text:
|
||||
```python
|
||||
tokens = roberta.encode('Hello world!')
|
||||
assert tokens.tolist() == [0, 31414, 232, 328, 2]
|
||||
roberta.decode(tokens) # 'Hello world!'
|
||||
```
|
||||
|
||||
##### Extract features from RoBERTa:
|
||||
```python
|
||||
# Extract the last layer's features
|
||||
last_layer_features = roberta.extract_features(tokens)
|
||||
assert last_layer_features.size() == torch.Size([1, 5, 1024])
|
||||
|
||||
# Extract all layer's features (layer 0 is the embedding layer)
|
||||
all_layers = roberta.extract_features(tokens, return_all_hiddens=True)
|
||||
assert len(all_layers) == 25
|
||||
assert torch.all(all_layers[-1] == last_layer_features)
|
||||
```
|
||||
|
||||
##### Use RoBERTa for sentence-pair classification tasks:
|
||||
```python
|
||||
# Download RoBERTa already finetuned for MNLI
|
||||
roberta = torch.hub.load('pytorch/fairseq', 'roberta.large.mnli')
|
||||
roberta.eval() # disable dropout for evaluation
|
||||
|
||||
# Encode a pair of sentences and make a prediction
|
||||
tokens = roberta.encode('Roberta is a heavily optimized version of BERT.', 'Roberta is not very optimized.')
|
||||
roberta.predict('mnli', tokens).argmax() # 0: contradiction
|
||||
|
||||
# Encode another pair of sentences
|
||||
tokens = roberta.encode('Roberta is a heavily optimized version of BERT.', 'Roberta is based on BERT.')
|
||||
roberta.predict('mnli', tokens).argmax() # 2: entailment
|
||||
```
|
||||
|
||||
##### Register a new (randomly initialized) classification head:
|
||||
```python
|
||||
roberta.register_classification_head('new_task', num_classes=3)
|
||||
logprobs = roberta.predict('new_task', tokens) # tensor([[-1.1050, -1.0672, -1.1245]], grad_fn=<LogSoftmaxBackward>)
|
||||
```
|
||||
|
||||
##### Batched prediction:
|
||||
```python
|
||||
import torch
|
||||
from fairseq.data.data_utils import collate_tokens
|
||||
|
||||
roberta = torch.hub.load('pytorch/fairseq', 'roberta.large.mnli')
|
||||
roberta.eval()
|
||||
|
||||
batch_of_pairs = [
|
||||
['Roberta is a heavily optimized version of BERT.', 'Roberta is not very optimized.'],
|
||||
['Roberta is a heavily optimized version of BERT.', 'Roberta is based on BERT.'],
|
||||
['potatoes are awesome.', 'I like to run.'],
|
||||
['Mars is very far from earth.', 'Mars is very close.'],
|
||||
]
|
||||
|
||||
batch = collate_tokens(
|
||||
[roberta.encode(pair[0], pair[1]) for pair in batch_of_pairs], pad_idx=1
|
||||
)
|
||||
|
||||
logprobs = roberta.predict('mnli', batch)
|
||||
print(logprobs.argmax(dim=1))
|
||||
# tensor([0, 2, 1, 0])
|
||||
```
|
||||
|
||||
##### Using the GPU:
|
||||
```python
|
||||
roberta.cuda()
|
||||
roberta.predict('new_task', tokens) # tensor([[-1.1050, -1.0672, -1.1245]], device='cuda:0', grad_fn=<LogSoftmaxBackward>)
|
||||
```
|
||||
|
||||
## Advanced usage
|
||||
|
||||
#### Filling masks:
|
||||
|
||||
RoBERTa can be used to fill `<mask>` tokens in the input. Some examples from the
|
||||
[Natural Questions dataset](https://ai.google.com/research/NaturalQuestions/):
|
||||
```python
|
||||
roberta.fill_mask('The first Star wars movie came out in <mask>', topk=3)
|
||||
# [('The first Star wars movie came out in 1977', 0.9504708051681519, ' 1977'), ('The first Star wars movie came out in 1978', 0.009986862540245056, ' 1978'), ('The first Star wars movie came out in 1979', 0.009574787691235542, ' 1979')]
|
||||
|
||||
roberta.fill_mask('Vikram samvat calender is official in <mask>', topk=3)
|
||||
# [('Vikram samvat calender is official in India', 0.21878819167613983, ' India'), ('Vikram samvat calender is official in Delhi', 0.08547237515449524, ' Delhi'), ('Vikram samvat calender is official in Gujarat', 0.07556215673685074, ' Gujarat')]
|
||||
|
||||
roberta.fill_mask('<mask> is the common currency of the European Union', topk=3)
|
||||
# [('Euro is the common currency of the European Union', 0.9456493854522705, 'Euro'), ('euro is the common currency of the European Union', 0.025748178362846375, 'euro'), ('€ is the common currency of the European Union', 0.011183084920048714, '€')]
|
||||
```
|
||||
|
||||
#### Pronoun disambiguation (Winograd Schema Challenge):
|
||||
|
||||
RoBERTa can be used to disambiguate pronouns. First install spaCy and download the English-language model:
|
||||
```bash
|
||||
pip install spacy
|
||||
python -m spacy download en_core_web_lg
|
||||
```
|
||||
|
||||
Next load the `roberta.large.wsc` model and call the `disambiguate_pronoun`
|
||||
function. The pronoun should be surrounded by square brackets (`[]`) and the
|
||||
query referent surrounded by underscores (`_`), or left blank to return the
|
||||
predicted candidate text directly:
|
||||
```python
|
||||
roberta = torch.hub.load('pytorch/fairseq', 'roberta.large.wsc', user_dir='examples/roberta/wsc')
|
||||
roberta.cuda() # use the GPU (optional)
|
||||
|
||||
roberta.disambiguate_pronoun('The _trophy_ would not fit in the brown suitcase because [it] was too big.')
|
||||
# True
|
||||
roberta.disambiguate_pronoun('The trophy would not fit in the brown _suitcase_ because [it] was too big.')
|
||||
# False
|
||||
|
||||
roberta.disambiguate_pronoun('The city councilmen refused the demonstrators a permit because [they] feared violence.')
|
||||
# 'The city councilmen'
|
||||
roberta.disambiguate_pronoun('The city councilmen refused the demonstrators a permit because [they] advocated violence.')
|
||||
# 'demonstrators'
|
||||
```
|
||||
|
||||
See the [RoBERTA Winograd Schema Challenge (WSC) README](wsc/README.md) for more details on how to train this model.
|
||||
|
||||
#### Extract features aligned to words:
|
||||
|
||||
By default RoBERTa outputs one feature vector per BPE token. You can instead
|
||||
realign the features to match [spaCy's word-level tokenization](https://spacy.io/usage/linguistic-features#tokenization)
|
||||
with the `extract_features_aligned_to_words` method. This will compute a
|
||||
weighted average of the BPE-level features for each word and expose them in
|
||||
spaCy's `Token.vector` attribute:
|
||||
```python
|
||||
doc = roberta.extract_features_aligned_to_words('I said, "hello RoBERTa."')
|
||||
assert len(doc) == 10
|
||||
for tok in doc:
|
||||
print('{:10}{} (...)'.format(str(tok), tok.vector[:5]))
|
||||
# <s> tensor([-0.1316, -0.0386, -0.0832, -0.0477, 0.1943], grad_fn=<SliceBackward>) (...)
|
||||
# I tensor([ 0.0559, 0.1541, -0.4832, 0.0880, 0.0120], grad_fn=<SliceBackward>) (...)
|
||||
# said tensor([-0.1565, -0.0069, -0.8915, 0.0501, -0.0647], grad_fn=<SliceBackward>) (...)
|
||||
# , tensor([-0.1318, -0.0387, -0.0834, -0.0477, 0.1944], grad_fn=<SliceBackward>) (...)
|
||||
# " tensor([-0.0486, 0.1818, -0.3946, -0.0553, 0.0981], grad_fn=<SliceBackward>) (...)
|
||||
# hello tensor([ 0.0079, 0.1799, -0.6204, -0.0777, -0.0923], grad_fn=<SliceBackward>) (...)
|
||||
# RoBERTa tensor([-0.2339, -0.1184, -0.7343, -0.0492, 0.5829], grad_fn=<SliceBackward>) (...)
|
||||
# . tensor([-0.1341, -0.1203, -0.1012, -0.0621, 0.1892], grad_fn=<SliceBackward>) (...)
|
||||
# " tensor([-0.1341, -0.1203, -0.1012, -0.0621, 0.1892], grad_fn=<SliceBackward>) (...)
|
||||
# </s> tensor([-0.0930, -0.0392, -0.0821, 0.0158, 0.0649], grad_fn=<SliceBackward>) (...)
|
||||
```
|
||||
|
||||
#### Evaluating the `roberta.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
|
||||
roberta.cuda()
|
||||
roberta.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 = roberta.encode(sent1, sent2)
|
||||
prediction = roberta.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.9060
|
||||
```
|
||||
|
||||
## Finetuning
|
||||
|
||||
- [Finetuning on GLUE](README.glue.md)
|
||||
- [Finetuning on custom classification tasks (e.g., IMDB)](README.custom_classification.md)
|
||||
- [Finetuning on Winograd Schema Challenge (WSC)](wsc/README.md)
|
||||
- [Finetuning on Commonsense QA (CQA)](commonsense_qa/README.md)
|
||||
|
||||
## Pretraining using your own data
|
||||
|
||||
See the [tutorial for pretraining RoBERTa using your own data](README.pretraining.md).
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
@article{liu2019roberta,
|
||||
title = {RoBERTa: A Robustly Optimized BERT Pretraining Approach},
|
||||
author = {Yinhan Liu and Myle Ott and Naman Goyal and Jingfei Du and
|
||||
Mandar Joshi and Danqi Chen and Omer Levy and Mike Lewis and
|
||||
Luke Zettlemoyer and Veselin Stoyanov},
|
||||
journal={arXiv preprint arXiv:1907.11692},
|
||||
year = {2019},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
# Pretraining RoBERTa using your own data
|
||||
|
||||
This tutorial will walk you through pretraining RoBERTa over your own data.
|
||||
|
||||
### 1) Preprocess the data
|
||||
|
||||
Data should be preprocessed following the [language modeling format](/examples/language_model), i.e. each document should be separated by an empty line (only useful with `--sample-break-mode complete_doc`). Lines will be concatenated as a 1D text stream during training.
|
||||
|
||||
We'll use the [WikiText-103 dataset](https://www.salesforce.com/products/einstein/ai-research/the-wikitext-dependency-language-modeling-dataset/)
|
||||
to demonstrate how to preprocess raw text data with the GPT-2 BPE. Of course
|
||||
this dataset is quite small, so the resulting pretrained model will perform
|
||||
poorly, but it gives the general idea.
|
||||
|
||||
First download the dataset:
|
||||
```bash
|
||||
wget https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-raw-v1.zip
|
||||
unzip wikitext-103-raw-v1.zip
|
||||
```
|
||||
|
||||
Next encode it with the GPT-2 BPE:
|
||||
```bash
|
||||
mkdir -p gpt2_bpe
|
||||
wget -O gpt2_bpe/encoder.json https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json
|
||||
wget -O gpt2_bpe/vocab.bpe https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe
|
||||
for SPLIT in train valid test; do \
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json gpt2_bpe/encoder.json \
|
||||
--vocab-bpe gpt2_bpe/vocab.bpe \
|
||||
--inputs wikitext-103-raw/wiki.${SPLIT}.raw \
|
||||
--outputs wikitext-103-raw/wiki.${SPLIT}.bpe \
|
||||
--keep-empty \
|
||||
--workers 60; \
|
||||
done
|
||||
```
|
||||
|
||||
Finally preprocess/binarize the data using the GPT-2 fairseq dictionary:
|
||||
```bash
|
||||
wget -O gpt2_bpe/dict.txt https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--srcdict gpt2_bpe/dict.txt \
|
||||
--trainpref wikitext-103-raw/wiki.train.bpe \
|
||||
--validpref wikitext-103-raw/wiki.valid.bpe \
|
||||
--testpref wikitext-103-raw/wiki.test.bpe \
|
||||
--destdir data-bin/wikitext-103 \
|
||||
--workers 60
|
||||
```
|
||||
|
||||
### 2) Train RoBERTa base
|
||||
```bash
|
||||
TOTAL_UPDATES=125000 # Total number of training steps
|
||||
WARMUP_UPDATES=10000 # Warmup the learning rate over this many updates
|
||||
PEAK_LR=0.0005 # Peak learning rate, adjust as needed
|
||||
TOKENS_PER_SAMPLE=512 # Max sequence length
|
||||
MAX_POSITIONS=512 # Num. positional embeddings (usually same as above)
|
||||
MAX_SENTENCES=16 # Number of sequences per batch (batch size)
|
||||
UPDATE_FREQ=16 # Increase the batch size 16x
|
||||
|
||||
DATA_DIR=data-bin/wikitext-103
|
||||
|
||||
fairseq-train --fp16 $DATA_DIR \
|
||||
--task masked_lm --criterion masked_lm \
|
||||
--arch roberta_base --sample-break-mode complete --tokens-per-sample $TOKENS_PER_SAMPLE \
|
||||
--optimizer adam --adam-betas '(0.9,0.98)' --adam-eps 1e-6 --clip-norm 0.0 \
|
||||
--lr-scheduler polynomial_decay --lr $PEAK_LR --warmup-updates $WARMUP_UPDATES --total-num-update $TOTAL_UPDATES \
|
||||
--dropout 0.1 --attention-dropout 0.1 --weight-decay 0.01 \
|
||||
--batch-size $MAX_SENTENCES --update-freq $UPDATE_FREQ \
|
||||
--max-update $TOTAL_UPDATES --log-format simple --log-interval 1
|
||||
```
|
||||
|
||||
**Note:** You can optionally resume training the released RoBERTa base model by
|
||||
adding `--restore-file /path/to/roberta.base/model.pt`.
|
||||
|
||||
**Note:** The above command assumes training on 8x32GB V100 GPUs. Each GPU uses
|
||||
a batch size of 16 sequences (`$MAX_SENTENCES`) and accumulates gradients to
|
||||
further increase the batch size by 16x (`$UPDATE_FREQ`), for a total batch size
|
||||
of 2048 sequences. If you have fewer GPUs or GPUs with less memory you may need
|
||||
to reduce `$MAX_SENTENCES` and increase `$UPDATE_FREQ` to compensate.
|
||||
Alternatively if you have more GPUs you can decrease `$UPDATE_FREQ` accordingly
|
||||
to increase training speed.
|
||||
|
||||
**Note:** The learning rate and batch size are tightly connected and need to be
|
||||
adjusted together. We generally recommend increasing the learning rate as you
|
||||
increase the batch size according to the following table (although it's also
|
||||
dataset dependent, so don't rely on the following values too closely):
|
||||
|
||||
batch size | peak learning rate
|
||||
---|---
|
||||
256 | 0.0001
|
||||
2048 | 0.0005
|
||||
8192 | 0.0007
|
||||
|
||||
### 3) Load your pretrained model
|
||||
```python
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
roberta = RobertaModel.from_pretrained('checkpoints', 'checkpoint_best.pt', 'path/to/data')
|
||||
assert isinstance(roberta.model, torch.nn.Module)
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# Finetuning RoBERTa on RACE tasks
|
||||
|
||||
### 1) Download the data from RACE website (http://www.cs.cmu.edu/~glai1/data/race/)
|
||||
|
||||
### 2) Preprocess RACE data:
|
||||
```bash
|
||||
python ./examples/roberta/preprocess_RACE.py --input-dir <input-dir> --output-dir <extracted-data-dir>
|
||||
./examples/roberta/preprocess_RACE.sh <extracted-data-dir> <output-dir>
|
||||
```
|
||||
|
||||
### 3) Fine-tuning on RACE:
|
||||
|
||||
```bash
|
||||
MAX_EPOCH=5 # Number of training epochs.
|
||||
LR=1e-05 # Peak LR for fixed LR scheduler.
|
||||
NUM_CLASSES=4
|
||||
MAX_SENTENCES=1 # Batch size per GPU.
|
||||
UPDATE_FREQ=8 # Accumulate gradients to simulate training on 8 GPUs.
|
||||
DATA_DIR=/path/to/race-output-dir
|
||||
ROBERTA_PATH=/path/to/roberta/model.pt
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1 fairseq-train $DATA_DIR --ddp-backend=no_c10d \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \
|
||||
--task sentence_ranking \
|
||||
--num-classes $NUM_CLASSES \
|
||||
--init-token 0 --separator-token 2 \
|
||||
--max-option-length 128 \
|
||||
--max-positions 512 \
|
||||
--shorten-method "truncate" \
|
||||
--arch roberta_large \
|
||||
--dropout 0.1 --attention-dropout 0.1 --weight-decay 0.01 \
|
||||
--criterion sentence_ranking \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --adam-eps 1e-06 \
|
||||
--clip-norm 0.0 \
|
||||
--lr-scheduler fixed --lr $LR \
|
||||
--fp16 --fp16-init-scale 4 --threshold-loss-scale 1 --fp16-scale-window 128 \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--required-batch-size-multiple 1 \
|
||||
--update-freq $UPDATE_FREQ \
|
||||
--max-epoch $MAX_EPOCH
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
||||
a) As contexts in RACE are relatively long, we are using smaller batch size per GPU while increasing update-freq to achieve larger effective batch size.
|
||||
|
||||
b) Above cmd-args and hyperparams are tested on one 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`.
|
||||
|
||||
c) The setting in above command is based on our hyperparam search within a fixed search space (for careful comparison across models). You might be able to find better metrics with wider hyperparam search.
|
||||
|
||||
### 4) Evaluation:
|
||||
|
||||
```
|
||||
DATA_DIR=/path/to/race-output-dir # data directory used during training
|
||||
MODEL_PATH=/path/to/checkpoint_best.pt # path to the finetuned model checkpoint
|
||||
PREDS_OUT=preds.tsv # output file path to save prediction
|
||||
TEST_SPLIT=test # can be test (Middle) or test1 (High)
|
||||
fairseq-validate \
|
||||
$DATA_DIR \
|
||||
--valid-subset $TEST_SPLIT \
|
||||
--path $MODEL_PATH \
|
||||
--batch-size 1 \
|
||||
--task sentence_ranking \
|
||||
--criterion sentence_ranking \
|
||||
--save-predictions $PREDS_OUT
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Finetuning RoBERTa on Commonsense QA
|
||||
|
||||
We follow a similar approach to [finetuning RACE](../README.race.md). Specifically
|
||||
for each question we construct five inputs, one for each of the five candidate
|
||||
answer choices. Each input is constructed by concatenating the question and
|
||||
candidate answer. We then encode each input and pass the resulting "[CLS]"
|
||||
representations through a fully-connected layer to predict the correct answer.
|
||||
We train with a standard cross-entropy loss.
|
||||
|
||||
We also found it helpful to prepend a prefix of `Q:` to the question and `A:` to
|
||||
the answer. The complete input format is:
|
||||
```
|
||||
<s> Q: Where would I not want a fox? </s> A: hen house </s>
|
||||
```
|
||||
|
||||
Our final submission is based on a hyperparameter search over the learning rate
|
||||
(1e-5, 2e-5, 3e-5), batch size (8, 16), number of training steps (2000, 3000,
|
||||
4000) and random seed. We selected the model with the best performance on the
|
||||
development set after 100 trials.
|
||||
|
||||
### 1) Download data from the Commonsense QA website (https://www.tau-nlp.org/commonsenseqa)
|
||||
```bash
|
||||
bash examples/roberta/commonsense_qa/download_cqa_data.sh
|
||||
```
|
||||
|
||||
### 2) Finetune
|
||||
|
||||
```bash
|
||||
MAX_UPDATES=3000 # Number of training steps.
|
||||
WARMUP_UPDATES=150 # Linearly increase LR over this many steps.
|
||||
LR=1e-05 # Peak LR for polynomial LR scheduler.
|
||||
MAX_SENTENCES=16 # Batch size.
|
||||
SEED=1 # Random seed.
|
||||
ROBERTA_PATH=/path/to/roberta/model.pt
|
||||
DATA_DIR=data/CommonsenseQA
|
||||
|
||||
# we use the --user-dir option to load the task from
|
||||
# the examples/roberta/commonsense_qa directory:
|
||||
FAIRSEQ_PATH=/path/to/fairseq
|
||||
FAIRSEQ_USER_DIR=${FAIRSEQ_PATH}/examples/roberta/commonsense_qa
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 fairseq-train --fp16 --ddp-backend=no_c10d \
|
||||
$DATA_DIR \
|
||||
--user-dir $FAIRSEQ_USER_DIR \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--no-epoch-checkpoints --no-last-checkpoints --no-save-optimizer-state \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \
|
||||
--task commonsense_qa --init-token 0 --bpe gpt2 \
|
||||
--arch roberta_large --max-positions 512 \
|
||||
--dropout 0.1 --attention-dropout 0.1 --weight-decay 0.01 \
|
||||
--criterion sentence_ranking --num-classes 5 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --adam-eps 1e-06 --clip-norm 0.0 \
|
||||
--lr-scheduler polynomial_decay --lr $LR \
|
||||
--warmup-updates $WARMUP_UPDATES --total-num-update $MAX_UPDATES \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-update $MAX_UPDATES \
|
||||
--log-format simple --log-interval 25 \
|
||||
--seed $SEED
|
||||
```
|
||||
|
||||
The above command assumes training on 1 GPU with 32GB of RAM. For GPUs with
|
||||
less memory, decrease `--batch-size` and increase `--update-freq`
|
||||
accordingly to compensate.
|
||||
|
||||
### 3) Evaluate
|
||||
```python
|
||||
import json
|
||||
import torch
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
from examples.roberta import commonsense_qa # load the Commonsense QA task
|
||||
roberta = RobertaModel.from_pretrained('checkpoints', 'checkpoint_best.pt', 'data/CommonsenseQA')
|
||||
roberta.eval() # disable dropout
|
||||
roberta.cuda() # use the GPU (optional)
|
||||
nsamples, ncorrect = 0, 0
|
||||
with open('data/CommonsenseQA/valid.jsonl') as h:
|
||||
for line in h:
|
||||
example = json.loads(line)
|
||||
scores = []
|
||||
for choice in example['question']['choices']:
|
||||
input = roberta.encode(
|
||||
'Q: ' + example['question']['stem'],
|
||||
'A: ' + choice['text'],
|
||||
no_separator=True
|
||||
)
|
||||
score = roberta.predict('sentence_classification_head', input, return_logits=True)
|
||||
scores.append(score)
|
||||
pred = torch.cat(scores).argmax()
|
||||
answer = ord(example['answerKey']) - ord('A')
|
||||
nsamples += 1
|
||||
if pred == answer:
|
||||
ncorrect += 1
|
||||
|
||||
print('Accuracy: ' + str(ncorrect / float(nsamples)))
|
||||
# Accuracy: 0.7846027846027847
|
||||
```
|
||||
|
||||
The above snippet is not batched, which makes it quite slow. See [instructions
|
||||
for batched prediction with RoBERTa](https://github.com/pytorch/fairseq/tree/master/examples/roberta#batched-prediction).
|
||||
@@ -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 commonsense_qa_task # noqa
|
||||
@@ -0,0 +1,190 @@
|
||||
# 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 json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from fairseq.data import (
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
ListDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
RawLabelDataset,
|
||||
RightPadDataset,
|
||||
SortDataset,
|
||||
data_utils,
|
||||
encoders,
|
||||
)
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
|
||||
@register_task("commonsense_qa")
|
||||
class CommonsenseQATask(LegacyFairseqTask):
|
||||
"""Task to finetune RoBERTa for Commonsense QA."""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data", metavar="DIR", help="path to data directory; we load <split>.jsonl"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-token",
|
||||
type=int,
|
||||
default=None,
|
||||
help="add token at the beginning of each batch item",
|
||||
)
|
||||
parser.add_argument("--num-classes", type=int, default=5)
|
||||
|
||||
def __init__(self, args, vocab):
|
||||
super().__init__(args)
|
||||
self.vocab = vocab
|
||||
self.mask = vocab.add_symbol("<mask>")
|
||||
|
||||
self.bpe = encoders.build_bpe(args)
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
"""Load the dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
dictionary = Dictionary.load(filename)
|
||||
dictionary.add_symbol("<mask>")
|
||||
return dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
assert (
|
||||
args.criterion == "sentence_ranking"
|
||||
), "Must set --criterion=sentence_ranking"
|
||||
|
||||
# load data and label dictionaries
|
||||
vocab = cls.load_dictionary(os.path.join(args.data, "dict.txt"))
|
||||
print("| dictionary: {} types".format(len(vocab)))
|
||||
|
||||
return cls(args, vocab)
|
||||
|
||||
def load_dataset(
|
||||
self, split, epoch=1, combine=False, data_path=None, return_only=False, **kwargs
|
||||
):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
|
||||
def binarize(s, append_bos=False):
|
||||
if self.bpe is not None:
|
||||
s = self.bpe.encode(s)
|
||||
tokens = self.vocab.encode_line(
|
||||
s,
|
||||
append_eos=True,
|
||||
add_if_not_exist=False,
|
||||
).long()
|
||||
if append_bos and self.args.init_token is not None:
|
||||
tokens = torch.cat([tokens.new([self.args.init_token]), tokens])
|
||||
return tokens
|
||||
|
||||
if data_path is None:
|
||||
data_path = os.path.join(self.args.data, split + ".jsonl")
|
||||
if not os.path.exists(data_path):
|
||||
raise FileNotFoundError("Cannot find data: {}".format(data_path))
|
||||
|
||||
src_tokens = [[] for i in range(self.args.num_classes)]
|
||||
src_lengths = [[] for i in range(self.args.num_classes)]
|
||||
labels = []
|
||||
|
||||
with open(data_path) as h:
|
||||
for line in h:
|
||||
example = json.loads(line.strip())
|
||||
if "answerKey" in example:
|
||||
label = ord(example["answerKey"]) - ord("A")
|
||||
labels.append(label)
|
||||
question = example["question"]["stem"]
|
||||
assert len(example["question"]["choices"]) == self.args.num_classes
|
||||
# format: `<s> Q: Where would I not want a fox? </s> A: hen house </s>`
|
||||
question = "Q: " + question
|
||||
question_toks = binarize(question, append_bos=True)
|
||||
for i, choice in enumerate(example["question"]["choices"]):
|
||||
src = "A: " + choice["text"]
|
||||
src_bin = torch.cat([question_toks, binarize(src)])
|
||||
src_tokens[i].append(src_bin)
|
||||
src_lengths[i].append(len(src_bin))
|
||||
assert all(
|
||||
len(src_tokens[0]) == len(src_tokens[i])
|
||||
for i in range(self.args.num_classes)
|
||||
)
|
||||
assert len(src_tokens[0]) == len(src_lengths[0])
|
||||
assert len(labels) == 0 or len(labels) == len(src_tokens[0])
|
||||
|
||||
for i in range(self.args.num_classes):
|
||||
src_lengths[i] = np.array(src_lengths[i])
|
||||
src_tokens[i] = ListDataset(src_tokens[i], src_lengths[i])
|
||||
src_lengths[i] = ListDataset(src_lengths[i])
|
||||
|
||||
dataset = {
|
||||
"id": IdDataset(),
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(src_tokens[0], reduce=True),
|
||||
}
|
||||
|
||||
for i in range(self.args.num_classes):
|
||||
dataset.update(
|
||||
{
|
||||
"net_input{}".format(i + 1): {
|
||||
"src_tokens": RightPadDataset(
|
||||
src_tokens[i],
|
||||
pad_idx=self.source_dictionary.pad(),
|
||||
),
|
||||
"src_lengths": src_lengths[i],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if len(labels) > 0:
|
||||
dataset.update({"target": RawLabelDataset(labels)})
|
||||
|
||||
dataset = NestedDictionaryDataset(
|
||||
dataset,
|
||||
sizes=[np.maximum.reduce([src_token.sizes for src_token in src_tokens])],
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
dataset = SortDataset(
|
||||
dataset,
|
||||
# shuffle
|
||||
sort_order=[np.random.permutation(len(dataset))],
|
||||
)
|
||||
|
||||
print("| Loaded {} with {} samples".format(split, len(dataset)))
|
||||
|
||||
self.datasets[split] = dataset
|
||||
return self.datasets[split]
|
||||
|
||||
def build_model(self, args):
|
||||
from fairseq import models
|
||||
|
||||
model = models.build_model(args, self)
|
||||
|
||||
model.register_classification_head(
|
||||
"sentence_classification_head",
|
||||
num_classes=1,
|
||||
)
|
||||
|
||||
return model
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.vocab
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.vocab
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/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.
|
||||
|
||||
OUTDIR=data/CommonsenseQA
|
||||
|
||||
mkdir -p $OUTDIR
|
||||
|
||||
wget -O $OUTDIR/train.jsonl https://s3.amazonaws.com/commensenseqa/train_rand_split.jsonl
|
||||
wget -O $OUTDIR/valid.jsonl https://s3.amazonaws.com/commensenseqa/dev_rand_split.jsonl
|
||||
wget -O $OUTDIR/test.jsonl https://s3.amazonaws.com/commensenseqa/test_rand_split_no_answers.jsonl
|
||||
wget -O $OUTDIR/dict.txt https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
# 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 argparse
|
||||
import contextlib
|
||||
import sys
|
||||
from collections import Counter
|
||||
from multiprocessing import Pool
|
||||
|
||||
from fairseq.data.encoders.gpt2_bpe import get_encoder
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Helper script to encode raw text with the GPT-2 BPE using multiple processes.
|
||||
|
||||
The encoder.json and vocab.bpe files can be obtained here:
|
||||
- https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/encoder.json
|
||||
- https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/vocab.bpe
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--encoder-json",
|
||||
help="path to encoder.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vocab-bpe",
|
||||
type=str,
|
||||
help="path to vocab.bpe",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inputs",
|
||||
nargs="+",
|
||||
default=["-"],
|
||||
help="input files to filter/encode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--outputs",
|
||||
nargs="+",
|
||||
default=["-"],
|
||||
help="path to save encoded outputs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-empty",
|
||||
action="store_true",
|
||||
help="keep empty lines",
|
||||
)
|
||||
parser.add_argument("--workers", type=int, default=20)
|
||||
args = parser.parse_args()
|
||||
|
||||
assert len(args.inputs) == len(
|
||||
args.outputs
|
||||
), "number of input and output paths should match"
|
||||
|
||||
with contextlib.ExitStack() as stack:
|
||||
inputs = [
|
||||
stack.enter_context(open(input, "r", encoding="utf-8"))
|
||||
if input != "-"
|
||||
else sys.stdin
|
||||
for input in args.inputs
|
||||
]
|
||||
outputs = [
|
||||
stack.enter_context(open(output, "w", encoding="utf-8"))
|
||||
if output != "-"
|
||||
else sys.stdout
|
||||
for output in args.outputs
|
||||
]
|
||||
|
||||
encoder = MultiprocessingEncoder(args)
|
||||
pool = Pool(args.workers, initializer=encoder.initializer)
|
||||
encoded_lines = pool.imap(encoder.encode_lines, zip(*inputs), 100)
|
||||
|
||||
stats = Counter()
|
||||
for i, (filt, enc_lines) in enumerate(encoded_lines, start=1):
|
||||
if filt == "PASS":
|
||||
for enc_line, output_h in zip(enc_lines, outputs):
|
||||
print(enc_line, file=output_h)
|
||||
else:
|
||||
stats["num_filtered_" + filt] += 1
|
||||
if i % 10000 == 0:
|
||||
print("processed {} lines".format(i), file=sys.stderr)
|
||||
|
||||
for k, v in stats.most_common():
|
||||
print("[{}] filtered {} lines".format(k, v), file=sys.stderr)
|
||||
|
||||
|
||||
class MultiprocessingEncoder(object):
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
def initializer(self):
|
||||
global bpe
|
||||
bpe = get_encoder(self.args.encoder_json, self.args.vocab_bpe)
|
||||
|
||||
def encode(self, line):
|
||||
global bpe
|
||||
ids = bpe.encode(line)
|
||||
return list(map(str, ids))
|
||||
|
||||
def decode(self, tokens):
|
||||
global bpe
|
||||
return bpe.decode(tokens)
|
||||
|
||||
def encode_lines(self, lines):
|
||||
"""
|
||||
Encode a set of lines. All lines will be encoded together.
|
||||
"""
|
||||
enc_lines = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if len(line) == 0 and not self.args.keep_empty:
|
||||
return ["EMPTY", None]
|
||||
tokens = self.encode(line)
|
||||
enc_lines.append(" ".join(tokens))
|
||||
return ["PASS", enc_lines]
|
||||
|
||||
def decode_lines(self, lines):
|
||||
dec_lines = []
|
||||
for line in lines:
|
||||
tokens = map(int, line.strip().split())
|
||||
dec_lines.append(self.decode(tokens))
|
||||
return ["PASS", dec_lines]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/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.
|
||||
|
||||
|
||||
# raw glue data as downloaded by glue download script (https://gist.github.com/W4ngatang/60c2bdb54d156a41194446737ce03e2e)
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Run as following:"
|
||||
echo "./examples/roberta/preprocess_GLUE_tasks.sh <glud_data_folder> <task_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GLUE_DATA_FOLDER=$1
|
||||
|
||||
# download bpe encoder.json, vocabulary and fairseq dictionary
|
||||
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'
|
||||
|
||||
TASKS=$2 # QQP
|
||||
|
||||
if [ "$TASKS" = "ALL" ]
|
||||
then
|
||||
TASKS="QQP MNLI QNLI MRPC RTE STS-B SST-2 CoLA"
|
||||
fi
|
||||
|
||||
for TASK in $TASKS
|
||||
do
|
||||
echo "Preprocessing $TASK"
|
||||
|
||||
TASK_DATA_FOLDER="$GLUE_DATA_FOLDER/$TASK"
|
||||
echo "Raw data as downloaded from glue website: $TASK_DATA_FOLDER"
|
||||
|
||||
SPLITS="train dev test"
|
||||
INPUT_COUNT=2
|
||||
if [ "$TASK" = "QQP" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 4 5 )
|
||||
TEST_INPUT_COLUMNS=( 2 3 )
|
||||
LABEL_COLUMN=6
|
||||
elif [ "$TASK" = "MNLI" ]
|
||||
then
|
||||
SPLITS="train dev_matched dev_mismatched test_matched test_mismatched"
|
||||
INPUT_COLUMNS=( 9 10 )
|
||||
TEST_INPUT_COLUMNS=( 9 10 )
|
||||
DEV_LABEL_COLUMN=16
|
||||
LABEL_COLUMN=12
|
||||
elif [ "$TASK" = "QNLI" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 2 3 )
|
||||
TEST_INPUT_COLUMNS=( 2 3 )
|
||||
LABEL_COLUMN=4
|
||||
elif [ "$TASK" = "MRPC" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 4 5 )
|
||||
TEST_INPUT_COLUMNS=( 4 5 )
|
||||
LABEL_COLUMN=1
|
||||
elif [ "$TASK" = "RTE" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 2 3 )
|
||||
TEST_INPUT_COLUMNS=( 2 3 )
|
||||
LABEL_COLUMN=4
|
||||
elif [ "$TASK" = "STS-B" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 8 9 )
|
||||
TEST_INPUT_COLUMNS=( 8 9 )
|
||||
LABEL_COLUMN=10
|
||||
# Following are single sentence tasks.
|
||||
elif [ "$TASK" = "SST-2" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 1 )
|
||||
TEST_INPUT_COLUMNS=( 2 )
|
||||
LABEL_COLUMN=2
|
||||
INPUT_COUNT=1
|
||||
elif [ "$TASK" = "CoLA" ]
|
||||
then
|
||||
INPUT_COLUMNS=( 4 )
|
||||
TEST_INPUT_COLUMNS=( 2 )
|
||||
LABEL_COLUMN=2
|
||||
INPUT_COUNT=1
|
||||
fi
|
||||
|
||||
# Strip out header and filter lines that don't have expected number of fields.
|
||||
rm -rf "$TASK_DATA_FOLDER/processed"
|
||||
mkdir -p "$TASK_DATA_FOLDER/processed"
|
||||
for SPLIT in $SPLITS
|
||||
do
|
||||
# CoLA train and dev doesn't have header.
|
||||
if [[ ( "$TASK" = "CoLA") && ( "$SPLIT" != "test" ) ]]
|
||||
then
|
||||
cp "$TASK_DATA_FOLDER/$SPLIT.tsv" "$TASK_DATA_FOLDER/processed/$SPLIT.tsv.temp";
|
||||
else
|
||||
tail -n +2 "$TASK_DATA_FOLDER/$SPLIT.tsv" > "$TASK_DATA_FOLDER/processed/$SPLIT.tsv.temp";
|
||||
fi
|
||||
|
||||
# Remove unformatted lines from train and dev files for QQP dataset.
|
||||
if [[ ( "$TASK" = "QQP") && ( "$SPLIT" != "test" ) ]]
|
||||
then
|
||||
awk -F '\t' -v NUM_FIELDS=6 'NF==NUM_FIELDS{print}{}' "$TASK_DATA_FOLDER/processed/$SPLIT.tsv.temp" > "$TASK_DATA_FOLDER/processed/$SPLIT.tsv";
|
||||
else
|
||||
cp "$TASK_DATA_FOLDER/processed/$SPLIT.tsv.temp" "$TASK_DATA_FOLDER/processed/$SPLIT.tsv";
|
||||
fi
|
||||
rm "$TASK_DATA_FOLDER/processed/$SPLIT.tsv.temp";
|
||||
done
|
||||
|
||||
# Split into input0, input1 and label
|
||||
for SPLIT in $SPLITS
|
||||
do
|
||||
for INPUT_TYPE in $(seq 0 $((INPUT_COUNT-1)))
|
||||
do
|
||||
if [[ "$SPLIT" != test* ]]
|
||||
then
|
||||
COLUMN_NUMBER=${INPUT_COLUMNS[$INPUT_TYPE]}
|
||||
else
|
||||
COLUMN_NUMBER=${TEST_INPUT_COLUMNS[$INPUT_TYPE]}
|
||||
fi
|
||||
cut -f"$COLUMN_NUMBER" "$TASK_DATA_FOLDER/processed/$SPLIT.tsv" > "$TASK_DATA_FOLDER/processed/$SPLIT.raw.input$INPUT_TYPE";
|
||||
done
|
||||
|
||||
if [[ "$SPLIT" != test* ]]
|
||||
then
|
||||
if [ "$TASK" = "MNLI" ] && [ "$SPLIT" != "train" ]
|
||||
then
|
||||
cut -f"$DEV_LABEL_COLUMN" "$TASK_DATA_FOLDER/processed/$SPLIT.tsv" > "$TASK_DATA_FOLDER/processed/$SPLIT.label";
|
||||
else
|
||||
cut -f"$LABEL_COLUMN" "$TASK_DATA_FOLDER/processed/$SPLIT.tsv" > "$TASK_DATA_FOLDER/processed/$SPLIT.label";
|
||||
fi
|
||||
fi
|
||||
|
||||
# BPE encode.
|
||||
for INPUT_TYPE in $(seq 0 $((INPUT_COUNT-1)))
|
||||
do
|
||||
LANG="input$INPUT_TYPE"
|
||||
echo "BPE encoding $SPLIT/$LANG"
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json encoder.json \
|
||||
--vocab-bpe vocab.bpe \
|
||||
--inputs "$TASK_DATA_FOLDER/processed/$SPLIT.raw.$LANG" \
|
||||
--outputs "$TASK_DATA_FOLDER/processed/$SPLIT.$LANG" \
|
||||
--workers 60 \
|
||||
--keep-empty;
|
||||
done
|
||||
done
|
||||
|
||||
# Remove output directory.
|
||||
rm -rf "$TASK-bin"
|
||||
|
||||
DEVPREF="$TASK_DATA_FOLDER/processed/dev.LANG"
|
||||
TESTPREF="$TASK_DATA_FOLDER/processed/test.LANG"
|
||||
if [ "$TASK" = "MNLI" ]
|
||||
then
|
||||
DEVPREF="$TASK_DATA_FOLDER/processed/dev_matched.LANG,$TASK_DATA_FOLDER/processed/dev_mismatched.LANG"
|
||||
TESTPREF="$TASK_DATA_FOLDER/processed/test_matched.LANG,$TASK_DATA_FOLDER/processed/test_mismatched.LANG"
|
||||
fi
|
||||
|
||||
# Run fairseq preprocessing:
|
||||
for INPUT_TYPE in $(seq 0 $((INPUT_COUNT-1)))
|
||||
do
|
||||
LANG="input$INPUT_TYPE"
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref "$TASK_DATA_FOLDER/processed/train.$LANG" \
|
||||
--validpref "${DEVPREF//LANG/$LANG}" \
|
||||
--testpref "${TESTPREF//LANG/$LANG}" \
|
||||
--destdir "$TASK-bin/$LANG" \
|
||||
--workers 60 \
|
||||
--srcdict dict.txt;
|
||||
done
|
||||
if [[ "$TASK" != "STS-B" ]]
|
||||
then
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref "$TASK_DATA_FOLDER/processed/train.label" \
|
||||
--validpref "${DEVPREF//LANG/label}" \
|
||||
--destdir "$TASK-bin/label" \
|
||||
--workers 60;
|
||||
else
|
||||
# For STS-B output range is converted to be between: [0.0, 1.0]
|
||||
mkdir -p "$TASK-bin/label"
|
||||
awk '{print $1 / 5.0 }' "$TASK_DATA_FOLDER/processed/train.label" > "$TASK-bin/label/train.label"
|
||||
awk '{print $1 / 5.0 }' "$TASK_DATA_FOLDER/processed/dev.label" > "$TASK-bin/label/valid.label"
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python
|
||||
# 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 argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class InputExample:
|
||||
def __init__(self, paragraph, qa_list, label):
|
||||
self.paragraph = paragraph
|
||||
self.qa_list = qa_list
|
||||
self.label = label
|
||||
|
||||
|
||||
def get_examples(data_dir, set_type):
|
||||
"""
|
||||
Extract paragraph and question-answer list from each json file
|
||||
"""
|
||||
examples = []
|
||||
|
||||
levels = ["middle", "high"]
|
||||
set_type_c = set_type.split("-")
|
||||
if len(set_type_c) == 2:
|
||||
levels = [set_type_c[1]]
|
||||
set_type = set_type_c[0]
|
||||
for level in levels:
|
||||
cur_dir = os.path.join(data_dir, set_type, level)
|
||||
for filename in os.listdir(cur_dir):
|
||||
cur_path = os.path.join(cur_dir, filename)
|
||||
with open(cur_path, "r") as f:
|
||||
cur_data = json.load(f)
|
||||
answers = cur_data["answers"]
|
||||
options = cur_data["options"]
|
||||
questions = cur_data["questions"]
|
||||
context = cur_data["article"].replace("\n", " ")
|
||||
context = re.sub(r"\s+", " ", context)
|
||||
for i in range(len(answers)):
|
||||
label = ord(answers[i]) - ord("A")
|
||||
qa_list = []
|
||||
question = questions[i]
|
||||
for j in range(4):
|
||||
option = options[i][j]
|
||||
if "_" in question:
|
||||
qa_cat = question.replace("_", option)
|
||||
else:
|
||||
qa_cat = " ".join([question, option])
|
||||
qa_cat = re.sub(r"\s+", " ", qa_cat)
|
||||
qa_list.append(qa_cat)
|
||||
examples.append(InputExample(context, qa_list, label))
|
||||
|
||||
return examples
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Helper script to extract paragraphs questions and answers from RACE datasets.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--input-dir",
|
||||
help="input directory for downloaded RACE dataset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
help="output directory for extracted data",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.output_dir):
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
|
||||
for set_type in ["train", "dev", "test-middle", "test-high"]:
|
||||
examples = get_examples(args.input_dir, set_type)
|
||||
qa_file_paths = [
|
||||
os.path.join(args.output_dir, set_type + ".input" + str(i + 1))
|
||||
for i in range(4)
|
||||
]
|
||||
qa_files = [open(qa_file_path, "w") for qa_file_path in qa_file_paths]
|
||||
outf_context_path = os.path.join(args.output_dir, set_type + ".input0")
|
||||
outf_label_path = os.path.join(args.output_dir, set_type + ".label")
|
||||
outf_context = open(outf_context_path, "w")
|
||||
outf_label = open(outf_label_path, "w")
|
||||
for example in examples:
|
||||
outf_context.write(example.paragraph + "\n")
|
||||
for i in range(4):
|
||||
qa_files[i].write(example.qa_list[i] + "\n")
|
||||
outf_label.write(str(example.label) + "\n")
|
||||
|
||||
for f in qa_files:
|
||||
f.close()
|
||||
outf_label.close()
|
||||
outf_context.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/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.
|
||||
|
||||
|
||||
# data should be downloaded and processed with reprocess_RACE.py
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Run as following:"
|
||||
echo "./examples/roberta/preprocess_RACE.sh <race_data_folder> <output_folder>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RACE_DATA_FOLDER=$1
|
||||
OUT_DATA_FOLDER=$2
|
||||
|
||||
# download bpe encoder.json, vocabulary and fairseq dictionary
|
||||
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'
|
||||
|
||||
SPLITS="train dev test-middle test-high"
|
||||
INPUT_TYPES="input0 input1 input2 input3 input4"
|
||||
for INPUT_TYPE in $INPUT_TYPES
|
||||
do
|
||||
for SPLIT in $SPLITS
|
||||
do
|
||||
echo "BPE encoding $SPLIT/$INPUT_TYPE"
|
||||
python -m examples.roberta.multiprocessing_bpe_encoder \
|
||||
--encoder-json encoder.json \
|
||||
--vocab-bpe vocab.bpe \
|
||||
--inputs "$RACE_DATA_FOLDER/$SPLIT.$INPUT_TYPE" \
|
||||
--outputs "$RACE_DATA_FOLDER/$SPLIT.$INPUT_TYPE.bpe" \
|
||||
--workers 10 \
|
||||
--keep-empty;
|
||||
|
||||
done
|
||||
done
|
||||
|
||||
for INPUT_TYPE in $INPUT_TYPES
|
||||
do
|
||||
LANG="input$INPUT_TYPE"
|
||||
fairseq-preprocess \
|
||||
--only-source \
|
||||
--trainpref "$RACE_DATA_FOLDER/train.$INPUT_TYPE.bpe" \
|
||||
--validpref "$RACE_DATA_FOLDER/dev.$INPUT_TYPE.bpe" \
|
||||
--testpref "$RACE_DATA_FOLDER/test-middle.$INPUT_TYPE.bpe,$RACE_DATA_FOLDER/test-high.$INPUT_TYPE.bpe" \
|
||||
--destdir "$OUT_DATA_FOLDER/$INPUT_TYPE" \
|
||||
--workers 10 \
|
||||
--srcdict dict.txt;
|
||||
done
|
||||
|
||||
rm -rf "$OUT_DATA_FOLDER/label"
|
||||
mkdir -p "$OUT_DATA_FOLDER/label"
|
||||
cp "$RACE_DATA_FOLDER/train.label" "$OUT_DATA_FOLDER/label/"
|
||||
cp "$RACE_DATA_FOLDER/dev.label" "$OUT_DATA_FOLDER/label/valid.label"
|
||||
cp "$RACE_DATA_FOLDER/test-middle.label" "$OUT_DATA_FOLDER/label/test.label"
|
||||
cp "$RACE_DATA_FOLDER/test-high.label" "$OUT_DATA_FOLDER/label/test1.label"
|
||||
@@ -0,0 +1,125 @@
|
||||
# Finetuning RoBERTa on Winograd Schema Challenge (WSC) data
|
||||
|
||||
The following instructions can be used to finetune RoBERTa on the WSC training
|
||||
data provided by [SuperGLUE](https://super.gluebenchmark.com/).
|
||||
|
||||
Note that there is high variance in the results. For our GLUE/SuperGLUE
|
||||
submission we swept over the learning rate (1e-5, 2e-5, 3e-5), batch size (16,
|
||||
32, 64) and total number of updates (500, 1000, 2000, 3000), as well as the
|
||||
random seed. Out of ~100 runs we chose the best 7 models and ensembled them.
|
||||
|
||||
**Approach:** The instructions below use a slightly different loss function than
|
||||
what's described in the original RoBERTa arXiv paper. In particular,
|
||||
[Kocijan et al. (2019)](https://arxiv.org/abs/1905.06290) introduce a margin
|
||||
ranking loss between `(query, candidate)` pairs with tunable hyperparameters
|
||||
alpha and beta. This is supported in our code as well with the `--wsc-alpha` and
|
||||
`--wsc-beta` arguments. However, we achieved slightly better (and more robust)
|
||||
results on the development set by instead using a single cross entropy loss term
|
||||
over the log-probabilities for the query and all mined candidates. **The
|
||||
candidates are mined using spaCy from each input sentence in isolation, so the
|
||||
approach remains strictly pointwise.** This reduces the number of
|
||||
hyperparameters and our best model achieved 92.3% development set accuracy,
|
||||
compared to ~90% accuracy for the margin loss. Later versions of the RoBERTa
|
||||
arXiv paper will describe this updated formulation.
|
||||
|
||||
### 1) Download the WSC data from the SuperGLUE website:
|
||||
```bash
|
||||
wget https://dl.fbaipublicfiles.com/glue/superglue/data/v2/WSC.zip
|
||||
unzip WSC.zip
|
||||
|
||||
# we also need to copy the RoBERTa dictionary into the same directory
|
||||
wget -O WSC/dict.txt https://dl.fbaipublicfiles.com/fairseq/gpt2_bpe/dict.txt
|
||||
```
|
||||
|
||||
### 2) Finetune over the provided training data:
|
||||
```bash
|
||||
TOTAL_NUM_UPDATES=2000 # Total number of training steps.
|
||||
WARMUP_UPDATES=250 # Linearly increase LR over this many steps.
|
||||
LR=2e-05 # Peak LR for polynomial LR scheduler.
|
||||
MAX_SENTENCES=16 # Batch size per GPU.
|
||||
SEED=1 # Random seed.
|
||||
ROBERTA_PATH=/path/to/roberta/model.pt
|
||||
|
||||
# we use the --user-dir option to load the task and criterion
|
||||
# from the examples/roberta/wsc directory:
|
||||
FAIRSEQ_PATH=/path/to/fairseq
|
||||
FAIRSEQ_USER_DIR=${FAIRSEQ_PATH}/examples/roberta/wsc
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 fairseq-train WSC/ \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--no-epoch-checkpoints --no-last-checkpoints --no-save-optimizer-state \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \
|
||||
--valid-subset val \
|
||||
--fp16 --ddp-backend no_c10d \
|
||||
--user-dir $FAIRSEQ_USER_DIR \
|
||||
--task wsc --criterion wsc --wsc-cross-entropy \
|
||||
--arch roberta_large --bpe gpt2 --max-positions 512 \
|
||||
--dropout 0.1 --attention-dropout 0.1 --weight-decay 0.01 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --adam-eps 1e-06 \
|
||||
--lr-scheduler polynomial_decay --lr $LR \
|
||||
--warmup-updates $WARMUP_UPDATES --total-num-update $TOTAL_NUM_UPDATES \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-update $TOTAL_NUM_UPDATES \
|
||||
--log-format simple --log-interval 100 \
|
||||
--seed $SEED
|
||||
```
|
||||
|
||||
The above command assumes training on 4 GPUs, but you can achieve the same
|
||||
results on a single GPU by adding `--update-freq=4`.
|
||||
|
||||
### 3) Evaluate
|
||||
```python
|
||||
from fairseq.models.roberta import RobertaModel
|
||||
from examples.roberta.wsc import wsc_utils # also loads WSC task and criterion
|
||||
roberta = RobertaModel.from_pretrained('checkpoints', 'checkpoint_best.pt', 'WSC/')
|
||||
roberta.cuda()
|
||||
nsamples, ncorrect = 0, 0
|
||||
for sentence, label in wsc_utils.jsonl_iterator('WSC/val.jsonl', eval=True):
|
||||
pred = roberta.disambiguate_pronoun(sentence)
|
||||
nsamples += 1
|
||||
if pred == label:
|
||||
ncorrect += 1
|
||||
print('Accuracy: ' + str(ncorrect / float(nsamples)))
|
||||
# Accuracy: 0.9230769230769231
|
||||
```
|
||||
|
||||
## RoBERTa training on WinoGrande dataset
|
||||
We have also provided `winogrande` task and criterion for finetuning on the
|
||||
[WinoGrande](https://mosaic.allenai.org/projects/winogrande) like datasets
|
||||
where there are always two candidates and one is correct.
|
||||
It's more efficient implementation for such subcases.
|
||||
|
||||
```bash
|
||||
TOTAL_NUM_UPDATES=23750 # Total number of training steps.
|
||||
WARMUP_UPDATES=2375 # Linearly increase LR over this many steps.
|
||||
LR=1e-05 # Peak LR for polynomial LR scheduler.
|
||||
MAX_SENTENCES=32 # Batch size per GPU.
|
||||
SEED=1 # Random seed.
|
||||
ROBERTA_PATH=/path/to/roberta/model.pt
|
||||
|
||||
# we use the --user-dir option to load the task and criterion
|
||||
# from the examples/roberta/wsc directory:
|
||||
FAIRSEQ_PATH=/path/to/fairseq
|
||||
FAIRSEQ_USER_DIR=${FAIRSEQ_PATH}/examples/roberta/wsc
|
||||
|
||||
cd fairseq
|
||||
CUDA_VISIBLE_DEVICES=0 fairseq-train winogrande_1.0/ \
|
||||
--restore-file $ROBERTA_PATH \
|
||||
--reset-optimizer --reset-dataloader --reset-meters \
|
||||
--no-epoch-checkpoints --no-last-checkpoints --no-save-optimizer-state \
|
||||
--best-checkpoint-metric accuracy --maximize-best-checkpoint-metric \
|
||||
--valid-subset val \
|
||||
--fp16 --ddp-backend no_c10d \
|
||||
--user-dir $FAIRSEQ_USER_DIR \
|
||||
--task winogrande --criterion winogrande \
|
||||
--wsc-margin-alpha 5.0 --wsc-margin-beta 0.4 \
|
||||
--arch roberta_large --bpe gpt2 --max-positions 512 \
|
||||
--dropout 0.1 --attention-dropout 0.1 --weight-decay 0.01 \
|
||||
--optimizer adam --adam-betas '(0.9, 0.98)' --adam-eps 1e-06 \
|
||||
--lr-scheduler polynomial_decay --lr $LR \
|
||||
--warmup-updates $WARMUP_UPDATES --total-num-update $TOTAL_NUM_UPDATES \
|
||||
--batch-size $MAX_SENTENCES \
|
||||
--max-update $TOTAL_NUM_UPDATES \
|
||||
--log-format simple --log-interval 100
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
from . import wsc_criterion # noqa
|
||||
from . import wsc_task # noqa
|
||||
@@ -0,0 +1,167 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import utils
|
||||
from fairseq.criterions import LegacyFairseqCriterion, register_criterion
|
||||
from fairseq.data import encoders
|
||||
|
||||
|
||||
@register_criterion("wsc")
|
||||
class WSCCriterion(LegacyFairseqCriterion):
|
||||
def __init__(self, args, task):
|
||||
super().__init__(args, task)
|
||||
if self.args.save_predictions is not None:
|
||||
self.prediction_h = open(self.args.save_predictions, "w")
|
||||
else:
|
||||
self.prediction_h = None
|
||||
self.bpe = encoders.build_bpe(args.bpe)
|
||||
self.tokenizer = encoders.build_tokenizer(args.tokenizer)
|
||||
|
||||
def __del__(self):
|
||||
if self.prediction_h is not None:
|
||||
self.prediction_h.close()
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add criterion-specific arguments to the parser."""
|
||||
parser.add_argument("--wsc-margin-alpha", type=float, metavar="A", default=1.0)
|
||||
parser.add_argument("--wsc-margin-beta", type=float, metavar="B", default=0.0)
|
||||
parser.add_argument(
|
||||
"--wsc-cross-entropy",
|
||||
action="store_true",
|
||||
help="use cross entropy formulation instead of margin loss",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--save-predictions", metavar="FILE", help="file to save predictions to"
|
||||
)
|
||||
|
||||
def get_masked_input(self, tokens, mask):
|
||||
masked_tokens = tokens.clone()
|
||||
masked_tokens[mask] = self.task.mask
|
||||
return masked_tokens
|
||||
|
||||
def get_lprobs(self, model, tokens, mask):
|
||||
logits, _ = model(src_tokens=self.get_masked_input(tokens, mask))
|
||||
lprobs = F.log_softmax(logits, dim=-1, dtype=torch.float)
|
||||
scores = lprobs.gather(2, tokens.unsqueeze(-1)).squeeze(-1)
|
||||
mask = mask.type_as(scores)
|
||||
scores = (scores * mask).sum(dim=-1) / mask.sum(dim=-1)
|
||||
return scores
|
||||
|
||||
def get_loss(self, query_lprobs, cand_lprobs):
|
||||
if self.args.wsc_cross_entropy:
|
||||
return F.cross_entropy(
|
||||
torch.cat([query_lprobs, cand_lprobs]).unsqueeze(0),
|
||||
query_lprobs.new([0]).long(),
|
||||
)
|
||||
else:
|
||||
return (
|
||||
-query_lprobs
|
||||
+ self.args.wsc_margin_alpha
|
||||
* (cand_lprobs - query_lprobs + self.args.wsc_margin_beta).clamp(min=0)
|
||||
).sum()
|
||||
|
||||
def forward(self, model, sample, reduce=True):
|
||||
# compute loss and accuracy
|
||||
loss, nloss = 0.0, 0
|
||||
ncorrect, nqueries = 0, 0
|
||||
|
||||
for i, label in enumerate(sample["labels"]):
|
||||
query_lprobs = self.get_lprobs(
|
||||
model,
|
||||
sample["query_tokens"][i].unsqueeze(0),
|
||||
sample["query_masks"][i].unsqueeze(0),
|
||||
)
|
||||
cand_lprobs = self.get_lprobs(
|
||||
model,
|
||||
sample["candidate_tokens"][i],
|
||||
sample["candidate_masks"][i],
|
||||
)
|
||||
|
||||
pred = (query_lprobs >= cand_lprobs).all().item()
|
||||
|
||||
if label is not None:
|
||||
label = 1 if label else 0
|
||||
ncorrect += 1 if pred == label else 0
|
||||
nqueries += 1
|
||||
|
||||
if label:
|
||||
# only compute a loss for positive instances
|
||||
nloss += 1
|
||||
loss += self.get_loss(query_lprobs, cand_lprobs)
|
||||
|
||||
id = sample["id"][i].item()
|
||||
if self.prediction_h is not None:
|
||||
print("{}\t{}\t{}".format(id, pred, label), file=self.prediction_h)
|
||||
|
||||
if nloss == 0:
|
||||
loss = torch.tensor(0.0, requires_grad=True)
|
||||
|
||||
sample_size = nqueries if nqueries > 0 else 1
|
||||
logging_output = {
|
||||
"loss": utils.item(loss.data) if reduce else loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["nsentences"],
|
||||
"sample_size": sample_size,
|
||||
"ncorrect": ncorrect,
|
||||
"nqueries": nqueries,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
|
||||
@staticmethod
|
||||
def aggregate_logging_outputs(logging_outputs):
|
||||
"""Aggregate logging outputs from data parallel training."""
|
||||
loss_sum = sum(log.get("loss", 0) for log in logging_outputs)
|
||||
ntokens = sum(log.get("ntokens", 0) for log in logging_outputs)
|
||||
nsentences = sum(log.get("nsentences", 0) for log in logging_outputs)
|
||||
sample_size = sum(log.get("sample_size", 0) for log in logging_outputs)
|
||||
|
||||
agg_output = {
|
||||
"loss": loss_sum / sample_size / math.log(2),
|
||||
"ntokens": ntokens,
|
||||
"nsentences": nsentences,
|
||||
"sample_size": sample_size,
|
||||
}
|
||||
|
||||
ncorrect = sum(log.get("ncorrect", 0) for log in logging_outputs)
|
||||
nqueries = sum(log.get("nqueries", 0) for log in logging_outputs)
|
||||
if nqueries > 0:
|
||||
agg_output["accuracy"] = ncorrect / float(nqueries)
|
||||
|
||||
return agg_output
|
||||
|
||||
|
||||
@register_criterion("winogrande")
|
||||
class WinograndeCriterion(WSCCriterion):
|
||||
def forward(self, model, sample, reduce=True):
|
||||
# compute loss and accuracy
|
||||
query_lprobs = self.get_lprobs(
|
||||
model,
|
||||
sample["query_tokens"],
|
||||
sample["query_masks"],
|
||||
)
|
||||
cand_lprobs = self.get_lprobs(
|
||||
model,
|
||||
sample["candidate_tokens"],
|
||||
sample["candidate_masks"],
|
||||
)
|
||||
pred = query_lprobs >= cand_lprobs
|
||||
loss = self.get_loss(query_lprobs, cand_lprobs)
|
||||
|
||||
sample_size = sample["query_tokens"].size(0)
|
||||
ncorrect = pred.sum().item()
|
||||
logging_output = {
|
||||
"loss": utils.item(loss.data) if reduce else loss.data,
|
||||
"ntokens": sample["ntokens"],
|
||||
"nsentences": sample["nsentences"],
|
||||
"sample_size": sample_size,
|
||||
"ncorrect": ncorrect,
|
||||
"nqueries": sample_size,
|
||||
}
|
||||
return loss, sample_size, logging_output
|
||||
@@ -0,0 +1,401 @@
|
||||
# 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 json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from fairseq import utils
|
||||
from fairseq.data import (
|
||||
Dictionary,
|
||||
IdDataset,
|
||||
ListDataset,
|
||||
NestedDictionaryDataset,
|
||||
NumelDataset,
|
||||
NumSamplesDataset,
|
||||
PadDataset,
|
||||
SortDataset,
|
||||
data_utils,
|
||||
encoders,
|
||||
)
|
||||
from fairseq.tasks import LegacyFairseqTask, register_task
|
||||
|
||||
from . import wsc_utils
|
||||
|
||||
|
||||
@register_task("wsc")
|
||||
class WSCTask(LegacyFairseqTask):
|
||||
"""Task to finetune RoBERTa for Winograd Schemas."""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add task-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"data", metavar="DIR", help="path to data directory; we load <split>.jsonl"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--init-token",
|
||||
type=int,
|
||||
default=None,
|
||||
help="add token at the beginning of each batch item",
|
||||
)
|
||||
|
||||
def __init__(self, args, vocab):
|
||||
super().__init__(args)
|
||||
self.vocab = vocab
|
||||
self.mask = vocab.add_symbol("<mask>")
|
||||
|
||||
self.bpe = encoders.build_bpe(args)
|
||||
self.tokenizer = encoders.build_tokenizer(args)
|
||||
|
||||
# hack to handle GPT-2 BPE, which includes leading spaces
|
||||
if args.bpe == "gpt2":
|
||||
self.leading_space = True
|
||||
self.trailing_space = False
|
||||
else:
|
||||
self.leading_space = False
|
||||
self.trailing_space = True
|
||||
|
||||
@classmethod
|
||||
def load_dictionary(cls, filename):
|
||||
"""Load the dictionary from the filename
|
||||
|
||||
Args:
|
||||
filename (str): the filename
|
||||
"""
|
||||
dictionary = Dictionary.load(filename)
|
||||
dictionary.add_symbol("<mask>")
|
||||
return dictionary
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
assert args.criterion == "wsc", "Must set --criterion=wsc"
|
||||
|
||||
# load data and label dictionaries
|
||||
vocab = cls.load_dictionary(os.path.join(args.data, "dict.txt"))
|
||||
print("| dictionary: {} types".format(len(vocab)))
|
||||
|
||||
return cls(args, vocab)
|
||||
|
||||
def binarize(self, s: str, append_eos: bool = False):
|
||||
if self.tokenizer is not None:
|
||||
s = self.tokenizer.encode(s)
|
||||
if self.bpe is not None:
|
||||
s = self.bpe.encode(s)
|
||||
tokens = self.vocab.encode_line(
|
||||
s,
|
||||
append_eos=append_eos,
|
||||
add_if_not_exist=False,
|
||||
).long()
|
||||
if self.args.init_token is not None:
|
||||
tokens = torch.cat([tokens.new([self.args.init_token]), tokens])
|
||||
return tokens
|
||||
|
||||
def binarize_with_mask(self, txt, prefix, suffix, leading_space, trailing_space):
|
||||
toks = self.binarize(
|
||||
prefix + leading_space + txt + trailing_space + suffix,
|
||||
append_eos=True,
|
||||
)
|
||||
mask = torch.zeros_like(toks, dtype=torch.bool)
|
||||
mask_start = len(self.binarize(prefix))
|
||||
mask_size = len(self.binarize(leading_space + txt))
|
||||
mask[mask_start : mask_start + mask_size] = 1
|
||||
return toks, mask
|
||||
|
||||
def load_dataset(
|
||||
self, split, epoch=1, combine=False, data_path=None, return_only=False, **kwargs
|
||||
):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
if data_path is None:
|
||||
data_path = os.path.join(self.args.data, split + ".jsonl")
|
||||
if not os.path.exists(data_path):
|
||||
raise FileNotFoundError("Cannot find data: {}".format(data_path))
|
||||
|
||||
query_tokens = []
|
||||
query_masks = []
|
||||
query_lengths = []
|
||||
candidate_tokens = []
|
||||
candidate_masks = []
|
||||
candidate_lengths = []
|
||||
labels = []
|
||||
|
||||
for sentence, pronoun_span, query, label in wsc_utils.jsonl_iterator(data_path):
|
||||
prefix = sentence[: pronoun_span.start].text
|
||||
suffix = sentence[pronoun_span.end :].text_with_ws
|
||||
|
||||
# spaCy spans include trailing spaces, but we need to know about
|
||||
# leading spaces for the GPT-2 BPE
|
||||
leading_space = (
|
||||
" " if sentence[: pronoun_span.start].text_with_ws.endswith(" ") else ""
|
||||
)
|
||||
trailing_space = " " if pronoun_span.text_with_ws.endswith(" ") else ""
|
||||
|
||||
# get noun phrases, excluding pronouns and anything overlapping with the query
|
||||
cand_spans = wsc_utils.filter_noun_chunks(
|
||||
wsc_utils.extended_noun_chunks(sentence),
|
||||
exclude_pronouns=True,
|
||||
exclude_query=query,
|
||||
exact_match=False,
|
||||
)
|
||||
|
||||
if query is not None:
|
||||
query_toks, query_mask = self.binarize_with_mask(
|
||||
query, prefix, suffix, leading_space, trailing_space
|
||||
)
|
||||
query_len = len(query_toks)
|
||||
else:
|
||||
query_toks, query_mask, query_len = None, None, 0
|
||||
|
||||
query_tokens.append(query_toks)
|
||||
query_masks.append(query_mask)
|
||||
query_lengths.append(query_len)
|
||||
|
||||
cand_toks, cand_masks = [], []
|
||||
for cand_span in cand_spans:
|
||||
toks, mask = self.binarize_with_mask(
|
||||
cand_span.text,
|
||||
prefix,
|
||||
suffix,
|
||||
leading_space,
|
||||
trailing_space,
|
||||
)
|
||||
cand_toks.append(toks)
|
||||
cand_masks.append(mask)
|
||||
|
||||
# collate candidates
|
||||
cand_toks = data_utils.collate_tokens(cand_toks, pad_idx=self.vocab.pad())
|
||||
cand_masks = data_utils.collate_tokens(cand_masks, pad_idx=0)
|
||||
assert cand_toks.size() == cand_masks.size()
|
||||
|
||||
candidate_tokens.append(cand_toks)
|
||||
candidate_masks.append(cand_masks)
|
||||
candidate_lengths.append(cand_toks.size(1))
|
||||
|
||||
labels.append(label)
|
||||
|
||||
query_lengths = np.array(query_lengths)
|
||||
query_tokens = ListDataset(query_tokens, query_lengths)
|
||||
query_masks = ListDataset(query_masks, query_lengths)
|
||||
|
||||
candidate_lengths = np.array(candidate_lengths)
|
||||
candidate_tokens = ListDataset(candidate_tokens, candidate_lengths)
|
||||
candidate_masks = ListDataset(candidate_masks, candidate_lengths)
|
||||
|
||||
labels = ListDataset(labels, [1] * len(labels))
|
||||
|
||||
dataset = {
|
||||
"id": IdDataset(),
|
||||
"query_tokens": query_tokens,
|
||||
"query_masks": query_masks,
|
||||
"candidate_tokens": candidate_tokens,
|
||||
"candidate_masks": candidate_masks,
|
||||
"labels": labels,
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(query_tokens, reduce=True),
|
||||
}
|
||||
|
||||
nested_dataset = NestedDictionaryDataset(
|
||||
dataset,
|
||||
sizes=[query_lengths],
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
shuffle = np.random.permutation(len(query_tokens))
|
||||
dataset = SortDataset(
|
||||
nested_dataset,
|
||||
# shuffle
|
||||
sort_order=[shuffle],
|
||||
)
|
||||
|
||||
if return_only:
|
||||
return dataset
|
||||
|
||||
self.datasets[split] = dataset
|
||||
return self.datasets[split]
|
||||
|
||||
def build_dataset_for_inference(self, sample_json):
|
||||
with tempfile.NamedTemporaryFile(buffering=0) as h:
|
||||
h.write((json.dumps(sample_json) + "\n").encode("utf-8"))
|
||||
dataset = self.load_dataset(
|
||||
"disambiguate_pronoun",
|
||||
data_path=h.name,
|
||||
return_only=True,
|
||||
)
|
||||
return dataset
|
||||
|
||||
def disambiguate_pronoun(self, model, sentence, use_cuda=False):
|
||||
sample_json = wsc_utils.convert_sentence_to_json(sentence)
|
||||
dataset = self.build_dataset_for_inference(sample_json)
|
||||
sample = dataset.collater([dataset[0]])
|
||||
if use_cuda:
|
||||
sample = utils.move_to_cuda(sample)
|
||||
|
||||
def get_masked_input(tokens, mask):
|
||||
masked_tokens = tokens.clone()
|
||||
masked_tokens[mask.bool()] = self.mask
|
||||
return masked_tokens
|
||||
|
||||
def get_lprobs(tokens, mask):
|
||||
logits, _ = model(src_tokens=get_masked_input(tokens, mask))
|
||||
lprobs = F.log_softmax(logits, dim=-1, dtype=torch.float)
|
||||
scores = lprobs.gather(2, tokens.unsqueeze(-1)).squeeze(-1)
|
||||
mask = mask.type_as(scores)
|
||||
scores = (scores * mask).sum(dim=-1) / mask.sum(dim=-1)
|
||||
return scores
|
||||
|
||||
cand_lprobs = get_lprobs(
|
||||
sample["candidate_tokens"][0],
|
||||
sample["candidate_masks"][0],
|
||||
)
|
||||
if sample["query_tokens"][0] is not None:
|
||||
query_lprobs = get_lprobs(
|
||||
sample["query_tokens"][0].unsqueeze(0),
|
||||
sample["query_masks"][0].unsqueeze(0),
|
||||
)
|
||||
return (query_lprobs >= cand_lprobs).all().item() == 1
|
||||
else:
|
||||
best_idx = cand_lprobs.argmax().item()
|
||||
full_cand = sample["candidate_tokens"][0][best_idx]
|
||||
mask = sample["candidate_masks"][0][best_idx]
|
||||
toks = full_cand[mask.bool()]
|
||||
return self.bpe.decode(self.source_dictionary.string(toks)).strip()
|
||||
|
||||
@property
|
||||
def source_dictionary(self):
|
||||
return self.vocab
|
||||
|
||||
@property
|
||||
def target_dictionary(self):
|
||||
return self.vocab
|
||||
|
||||
|
||||
@register_task("winogrande")
|
||||
class WinograndeTask(WSCTask):
|
||||
"""
|
||||
Task for WinoGrande dataset. Efficient implementation for Winograd schema
|
||||
tasks with exactly two candidates, one of which is correct.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setup_task(cls, args, **kwargs):
|
||||
assert args.criterion == "winogrande", "Must set --criterion=winogrande"
|
||||
|
||||
# load data and label dictionaries
|
||||
vocab = cls.load_dictionary(os.path.join(args.data, "dict.txt"))
|
||||
print("| dictionary: {} types".format(len(vocab)))
|
||||
|
||||
return cls(args, vocab)
|
||||
|
||||
def load_dataset(
|
||||
self, split, epoch=1, combine=False, data_path=None, return_only=False, **kwargs
|
||||
):
|
||||
"""Load a given dataset split.
|
||||
|
||||
Args:
|
||||
split (str): name of the split (e.g., train, valid, test)
|
||||
"""
|
||||
if data_path is None:
|
||||
data_path = os.path.join(self.args.data, split + ".jsonl")
|
||||
if not os.path.exists(data_path):
|
||||
raise FileNotFoundError("Cannot find data: {}".format(data_path))
|
||||
|
||||
query_tokens = []
|
||||
query_masks = []
|
||||
query_lengths = []
|
||||
candidate_tokens = []
|
||||
candidate_masks = []
|
||||
candidate_lengths = []
|
||||
|
||||
itr = wsc_utils.winogrande_jsonl_iterator(data_path, eval=(split == "test"))
|
||||
|
||||
for sample in itr:
|
||||
sentence, pronoun_span, query, cand_text = sample
|
||||
prefix = sentence[: pronoun_span[0]].rstrip()
|
||||
suffix = sentence[pronoun_span[1] :]
|
||||
|
||||
leading_space = " " if sentence[: pronoun_span[0]].endswith(" ") else ""
|
||||
trailing_space = ""
|
||||
|
||||
if query is not None:
|
||||
query_toks, query_mask = self.binarize_with_mask(
|
||||
query,
|
||||
prefix,
|
||||
suffix,
|
||||
leading_space,
|
||||
trailing_space,
|
||||
)
|
||||
query_len = len(query_toks)
|
||||
else:
|
||||
query_toks, query_mask, query_len = None, None, 0
|
||||
|
||||
query_tokens.append(query_toks)
|
||||
query_masks.append(query_mask)
|
||||
query_lengths.append(query_len)
|
||||
|
||||
cand_toks, cand_mask = self.binarize_with_mask(
|
||||
cand_text,
|
||||
prefix,
|
||||
suffix,
|
||||
leading_space,
|
||||
trailing_space,
|
||||
)
|
||||
|
||||
candidate_tokens.append(cand_toks)
|
||||
candidate_masks.append(cand_mask)
|
||||
candidate_lengths.append(cand_toks.size(0))
|
||||
|
||||
query_lengths = np.array(query_lengths)
|
||||
|
||||
def get_pad_dataset_fn(tokens, length, pad_idx):
|
||||
return PadDataset(
|
||||
ListDataset(tokens, length),
|
||||
pad_idx=pad_idx,
|
||||
left_pad=False,
|
||||
)
|
||||
|
||||
query_tokens = get_pad_dataset_fn(query_tokens, query_lengths, self.vocab.pad())
|
||||
query_masks = get_pad_dataset_fn(query_masks, query_lengths, 0)
|
||||
|
||||
candidate_lengths = np.array(candidate_lengths)
|
||||
candidate_tokens = get_pad_dataset_fn(
|
||||
candidate_tokens, candidate_lengths, self.vocab.pad()
|
||||
)
|
||||
candidate_masks = get_pad_dataset_fn(candidate_masks, candidate_lengths, 0)
|
||||
|
||||
dataset = {
|
||||
"id": IdDataset(),
|
||||
"query_tokens": query_tokens,
|
||||
"query_masks": query_masks,
|
||||
"candidate_tokens": candidate_tokens,
|
||||
"candidate_masks": candidate_masks,
|
||||
"nsentences": NumSamplesDataset(),
|
||||
"ntokens": NumelDataset(query_tokens, reduce=True),
|
||||
}
|
||||
|
||||
nested_dataset = NestedDictionaryDataset(
|
||||
dataset,
|
||||
sizes=[query_lengths],
|
||||
)
|
||||
|
||||
with data_utils.numpy_seed(self.args.seed):
|
||||
shuffle = np.random.permutation(len(query_tokens))
|
||||
dataset = SortDataset(
|
||||
nested_dataset,
|
||||
# shuffle
|
||||
sort_order=[shuffle],
|
||||
)
|
||||
|
||||
if return_only:
|
||||
return dataset
|
||||
|
||||
self.datasets[split] = dataset
|
||||
return self.datasets[split]
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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 json
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
def convert_sentence_to_json(sentence):
|
||||
if "_" in sentence:
|
||||
prefix, rest = sentence.split("_", 1)
|
||||
query, rest = rest.split("_", 1)
|
||||
query_index = len(prefix.rstrip().split(" "))
|
||||
else:
|
||||
query, query_index = None, None
|
||||
|
||||
prefix, rest = sentence.split("[", 1)
|
||||
pronoun, rest = rest.split("]", 1)
|
||||
pronoun_index = len(prefix.rstrip().split(" "))
|
||||
|
||||
sentence = sentence.replace("_", "").replace("[", "").replace("]", "")
|
||||
|
||||
return {
|
||||
"idx": 0,
|
||||
"text": sentence,
|
||||
"target": {
|
||||
"span1_index": query_index,
|
||||
"span1_text": query,
|
||||
"span2_index": pronoun_index,
|
||||
"span2_text": pronoun,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extended_noun_chunks(sentence):
|
||||
noun_chunks = {(np.start, np.end) for np in sentence.noun_chunks}
|
||||
np_start, cur_np = 0, "NONE"
|
||||
for i, token in enumerate(sentence):
|
||||
np_type = token.pos_ if token.pos_ in {"NOUN", "PROPN"} else "NONE"
|
||||
if np_type != cur_np:
|
||||
if cur_np != "NONE":
|
||||
noun_chunks.add((np_start, i))
|
||||
if np_type != "NONE":
|
||||
np_start = i
|
||||
cur_np = np_type
|
||||
if cur_np != "NONE":
|
||||
noun_chunks.add((np_start, len(sentence)))
|
||||
return [sentence[s:e] for (s, e) in sorted(noun_chunks)]
|
||||
|
||||
|
||||
def find_token(sentence, start_pos):
|
||||
found_tok = None
|
||||
for tok in sentence:
|
||||
if tok.idx == start_pos:
|
||||
found_tok = tok
|
||||
break
|
||||
return found_tok
|
||||
|
||||
|
||||
def find_span(sentence, search_text, start=0):
|
||||
search_text = search_text.lower()
|
||||
for tok in sentence[start:]:
|
||||
remainder = sentence[tok.i :].text.lower()
|
||||
if remainder.startswith(search_text):
|
||||
len_to_consume = len(search_text)
|
||||
start_idx = tok.idx
|
||||
for next_tok in sentence[tok.i :]:
|
||||
end_idx = next_tok.idx + len(next_tok.text)
|
||||
if end_idx - start_idx == len_to_consume:
|
||||
span = sentence[tok.i : next_tok.i + 1]
|
||||
return span
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_detokenizer():
|
||||
from sacremoses import MosesDetokenizer
|
||||
|
||||
detok = MosesDetokenizer(lang="en")
|
||||
return detok
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_spacy_nlp():
|
||||
import en_core_web_lg
|
||||
|
||||
nlp = en_core_web_lg.load()
|
||||
return nlp
|
||||
|
||||
|
||||
def jsonl_iterator(input_fname, positive_only=False, ngram_order=3, eval=False):
|
||||
detok = get_detokenizer()
|
||||
nlp = get_spacy_nlp()
|
||||
|
||||
with open(input_fname) as fin:
|
||||
for line in fin:
|
||||
sample = json.loads(line.strip())
|
||||
|
||||
if positive_only and "label" in sample and not sample["label"]:
|
||||
# only consider examples where the query is correct
|
||||
continue
|
||||
|
||||
target = sample["target"]
|
||||
|
||||
# clean up the query
|
||||
query = target["span1_text"]
|
||||
if query is not None:
|
||||
if "\n" in query:
|
||||
continue
|
||||
if query.endswith(".") or query.endswith(","):
|
||||
query = query[:-1]
|
||||
|
||||
# split tokens
|
||||
tokens = sample["text"].split(" ")
|
||||
|
||||
def strip_pronoun(x):
|
||||
return x.rstrip('.,"')
|
||||
|
||||
# find the pronoun
|
||||
pronoun_idx = target["span2_index"]
|
||||
pronoun = strip_pronoun(target["span2_text"])
|
||||
if strip_pronoun(tokens[pronoun_idx]) != pronoun:
|
||||
# hack: sometimes the index is misaligned
|
||||
if strip_pronoun(tokens[pronoun_idx + 1]) == pronoun:
|
||||
pronoun_idx += 1
|
||||
else:
|
||||
raise Exception("Misaligned pronoun!")
|
||||
assert strip_pronoun(tokens[pronoun_idx]) == pronoun
|
||||
|
||||
# split tokens before and after the pronoun
|
||||
before = tokens[:pronoun_idx]
|
||||
after = tokens[pronoun_idx + 1 :]
|
||||
|
||||
# the GPT BPE attaches leading spaces to tokens, so we keep track
|
||||
# of whether we need spaces before or after the pronoun
|
||||
leading_space = " " if pronoun_idx > 0 else ""
|
||||
trailing_space = " " if len(after) > 0 else ""
|
||||
|
||||
# detokenize
|
||||
before = detok.detokenize(before, return_str=True)
|
||||
pronoun = detok.detokenize([pronoun], return_str=True)
|
||||
after = detok.detokenize(after, return_str=True)
|
||||
|
||||
# hack: when the pronoun ends in a period (or comma), move the
|
||||
# punctuation to the "after" part
|
||||
if pronoun.endswith(".") or pronoun.endswith(","):
|
||||
after = pronoun[-1] + trailing_space + after
|
||||
pronoun = pronoun[:-1]
|
||||
|
||||
# hack: when the "after" part begins with a comma or period, remove
|
||||
# the trailing space
|
||||
if after.startswith(".") or after.startswith(","):
|
||||
trailing_space = ""
|
||||
|
||||
# parse sentence with spacy
|
||||
sentence = nlp(before + leading_space + pronoun + trailing_space + after)
|
||||
|
||||
# find pronoun span
|
||||
start = len(before + leading_space)
|
||||
first_pronoun_tok = find_token(sentence, start_pos=start)
|
||||
pronoun_span = find_span(sentence, pronoun, start=first_pronoun_tok.i)
|
||||
assert pronoun_span.text == pronoun
|
||||
|
||||
if eval:
|
||||
# convert to format where pronoun is surrounded by "[]" and
|
||||
# query is surrounded by "_"
|
||||
query_span = find_span(sentence, query)
|
||||
query_with_ws = "_{}_{}".format(
|
||||
query_span.text,
|
||||
(" " if query_span.text_with_ws.endswith(" ") else ""),
|
||||
)
|
||||
pronoun_with_ws = "[{}]{}".format(
|
||||
pronoun_span.text,
|
||||
(" " if pronoun_span.text_with_ws.endswith(" ") else ""),
|
||||
)
|
||||
if query_span.start < pronoun_span.start:
|
||||
first = (query_span, query_with_ws)
|
||||
second = (pronoun_span, pronoun_with_ws)
|
||||
else:
|
||||
first = (pronoun_span, pronoun_with_ws)
|
||||
second = (query_span, query_with_ws)
|
||||
sentence = (
|
||||
sentence[: first[0].start].text_with_ws
|
||||
+ first[1]
|
||||
+ sentence[first[0].end : second[0].start].text_with_ws
|
||||
+ second[1]
|
||||
+ sentence[second[0].end :].text
|
||||
)
|
||||
yield sentence, sample.get("label", None)
|
||||
else:
|
||||
yield sentence, pronoun_span, query, sample.get("label", None)
|
||||
|
||||
|
||||
def winogrande_jsonl_iterator(input_fname, eval=False):
|
||||
with open(input_fname) as fin:
|
||||
for line in fin:
|
||||
sample = json.loads(line.strip())
|
||||
sentence, option1, option2 = (
|
||||
sample["sentence"],
|
||||
sample["option1"],
|
||||
sample["option2"],
|
||||
)
|
||||
|
||||
pronoun_span = (sentence.index("_"), sentence.index("_") + 1)
|
||||
|
||||
if eval:
|
||||
query, cand = option1, option2
|
||||
else:
|
||||
query = option1 if sample["answer"] == "1" else option2
|
||||
cand = option2 if sample["answer"] == "1" else option1
|
||||
yield sentence, pronoun_span, query, cand
|
||||
|
||||
|
||||
def filter_noun_chunks(
|
||||
chunks, exclude_pronouns=False, exclude_query=None, exact_match=False
|
||||
):
|
||||
if exclude_pronouns:
|
||||
chunks = [
|
||||
np
|
||||
for np in chunks
|
||||
if (np.lemma_ != "-PRON-" and not all(tok.pos_ == "PRON" for tok in np))
|
||||
]
|
||||
|
||||
if exclude_query is not None:
|
||||
excl_txt = [exclude_query.lower()]
|
||||
filtered_chunks = []
|
||||
for chunk in chunks:
|
||||
lower_chunk = chunk.text.lower()
|
||||
found = False
|
||||
for excl in excl_txt:
|
||||
if (
|
||||
not exact_match and (lower_chunk in excl or excl in lower_chunk)
|
||||
) or lower_chunk == excl:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
filtered_chunks.append(chunk)
|
||||
chunks = filtered_chunks
|
||||
|
||||
return chunks
|
||||
Reference in New Issue
Block a user