GitHubじゃ!Pythonじゃ!

GitHubからPython関係の優良リポジトリを探したかったのじゃー、でも英語は出来ないから日本語で読むのじゃー、英語社会世知辛いのじゃー

tensorflow

nmt – TensorFlow神経機械翻訳チュートリアル

投稿日:

TensorFlow神経機械翻訳チュートリアル

神経機械翻訳(seq2seq)チュートリアル

著者:Thang Luong、Eugene Brevdo、Rui Zhao( Google Research BlogpostGithub

このバージョンのチュートリアルにはTensorFlow Nightlyが必要です 安定したTensorFlowバージョンを使用するには、 tf-1.4などの他のブランチを検討してください。

あなたの研究にこのコードベースを利用するなら、これを引用してください。

前書き

シーケンス変換(seq2seq)モデル( Sutskever et al。、2014Cho et al。、2014 )は、機械翻訳、音声認識、テキスト要約などのさまざまなタスクで大きな成功を収めています。 このチュートリアルでは、読者にseq2seqモデルの完全な理解を提供し、競争力のあるseq2seqモデルを最初から構築する方法を示します。 私たちは、野生の成功を収めた seq2seqモデルの最初のテストベッドであったニューラルマシントランスレーション(NMT)の作業に焦点を当てます。 含まれているコードは、軽量で高品質で、プロダクションの準備ができており、最新の研究アイデアに組み込まれています。 私たちは、

  1. 最近のデコーダ/アテンションラッパーAPIを使用して 、TensorFlow 1.2データイテレータ
  2. recurrentおよびseq2seqモデルの構築に関する当社の強力な専門知識を取り入れる
  3. 最高のNMTモデルを構築し、 GoogleのNMT(GNMT)システムを複製するためのヒントを提供します。

人々が簡単に複製できるベンチマークを提供することが重要だと考えています。 その結果、私たちは完全な実験結果を提供し、次の公に利用可能なデータセットでモデルを事前に試しました:

  1. 小規模IWSLT評価キャンペーンで提供されたTED協議の英語 – ベトナム語の並列コーパス(133K文ペア)。
  2. 大規模WMT評価キャンペーンで提供されたドイツ語 – 英語の並列コーパス(4.5M文ペア)。

まず、NMTのseq2seqモデルに関する基本的な知識を構築し、バニラNMTモデルを構築してトレーニングする方法を説明します。 第2部では、注意メカニズムを備えた競争的なNMTモデルの構築について詳しく説明します。 次に、TensorFlowのベストプラクティス(バッチ処理、バケット化)、双方向RNN、ビーム探索、GNMTの注意を払って複数のGPUにスケールアップするなど、可能な限り最高のNMTモデル(速度と翻訳品質の両方)を構築するヒントとテクニックについて説明します。

ベーシック

神経機械翻訳の背景

昔、伝統的なフレーズベースの翻訳システムは、原文を複数のチャンクに分割してフレーズごとに翻訳して、その作業を行いました。 これは翻訳結果の不透明さを招き、人間、翻訳者とはまったく似ていませんでした。 原文全体を読み、その意味を理解し、翻訳を作成します。 ニューラルマシン翻訳(NMT)はそれを模倣しています!


図1. エンコーダ/デコーダアーキテクチャ – NMTの一般的なアプローチの例 エンコーダは、原文をデコーダを通過して翻訳を生成する「意味のある」ベクトルに変換する。

具体的には、NMTシステムはまず、 エンコーダを使用して原文を読み取り、文の意味を表す一連の数字「思考」ベクトルを構築する。 デコーダは 、図1に示すように、センテンスベクトルを処理して翻訳を出力する。これは、しばしばエンコーダ – デコーダアーキテクチャと呼ばれる。 このようにして、NMTは従来のフレーズベースのアプローチでローカル翻訳の問題に取り組んでいます。すなわち、ジェンダーアグリーメントなどの言語による長期依存性を取得できます。 構文構造。 Google Neural Machine Translationシステムで実証されているように、はるかに流暢な翻訳を生成します

NMTモデルは、正確なアーキテクチャの点で異なります。 シーケンシャルデータの自然な選択は、ほとんどのNMTモデルで使用されるリカレントニューラルネットワーク(RNN)です。 通常、エンコーダとデコーダの両方にRNNが使用されます。 しかしながら、RNNモデルは、(a) 指向性 – 単方向性または双方向性; (b) 深さ – 単層または多層。 (c) タイプ – 多くの場合、バニラRNN、ロング短期記憶(LSTM)、またはゲート再帰ユニット(GRU)のいずれか。 興味のある読者は、このブログの記事で RNNとLSTMに関する詳細を見つけることができます。

このチュートリアルでは、単方向性を持ち、繰り返し単位としてLSTMを使用する深層マルチレイヤRNNを例として考えます。 そのようなモデルの例を図2に示します。この例では、ソース文「私は学生です」をターゲット文「Je suisétudiant」に翻訳するためのモデルを作成します。 高レベルでは、NMTモデルは、2つのリカレントニューラルネットワークで構成されています。 エンコーダ RNNは、予測を行わずに単に入力ソースワードを消費します。 一方、 デコーダは、次の単語を予測しながら、目標文を処理する。

詳細については、このチュートリアルが基づいているLuong(2016)に読者を紹介します。


図2. ニューラルマシンの翻訳 – ソース文「私は学生です」をターゲット文「Je suisétudiant」に翻訳するために提案された深刻な繰り返しアーキテクチャの例。 ここで、 “<s>”はデコード処理の開始を示し、 “</ s>”はデコーダに停止を指示します。

チュートリアルのインストール

このチュートリアルをインストールするには、システムにTensorFlowをインストールする必要があります。 このチュートリアルではTensorFlow Nightlyが必要です。 TensorFlowをインストールするには、 ここインストール手順に従ってください

TensorFlowがインストールされたら、次のコマンドを実行してこのチュートリアルのソースコードをダウンロードできます。

git clone https://github.com/tensorflow/nmt/

トレーニング – 最初のNMTシステムを構築する方法

最初に具体的なコードスニペットを使ってNMTモデルを構築してみましょう。図2を詳しく説明します。 私たちはデータ準備と完全なコードを後で延期します。 この部分はmodel.pyというファイルを参照します。

最下層では、エンコーダおよびデコーダのRNNは、入力として、最初に原文を、続いてエンコーディングからデコードモードへの遷移を示す境界マーカ「<s>」およびターゲット文を入力として受け取る。 訓練のために、システムに次のテンソルを供給します。テンソルは時間軸の形式であり、単語インデックスを含みます。

  • encoder_inputs [max_encoder_time、batch_size]:ソース入力単語。
  • decoder_inputs [max_decoder_time、batch_size]:ターゲット入力単語。
  • decoder_outputs [max_decoder_time、batch_size]:ターゲット出力ワード。これは、右端に文末タグが付加された、1回のタイムステップだけ左にシフトされたdecoder_inputsです。

効率を上げるために、複数の文章(batch_size)を一度に練習します。 テストは若干異なりますので、後で説明します。

埋め込み

単語のカテゴリ的な性質が与えられると、モデルは、まず、対応する単語表現を検索するために、ソースおよびターゲット埋め込みを検索しなければならない。 この埋め込みレイヤーが機能するためには、最初に各言語の語彙が選択されます。 通常、語彙サイズVが選択され、最も頻度の高いV語だけが一意性として扱われる。 他のすべての単語は「未知の」トークンに変換され、すべて同じ埋め込みを取得します。 埋め込みウェイトは、言語ごとに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_decoderdecoder_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)

