OpenNMT 2.0.0rc1 使用手册

最近在用OpenNMT训练机器翻译模型。OpenNMT 全称 Open-Source Neural Machine Translation,是https://nlp.seas.harvard.edu/SYSTRAN 共同开发的适用于机器翻译的集成库(现在由SYSTRANUbiqus 维护)。

OpenNMT有两个版本,分别为依赖PyTorchTensorFlow的。从使用人数上来说,PyTorch用的人多得多,更新的速度也快一点,所以就选了OpenNMT-py 的版本。据说Academia的趋势也是PyTorch增多,考虑复用的话PyTorch是你的好朋友 :p

不得不说,OpenNMT开发得确实快,版本更新也快,眼睁睁看着它两个月更新一个版本,也是挺6。但这也导致文档update的速度跟不上开发的速度 🐶

所以本文记录下从头训练一个NMT的pipeline,也记录一些useful 的七七八八。

Why OpenNMT 2.0.0rc

版本更新挺快,之前的version比较稳定的是1.2.0,但最近用起来还是有点不趁手。而且2.0.0提供了pretrained tokenizer,不用白不用。所以本文之后的安装和使用,都是建立在这个版本上。

Installation

Prerequisite

  • Python >= 3.6
  • Pytorch == 1.6.0

接下来两种安装方法,一种pip安装,一种from source。

From pip

1
pip install OpenNMT-py==2.0.0rc1

如果用pip安装的时候提示MemoryError,则用:

1
pip install OpenNMT-py==2.0.0rc1 --no-cache-dir

From source

1
2
3
git clone https://github.com/OpenNMT/OpenNMT-py.git
cd OpenNMT-py
python setup.py install

如果想用pretrained model 或transformers,还要运行以下这句:

1
pip install -r requirements.opt.txt

注意:

clone 之前记得切换分支!master 分支是开发中的分支,如果碰上他们正在更新(是我的血泪史了QAQ),有的code 或api还没写完,很坑。

切换方式master -> Tags -> 2.0.0rc1.

PS. 就在写这篇博客的当下,他们又更新到 2.0.0rc2 了(看了一下更新时间,14 days ago) = = 虽然敏捷开发是没有错,但是也太快了,文档维护没跟上,有点坑。。。

Check installation

检测是否安装成功:

1
onmt_train -h

如果能输出help信息,说明安装成功。

Step 0. Training data preparation

Dataset preparation

如果选择install from source的方法,就可以看到在 OpenNMT-py/data/ 的文件夹下已经下载好 英译德 的dataset:

  • For training:
    • src-train.txt
    • tgt-train.txt
  • For validation:
    • src-val.txt
    • tgt-val.txt

但这个dataset很小,训练效果不会好,所以这里下载更多的dataset。

(如果只打算跑通一个例子,可以直接跳到下一个小节,不用重新下载新的dataset)

Dataset Download

WMT DATASET

Link: https://www.statmt.org/wmt13/translation-task.html#download

parallel

WMT 数据集,每年都有。上面的link是WMT13年的link。要找哪一年的dataset,就把 wmt13 这个数字改成那一年。比如要找WMT19的dataset,下载地址就是 https://www.statmt.org/wmt19/translation-task.html#download。

