Transformerを用いたニューラル機械翻訳をやってみよう

奈良で機械翻訳の研究をしているokayuといいます．
タイトルの通り，ゼロから英日ニューラル機械翻訳をやってみましょう．

#ニューラル機械翻訳とは？
ディープラーニングを用いた機械翻訳の手法全般を指します．現在主流となっている機械翻訳手法で，有名なモデルとしてTransformerがあります．

- [Attention Is All You Need](https://arxiv.org/abs/1706.03762) (原論文)
- [論文解説 Attention Is All You Need (Transformer)](http://deeplearning.hatenablog.com/entry/transformer) (Ryobotさんによる日本語の解説記事)

今回はこのTransformerモデルを用いて英日ニューラル機械翻訳をやってみたいと思います！
Sentenecepiece, OpenNMT, MeCab, multi-bleu.perlを用います．シングルGPUでの学習を想定しています．
この記事ではTransformerなど技術の解説などはしません…

#ニューラル機械翻訳の流れ
大まかな流れは以下のようになります

1. [データを用意しよう](#anchor0)
1. [前処理](#anchor2)
1. [学習](#anchor3)
1. [翻訳](#anchor4) 
1. [デトークナイズ](#anchor5)
1. [単語分割](#anchor6)
1. [評価](#anchor7)

今回はOpenNMT-pyを用いるので，環境はpython3.5以降，PyTorch 1.4です．

##データを用意しよう
<a id="anchor0"></a>
コーパスとは，自然言語処理で用いられる言語のデータのことです．
[ASPEC](http://orchid.kuee.kyoto-u.ac.jp/ASPEC/)という日英の特許文から構成されるデータセットや，[KFTT](http://www.phontron.com/kftt/index-ja.html)，[JParaCrawl](http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl/)が現在研究目的なら無償で提供されています．
Graham Neubig先生の[こちら](http://www.phontron.com/japanese-translation-data.php?lang=ja)のページに他のコーパスの内容も載っています．日英以外にも，[ParaCrawl](https://paracrawl.eu/)などのコーパスがあります．

まず，学習用の対訳文データセットを用意しましょう．必要なものは以下の通りです．

- train.ja - train用の日本語文
- train.en - train用の英語文
- dev.ja - validation用の日本語文
- dev.en - validation用の英語文
- test.ja - test用の日本語文
- test.en - test用の英語

今回は英日翻訳なので，test.jaが参照文，test.enが原文となります．学習にはtrain.ja，train.en，dev.ja，dev.enを用います．
.enと.jaのファイルはそれぞれ行と対訳が一致しています．

実際の例として，ASPECではtrain,validation(developement),test用にそれぞれ対訳文が用意されています．

    B-94A0894379 ||| 3 ||| 材料開発における発想支援のためには，ユーザの側の 操作が重要であるため，原子レベルで の物質操作のためのインターフェイスを 開発した［１９９４．８］ ||| Because user operation is important for the idea support in material development, an interface for a substance operation at atomic level was developed.

記事ID，対訳となっている日本語文，英語文が一行にまとめられています．
このような文がtrain1.txtには100万文，dev.txtには1790文，test.txtには1812文用意されています．このままでは学習には用いることができません，英語のみのデータセット，日本語のみのデータセットに分割する必要があります．（この例だと，train1.txtはtrain.jaとtrain.en，dev.txtはdev.jaとdev.en，test.txtはtest.jaとtest.enに分割する）
このとき，.enと.jaのファイルで行と対訳が一致するようにしてください．

##前処理
###トークナイズ
<a id="anchor2"></a>[Sentencepiece](https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c)を用いてトークナイズを行います．[MeCab](https://taku910.github.io/mecab/)や[KyTea](http://www.phontron.com/kytea/index-ja.html)を用いることもありますが，今回は単語単位ではなくサブワード単位でトークナイズを行います．
今回，語彙数は16000，語彙は英日で共有するとします，`cat train.en train.ja > tmp.txt`で一時的に英日のtrainデータを含んだ文データを作成します．

####pythonから
`pip install sentencepiece`でインストールします．
次に，sentencepieceで学習をします．

```python3:spm_train.py
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
spm.SentencePieceTrainer.Train("--input=tmp.txt --model_prefix=spm_trained_model --vocab_size=16000")
```
を実行し(`python spm_train.py`)，学習します．`spm_trained_model.model`と`spm_trained_model.vocab`が作成されます．`tmp.txt`はもう使わないので削除します．
全ての対訳文データセットのトークナイズをします．(`python spm_tok.py`)

```python3:spm_tok.py
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.load('spm_trained_model.model')

def detok(filename, outputfilename):
    f = open(filename, mode='r')
    foutput = open(outputfilename, mode='w')
    d = f.read()
    d = d.split('\n')
    for i in d:
        data = sp.EncodeAsPieces(str(i))
        data = ' '.join(data)
        foutput.write(data + '\n')
    f.close()
    foutput.close()

detok('train.en','train.en.atok')
detok('train.ja','train.ja.atok')
detok('dev.en','dev.en.atok')
detok('dev.ja','dev.ja.atok')
detok('test.en','test.en.atok')
detok('test.ja','test.ja.atok')
```
トークナイズされた，`train.en.atok`，`train.ja.atok`，`dev.en.atok`，`dev.ja.atok`，`test.en.atok`，`test.ja.atok`が作成されました．
(OpenNMTだとtest.ja.atokは使わないのですが，fairseqだと使うので一応作成しました)
コマンドラインからやる方法は公式のgithubに載っているのでそちらを参照してください．([Sentencepiece/GitHub](https://github.com/google/sentencepiece))

###シリアライズ化
今回は[OpenNMT](https://github.com/OpenNMT/OpenNMT-py)を用いて学習すると考えます．OpenNMT以外にも[fairseq](https://github.com/pytorch/fairseq)などがあります．
OpenNMTもfairseqもそれぞれ特有?の前処理を行います．文を学習しやすいデータ構造に変換します．

`OpenNMT-py`をインストールします．

```
git clone https://github.com/OpenNMT/OpenNMT-py.git
cd OpenNMT-py
python setup.py install
```
今後全ての処理を`OpenNMT-py`ディレクトリ内で行うとします．
次に，`preprocess.py`でシリアライズ化します．今回は英日翻訳なので，srcには英語，tgtには日本語のデータセットを指定します．

```
python preprocess.py -train_src train.en.atok -train_tgt train.ja.atok -valid_src dev.en.atok -valid_tgt dev.ja.atok -save_data preprocessed_dataset
```
実行すると，`preprocessed_dataset.train.pt`，`preprocessed_dataset.valid.pt`，`preprocessed_dataset.vocab.pt`，というファイルが作成されます．（データ量によっては複数作成されます）
各データの説明は以下の通りです．（公式GitHubからの引用です）
>After running the preprocessing, the following files are generated:
> - demo.train.pt: serialized PyTorch file containing training data
> - demo.valid.pt: serialized PyTorch file containing validation data
> - demo.vocab.pt: serialized PyTorch file containing vocabulary data

##学習
<a id="anchor3"></a>
以下のコマンド(引用：[OpenNMTFAQ](https://opennmt.net/OpenNMT-py/FAQ.html?highlight=transformer))で学習を行います．ハイパーパラメータの説明は[公式ドキュメント（英語）](https://opennmt.net/OpenNMT-py/options/train.html)を参照してください．シングルGPUで学習を行うので，CUDAのデバイスを指定し，world_sizeを１にしています．

```
export CUDA_VISIBLE_DEVICES=0 && \
python train.py -data preprocessed_dataset -save_model save_model_name \
-layers 6 -rnn_size 512 -word_vec_size 512 \
-transformer_ff 2048 -heads 8 -encoder_type transformer -decoder_type transformer -position_encoding \
-train_steps 200000 -max_generator_batches 2 -dropout 0.1 -batch_size 4096 -batch_type tokens \
-normalization tokens -accum_count 2 -optim adam -adam_beta2 0.998 -decay_method noam \
-warmup_steps 8000 -learning_rate 2 -max_grad_norm 0 -param_init 0 -param_init_glorot \
-label_smoothing 0.1 -valid_steps 10000 -save_checkpoint_steps 40000 -world_size 1 -gpu_ranks 0 
```
実行すると，このような感じで始まります．NMTModel(以降はモデル構造が表示されるので少し長いので省略してます．

```
[2020-06-16 14:50:34,141 INFO]  * src vocab size = 8071
[2020-06-16 14:50:34,143 INFO]  * tgt vocab size = 14921
[2020-06-16 14:50:34,145 INFO] Building model...
[2020-06-16 14:50:38,414 INFO] NMTModel(
```

このようにStepが進むと学習が進んでいる証拠です（以下の例は数値を少し消してます）

```
[2020-06-18 00:59:56,869 INFO] Step 100/200000; acc:  *; ppl:  *; xent: *; lr: *; 12933/10876 tok/s; 122958 sec
```

終わるときはこんな感じです

```
[2020-06-18 01:01:23,330 INFO] Step 200000/200000; acc:  *; ppl:  *; xent: *; lr: *; 13220/10803 tok/s; 123045 sec
[2020-06-18 01:01:23,336 INFO] Loading dataset from preprocessed_dataset.valid.pt
[2020-06-18 01:01:23,473 INFO] number of examples: 1791
[2020-06-18 01:01:26,183 INFO] Validation perplexity: *
[2020-06-18 01:01:26,185 INFO] Validation accuracy: *
[2020-06-18 01:01:26,265 INFO] Saving checkpoint save_model_name_step_200000.pt
```

ASPECデータセット，シングルGPUで１日〜２日くらいで学習が終わります．（GPUによる,CPUだともっと遅い）

##翻訳
<a id="anchor4"></a>
学習が終わると，`save_model_name_step_200000.pt`などといったチェックポイントが生成されます．これを用いて翻訳をします．
原文（翻訳したい英語文）には，先ほど作ったトークナイズされた`test.en.atok`を用います．(`test.en`ではダメです)

```
python translate.py -model save_model_name_step_200000.pt -src test.en.atok -output output.txt -replace_unk -verbose -gpu 0
```
翻訳された文ファイル`output.txt`が作成されました！

##デトークナイズ
<a id="anchor5"></a>翻訳文`output.txt`は，トークナイズされています，そのためSentencepieceでデトークナイズをし，生文に戻します．(`python spm_detok.py`)

```python3:spm_detok.py
import sentencepiece as spm
import sys

sp = spm.SentencePieceProcessor()
sp.load('spm_trained_model.model')
openfile = 'output.txt'
f = open(openfile,mode='r')
data = f.read()
outputsentence = data.split('\n')
f.close()
f2 = open(openfile+'detok', mode='w')
for i in outputsentence:
    i = i.split(' ')
    tmp = sp.decode_pieces(i)
    f2.write(tmp + '\n')
f2.close()
```
これで生文状態の日本語翻訳文`output.txt.detok`ができました！
ニューラル機械翻訳の基本の流れは以上です．生成された文章はどうやって評価されるのか，を以下ではやっていきます．

##単語分割
<a id="anchor6"></a>
あとでも出てきますが，評価にはngramによる単語の重なりの類似度が元となっているBLEUを用います．日本語は英語などと違い，空白を文章内に含まないので，単語分割を行う必要があります．
今回は単語分割に[MeCab](https://taku910.github.io/mecab/)を用いようと思います．（[KyTea](http://www.phontron.com/kytea/index-ja.html)使っている人もいるみたいです）

[こちらの手順](https://qiita.com/taroc/items/b9afd914432da08dafc8)にしたがって辞書とMeCabをインストールしましょう．
次に`python mecab_owakati.py output.txt.detok`，`python mecab_owakati.py test.ja`で生成翻訳文，参照文をそれぞれ単語分割します．

```python3:mecab_owakati.py
import MeCab
import sys
m = MeCab.Tagger('-Owakati')
openfile = sys.argv[1]
f = open(openfile,mode='r')
f2 = open(openfile+'.mecab', mode='w')
data = f.read()
f.close()
data = data.split('\n')
for i in data:
    k = m.parse(i)
    f2.write(k)
f2.close()
```
単語分割された日本語翻訳文`output.txt.detok.mecab`単語分割された参照文`test.ja.mecab`が生成されました．次はこれらを用いて生成された翻訳文を評価します．

##評価
<a id="anchor7"></a>
機械翻訳の評価では，BLEUがよく用いられているので，それを使って評価をします．

- [BLEU: a Method for Automatic Evaluation of Machine Translation](https://www.aclweb.org/anthology/P02-1040.pdf) (原論文)
- [自動評価尺度BLEU](http://www2.nict.go.jp/astrec-att/member/mutiyama/corpmt/4.pdf) (内山将夫氏によるNICTのBLEU解説)

近年，BLEUではなくBERTベースの評価手法なども出てきています(BERTscoreとか？)RIBESやROUGEという評価尺度もあります．今回は大人しくBLEUを使います．評価には`multi-bleu.perl`を用います．OpenNMT-pyをcloneした時に，`tools/`ディレクトリ内にデフォルトで入っています.

`perl tools/multi-bleu.perl test.ja.mecab < output.txt.detok.mecab`を実行すると，BLEUの値がでます．

```
BLEU = *, */*/*/* (BP=*, ratio=*, hyp_len=*, ref_len=*)
#BLEU = global-BLEU score, precisions of 1-grams/2-grams/3-grams/4-grams (BP=brevity penalty, ratio=length ratio， hyp_len=hypothesis length, ref_len=reference length)
```

ちなみに，WATでの評価結果が[こちら](http://lotus.kuee.kyoto-u.ac.jp/WAT/evaluation/index.html)に載っています．ASPECで上記のハイパーパラメータだったらBLEU38-40?だと思います．(学習するときのハイパーパラメータに大きくよります)それと比べると某N社と某N|CTがお強いんだなあって思いました．
また，今回はmulti-bleu.perlを用いましたが，[sacreBLEU](https://github.com/mjpost/sacrebleu)を用いてもいいと思います．最近日本語対応をし始めました．(Mecabのみ)

#最後に
機械翻訳をやるときに，何をどうしたらいいのかわからなかったので，全く機械翻訳をやったことがない人に向けて書いてみました（日本語記事がなく，英語が得意ではない自分が苦労した）．わかりにくかったら申し訳ないです．
これをきっかけに少しでも自然言語処理や機械翻訳に興味を持っていただけたら嬉しいです．

Transformer以外のモデルに興味がある方はfairseqの方をやってみると楽しいかもしれません．色々実装されています．
何かご指摘・ご意見等ありましたらよろしくお願いいたします．問題あったら消します．

~~コマンドラインから最近はやっているんですが，私が何もわからない頃はpythonで実行してやってました…無知…~~