文の長さは計算の浪費を避けるために異なっていることに注意してください。source_sequence_lengthを通して正確なソース文の長さをdynamic_rnnに伝えます 私たちの入力は時間のメジャーなので、 time_major = Trueを設定します ここでは、単一層LSTM、 encoder_cellだけを構築します。 多層LSTMを構築し、ドロップアウトを追加し、後のセクションで注意を引く方法について説明します。

デコーダ

デコーダはソース情報へのアクセスも必要とし、それを達成するための簡単な方法の1つは、エンコーダの最後の隠れた状態であるencoder_stateで初期化することです。 図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_cellencoder_cellに似ています)、 ヘルパー 、および前のencoder_stateを入力として受け取ります。 デコーダとヘルパーを分離することで、異なるコードベースを再利用することができます。たとえば、 GreedyEmbeddingHelperTrainingHelperに置き換えて、貪欲なデコードを行うことができます。 helper.pyの詳細を参照してください。

最後に、頂点の隠れ状態を次元Vのロジットベクトルに変えるための密行列であるprojection_layerについては言及していません。このプロセスを図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でマスクします。

重要な注意点 :損失をbatch_sizeで割ることを指摘する価値はあるので、hyper_parameterはbatch_sizeに対して “不変”です。 いくつかの人々は、( batch_size * num_time_steps )でロスを分けます。これは、短い文章の誤りを演奏します。 微妙に、私たちのハイパーパラメータ(前者の方法に適用された)は、後者の方法では使用できません。 たとえば、両方のアプローチが1.0の学習でSGDを使用する場合、後者のアプローチでは、1 / num_time_stepsのはるかに小さい学習速度が効果的に使用されます。