因为每一年翻译任务的语言不一样,所以根据要训练的翻译模型自己找年份 :(

下载解压之后,一般有两个文件夹。根据dataset不同,名字可能不同,但能看出来一个是源语言(src),一个是目标语言(tgt)。文件的形式是一行一句话。

OPUS

Link: http://opus.nlpl.eu/

这个网站也包含WMT的入口,很杂很乱,但好在可以选source,target language, customize 的空间很大。

下载之后的形式和WMT 的dataset很像,也是一行一句话。

orpus

Tatoeba Kaggle

http://www.manythings.org/anki/

这个网站的好处是简洁,一看就知道在哪里下载。

缺点是数据集不大,且双语放在同一个文件中,格式为English + TAB + The Other Language + TAB + Attribution。 如:

1
2
3
4
This work isn't easy.	この仕事は簡単じゃない。	CC-BY 2.0 (France) Attribution: tatoeba.org #3737550 (CK) & #7977622 (Ninja)
Those are sunflowers. それはひまわりです。 CC-BY 2.0 (France) Attribution: tatoeba.org #441940 (CK) & #205407 (arnab)
Tom bought a new car. トムは新車を買った。 CC-BY 2.0 (France) Attribution: tatoeba.org #1026984 (CK) & #2733633 (tommy_san)
This watch is broken. この時計は壊れている。 CC-BY 2.0 (France) Attribution: tatoeba.org #58929 (CK) & #221604 (bunbuku)

所以下载下来需要预处理,将两种语言分开存在不同文件中,方便后续使用。

(Optional) Preprocess

一般上述网站提供的数据集已经进过一定程度的清洗,但还可以根据自己的需求继续做一些预处理。这里不做赘述,可根据需要处理。

注意:

OpenNMT 2.0.0rc1 提供去除长句、特殊词汇等功能,可以不用自己处理,美滋滋 :p

Step 1. Build Vocabulary

准备好数据集之后,先建立词汇表。

OpenNMT提供onmt_build_vocab 命令,输入onmt_build_vocab可以看到所有的arguments。为了避免每次输入一堆argument的麻烦,OpenNMT 可以接收一整份configuration作为输入,这样就省去了很多麻烦。

Example configuration for Build Vocab

This is the example provided here. 这个例子里用作分词的语言模型是subword,需要提前训练, 保存在data/wmt/wmtende.model。如果不想做这一步,可以直接看下一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# wmt14_en_de.yaml
save_data: data/wmt/run/example
## Where the vocab(s) will be written
src_vocab: data/wmt/run/example.vocab.src
tgt_vocab: data/wmt/run/example.vocab.tgt

# Corpus opts:
data:
commoncrawl:
path_src: data/wmt/commoncrawl.de-en.en
path_tgt: data/wmt/commoncrawl.de-en.de
transforms: [sentencepiece, filtertoolong]
weight: 23
europarl:
path_src: data/wmt/europarl-v7.de-en.en
path_tgt: data/wmt/europarl-v7.de-en.de
transforms: [sentencepiece, filtertoolong]
weight: 19
news_commentary:
path_src: data/wmt/news-commentary-v11.de-en.en
path_tgt: data/wmt/news-commentary-v11.de-en.de
transforms: [sentencepiece, filtertoolong]
weight: 3
valid:
path_src: data/wmt/valid.en
path_tgt: data/wmt/valid.de
transforms: [sentencepiece]

### Transform related opts:
#### Subword
src_subword_model: data/wmt/wmtende.model
tgt_subword_model: data/wmt/wmtende.model
src_subword_nbest: 1
src_subword_alpha: 0.0
tgt_subword_nbest: 1
tgt_subword_alpha: 0.0
#### Filter
src_seq_length: 150
tgt_seq_length: 150

# silently ignore empty lines in the data
skip_empty_level: silent

以一个不用语言模型的分词作例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# wmt14_en_de.yaml
save_data: data/wmt/run/example
## Where the vocab(s) will be written
src_vocab: data/wmt/run/example.vocab.src
tgt_vocab: data/wmt/run/example.vocab.tgt


# Corpus opts:
data:
commoncrawl:
path_src: data/wmt/commoncrawl.de-en.en
path_tgt: data/wmt/commoncrawl.de-en.de
transforms: [ ]
weight: 23
europarl:
path_src: data/wmt/europarl-v7.de-en.en
path_tgt: data/wmt/europarl-v7.de-en.de
transforms: [ ]
weight: 19
news_commentary:
path_src: data/wmt/news-commentary-v11.de-en.en
path_tgt: data/wmt/news-commentary-v11.de-en.de
transforms: [ ]
weight: 3
valid:
path_src: data/wmt/valid.en
path_tgt: data/wmt/valid.de
transforms: [ ]

src_vocab_size: 32000
tgt_vocab_size: 32000
vocab_size_multiple: 8
src_words_min_frequency: 10
tgt_words_min_frequency: 10
share_vocab: true

这个例子用到四个数据集,分别设置了不同的权重,src和tgt语言共享一个大小为32000的单词表,单词表存在src_vocabtgt_vocab指定的路径下。

保存这个config文件,运行:(注意替换url-to-config-file 成存储的路径, 如 config/build_vocab.yaml)

1
onmt_build_vocab -config url-to-config-file -n_sample -1

这里n_sample 是用到的数据数量,-1指用全部。

Data Transformer

OpenNMT 2.0 以后提供常用的预处理方法和分词模型,如去除长句,bpe等。

用法

在每个dataset的transform中填写preset transformer,例如用sentencepiece 做分词;

1
2
3
4
5
6
data:
commoncrawl:
path_src: data/wmt/commoncrawl.de-en.en
path_tgt: data/wmt/commoncrawl.de-en.de
transforms: [sentencepiece]
weight: 23

Preprocess - Filterlong

filterlong做预处理去掉过长句子,搭配以下comment一起使用:

1
2
3
#### Filter
src_seq_length: 150
tgt_seq_length: 150

不一一赘述了。

注意:sentencepiece如果

Tokenization

No special tokenization.

相当于直接用空格当作tokenization. 缺点是vocabulary会很大,很多同源词会被当作独立的单词。好处是简单,无需用到什么语言模型。

1
2
3
4
5
6
data:
commoncrawl:
path_src: data/wmt/commoncrawl.de-en.en
path_tgt: data/wmt/commoncrawl.de-en.de
transforms: [ ]
weight: 23
Sentencepiece

Sentencepiece要预训练一个语言模型,然后用语言模型将source 和target文件进行分词,保存相应的vocabulary。

(a) 训练语言模型

1
2
3
4
5
6
import sentencepiece as spm

spm.SentencePieceTrainer.Train(input='url-to-your-source-or-target-file',
model_prefix='/data/model',
vocab_size=32000,
character_coverage=1)

参数inputmodel_prefix要指定, vocab_size 可以自定义,这里设置为32000。

(b) Encode

1
2
3
spp = spm.SentencePieceProcessor()
spp.Load(model_file='url-to-model-you-trained')
output = spp.encode(line, out_type="str") # here, `line' should be specified, can be read from file.

model_file指定到刚刚训练好的语言模型路径,line 是待encoding的句子/段落。

(c) Decode (After translation)

训练模型之前不需要做这一步,这一步是提供给训练模型之后的。

1
2
3
spp = spm.SentencePieceProcessor()
spp.Load(model_file='url-to-model-you-trained')
output = spp.decode(line)

model_file指定到刚刚训练好的语言模型路径,line 是待decoding的句子列表。

BPE

tools/下有bpe_pipeline.sh 可以直接用。

其他可用的data transformers 在这里

Step 2. Train the model

把下面的configuration加在之前build vocabulary 的config 后面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# General opts
save_model: data/wmt/run/model
keep_checkpoint: 50
save_checkpoint_steps: 5000
average_decay: 0.0005
seed: 1234
report_every: 100
train_steps: 100000
valid_steps: 5000

# Batching
queue_size: 10000
bucket_size: 32768
world_size: 2
gpu_ranks: [0, 1]
batch_type: "tokens"
batch_size: 4096
valid_batch_size: 16
batch_size_multiple: 1
max_generator_batches: 0
accum_count: [3]
accum_steps: [0]

# Optimization
model_dtype: "fp32"
optim: "adam"
learning_rate: 2
warmup_steps: 8000
decay_method: "noam"
adam_beta2: 0.998
max_grad_norm: 0
label_smoothing: 0.1
param_init: 0
param_init_glorot: true
normalization: "tokens"

# Model
encoder_type: transformer
decoder_type: transformer
enc_layers: 6
dec_layers: 6
heads: 8
rnn_size: 512
word_vec_size: 512
transformer_ff: 2048
dropout_steps: [0]
dropout: [0.1]
attention_dropout: [0.1]
share_decoder_embeddings: true
share_embeddings: true

注意:

  • gpu_ranks: 指定前两个gpu core,这里根据需要设置要用的gpu序号和个数。
  • 这里的模型是 encoder,decoder都为6层transformer 的attention model,各种参数都可调
  • 如果想从某个checkpoint开始train起,加上参数train_from: path-to-pt-file
  • 可以指定log路径:log_file: path-you-want-to-save-log-file
  • 用几个GPU进行训练,world_size 就设置成几。
  • 如果share_vocab: false, 则 share_decoder_embeddings 也要为false。

Step 3. Translate

用法

1
onmt_translate -src src-file -tgt tgt-file -model url-to-the-model -replace_unk -gpu 0 -verbose

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
[2020-12-29 12:03:14,773 INFO]
SENT 178: ['The', 'governing', 'Justice', 'and', 'Development', 'party', '-', 'the', 'AKP', '-', 'has', 'crafted', 'a', 'whole', 'new', 'foreign', 'policy', 'for', 'the', 'country.']
PRED 178: Toda la política exterior y de desarrollo ha crafted un nuevo partido para el país whole
PRED SCORE: -15.3076
GOLD 178: El Partido de Justicia y Desarrollo que está al gobierno - el <unk> - ha planeado una política exterior nueva para el país.
GOLD SCORE: -61.9490

[2020-12-29 12:03:14,773 INFO]
SENT 179: ['The', 'relative', 'isolation', 'from', 'its', 'surrounding', 'region,', 'engendered', 'by', 'the', 'frozen', 'boundaries', 'of', 'the', 'Cold', 'War,', 'has', 'gone.']
PRED 179: La Guerra Fría, frozen por su aislamiento relativo de la región, ha frozen los límites de la Cold
PRED SCORE: -13.5960
GOLD 179: El relativo aislamiento de las regiones <unk> generado por las fronteras <unk> de la Guerra Fría, ha desaparecido.
GOLD SCORE: -50.6623

注意

  • 这里的src-file, tgt-file, url-to-the-model 需指定
  • 可以用多个gpu进行计算,用法-gpu 0 1
  • 如果不使用-replace_unk,预测出来的句子就都是
  • -verbose打印出每个句子的具体翻译情况
  • 如果用了语言模型做分词,要先将source 和target 文件用相应的语言模型进行处理,再translate。

写在最后

文档不是很完善,但是看代码还是可以。版本迭代很快,well-maintained,看看代码还是能跟上的。总体来说是个提高效率的codebook吧。

BTW,内存管理有些问题,不确定问题出在OpenNMT还是pytorch上。