TensorFlow : Tutorials : Sequences : ニューラル機械翻訳 (seq2seq) チュートリアル (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 09/02, 07/16/2018
作成日時 : 06/07/2018
* TensorFlow 1.9 でドキュメント構成が変わりましたので調整しました。
* 本ページは、TensorFlow 本家サイトの Tutorials – Sequences – Neural Machine Translation (seq2seq) Tutorial を
翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
Sequence-to-sequence (seq2seq) モデル (Sutskever et al., 2014, Cho et al., 2014) は機械翻訳、音声認識、そしてテキスト要約のような様々なタスクで大きな成功を楽しんでいます。このチュートリアルは読者に seq2seq モデルの完全な理解を与えて競争力のある seq2seq モデルをスクラッチからどのように構築するかを示します。ニューラル機械翻訳 (NMT) のタスクにフォーカスします、これは大成功 (= wild success) した seq2seq モデルのための本当に最初のテストベッドでした。含まれるコードは軽量で、高品質で、プロダクション・レディで、そして最新の研究アイデアが組み込まれています。この目標を以下により獲得します :
- 最近のデコーダ / attention ラッパー API、TensorFlow 1.2 データ iterator を使用する。
- 私達の強い専門技術をリカレントと seq2seq モデルの構築に組み入れる。
- 非常にベストな NMT モデルの構築と Google の NMT (GNMT) システム のレプリカのためのティップスとトリックを提供する。
私達は簡単にレプリカが作成できるベンチマークを提供することが重要であると信じます。その結果、完全な実験結果を提供して次の公に利用可能なデータセット上でモデルを事前訓練しました :
- Small-スケール: IWSLT Evaluation Campaign により提供される、英越 (= English-Vietnamese) パラレルコーパス of TED talks (133K センテンス・ペア)
- Large-スケール: WMT Evaluation Campaign により提供される、独英パラレルコーパス (4.5M センテンス・ペア)
最初に NMT のための seq2seq モデルについての何某かの基本知識を築き上げて、vanilla NMT モデルをどのように構築して訓練するかを説明します。2 番目のパートは attention メカニズムを持つ競争力のある NMT モデルの構築の詳細に入ります。それから、TensorFlow ベストプラクティス、bidirectional (双方向) RNN、ビームサーチ、更に GNMT attention を使用してマルチ GPU へスケールアップするというような、(速度と翻訳品質の両者で) ベストな可能な NMT モデルを構築するためにティップスとトリックを議論します。
ニューラル機械翻訳の背景
昔に戻れば、伝統的なフレーズ・ベースの翻訳システムはソース・センテンスを複数のチャンクに分解することによりタスクを遂行して、それからそれらをフレーズ・バイ・フレーズに翻訳しました。これは翻訳出力において流暢ではなく私達、人間が、翻訳するようなものではありませんでした。私達はソース・センテンス全体を読み、その意味を理解し、そしてそれから翻訳を生成します。ニューラル機械翻訳 (NMT) はそれを模倣します!
Figure 1. エンコーダ-デコーダ・アーキテクチャ – NMT のための一般的なアプローチの例です。エンコーダはソース・センテンスを “意味” ベクトルに変換します、これは翻訳を生成するためにデコーダを通して渡されます。
具体的には、NMT システムは最初に “thought” ベクトル を構築するためにエンコーダを使用して、ソース・センテンスを読みます、それは数字のシークエンスでセンテンスの意味を表します ; それからデコーダは、Figure 1 で示されるように翻訳を吐くためにセンテンス・ベクトルを処理します。これはしばしばエンコーダ-デコーダ・アーキテクチャとして参照されます。この方法で、NMT は伝統的なフレーズ・ベースのアプローチの局所翻訳問題に対処します : それは言語の long-range dependencies, e.g., gender agreements; シンタクス構造; etc., を捕捉することができて、そして Google Neural Machine Translation systems でデモされるような遥かにより流暢な翻訳を生成します。
NMT モデルはそれらの正確なアーキテクチャの観点から様々です。シーケンシャル・データのための自然な選択はリカレント・ニューラルネットワーク (RNN) で、殆どの NMT モデルで使用されます。通常は RNN はエンコーダとデコーダの両者のために使用されます。RNN モデルは、けれども、以下の観点から異なります : (a) 方向性 – 単方向 or 双方向 ; (b) 深さ – シングル- or マルチ層 ; そして (c) タイプ – しばしば vanilla RNN、Long Short-term Memory (LSTM)、または gated recurrent unit (GRU)。興味ある読者は RNN と LSTM についての更なる情報はこの ブログ投稿 で見つけることができます。
このチュートリアルでは、サンプルとして深層マルチ層 RNN を考えます、これは単方向でリカレント・ユニットとして LSTM を使用します。そのようなモデルのサンプルを Figure 2 で示します。このサンプルでは、ソース・センテンス “I am a student” をターゲット・センテンス “Je suis étudiant” に翻訳するモデルを構築します。高いレベルでは、NMT モデルは 2 つのリカレント・ニューラルネットワークから成ります : エンコーダ RNN はどのような予測をすることもなく単純に入力ソース単語を消費します ; デコーダは、一方で、次の単語を予測しながらターゲット・センテンスを処理します。
更なる情報のためには、読者は Luong (2016) を参照してください、これはこのチュートリアルがベースとしているものです。
Figure 2. ニューラル機械翻訳 – ソース・センテンス “I am a student” をターゲット・センテンス “Je suis étudiant” に翻訳するために提案された深層リカレント・アーキテクチャのサンプルです。ここで、”<s>” はデコーディング・プロセスの開始をマークしてその一方で “</s>” はデコーダに停止を伝えます。
チュートリアルをインストールする
このチュートリアルをインストールするためには、TensorFlow を貴方のシステム上にインストールする必要があります。このチュートリアルは TensorFlow Nightly を必要とします。TensorFlow をインストールするためには、ここのインストール手順 に従ってください。
ひとたび TensorFlow がインストールされれば、次を実行することでこのチュートリアルのソースコードをダウンロードできます :
git clone https://github.com/tensorflow/nmt/
訓練 – 最初の NMT システムをどのようにビルドするか
最初に具体的なコード・スニペットで NMT モデルの構築の中心へと飛び込みましょう、それを通して Figure 2 をより詳細に説明します。データ準備とフルコードは後に回します。このパートはファイル model.py を参照します。
ボトム層では、エンコーダとデコーダ RNN は入力として次を受け取ります : 最初に、ソース・センテンス、それから境界マーカー “\<s>”、これはエンコーディングからデコーディング・モードへの移行を示します、そしてターゲット・センテンスです。訓練のためには、次の tensor をシステムに供給します、これらは time-major 形式で単語インデックスを含みます :
- encoder_inputs [max_encoder_time, batch_size]: ソース入力単語。
- decoder_inputs [max_decoder_time, batch_size]: ターゲット入力単語。.
- decoder_outputs [max_decoder_time, batch_size]: ターゲット出力単語、これらは右に追加された end-of-sentence タグを伴う左に 1 時間ステップシフトされた decoder_inputs です。
ここでは効率のために、複数のセンテンス (batch_size) で一度に訓練します。テスティングは少し異なりまるので、それは後で議論しましょう。
埋め込み
単語のカテゴリー的な性質が与えられたとき、モデルは対応する単語表現を取得するために最初にソースとターゲット埋め込みを調べなければなりません。この埋め込み層を動作させるために、最初に各言語のために語彙が選択されます。通常は、語彙サイズ V が選択され、そして最も頻度の高い V 単語だけが一意に扱われます。総ての他の単語は “unknown (未知)” トークンに変換されて総て同じ埋め込みを得ます。埋め込み重み、言語毎に 1 セット、は通常は訓練の間に学習されます。
# Embedding embedding_encoder = variable_scope.get_variable( "embedding_encoder", [src_vocab_size, embedding_size], ...) # Look up embedding: # encoder_inputs: [max_time, batch_size] # encoder_emb_inp: [max_time, batch_size, embedding_size] encoder_emb_inp = embedding_ops.embedding_lookup( embedding_encoder, encoder_inputs)
同様に、embedding_decoder と decoder_emb_inp を構築することができます。word2vec や Glove ベクトルのような事前訓練された単語表現で埋め込み重みを初期化することを選択できることに注意してください。一般に、訓練データの巨大な総量が与えられた場合にはスクラッチからこれらの埋め込みを学習できます。
エンコーダ
ひとたび取得されれば、単語埋め込みはそれから入力としてメイン・ネットワークに供給されます、これは 2 つのマルチ層 RNN から成ります – ソース言語のためのエンコーダとターゲット言語のためのデコーダです。これらの 2 つの RNN は、原理的には、同じ重みを共有できます ; けれども、実際には、2 つの異なる RNN パラメータをしばしば使用します (そのようなモデルは巨大な訓練データセットにフィットするときより良いジョブを行ないます)。エンコーダ RNN はその開始状態としてゼロ・ベクトルを使用して次のように構築されます :
# Build RNN cell encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units) # Run Dynamic RNN # encoder_outputs: [max_time, batch_size, num_units] # encoder_state: [batch_size, num_units] encoder_outputs, encoder_state = tf.nn.dynamic_rnn( encoder_cell, encoder_emb_inp, sequence_length=source_sequence_length, time_major=True)
センテンスは異なる長さを持つことに注意してください、無駄な計算を回避するために、dynamic_rnn に正確なソース・センテンスの長さを source_sequence_length を通して伝えます。私達の入力は time major ですので、time_major=True を設定します。ここでは、シングル層 LSTM, encoder_cell だけを構築します。どのようにマルチ層 LSTM を構築し、dropout を追加し、そして attention を使用するかについては後のセクションで説明します。
デコーダ
デコーダもまたソース情報へのアクセスを持つ必要があり、そしてそれを達成する一つの単純な方法はエンコーダの最後の隠れ状態, encoder_state でそれを初期化することです。Figure 2 で、ソース単語 “student” における隠れ状態をデコーダ側に渡します。
# Build RNN cell decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
# Helper helper = tf.contrib.seq2seq.TrainingHelper( decoder_emb_inp, decoder_lengths, time_major=True) # Decoder decoder = tf.contrib.seq2seq.BasicDecoder( decoder_cell, helper, encoder_state, output_layer=projection_layer) # Dynamic decoding outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...) logits = outputs.rnn_output
ここで、このコードの中心パートは BasicDecoder, decoder, で、これは入力として (encoder_cell と同様の) decoder_cell、ヘルパー、そして前の encoder_state を受け取ります。デコーダとヘルパーを分離することにより、異なるコードベースを再利用することができます、e.g., TrainingHelper は greedy デコーディングを行なうために GreedyEmbeddingHelper で置き換えられます。それ以上は helper.py を見てださい。
最後に、projection_layer に言及していませんでしたが、これはトップの隠れ状態を次元 V のロジット・ベクトルに変える dense 行列です。このプロセスを Figure 2 のトップに示します。
projection_layer = layers_core.Dense( tgt_vocab_size, use_bias=False)
損失
上でロジットが与えられ、訓練損失を計算する用意ができました :
crossent = tf.nn.sparse_softmax_cross_entropy_with_logits( labels=decoder_outputs, logits=logits) train_loss = (tf.reduce_sum(crossent * target_weights) / batch_size)
ここで、target_weights は decoder_outputs と同じサイズの 0-1 (= zero-one) 行列です。それはターゲット・シークエンス長の外側の位置のパディングを値 0 でマスクします。
重要なノート: 損失を batch_size で除算するので、ハイパーパラメータは batch_size に対して “不変 (= invariant)” であることは指摘する価値があります。人によっては損失を (batch_size * num_time_steps) で分割しますが、これは短いセンテンス上のエラーを軽視します。より微妙なことに、(前者に適用された) 私達のハイパーパラメータは後者の方法のためには使用できません。例えば、両者のアプローチが 1.0 の学習率で SGD を使用する場合、後者のアプローチは事実上 1 / num_time_steps の遥かに小さい学習率を使用します。
勾配計算 & 最適化
私達は今 NMT モデルの forward パスを定義しました。backpropagation パスを計算することは単に数行のコードの事柄です :
# Calculate and clip gradients params = tf.trainable_variables() gradients = tf.gradients(train_loss, params) clipped_gradients, _ = tf.clip_by_global_norm( gradients, max_gradient_norm)
RNN の訓練における重要なステップの一つは勾配クリッピングです。ここで、global norm でクリップします。max value, max_gradient_norm はしばしば 5 or 1 のような値に設定されます。最後のステップは optimizer の選択です。Adam optimizer は一般的な選択です。学習率もまた選択します。learning_rate の値は通常は 0.0001 から 0.001 の範囲にあります ; そして訓練が進むにつれて減少するように設定できます。
# Optimization optimizer = tf.train.AdamOptimizer(learning_rate) update_step = optimizer.apply_gradients( zip(clipped_gradients, params))
私達自身の実験では、標準的な SGD (tf.train.GradientDescentOptimizer) を低下する学習率スケジュールで使用し、これはより良いパフォーマンスを生成します。ベンチマーク を見てください。
ハンズオン – NMT モデルを訓練しましょう
ベトナム語から英語に翻訳する、私達の本当に最初の NMT モデルを訓練しましょう、コードのエントリ・ポイントは nmt.py です。
この課題のために TED talks の small-スケールのパラレルコーパス ((133K 訓練サンプル) を使用します。ここで使用されるデータの総ては https://nlp.stanford.edu/projects/nmt/ で見つかります。tst2012 を dev データセットとして、そして tst2013 をテスト・データセットとして使用します。
NMT を訓練するためのデータをダンロードする次のコマンドを実行します :\
nmt/scripts/download_iwslt15.sh /tmp/nmt_data
訓練を開始するためには次のコマンドを実行します :
mkdir /tmp/nmt_model python -m nmt.nmt \ --src=vi --tgt=en \ --vocab_prefix=/tmp/nmt_data/vocab \ --train_prefix=/tmp/nmt_data/train \ --dev_prefix=/tmp/nmt_data/tst2012 \ --test_prefix=/tmp/nmt_data/tst2013 \ --out_dir=/tmp/nmt_model \ --num_train_steps=12000 \ --steps_per_stats=100 \ --num_layers=2 \ --num_units=128 \ --dropout=0.2 \ --metrics=bleu
上のコマンドは 128-dim 隠れユニットと埋め込みを持つ 2-層 LSTM seq2seq モデルを 12 エポック訓練します。0.2 の dropout 値 (確率 0.8 を保つ) を使用します。もしエラーがないのであれば、訓練につれての perplexity の減少とともに 下に似たログを見るはずです。
# First evaluation, global step 0 eval dev: perplexity 17193.66 eval test: perplexity 17193.27 # Start epoch 0, step 0, lr 1, Tue Apr 25 23:17:41 2017 sample train data: src_reverse: Điều đó , dĩ nhiên , là câu chuyện trích ra từ học thuyết của Karl Marx . ref: That , of course , was thedistilled from the theories of Karl Marx . epoch 0 step 100 lr 1 step-time 0.89s wps 5.78K ppl 1568.62 bleu 0.00 epoch 0 step 200 lr 1 step-time 0.94s wps 5.91K ppl 524.11 bleu 0.00 epoch 0 step 300 lr 1 step-time 0.96s wps 5.80K ppl 340.05 bleu 0.00 epoch 0 step 400 lr 1 step-time 1.02s wps 6.06K ppl 277.61 bleu 0.00 epoch 0 step 500 lr 1 step-time 0.95s wps 5.89K ppl 205.85 bleu 0.00
より詳細は train.py を見てください。
訓練の間にモデルの要約を見るために TensorBoard を開始することもできます :
tensorboard --port 22222 --logdir /tmp/nmt_model/
英語からベトナム語への反対の方向の訓練は単純に次の変更で成されます:\ –src=en –tgt=vi
推論 – 翻訳をどのように生成するか
NMT モデルを訓練している一方で (そしてひとたびモデルを訓練したのであれば)、以前に見ていないソース・センテンスが与えられたときに翻訳を得ることができます。このプロセスは推論と呼ばれます。訓練と推論 (テスティング) の間には明確な区別があります : 推論時には、ソース・センテンス, i.e., encoder_inputsへのアクセスを持つのみです。デコーディングを遂行するためには多くの方法があります。デコーディング・メソッドは greedy、サンプリングそしてビームサーチ・デコーディングを含みます。ここでは、greedy デコーディング・ストラテジーを議論します。
アイデアは単純でそれを Figure 3 で示します :
- encoder_state を得るために訓練の間と同じ方法でソース・センテンスを依然としてエンコードします、そしてこの encoder_state はデコーダを初期化するために使用されます。
- デコーディング (翻訳) プロセスはデコーダが開始シンボル “\<s>” (コードでは tgt_sos_id として参照) を受け取るとすぐに開始されます;
- デコーダ側の各 time ステップに対して、RNN 出力をロジットのセットとして扱います。最も尤もらしい単語、最大ロジット値に関連する id、を出力された単語として選択します (これは “greedy” の挙動です)。Figure 3 のサンプルでは、最初のデコーディング・ステップで単語 “moi” が最も高い翻訳確率を持ちます。それからこの単語を次の time ステップへの入力として供給します。
- このプロセスが出力シンボルとして文末マーカー “\</s>” (コードでは tgt_eos_id として参照) が生成されるまで続きます。
Figure 3. Greedy デコーディング – 訓練された NMT モデルが greedy サーチを使用してソース・センテンス “je suis étudiant” のためにどのように翻訳を生成するかのサンプルです。
ステップ 3 は推論を訓練とは異なるものにするものです。入力として常に正しいターゲット単語を供給する代わりに、推論はモデルにより推測される単語を使用します。ここに greedy デコーディングを成し遂げるコードがあります。それは訓練デコーダに非常に似ています。
# Helper helper = tf.contrib.seq2seq.GreedyEmbeddingHelper( embedding_decoder, tf.fill([batch_size], tgt_sos_id), tgt_eos_id) # Decoder decoder = tf.contrib.seq2seq.BasicDecoder( decoder_cell, helper, encoder_state, output_layer=projection_layer) # Dynamic decoding outputs, _ = tf.contrib.seq2seq.dynamic_decode( decoder, maximum_iterations=maximum_iterations) translations = outputs.sample_id
ここで、TrainingHelper の代わりに GreedyEmbeddingHelper を使用します。ターゲット・シークエンス長を前もって知らないので、翻訳の長さを制限するために maximum_iterations を使用します。一つの経験則はソース・センテンス長の 2 倍までデコードすることです。
maximum_iterations = tf.round(tf.reduce_max(source_sequence_length) * 2)
モデルを訓練したら、今では推論ファイルを作成して幾つかのセンテンスを翻訳することができます :
cat > /tmp/my_infer_file.vi # (copy and paste some sentences from /tmp/nmt_data/tst2013.vi) python -m nmt.nmt \ --out_dir=/tmp/nmt_model \ --inference_input_file=/tmp/my_infer_file.vi \ --inference_output_file=/tmp/nmt_model/output_infer cat /tmp/nmt_model/output_infer # To view the inference as output
訓練チェックポイントが存在する限りはモデルがまだ訓練中でも上のコマンドはまた実行できることに注意してください。より詳細は inference.py を見てください。
最も基本的な seq2seq モデルを通り抜けました、更に進みましょう!先端技術のニューラル翻訳システムを構築するためには、更なる “秘密のソース” が必要です : attention メカニズム、これは最初に Bahdanau et al., 2015 により導入され、後に Luong et al., 2015 他により洗練されました。attention メカニズムの鍵となるアイデアは、翻訳するときに関係するソースコンテンツに “注意 (attention)” を払うことによって、ターゲットとソース間の直接的なショートカット接続を確立することです。attention メカニズムの素晴らしい副産物は (Figure 4 で示されるような) ソースとターゲット・センテンス間の可視化しやすいアラインメント行列です。
Figure 4. Attention 可視化 – ソースとターゲット・センテンス間のアラインメントの例です。画像は (Bahdanau et al., 2015) から取られています。
vanilla seq2seq モデルでは、デコーディング・プロセスが始まるときエンコードからの最後のソース状態をデコーダに渡すことを思い出してください。これは短いあるいは中ぐらいの長さのセンテンスについては上手く動作します ; けれども、長いセンテンスに対しては、単一の固定長の隠れ状態は情報ボトルネックになります。ソース RNN で計算された隠れ状態の総てを捨てる代わりに、attention メカニズムはデコーダにそれらを覗き見ることを可能にします (それらをソース情報の動的メモリとして扱います)。
そのようにすることで、attention メカニズムはより長いセンテンスの翻訳を改善します。最近では、attention メカニズムはデファクト・スタンダードで多くの他のタスクに成功的に適用されてきています (画像キャプション生成、音声認識、そしてテキスト要約)。
Attention メカニズムの背景
(Luong et al., 2015) で提案された attention メカニズムの実例を今記述します、これはオープンソースのツールキット含む幾つかの先端技術システムとこのチュートリアルの TF seq2seq API で使用されているものです。attention メカニズムの他の変形へのコネクションもまた提供します。
Figure 5. Attention メカニズム – (Luong et al., 2015) で説明されている attention ベース NMT システムのサンプルです。attention 計算の最初のステップに詳細にハイライトしています。明確化のため、埋め込みと投射層 (= projection layer) は Figure (2) で示していません。
Figure 5 で図示されるように、attention 計算は総てのデコーダ time ステップで発生します。それは次の段階から成ります :
- (Figure 4 内で可視化される) attention 重みを導出するために現在のターゲット隠れ状態を総てのソース状態と比較します。
- attention 重みに基づいてソース状態の重み付けられた平均としてコンテキスト・ベクトルを計算します。
- 最終的な attention ベクトルを生成するためにコンテキスト・ベクトルを現在のターゲット隠れ状態と結合します。
- attention ベクトルは次の time ステップへの入力として供給されます (input feeding)。最初の 3 つのステップは下の等式によって要約できます :
ここで、関数 score はターゲット隠れ状態 $h_t$ をソース隠れ状態 $\overline{h}_s$ の各々と比較するために使用されて、結果は attention 重み (ソース位置に渡る分布) を生成するために正規化されます。scoring 関数の様々な選択があります ; ポピュラーな scoreing 関数は Eq. (4) で与えられる multiplicative and additive 形式を含みます。ひとたび計算されれば、attention ベクトルは softmax ロジットと損失を導出するために使用されます。これは vanilla seq2seq モデルのトップ層におけるターゲット隠れ状態に類似しています。関数 f はまた他の形式を取ることもできます。
attention メカニズムの様々な実装は attention_wrapper.py で見つけられます。
attention メカニズムでは何が重要でしょう?
上の等式でヒントが与えられているように、多くの異なる attention 変種があります。これらの変種は scoring 関数と attention 関数の形式、そして (Bahdanau et al., 2015) で元々提案されているように scoring 関数内で代わりに前の状態が使用されるか否かに依拠します。実証的に、私達は特定の選択だけが重要であることを見出しました。1 番目に、attention の基本的な形式、i.e. ターゲットとソースの直接コネクションが存在している必要があります。2 番目に、(Luong et al., 2015) で説明されているように過去の attention 決定についてネットワークに伝えるために attention ベクトルを次の timestep に供給することは重要です。最後に、scoring 関数の選択はしばしば異なるパフォーマンスの結果になります。詳細は (後述の) ベンチマーク結果のセクションを見てください。
Attention ラッパー API
AttentionWrapper の私達の実装では、(Weston et al., 2015) からメモリ・ネットワークのワークの幾つかの用語を借りています。読み書き可能なメモリを持つ代わりに、このチュートリアルで提案される attention メカニズムは read-only メモリです。特に、ソース隠れ状態 (あるいはそれらの変換されたバージョン、i.e. $W\overline{h}_s$ in Luong’s scoring スタイルまたは $W_2\overline{h}_s$ in Bahdanau’s scoring スタイル) のセットは “メモリ” として参照されます。各 time ステップで、メモリのどの部分を読むかを決定するために現在のターゲット隠れ状態を “query (問合せ)” として使用します。通常は、query は個々のメモリスロットに対応するキーと比較される必要があります。attention メカニズムの上の提示では、ソース隠れ状態 (あるいはそれらの変換されたバージョン、$W_1h_t$ i.e. in Bahdanau’s scoring スタイル) のセットを “キー” として偶々使用しています。他の attention 形式を導出するためにこのメモリ・ネットワーク用語によりインスパイアされるかもしれません!
attention ラッパーのおかげで、vanilla seq2seq コードを attention で拡張することは自明です。この部分はファイル attention_model.py に該当します。
最初に、attention メカニズム, e.g., from (Luong et al., 2015) を定義する必要があります :
# attention_states: [batch_size, max_time, num_units] attention_states = tf.transpose(encoder_outputs, [1, 0, 2]) # Create an attention mechanism attention_mechanism = tf.contrib.seq2seq.LuongAttention( num_units, attention_states, memory_sequence_length=source_sequence_length)
前のエンコーダのセクションでは、encoder_outputs はトップ層における総てのソース隠れ状態のセットで [max_time, batch_size, num_units] の shape を持ちます (何故ならば効率のために time_major を True に設定して dynamic_rnn を使用するからです)。attention メカニズムのためには、渡される “メモリ” が batch major であることを確かなものにする必要があります、そして attention_states を transpose する必要があります。 (non-padding 位置だけに渡り) attention 重みが適切に正規化されることを確かなものにするために source_sequence_length を attention メカニズムに渡します。attention メカニズムを定義しましたので、デコーディング・セルをラップするために AttentionWrapper を使用します :
decoder_cell = tf.contrib.seq2seq.AttentionWrapper( decoder_cell, attention_mechanism, attention_layer_size=num_units)
コードの残りはデコーダのセクション内のものと殆ど同じです!
ハンズオン – attention ベースの NMT モデルを構築する
attention を有効にするには、訓練の間 attention フラグとして luong, scaled_luong, bahdanau または normed_bahdanau の一つを使用する必要があります。フラグはどの attention メカニズムを使用するかを指定します。更に、attention モデルのための新しい辞書を作成する必要がありますので、前に訓練した基本 NMT モデルは再利用しません。
訓練を開始するためには次のコマンドを実行します :
mkdir /tmp/nmt_attention_model python -m nmt.nmt \ --attention=scaled_luong \ --src=vi --tgt=en \ --vocab_prefix=/tmp/nmt_data/vocab \ --train_prefix=/tmp/nmt_data/train \ --dev_prefix=/tmp/nmt_data/tst2012 \ --test_prefix=/tmp/nmt_data/tst2013 \ --out_dir=/tmp/nmt_attention_model \ --num_train_steps=12000 \ --steps_per_stats=100 \ --num_layers=2 \ --num_units=128 \ --dropout=0.2 \ --metrics=bleu
訓練後、推論のための新しい out_dir を伴う同じ推論コマンドを利用できます :
python -m nmt.nmt \ --out_dir=/tmp/nmt_attention_model \ --inference_input_file=/tmp/my_infer_file.vi \ --inference_output_file=/tmp/nmt_attention_model/output_infer
訓練、評価、そして推論グラフを構築する
TensorFlow で機械学習モデルを構築するとき、3 つの分離したグラフを構築することがしばしば最善です :
- 訓練グラフ、これは :
- ファイル/外部入力のセットから入力データをバッチ処理し、バケットに入れ、そしておそらくはサブサンプリングします。
- forward と backprop ops を含みます。
- optimizer を構築して、訓練 op を追加します。
- 評価グラフ、これは :
- ファイル/外部入力のセットから入力データをバッチ処理し、バケットに入れます。
- 訓練 forward ops と訓練では使用されない追加の評価 ops を含みます。
- 推論グラフ、これは :
- 入力データのバッチ処理はしないかもしれません。
- 入力データのサブサンプリングやバケット処理は行ないません。
- プレースホルダーから入力データを読みます (データは feed_dict を通して、あるいは C++ TensorFlow serving バイナリからデータはグラフに直接供給できます)。
- モデル forward ops のサブセットと、おそらくは session.run 呼び出し間の状態をストアするため追加の特別な入出力を含みます。
分離したグラフの構築は幾つかの利点があります :
- 推論グラフは通常は他の 2 つと非常に異なりますので、それを別に構築するのは意味があります。
- 評価グラフはより単純になります、何故ならばそれはもはや追加の逆伝播 ops の総てを持たないからです。
- データ供給は各グラフに対して別々に実装可能です。
- 変数 reuse は遥かにより単純です。例えば、評価グラフにおいて reuse=True を持つ変数スコープを再オープンする必要はありません、それは単に訓練モデルはこれらの変数を既に作成しているからです。従って reuse= 引数を至るところに撒き散らすことなしに同じコードが再利用できます。
- 分散訓練では、別々のワーカーに訓練、評価、そして推論を遂行させることはよくあります。これらはそれら自身のグラフを構築する必要がいずれにせよあります。従ってシステムをこの方法で構築することは分散訓練のために貴方を準備させることになります。
複雑さの主な源は単一のマシン設定で 3 つのグラフに渡り変数をどのように共有するかとなるでしょう。これは各グラフのために別々のセッションを使用することで解決されます。訓練セッションは定期的にチェックポイントをセーブし、そして評価セッションと推論セッションはチェックポイントからパラメータをレストアします。下のサンプルは 2 つのアプローチ間の主な違いを示します。
Before: 単一グラフ内の 3 つのモデルと単一セッションの共有
with tf.variable_scope('root'): train_inputs = tf.placeholder() train_op, loss = BuildTrainModel(train_inputs) initializer = tf.global_variables_initializer() with tf.variable_scope('root', reuse=True): eval_inputs = tf.placeholder() eval_loss = BuildEvalModel(eval_inputs) with tf.variable_scope('root', reuse=True): infer_inputs = tf.placeholder() inference_output = BuildInferenceModel(infer_inputs) sess = tf.Session() sess.run(initializer) for i in itertools.count(): train_input_data = ... sess.run([loss, train_op], feed_dict={train_inputs: train_input_data}) if i % EVAL_STEPS == 0: while data_to_eval: eval_input_data = ... sess.run([eval_loss], feed_dict={eval_inputs: eval_input_data}) if i % INFER_STEPS == 0: sess.run(inference_output, feed_dict={infer_inputs: infer_input_data})
After: 3 つのグラフ内の 3 つのモデル、3 つのセッションが同じ変数を共有する
train_graph = tf.Graph() eval_graph = tf.Graph() infer_graph = tf.Graph() with train_graph.as_default(): train_iterator = ... train_model = BuildTrainModel(train_iterator) initializer = tf.global_variables_initializer() with eval_graph.as_default(): eval_iterator = ... eval_model = BuildEvalModel(eval_iterator) with infer_graph.as_default(): infer_iterator, infer_inputs = ... infer_model = BuildInferenceModel(infer_iterator) checkpoints_path = "/tmp/model/checkpoints" train_sess = tf.Session(graph=train_graph) eval_sess = tf.Session(graph=eval_graph) infer_sess = tf.Session(graph=infer_graph) train_sess.run(initializer) train_sess.run(train_iterator.initializer) for i in itertools.count(): train_model.train(train_sess) if i % EVAL_STEPS == 0: checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i) eval_model.saver.restore(eval_sess, checkpoint_path) eval_sess.run(eval_iterator.initializer) while data_to_eval: eval_model.eval(eval_sess) if i % INFER_STEPS == 0: checkpoint_path = train_model.saver.save(train_sess, checkpoints_path, global_step=i) infer_model.saver.restore(infer_sess, checkpoint_path) infer_sess.run(infer_iterator.initializer, feed_dict={infer_inputs: infer_input_data}) while data_to_infer: infer_model.infer(infer_sess)
後者のアプローチは分散バージョンに変換されるための「準備」ができていることに注意してください。
新しいアプローチの一つの他の違いは各 session.run 呼び出しでデータを供給するために feed_dicts を使用する (そして関連してバッチ処理、バケット処理、そしてデータの操作を遂行する) 代わりに、stateful iterator オブジェクトを使用します。これらの iterator は入力パイプラインを単一マシンと分散設定の両者において遥かにより簡単にします。次のセクションで (TensorFlow 1.2 で導入された) 新しい入力データパイプラインをカバーします。
データ入力パイプライン
TensorFlow 1.2 の前には、ユーザは TensorFlow 訓練と評価パイプラインにデータを供給するために 2 つのオプションを持ちました :
- 各訓練 session.run 呼び出しで feed_dict を通してデータを直接供給する。
- tf.train (e.g. tf.train.batch) と tf.contrib.train の queueing メカニズムを使用する。
- tf.contrib.learn or tf.contrib.slim のような高位フレームワークからのヘルパーを使用する (これは事実上 #2 を使用しています)。
最初のアプローチは TensorFlow に精通していないか、Python でのみ行える変わった入力変更を行なう (i.e. 彼ら自身のミニバッチ queueing) 必要があるユーザのためにより容易です。2 番目と 3 番目のアプローチはより標準的ですが少し柔軟性に欠けます : それらは複数の python スレッドを開始することも要求します (queue runners)。更に、誤って使用される場合、キューはデッドロックか不明瞭なエラーメッセージに繋がります。それにもかかわらず、キューは feed_dict を使用するよりも著しくより効率的で単一マシンと分散訓練の両者に対して標準的です。
TensorFlow 1.2 からは、データを TensorFlow モデルに読み込むために利用可能な新しいシステムがあります : tf.data モジュールで見つかる、データセット iterator です。データ iterator は柔軟で、それについて考えて操作することが容易で、TensorFlow C++ ランタイムを利用して効率性とマルチスレッドを提供します。
dataset はバッチデータ Tensor、ファイル名、または複数のファイル名を含む Tensor から作成できます。幾つかのサンプルです :
# Training dataset consists of multiple files. train_dataset = tf.data.TextLineDataset(train_files) # Evaluation dataset uses a single file, but we may # point to a different file for each evaluation round. eval_file = tf.placeholder(tf.string, shape=()) eval_dataset = tf.data.TextLineDataset(eval_file) # For inference, feed input data to the dataset directly via feed_dict. infer_batch = tf.placeholder(tf.string, shape=(num_infer_examples,)) infer_dataset = tf.data.Dataset.from_tensor_slices(infer_batch)
総ての dataset は入力処理を通して同様に扱えます。これはデータの読み込みとクリーンアップ、(訓練と評価の場合) バケット処理、フィルタリング、そしてバッチ処理を含みます。
各センテンスを単語文字列のベクトルに変換するため、例えばですが、dataset map 変換を使用します :
dataset = dataset.map(lambda string: tf.string_split([string]).values)
それから各センテンス・ベクトルをベクトルとその動的長さを含むタプルに切り替えることができます :
dataset = dataset.map(lambda words: (words, tf.size(words))
最後に、各センテンス上で語彙検索を遂行することができます。検索テーブル・オブジェクト・テーブルが与えられる場合、この map は最初のタプル要素を文字列ベクトルから整数ベクトルに変換します。
dataset = dataset.map(lambda words, size: (table.lookup(words), size))
2 つのデータセットの結合もまた簡単です。もし 2 つのファイルが互いの line-by-line 変換を含みそして各々一つがそれ自身のデータセットに読み込まれる場合、zipped lines のタプルを含む新しい dataset が以下を通して作成されます :
source_target_dataset = tf.data.Dataset.zip((source_dataset, target_dataset))
可変長のセンテンスのバッチ処理もストレートです。次の変換は source_target_dataset から batch_size 要素をバッチ処理し、各バッチでソースとターゲット・ベクトルをそれぞれ最も長いソースとターゲット・ベクトルの長さにパディングします。
batched_dataset = source_target_dataset.padded_batch( batch_size, padded_shapes=((tf.TensorShape([None]), # source vectors of unknown size tf.TensorShape([])), # size(source) (tf.TensorShape([None]), # target vectors of unknown size tf.TensorShape([]))), # size(target) padding_values=((src_eos_id, # source vectors padded on the right with src_eos_id 0), # size(source) -- unused (tgt_eos_id, # target vectors padded on the right with tgt_eos_id 0))) # size(target) -- unused
この dataset から吐かれた値はネストされたタプルでその tensor はサイズ batch_size の一番左の次元を持ちます。その構造は :
- iterator[0][0] はバッチ化されてパディングされたソース・センテンス行列を持ちます。
- iterator[0][1] はバッチ化されたソース・サイズ・ベクトルを持ちます。
- iterator[1][0] はバッチ化されてパディングされたターゲット・センテンス行列を持ちます。
- iterator[1][1] はターゲット・サイズ・ベクトルを持ちます。
最後に、同じようなサイズのソース・センテンスを一緒にバッチ化するバケット処理もまた可能です。
より詳細と完全な実装のためにはファイル utils/iterator_utils.py を見てください。
Dataset からデータを読むにはコードの 3 行が必要です : iterator を作成し、その値を取得して、そしてそれを初期化します。
batched_iterator = batched_dataset.make_initializable_iterator() ((source, source_lengths), (target, target_lengths)) = batched_iterator.get_next() # At initialization time. session.run(batched_iterator.initializer, feed_dict={...})
iterator がひとたび初期化されれば、ソースまたはターゲット・テンソルにアクセスする総ての session.run 呼び出しは基礎的な dataset から次のミニバッチを要求するでしょう。
より良い NMT モデルのための他の詳細
Bidirectional RNN
エンコーダ側の双方向性は一般的に (より多くの層が使用されるので何某かの速度の低下を伴い) 良いパフォーマンスを与えます。ここでは単一の bidirectional 層を持つエンコーダをどのように構築するかの単純化されたサンプルを与えます :
# Construct forward and backward cells forward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units) backward_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units) bi_outputs, encoder_state = tf.nn.bidirectional_dynamic_rnn( forward_cell, backward_cell, encoder_emb_inp, sequence_length=source_sequence_length, time_major=True) encoder_outputs = tf.concat(bi_outputs, -1)
変数 encoder_outputs と encoder_state はエンコーダのセクションと同じ方法で使用できます。マルチ bidirectional 層のためには、encoder_state を少し操作する必要があることに注意してください、より詳細のためには model.py, メソッド _build_bidirectional_rnn() を見てください。
ビーム・サーチ
greedy デコーディングが非常に合理的な翻訳品質を与える一方で、ビーム・サーチ・デコーダはパフォーマンスを更にブーストします。ビーム・サーチの考えは、翻訳時にトップ候補の小さなセット回りを保持することにより総ての可能な翻訳の検索空間をより良く探検することです。ビームのサイズはビーム幅 (= beam width) と呼ばれます : 例えばサイズ 10 の最小のビーム幅で一般的には十分です。更なる情報のためには、読者は Neubig, (2017) のセクション 7.2.3 を参照してください。ここにはビーム・サーチがどのように成されるかのサンプルがあります :
# Replicate encoder infos beam_width times decoder_initial_state = tf.contrib.seq2seq.tile_batch( encoder_state, multiplier=hparams.beam_width) # Define a beam-search decoder decoder = tf.contrib.seq2seq.BeamSearchDecoder( cell=decoder_cell, embedding=embedding_decoder, start_tokens=start_tokens, end_token=end_token, initial_state=decoder_initial_state, beam_width=beam_width, output_layer=projection_layer, length_penalty_weight=0.0) # Dynamic decoding outputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)
デコーダのセクションと同様に、同じ dynamic_decode() API 呼び出しが使用されることに注意してください。ひとたびデコードされれば、次のように翻訳にアクセスできます :
translations = outputs.predicted_ids # Make sure translations shape is [batch_size, beam_width, time] if self.time_major: translations = tf.transpose(translations, perm=[1, 2, 0])
更なる詳細のためには model.py, メソッド _build_decoder() を見てください。
ハイパーパラメータ
追加のパフォーマンスに繋がることができる幾つかのハイパーパラメータがあります。ここでは、私達獅子にの経験に基づいて幾つかをリストします [ Disclaimers: 他の人は私達が書いたことに同意しないかもしれません! ]。
Optimizer: Adam が “unfamiliar” アーキテクチャに対して合理的な結果に繋がることができる一方で、もし SGD で訓練可能であればスケジューリングを伴う SGD は一般的により良いパフォーマンスに繋がるでしょう。
Attention: Bahdanau-スタイル attention は、上手く動作するためにはしばしばエンコーダ側で双方向性を要求します ; その一方で Luong-スタイル attention は異なる設定のために上手く動作する傾向があります。このチュートリアル・コードのためには、Luong & Bahdanau スタイル attention の 2 つの改良種を使用することを推奨します : scaled_luong & normed bahdanau です。
マルチ-GPU 訓練
NMT モデルの訓練は数日間かかるかもしれません。異なる RNN 層を異なる GPU 上に置けば訓練スピードを改善できます。マルチ GPU 上に RNN 層を作成するサンプルがここにあります。
cells = [] for i in range(num_layers): cells.append(tf.contrib.rnn.DeviceWrapper( tf.contrib.rnn.LSTMCell(num_units), "/gpu:%d" % (num_layers % num_gpus))) cell = tf.contrib.rnn.MultiRNNCell(cells)
更に、勾配計算を並列化するためには tf.gradients で colocate_gradients_with_ops オプションを有効にする必要があります。attention ベースの NMT モデルのスピード改良は GPU 数の増加につれて非常に小さいことに気がつくかもしれません。標準的な attention アーキテクチャの一つの大きな欠点は各 time ステップで attention に query するためにトップ (最終) 層の出力を使用することです。これは各デコーディング・ステップはその前のステップが完全に終了するのを待たなければならないことを意味します : それ故に、RNN 層をマルチ GPU 上に単純に置くことではデコーディング・プロセスを並列化できません。
GNMT attention アーキテクチャ は attention に query するためにボトム (最初の) 層の出力を使用してデコーダの計算を並列化します。従って、各デコーディング・ステップはその前の最初の層と attention 計算が終了すればすぐに開始できます。そのアーキテクチャを tf.contrib.rnn.MultiRNNCell のサブクラス、GNMTAttentionMultiCell で実装しました。GNMTAttentionMultiCell でどのようにデコーダ・セルを作成するかのサンプルがここにあります。
cells = [] for i in range(num_layers): cells.append(tf.contrib.rnn.DeviceWrapper( tf.contrib.rnn.LSTMCell(num_units), "/gpu:%d" % (num_layers % num_gpus))) attention_cell = cells.pop(0) attention_cell = tf.contrib.seq2seq.AttentionWrapper( attention_cell, attention_mechanism, attention_layer_size=None, # don't add an additional dense layer. output_attention=False,) cell = GNMTAttentionMultiCell(attention_cell, cells)
(訳注: ベンチマークについては原文の該当セクション: Benchmarks を参照してください。)
以上