勾配計算と最適化

NMTモデルのフォワードパスを定義しました。 バックプロパゲーションパスを計算することは、数行のコードの問題です。

# 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のトレーニングで重要なステップの1つは、グラジエントクリッピングです。 ここでは、グローバルな基準でクリップします。 max_max_gradient_normは5または1のような値に設定されることがよくあります。最後のステップはオプティマイザを選択することです。 Adamオプティマイザは一般的な選択です。 学習率も選択します。 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講演コーパス (133Kの訓練例)を使用します。 ここで使用したすべてのデータはhttps://nlp.stanford.edu/projects/nmt/にあります 私たちはdevデータセットとしてtst2012を使用し、テストデータセットとして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

上記のコマンドは、2層のLSTM seq2seqモデルを128ディメンションの隠しユニットと12エポックの埋め込みで構成します。 我々は0.2のドロップアウト値を用いる(確率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: </s> </s> Đ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 the <unk> distilled from the theories of Karl Marx . </s> </s> </s>
  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モデルを訓練している間(かつ訓練されたモデルを持っていれば)、以前に見えなかった原文を与えられた翻訳を得ることができます。 このプロセスを推論と呼びます。 トレーニングと推論( テスト )の間に明確な区別があります 。推論時には、ソース文、つまり、 encoder_inputsにしかアクセスできません。 デコードを実行するには多くの方法があります。 デコード方法には、グリーディ、サンプリング、およびビームサーチデコードが含まれます。 ここでは、貪欲なデコード戦略について説明します。

アイデアは簡単で、図3にそれを示します。

  1. 発明者らは、 エンコーディング状態を得るために、訓練中と同じようにソース文を依然としてエンコードし、このエンコーダ状態は、デコーダを初期化するために使用される。
  2. デコード(翻訳)プロセスは、デコーダが開始シンボル “<s>”を受け取るとすぐに開始されます(コードではtgt_sos_idを参照)。
  3. デコーダ側の各タイムステップについて、RNNの出力を一連のロジットとして扱います。 我々は最も可能性の高い単語、最大ロジット値に関連付けられたIDを放出された単語として選択する(これは「欲張りな」行動である)。 例えば、図3では、単語「moi」は、第1の復号化ステップにおいて最も高い翻訳確率を有する。 この単語を次のタイムステップへの入力としてフィードします。
  4. この処理は、文末記号「</ s>」が出力記号として生成されるまで継続されます(コードではtgt_eos_idを参照してください)。


図3. グリーディ復号化 – 訓練されたNMTモデルが、欲張り検索を使用してソース文「Je suisétudiant」の翻訳をどのように生成するかの例。

ステップ3は、推論を訓練とは異なるものにするものです。 推論では常に正しいターゲット単語を入力として入力するのではなく、モデルによって予測された単語を使用します。 貪欲なデコードを達成するためのコードは次のとおりです。 これはトレーニングデコーダに非常によく似ています。

# 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を使用します。 あらかじめターゲット配列の長さがわからないため、translation_lengthを制限するためにmaximum_iterationsを使用します。 1つのヒューリスティックは、原文の長さの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モデルを見て、もっと進んでみましょう! 最先端の神経機械翻訳システムを構築するために、我々はより多くの「秘密のソース」を必要とするでしょう: 2015年にBahdanauらによって最初に導入され、その後Luongらによって洗練された注意メカニズム 、2015その他。 注目メカニズムの重要なアイデアは、翻訳する際に関連するソースコンテンツに「注意」を払うことによって、ターゲットとソースとの間の直接的なショートカット接続を確立することです。 アテンションメカニズムの素晴らしい副産物は、ソース文とターゲット文の間の視覚化が容易なアラインメントマトリックスです(図4参照)。


図4. 注意の視覚化原文と目標文の間のアラインメントの例。 イメージは(Bahdanau et al。、2015)から取られている。

バニラseq2seqモデルでは、デコード処理を開始するときに、最後のソース状態をエンコーダからデコーダに渡すことに注意してください。 これは短い文章や中程度の文章ではうまくいきます。 しかし、長い文章では、単一の固定サイズの隠れた状態が情報のボトルネックになります。 ソースRNNで計算されたすべての隠しステートを破棄するのではなく、アテンションメカニズムは、デコーダがステレオスニーカーを見ることができるアプローチを提供します(ソース情報の動的メモリとして扱います)。 そうすることによって、注意メカニズムは、より長い文章の翻訳を改善する。 現在、注意メカニズムはデファクトスタンダードであり、画像キャプション生成、音声認識、テキスト要約などの他の多くのタスクにも首尾よく適用されています。

アテンション機構の背景

ここでは、 OpenNMTなどのオープンソースのツールキットや、これのTF seq2seq APIを含むいくつかの最新のシステムで使用されている(Luong et al。、2015)で提案された注意メカニズムのインスタンスについて説明します。チュートリアル。 また、アテンション機構の他の変種への接続も提供します。


図5. 注意機構 – 注目に基づくNMTシステムの例(Luong et al。、2015)。 我々は、注意の計算の第一歩を詳細に強調する。 わかりやすくするために、図2の埋込み層と投影層は示していません。

図5に示すように、アテンション計算はすべてのデコーダ時間ステップで行われる。 それは以下の段階で構成されています。

  1. 現在のターゲット隠れ状態をすべてのソース状態と比較して注目重みを導出する(図4のように視覚化することができる)。
  2. 注目重みに基づいて、ソース状態の加重平均としてコンテキストベクトルを計算する。
  3. コンテクストベクトルを現在のターゲット隠れ状態と組み合わせて最終的な注意ベクトルを生成する
  4. 注意ベクトルは、次の時間ステップ( 入力供給 )への入力として供給される。 最初の3つのステップは以下の式で要約できます。

ここで、関数scoreは、ターゲット隠れ状態$$ h_t $$を各ソース隠れ状態$$ \ overline {h} _s $$と比較するために使用され、その結果は、生成された注意重み付け(ソースポジション)。 スコアリング機能にはさまざまな選択肢があります。 一般的なスコアリング関数には、式(9)で与えられる乗法型と加法型が含まれます。 (4)。 計算されると、attentionmax $$ a_t $$がsoftmaxのロジットとロスを導出するために使用されます。 これは、バニラseq2seqモデルの最上層のターゲット非表示状態に似ています。 関数fは、他の形式をとることもできます。

attention_wrapper.pyには、注目メカニズムのさまざまな実装があります。

注目の仕組みには何が重要ですか?

上記の式に示唆されるように、多くの異なる注意変形が存在する。 これらの変種は、スコアリング関数と注意関数の形式と、元の状態$$ h_ {t-1} $$が、$$ h_t $$の代わりに(Bahdanau et al。、2015)。 経験的に、特定の選択だけが重要であることがわかった。 第一に、注意の基本的な形態、すなわちターゲットとソースとの間の直接的な接続が存在する必要がある。 第2に、注目ベクトルを次のタイムステップに供給して、ネットワークに過去の注意の決定を通知することが重要です(Luong et al。、2015)。 最後に、得点機能の選択は、しばしば異なる性能をもたらすことがある。 ベンチマーク結果の節を参照してください。

アテンションラッパーAPI

AttentionWrapperの実装では、 (Weston et al。、2015)からメモリネットワーク上のいくつかの用語を借用しています このチュートリアルでは、読み書き可能なメモリを持つ代わりに、 読み出し専用のメモリを使用しています。 具体的には、Luongのスコアリングスタイルでは$$ W \ overline {h} _s $$、Bahdanauのスコアリングスタイルでは$$ W_2 \ overline {h} _s $$などのソース隠れ状態の集合(またはそれらの変換されたバージョン) “メモリ”として。 各時間ステップでは、現在のターゲット隠れ状態を「照会」として使用して、メモリーのどの部分を読み取るかを決定します。 通常、クエリは個々のメモリスロットに対応するキーと比較する必要があります。 上記の注意メカニズムの提示では、Bahdanauのスコアリングスタイルで$$ W_1h_t $$などのソース隠れ状態のセット(または変換されたバージョン)を「キー」として使用します。 他の形式の注意を引き出すために、このメモリネットワーク用語に触発されることができます!

注意ラッパーのおかげで、私たちのバニラseq2seqコードを注目して拡張するのは簡単です。 この部分はファイルattention_model.pyを参照しています

まず、(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セクションでは、 encoder_outputsは最上位レイヤーのすべてのソース隠れ状態のセットで、 [max_time、batch_size、num_units]という形をしています(効率のためにtime_majorTrueに設定してdynamic_rnnを使用しているため)。 注意の仕組みについては、渡された「メモリ」がバッチメジャーであることを確認する必要があるため、 attention_statesをトランスポーズする必要があります。 注意の重みが正しく正規化されるように、 source_sequence_lengthをattentionメカニズムに渡します (パディング以外の位置のみ)。

アテンション機構を定義したら、 AttentionWrapperを使用してデコードセルをラップします:

decoder_cell = tf.contrib.seq2seq.AttentionWrapper(
    decoder_cell, attention_mechanism,
    attention_layer_size=num_units)

残りのコードはSection Decoderとほぼ同じです!

ハンズオン – 注目NMTモデルの構築

注意を引くには、トレーニング中のattentionフラグの値としてscaled_luongbahdanauscaled_luong 、またはnormed_bahdanau bahdanauかを使用する必要があります。 フラグは、どの注意メカニズムを使用するかを指定します。 さらに、注目モデル用の新しいディレクトリを作成する必要があるため、以前に訓練された基本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と同じinferenceコマンドを使うことができます:

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つの別々のグラフを作成することが最良です。

  • トレーニンググラフは次のとおりです。

    • バッチ、バケット、場合によってはサブサンプルは、ファイル/外部入力のセットからデータを入力します。
    • フォワードオペレーションとバックプロップオペレーションが含まれます。
    • オプティマイザを構築し、トレーニングオペレーションを追加します。
  • Evalグラフ:

    • バッチとバケットは、一連のファイル/外部入力からデータを入力します。
    • トレーニングのフォワード操作と、トレーニングに使用されない追加の評価操作を含みます。
  • 推測グラフ。

    • バッチ入力データではありません。
    • 入力データをサブサンプリングまたはバケット処理しません。
    • プレースホルダから入力データを読み込みます(データは、 feed_dictまたはC ++のTensorFlow提供バイナリからグラフに直接入力できます)。
    • モデルforward opのサブセットと、場合によってはsession.run呼び出し間の状態を格納するための追加の特殊入出力を含みます。

別々のグラフを作成することにはいくつかの利点があります。

  • 推論グラフは、通常、他の2つのグラフとは非常に異なるため、別々に構築するのが理にかなっています。
  • evalグラフは、追加のバックプロップ操作をすべてもはや持たないため、より簡単になります。
  • データ供給は、グラフごとに個別に実施することができる。
  • 変数の再利用はずっと簡単です。 たとえば、evalグラフでは、トレーニングモデルがこれらの変数を既に作成しているため、 reuse = Trueで変数スコープを再度開く必要はありません。 したがって、同じコードを再利用=引数をどこにでも振りかざすことなく再利用することができます。
  • 分散訓練では、別々の労働者に訓練、評価、推論を行わせるのが一般的です。 彼らはとにかく自分のグラフを構築する必要があります。 このようにシステムを構築することで、分散型トレーニングの準備が整います。

複雑さの主な原因は、1つのマシン設定で3つのグラフに変数を共有する方法になります。 これは、グラフごとに別々のセッションを使用することで解決されます。 トレーニングセッションは定期的にチェックポイントを保存し、evalセッションと推論セッションはチェックポイントからパラメータを復元します。 以下の例は、2つのアプローチの主な違いを示しています。

Before:1つのグラフで3つのモデルを共有し、1つのセッションを共有する

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)

後者のアプローチが分散バージョンに変換される “準備ができている”ことに注目してください。

新しいアプローチのもう1つの相違点は、 feed_dictsを使用して各session.run呼び出しでデータを供給するのではなく、独自のバッチ処理、バケット処理、およびデータの操作を行う代わりに、ステートフルなイテレータオブジェクトを使用する点です。 これらのイテレータは、単一マシンと分散環境の両方で入力パイプラインをはるかに簡単にします。 次のセクションでは、新しい入力データパイプライン(TensorFlow 1.2で導入されたもの)について説明します。

データ入力パイプライン

TensorFlow 1.2より前のユーザーには、TensorFlowトレーニングパイプラインと評価パイプラインにデータを供給するための2つのオプションがありました。

  1. 各training session.runコールでfeed_dictを介して直接データをフィードします。
  2. tf.train (例えば、 tf.train.batch )とtf.contrib.trainの待ち行列メカニズムを使用してください。
  3. tf.contrib.learntf.contrib.slim (#2を効果的に使用する)のような上位レベルのフレームワークのヘルパーを使用してください。

The first approach is easier for users who aren’t familiar with TensorFlow or need to do exotic input modification (ie, their own minibatch queueing) that can only be done in Python. The second and third approaches are more standard but a little less flexible; they also require starting multiple python threads (queue runners). Furthermore, if used incorrectly queues can lead to deadlocks or opaque error messages. Nevertheless, queues are significantly more efficient than using feed_dict and are the standard for both single-machine and distributed training.

Starting in TensorFlow 1.2, there is a new system available for reading data into TensorFlow models: dataset iterators, as found in the tf.data module. Data iterators are flexible, easy to reason about and to manipulate, and provide efficiency and multithreading by leveraging the TensorFlow C++ runtime.

A dataset can be created from a batch data Tensor, a filename, or a Tensor containing multiple filenames. いくつかの例:

# 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)

All datasets can be treated similarly via input processing. This includes reading and cleaning the data, bucketing (in the case of training and eval), filtering, and batching.

To convert each sentence into vectors of word strings, for example, we use the dataset map transformation:

dataset = dataset.map(lambda string: tf.string_split([string]).values)

We can then switch each sentence vector into a tuple containing both the vector and its dynamic length:

dataset = dataset.map(lambda words: (words, tf.size(words))

Finally, we can perform a vocabulary lookup on each sentence. Given a lookup table object table, this map converts the first tuple elements from a vector of strings to a vector of integers.

dataset = dataset.map(lambda words, size: (table.lookup(words), size))

Joining two datasets is also easy. If two files contain line-by-line translations of each other and each one is read into its own dataset, then a new dataset containing the tuples of the zipped lines can be created via:

source_target_dataset = tf.data.Dataset.zip((source_dataset, target_dataset))

Batching of variable-length sentences is straightforward. The following transformation batches batch_size elements from source_target_dataset , and respectively pads the source and target vectors to the length of the longest source and target vector in each batch.

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

Values emitted from this dataset will be nested tuples whose tensors have a leftmost dimension of size batch_size . The structure will be:

  • iterator[0][0] has the batched and padded source sentence matrices.
  • iterator[0][1] has the batched source size vectors.
  • iterator[1][0] has the batched and padded target sentence matrices.
  • iterator[1][1] has the batched target size vectors.

Finally, bucketing that batches similarly-sized source sentences together is also possible. Please see the file utils/iterator_utils.py for more details and the full implementation.

Reading data from a Dataset requires three lines of code: create the iterator, get its values, and initialize it.

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={...})

Once the iterator is initialized, every session.run call that accesses source or target tensors will request the next minibatch from the underlying dataset.

Other details for better NMT models

Bidirectional RNNs

Bidirectionality on the encoder side generally gives better performance (with some degradation in speed as more layers are used). Here, we give a simplified example of how to build an encoder with a single bidirectional layer:

# 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)

The variables encoder_outputs and encoder_state can be used in the same way as in Section Encoder. Note that, for multiple bidirectional layers, we need to manipulate the encoder_state a bit, see model.py , method _build_bidirectional_rnn() for more details.

ビームサーチ

While greedy decoding can give us quite reasonable translation quality, a beam search decoder can further boost performance. The idea of beam search is to better explore the search space of all possible translations by keeping around a small set of top candidates as we translate. The size of the beam is called beam width ; a minimal beam width of, say size 10, is generally sufficient. For more information, we refer readers to Section 7.2.3 of Neubig, (2017) . Here’s an example of how beam search can be done:

# 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, ...)

Note that the same dynamic_decode() API call is used, similar to the Section Decoder . Once decoded, we can access the translations as follows:

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])

See model.py , method _build_decoder() for more details.

ハイパーパラメータ

There are several hyperparameters that can lead to additional performances. Here, we list some based on our own experience [ Disclaimers: others might not agree on things we wrote! ]。

Optimizer : while Adam can lead to reasonable results for “unfamiliar” architectures, SGD with scheduling will generally lead to better performance if you can train with SGD.

Attention : Bahdanau-style attention often requires bidirectionality on the encoder side to work well; whereas Luong-style attention tends to work well for different settings. For this tutorial code, we recommend using the two improved variants of Luong & Bahdanau-style attentions: scaled_luong & normed bahdanau .

Multi-GPU training

Training a NMT model may take several days. Placing different RNN layers on different GPUs can improve the training speed. Here’s an example to create RNN layers on multiple GPUs.

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)

In addition, we need to enable the colocate_gradients_with_ops option in tf.gradients to parallelize the gradients computation.

You may notice the speed improvement of the attention based NMT model is very small as the number of GPUs increases. One major drawback of the standard attention architecture is using the top (final) layer’s output to query attention at each time step. That means each decoding step must wait its previous step completely finished; hence, we can’t parallelize the decoding process by simply placing RNN layers on multiple GPUs.

The GNMT attention architecture parallelizes the decoder’s computation by using the bottom (first) layer’s output to query attention. Therefore, each decoding step can start as soon as its previous step’s first layer and attention computation finished. We implemented the architecture in GNMTAttentionMultiCell , a subclass of tf.contrib.rnn.MultiRNNCell . Here’s an example of how to create a decoder cell with the 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

IWSLT English-Vietnamese

Train: 133K examples, vocab=vocab.(vi|en), train=train.(vi|en) dev=tst2012.(vi|en), test=tst2013.(vi|en), download script .

Training details . We train 2-layer LSTMs of 512 units with bidirectional encoder (ie, 1 bidirectional layers for the encoder), embedding dim is 512. LuongAttention (scale=True) is used together with dropout keep_prob of 0.8. All parameters are uniformly. We use SGD with learning rate 1.0 as follows: train for 12K steps (~ 12 epochs); after 8K steps, we start halving learning rate every 1K step.

Results .

Below are the averaged results of 2 models ( model 1 , model 2 ).
We measure the translation quality in terms of BLEU scores (Papineni et al., 2002) .

Systems tst2012 (dev) test2013 (test)
NMT (greedy) 23.2 25.5
NMT (beam=10) 23.8 26.1
(Luong & Manning, 2015) 23.3

Training Speed : (0.37s step-time, 15.3K wps) on K40m & (0.17s step-time, 32.2K wps) on TitanX .
Here, step-time means the time taken to run one mini-batch (of size 128). For wps, we count words on both the source and target.

WMT German-English

Train: 4.5M examples, vocab=vocab.bpe.32000.(de|en), train=train.tok.clean.bpe.32000.(de|en), dev=newstest2013.tok.bpe.32000.(de|en), test=newstest2015.tok.bpe.32000.(de|en), download script

Training details . Our training hyperparameters are similar to the English-Vietnamese experiments except for the following details. The data is split into subword units using BPE (32K operations). We train 4-layer LSTMs of 1024 units with bidirectional encoder (ie, 2 bidirectional layers for the encoder), embedding dim is 1024. We train for 350K steps (~ 10 epochs); after 170K steps, we start halving learning rate every 17K step.

Results .

The first 2 rows are the averaged results of 2 models ( model 1 , model 2 ). Results in the third row is with GNMT attention ( model ) ; trained with 4 GPUs.

Systems newstest2013 (dev) newstest2015
NMT (greedy) 27.1 27.6
NMT (beam=10) 28.0 28.9
NMT + GNMT attention (beam=10) 29.0 29.9
WMT SOTA 29.3

These results show that our code builds strong baseline systems for NMT.
(Note that WMT systems generally utilize a huge amount monolingual data which we currently do not.)

Training Speed : (2.1s step-time, 3.4K wps) on Nvidia K40m & (0.7s step-time, 8.7K wps) on Nvidia TitanX for standard models.
To see the speed-ups with GNMT attention, we benchmark on K40m only:

Systems 1 gpu 4 gpus 8 gpus
NMT (4 layers) 2.2s, 3.4K 1.9s, 3.9K
NMT (8 layers) 3.5s, 2.0K 2.9s, 2.4K
NMT + GNMT attention (4 layers) 2.6s, 2.8K 1.7s, 4.3K
NMT + GNMT attention (8 layers) 4.2s, 1.7K 1.9s, 3.8K

These results show that without GNMT attention, the gains from using multiple gpus are minimal.
With GNMT attention, we obtain from 50%-100% speed-ups with multiple gpus.

WMT English-German — Full Comparison

The first 2 rows are our models with GNMT attention: model 1 (4 layers) , model 2 (8 layers) .

Systems newstest2014 newstest2015
Ours — NMT + GNMT attention (4 layers) 23.7 26.5
Ours — NMT + GNMT attention (8 layers) 24.4 27.6
WMT SOTA 20.6 24.9
OpenNMT (Klein et al., 2017) 19.3
tf-seq2seq (Britz et al., 2017) 22.2 25.2
GNMT (Wu et al., 2016) 24.6

The above results show our models are very competitive among models of similar architectures.
[Note that OpenNMT uses smaller models and the current best result (as of this writing) is 28.4 obtained by the Transformer network (Vaswani et al., 2017) which has a significantly different architecture.]

Standard HParams

We have provided a set of standard hparams for using pre-trained checkpoint for inference or training NMT architectures used in the Benchmark.

We will use the WMT16 German-English data, you can download the data by the following command.

nmt/scripts/wmt16_en_de.sh /tmp/wmt16

Here is an example command for loading the pre-trained GNMT WMT German-English checkpoint for inference.

python -m nmt.nmt \
    --src=de --tgt=en \
    --ckpt=/path/to/checkpoint/translate.ckpt \
    --hparams_path=nmt/standard_hparams/wmt16_gnmt_4_layer.json \
    --out_dir=/tmp/deen_gnmt \
    --vocab_prefix=/tmp/wmt16/vocab.bpe.32000 \
    --inference_input_file=/tmp/wmt16/newstest2014.tok.bpe.32000.de \
    --inference_output_file=/tmp/deen_gnmt/output_infer \
    --inference_ref_file=/tmp/wmt16/newstest2014.tok.bpe.32000.en

Here is an example command for training the GNMT WMT German-English model.

python -m nmt.nmt \
    --src=de --tgt=en \
    --hparams_path=nmt/standard_hparams/wmt16_gnmt_4_layer.json \
    --out_dir=/tmp/deen_gnmt \
    --vocab_prefix=/tmp/wmt16/vocab.bpe.32000 \
    --train_prefix=/tmp/wmt16/train.tok.clean.bpe.32000 \
    --dev_prefix=/tmp/wmt16/newstest2013.tok.bpe.32000 \
    --test_prefix=/tmp/wmt16/newstest2015.tok.bpe.32000

その他のリソース

For deeper reading on Neural Machine Translation and sequence-to-sequence models, we highly recommend the following materials by Luong, Cho, Manning, (2016) ; Luong, (2016) ; and Neubig, (2017) .

There’s a wide variety of tools for building seq2seq models, so we pick one per language:
Stanford NMT https://nlp.stanford.edu/projects/nmt/ [Matlab]
tf-seq2seq https://github.com/google/seq2seq [TensorFlow]
Nemantus https://github.com/rsennrich/nematus [Theano]
OpenNMT http://opennmt.net/ [Torch]
OpenNMT-py https://github.com/OpenNMT/OpenNMT-py [PyTorch]

了承

We would like to thank Denny Britz, Anna Goldie, Derek Murray, and Cinjon Resnick for their work bringing new features to TensorFlow and the seq2seq library. Additional thanks go to Lukasz Kaiser for the initial help on the seq2seq codebase; Quoc Le for the suggestion to replicate GNMT; Yonghui Wu and Zhifeng Chen for details on the GNMT systems; as well as the Google Brain team for their support and feedback!

参考文献

BibTex

@article{luong17,
  author  = {Minh{-}Thang Luong and Eugene Brevdo and Rui Zhao},
  title   = {Neural Machine Translation (seq2seq) Tutorial},
  journal = {https://github.com/tensorflow/nmt},
  year    = {2017},
}







-tensorflow

執筆者: