TensorFlow教程翻譯 | Neural Machine Translation(seq2seq) Tutorial

寫在前面:讀TensorFlow的這篇官網教程,給了我很大的幫助,該教程對seq2seq模型在理論上和代碼實現上都有簡要介紹。感覺有必要翻譯一下做個記錄,文章很長,不會做到一字一句的翻譯,有些不好翻譯的地方我會給出原句,有不嚴謹的地方望諒解。

原文鏈接:Neural Machine Translation(seq2seq) Tutorial

本文目錄:

  • 前沿 | Introduction
  • 基礎 | Basic
  • 訓練- 如何構建我們的第一個NMT系統
    • 詞向量 | Embedding
    • 編碼器 | Encoder
    • 解碼器 | Decoder
    • 損失 | Loss
    • 梯度計算和優化 | Gradient computation & optimization
  • 實戰 - 訓練一個 NMT 模型
  • Inference - 如何得到翻譯結果
  • Intermediate
  • 注意力機制的基本知識
  • Attention Wrapper API
  • 實戰 - 構建一個 attention-based NMT 模型
  • 技巧和陷阱 | Tips & Tricks
  • 數據輸入管道 | Data Input Pipeline
  • 讓 NMT 模型更完美的其他技巧
    • 雙向 RNNs | Bidirectional RNNs
    • Beam Search
    • 超參數 | Hyperparameters
    • 多 GPU 訓練 | Multi-GPU training
  • Benchmarks
  • 其他資源 | Other resources
  • 參考資料 | References

該教程需要TensorFlow1.4版本。提醒:非1.4版本的TensorFlow有一個關於Beam Search的bug fix。

前言 | Introduction


Seq2seq 模型 (Sutskever et al., 2014, Cho et al., 2014) 在機器翻譯、語音識別、文本摘要等領域取得了巨大的成功。這篇教程將幫助讀者完全理解 seq2seq 模型並學會搭建一個完整的 seq2seq 模型。我們會以神經機器翻譯(NMT)為例,因為 seq2seq 模型最先應用於 NMT 並取得了成功。我們給出的代碼簡潔、質量高且可直接拿來使用,並且吸收了最新的研究成果。我們所做的工作包括:

  1. 使用最新的 decoder / attention wrapper API,TensorFlow 1.2 data iterator
  2. 在構建模型時加入了我們最成功的經驗
  3. 為構建最強大的 NMT 模型,我們提供了許多 tips 和 tricks,並複製了Google的 NMT系統

我們提供了一下語料:

  1. Small-scale: English-Vietnamese parallel corpus of TED talks (133K sentence pairs) provided by the IWSLT Evaluation Campaign.
  2. Large-scale: German-English parallel corpus (4.5M sentence pairs) provided by the WMT Evaluation Campaign.

我們首先為 NMT 介紹了關於seq2seq的基礎知識,解釋如何構建並訓練一個 vanilla NMT 模型。然後深入細節的詳細介紹如何構建一個完整的帶有注意力機制的 NMT 模型。然後我們將討論一些 ticks 和 tips,包括 batching, bucketing , bidirectional RNNs, beam search 以及如何使用多 GPU,這將幫助提高 NMT 模型的訓練速度和質量。

基礎 | Basic

神經機器翻譯的相關背景 | Background on Neural Machine Translation


傳統的 phrase-based 翻譯系統的工作原理是把源句子分成幾塊,然後一個短語一個短語的翻譯,但是翻譯結果的流暢度較差,並且不符合人類的翻譯方式,我們人類會讀取完整的句子,理解它的意思,然後進行翻譯。NMT 模型正是在模仿這個機制!

圖1. Encoder-decoder 結構 - encoder 把源句子轉化為一個「有意義」的vector,然後這個vector被decoder解碼,得到翻譯結果

NMT模型使用encoder讀取源句子,然後編碼得到一個「有意義」的 vector,即一串能代表源句子意思的數字。然後 decoder 將這個 vector 解碼得到翻譯,就想圖1展示的那樣。這就是所謂的 encoder-decoder 結構。用這種方式,NMT 解決了在傳統的 phrase-based 的翻譯模型中所存在的局部翻譯的問題(local translation problem),它能夠捕捉到語言中更長久的依賴關係,比如性別認同(gender agreements)、句法結構等,然後得到更流暢的翻譯結果。

NMT 模型有很多不同的結構,一個自然的選擇是使用 RNN,大部分的 NMT 模型都使用RNN。通常,encoder 和 decoder 都會使用 RNN。另外,不同的 RNN 模型也有一些差異:

(a)方向:單向的 或 雙向的;

(b)深度:單層的 或 多層的;

(c)類型:vanilla RNN 或 LSTM 或 GRU;

對此感興趣的讀者可以從這篇博客(Understanding LSTM)獲取更多關於 RNNs 或 LSTM 的知識。

本教程將以單向的多層(multi-layer) RNN 為例,其使用 LSTM 作為 RNN 單元。圖2展示了這樣一個模型的例子。這個例子中,我們構建了一個模型把源句子「I am a student」翻譯成目標句子「Je suis étudiant」。NMT 模型包含了兩個循環神經網路:encoder RNN 只對源句子進行編碼而沒有做任何的預測;decoder 在預測下一個單詞的時候使用了目標句子(譯者註:原句就是這麼說的,但是我想他所表達的意思應該是預測下一個單詞時用到了之前預測出的單詞)。

想要獲取更多的知識,請看Luong(2016),我們也是基於此完成此教程。

圖2. 神經機器翻譯 - 一個把源句子「I am a student」 翻譯成 「Je suis étudiant」的模型結構。其中""表示解碼的開始,「」表示解碼的結束。

訓練- 如何構建我們的第一個NMT系統

Traning - How to build our first NMT system


我們將用非常簡潔的代碼來更細緻的解釋圖2所示的 RNN 結構,並構建一個 NMT 模型。後面我們會給出數據準備和完整的代碼,這部分請參看 model.py。

在最底層,encoder 和 decoder RNNs 按照這樣的順序接收數據:首先是源句子,然後是標記「<s>」,該標記標誌著從編碼階段轉換到解碼階段,然後就是目標句子。在訓練過程中,我們將把以下 tensors feed 給系統,這些 tensors 都是 time-major 格式的,且其元素是句子索引:

  • encoder_inputs [max_encoder_time, batch_size]: source input words.
  • decoder_inputs [max_decoder_time, batch_size]: target input words.
  • decoder_outputs [max_decoder_time, batch_size]: target output words, 它們是 decoder_inputs 左移了一步,然後右邊加了一個標識句子結束的標識。

為了效率,我們會一次訓練多個(batch_size)句子。測試時稍有不同,我們後邊會討論。

譯者補充:所謂 time-major 格式的 tensor 是指把 max_encoder_time 放在第一維度,即 [max_encoder_time, batch_size],還有 batch-major 是把 batch_size 放在第一維度,即 [batch_size, max_encoder_time]。

詞向量 | Embedding

對於給定的自然語言的單詞,模型必須首先得到源句子和目標句子的詞向量,以檢索得到一致的詞向量表示(譯者註:此句翻譯的不好。原句是:Given the categorical nature of words, the model must first look up the source and target embeddings to retrieve the corresponding word representations)。要實現這個目標,每種語言都要有一個詞典,通常,詞典大小 V 是被人為設定的,並且只有頻率最高的 V 個單詞會被當做唯一,其他所有的單詞會被轉換為「unknown」,並且共享同一個詞向量。每種語言詞向量的值是不同的,這通常會在訓練過程中進行學習。

# Embeddingembedding_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 訓練的 vector 來初始詞向量。

編碼器 | Encoder

詞向量一旦被檢索到就會被輸入到神經網路中,該神經網路包含了兩個多層的 RNNs,一個與源語言相關的 encoder 和一個與目標語言相關的 decoder。原則上,這兩個 RNNs 可以共享參數;但是實際中,我們使用兩套不同的 RNN parameters(such models do a better job when fitting large training datasets)。Encoder RNN 使用零向量作為它的初始狀態(starting states),代碼如下:

# Build RNN cellencoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)# Run Dynamic RNN# encoder_outpus: [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,所以我們設置 time_major=True。這裡,我們只構建了一個單層的 LSTM,encoder_cell。在後面的章節中,我們將會講述如何構建一個多層的 LSTMs,還有使用 dropout 和 attention。

解碼器 | Decoder

Decoder 同樣需要用到源句子的信息,一個簡單的方法是使用 encoder 最後的隱藏層狀態 (hidden state), encoder_state,來初始化 decoder。在圖2中,我們把在源句子單詞「student」位置的隱藏層狀態(hidden state)傳給了 decoder。

# Build RNN celldecoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)

# Helperhelper = tf.contrib.seq2seq.TrainingHelper( decoder_emb_inp, decoder_lengths, time_major=True)# Decoderdecoder = tf.contrib.seq2seq.BasicDecoder( decoder_cell, helper, encoder_state, output_layer=projection_layer)# Dynamic decodingoutputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)logits = outputs.rnn_output

這部分代碼的核心是 BasicDecoder 的對象 decoderdecoder 接收了 decoder_cell(類似 encoder_cell)、一個 helper 以及 encoder_state 作為輸入。By separating out decoders and helpers, we can reuse different codebases(這句沒怎麼懂). GreedyEmbeddingHelper 可以替換 TrainingHelper 來實現 greedy decoding. 見代碼 helper.py 獲取更多。

最後,我們還有實現 projection_layer,這是一個稠密矩陣(dense matrix),用來把頂部的隱藏狀態(top hidden states)轉換為 V 維的 logit vecotrs。參照圖2的例子。

projection_layer = layers_core.Dense( tgt_vocab_size, use_bias=False)

損失 | Loss

有了上述的 logits,下面我們可以計算訓練損失了:

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 是一個 0-1 矩陣,其 size 與 decoder_outputs 相同。它用0來 mask 那些超過目標句子長度的 padding 的位置。

Important note: Its worth pointing out that we divide the loss by batch_size, so our hyperparameters are "invariant" to batch_size. Some people divide the loss by (batch_size * num_time_steps), which plays down the errors made on short sentences. More subtly, our hyperparameters (applied to the former way) cant be used for the latter way. For example, if both approaches use SGD with a learning of 1.0, the latter approach effectively uses a much smaller learning rate of 1 / num_time_steps.

重要提示:需要指出的是我們用 loss 除了個 batch_size,所以我們的超參數對 batch_size 來講是「不變的」(不相關的)。有些人用 loss 除以 (batch_size * num_time_steps),這樣做會減少在短句子上產生的錯誤。 More subtly, our hyperparameters (applied to the former way) cant be used for the latter way (譯者註:這句明白啥意思,有看懂望指點). 例如,如果兩種方法都使用學習率為 1.0 的 SGD 優化演算法,那麼後種方法實際上使用的是一個更小的學習率 1 / num_time_steps.

梯度計算和優化 | Gradient computation & optimization

現在我們已經定義了 NMT 模型的前向訓練過程,反向傳播過程就是幾行代碼的事:

# Calculate and clip gradientsparams = tf.trainable_variables()gradients = tf.gradients(train_loss, params)clipped_gradients, _ = tf.clip_by_global_norm( gradients, max_gradient_norm)

訓練 RNNs 最終的一步之一就是梯度截斷(gradient clipping)。這裡,我們使用 global norm 做截斷。Max_gradient_norm 通常設置為像 5 或 1 這樣的值。最後一步是選擇優化器(optimizer),Adam 優化器用的比較多。我們還要設置學習率。學習率的範圍一般為 0.0001 到 0.001,並且還可以設置隨著訓練的進行而減小。

# Optimizationoptimizer = tf.train.AdamOptimizer(learning_rate)update_step = optimizer.apply_gradients( zip(clipped_gradients, params))

在我們的實驗中,我們使用標準 SGD(tf.train.GradientDescentOptimizer),學習率會隨時間下降,因為這樣訓練效果會更好。

實戰 - 訓練一個 NMT 模型

Hands-on - Lets train an NMT model


讓我們來訓練我們的第一個 NMT 模型,把越南語(Vaietnamese)翻譯成英語(English)!完整的代碼見 nmt.py。

我們使用一個關於 TED talks 的小規模的平行語料(133K 的訓練數據),數據可見 nlp.stanford.edu/projec。我們將使用 tst2012 作為我們的 dev 數據集,使用 tst2013 作為 test數據集。

用下面的命令下載數據: nmt/scripts/download_iwslt15.sh /tmp/nmt_data

運行下面的命令開始訓練:

mkdir /tmp/nmt_modelpython -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 個 epoch。我們使用 0.2 的 dropout(保留概率0.8)。如果沒有錯誤,我們應該看到與下面展示的類似的 logs,它的困惑度(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à cau 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 來查看模型訓練過程中的 summary:

tensorboard --port 22222 --logdir /tmp/nmt_model/

如果想使用越南語翻譯成英語的方式訓練,可以簡單的修改: --src=en --tgt=vi。

譯者註:我們可以修改 nmt/scripts/download_iwslt15.sh 文件中的 vi,改成 cn,這樣就可以用中文訓練了。

Inference - 如何得到翻譯結果

Inference - How to generate translations


當你訓練你的 NMT 模型的時候(或者你已經有訓練好的模型),你就可以通過給定的源句子得到其翻譯結果了。這個過程叫做 inference。Training 和 inference(testing) 之間有一個明顯的區別就是:inference 的時候,我們只會用到源句子,也就是 encoder_inputs。Decoding 過程有很多種方法,包括 greedy,sampling,以及 beam-search decoding。這裡我們討論下 greedy decoding。

想法非常簡單,我們以圖3為例。

圖3. Greedy decoding - 展示了如何使用一個訓練好的 NMT 模型使用 greedy search 由源句子「Je suis étudiant」得到其翻譯

  1. 使用與訓練時同樣的方法對源句子進行編碼得到 encoder_state,並且用這個 encoder_state 來初始化 decoder。
  2. 在 decoder 接收到開始符號「<s>」(代碼中的 tgts_sos_id)的時候,解碼(翻譯)的過程開始。
  3. 對於 decoder 的每個 timestep,我們都把 RNN 的輸出當做一個 logits 的集合,並從中選擇出最有可能的單詞,我們把 logit 值最大的那個單詞的 id 作為最有可能的單詞(這就是所謂「貪心」)。像圖3中的例子,單詞「moi」在 decoding 的第一步擁有最高的可能,我們就把這個單詞輸入到下一步的翻譯中。
  4. 這個過程會一直持續,直到句子結束的標記「</s>」作為輸出字元出現(代碼中的 tgt_eos_id)。

第三步是與訓練不一樣的地方,訓練時,輸入的始終是正確的目標單詞,而在 inference 時,輸入的是模型預測出的單詞。下邊是 greedy decoding 的代碼,與 training decoder 非常相似:

# Helperhelper = tf.contrib.seq2seq.GreedyEmbeddingHelper( embedding_decoder, tf.fill([batch_size], tgt_sos_id), tgt_eos_id)# Decoderdecoder = tf.contrib.seq2seq.BasicDecoder( decoder_cell, helper, encoder_state, output_layer=projection_layer)# Dynamic decodingoutputs, _ = tf.contrib.seq2seq.dynamic_decode( decoder, maximum_iterations=maximum_iterations)translations = outputs.sample_id

這裡,我們使用 GreedyEmbeddingHelper 代替 TrainingHelper。因為我們不能預知目標句子的長度,所以使用 maximum_iterations 來限制翻譯句子的長度。一種啟發式的做法是讓令 maximum_iterations 為源句子長度的兩倍。

maximum_iterations = tf.round(tf.reduce_max(source_sequence_length) * 2)

有了訓練好的模型,我們就可以創建一個 inference 文件並且嘗試翻譯一些句子啦:

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_infercat /tmp/nmt_model/output_infer # To view the inference as output

注意上邊這個命令也可以在訓練過程中運行,只要本地已經保存了一個訓練的 checkpoint。更多細節見 inference.py。

(這個中文咋翻譯好?) | Intermediate


學習了這個最基本的 seq2seq 模型,讓我們玩點高級的!構建一個能達到業界最好水平的神經機器翻譯系統,我們需要一些「秘密的調味」:注意力機制。注意力機制最先由 Bahdanau et al., 2015 提出,後來又被 Luong et al., 2015 等人繼續完善。 注意力機制最關鍵的地方在於通過在翻譯時把「注意力」放在相關的源句子單詞上的方式,在目標句子和源句子之間建一個直接相連的捷徑(direct short-up connections)。注意力機制的一個非常漂亮的技巧在於可以非常容易的可視化源句子與目標句子之間的對齊矩陣(alignment matrix),見圖4.

圖四. Attention visualization - example of the alignments between source and target sentences. Image is taken from (Bahdanau et al., 2015)

還記得嗎,在 vanilla seq2seq 模型中,我們在編碼開始的時候把 encoder 最後的狀態(state)傳給 decoder,這種方法對於中短的句子來說效果還好。但是對於長句子,這種單一的固定大小的隱藏狀態(single fixed-size hidden state)就變成了信息瓶頸(information bottleneck)。

與丟棄 source RNN 中所有的隱藏狀態不同,注意力機制提供了一種 decoder 「偷看」這些隱藏狀態的方法(把它們當做源句子信息的動態內存)。通過這種方法,注意力機制提升了長句子的翻譯效果。現在注意力機制已經成為了一種事實上的標準,並且已經成功的應用於許多其他的任務(包括圖片說明生成,語音識別,文本摘要)。

注意力機制的基本知識

Backgound on the Attention Mechanism


我們簡要敘述了(Luong et al., 2015)提出的注意力機制模型,該模型已經被應用於幾個達到業界最好水平的系統,比如開源的工具包 OpenNMT 以及本教程中的 TF seq2seq API.

圖5. 注意力機制 -(Luong et al., 2015)中 attention-based NMT 系統的一個例子。我們高亮了attention計算的第一步(We highlight in detail the first step of the attention computation.)。簡單起見,我們沒有展示如圖2所示的詞向量和 projection layer.

正如圖5所展示的, decoder 的每一步都會有 attention 的計算。計算過程如下:

  1. The current target hidden state is compared with all source states to derive attention weights (can be visualized as in Figure 4).
  2. Based on the attention weights we compute a context vector as the weighted average of the source states.
  3. Combine the context vector with the current target hidden state to yield the final attention vector
  4. The attention vector is fed as an input to the next time step (input feeding). The first three steps can be summarized by the equations below
  1. 當前目標隱藏狀態與所有的源句子中的狀態進行比較計算,得到 attention 權重(正如圖四所展示的)。
  2. 基於1中得到的 attention 權重,通過加權平均計算源句子狀態的 context vector。
  3. 由 context vector 和當前目標隱藏狀態得到最後的 attention vector。
  4. 把這個 attention vector 當做輸入數據餵給下一步的計算。前述的三個步驟可以用下邊三個等式來總結:

其中,函數 score 用來比較計算目標隱藏狀態 h_{t} 和每個源句子的隱藏狀態 ar{h}_{s} ,然後歸一化得到 attention 權重(一個在所有源句子單詞位置上的分布)。這個 scoring 函數的計算方法有很多,公式4中的乘法和加法的方式是比較常用的。得到 attention vector a_{t} 之後,就可以用來計算 softmax logit 和 loss了。這與上述的 vanilla seq2seq 模型頂層的目標隱藏狀態計算方法相似,函數 f 也可以採用其他的形式:

更多的 attention 機制的實現方式可見 attention_wrapper.py.

what matters in the attention mechanism?

注意力機制最關鍵的是啥?

As hinted in the above equations, there are many different attention variants. These variants depend on the form of the scoring function and the attention function, and on whether the previous state ht?1 is used instead of ht in the scoring function as originally suggested in (Bahdanau et al., 2015). Empirically, we found that only certain choices matter. First, the basic form of attention, i.e., direct connections between target and source, needs to be present. Second, its important to feed the attention vector to the next timestep to inform the network about past attention decisions as demonstrated in (Luong et al., 2015). Lastly, choices of the scoring function can often result in different performance. See more in the benchmark results section.

中文翻譯(僅供參考):如上所說,還有很多不同的 attention 變種。他們的區別在於 scoring 函數、attention 函數的不同,以及是否在 scoring 函數中中使用前一步狀態 h_{t-1} 替換 h_{t} ,正如(Bahdanau et al., 2015)最開始建議的那樣。根據經驗,我們發現只有你的實際選擇才是注意力機制的關鍵。首先,注意力機制最基本的形式,也就是目標句子與源句子之間的連接,是必須要遵守和實現的。其次,正如(Luong et al., 2015)所提到的,在下一步計算時把 attention vector 傳入是非常重要的,它可以為神經網路提供一些之前的「attention的選擇」。最後,不同的 scoring 函數的選擇會得到不同的翻譯效果。可參考 benckmark results 小節。

Attention Wrapper API


在 AttentionWrapper 的實現中,我們借鑒了 (Weston et al., 2015) 中關於 memory network 的一些技巧。在本教程中,我們使用 read-only memory 來替換 readable & writable memory. 所謂的 「memory」其實就是源句子隱藏狀態的集合(或者是 Luong的scoring 中的轉換版本,即 War{h}_{s} ,或Bahdanau 中的版本 W_{2}ar{h}_{s} )。每一步,我們使用當前目標隱藏狀態作為「query」來決定讀取 memory 的哪一部分。通常,這個 query 要與 memory 的每個位置的「關鍵值」進行比較計算,在我們上述的注意力機制中,我們把源句子的隱藏狀態(或其轉換版本)當做這個「關鍵值」。你可以根據這個 memory-network 技術來提出更多的 attention 的形式。

有了 attention wrapper,由 vanilla seq2seq 的代碼得到 attention 版本就非常簡單啦。這部分代碼見 attention_model.py。

首先,我們需要定義一種 attention 機制,比如(Luong et al., 2015):

# attention_states: [batch_size, max_time, num_units]attention_states = tf.transpose(encoder_outputs, [1, 0, 2])# Create an attention mechanismattention_mechanism = tf.contrib.seq2seq.LuongAttention( num_units, attention_states, memory_sequence_length=source_sequence_length)

在前面的 Encoder 小節,encoder_outputs 是頂層的所有源句子單詞的隱藏狀態的集合,其shape 是 [max_time, batch_size, num_units](因為我們使用 dynamic_rnn,為了高效,所以設置 time_major=True)。為實現 attention 機制,我們需要保證 「memory」是 batch major的,所以我們需要用 transpose 轉換 attention_states 的 shape。我們還用到了 source_sequence_length ,以確保 attention 權重能正確的被歸一化(只歸一化沒有padding 0的位置)。

定義 attention 機制之後,我們使用 AttentionWrapper 來 wrap 這個 decoding cell:

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

其餘代碼可見 Decoder 小節。

實戰 - 構建一個 attention-based NMT 模型

Hands-on - building an attention-based NMT model


為了能使用 attention,在訓練過程中我們使用 luong, scaled_luong, bahdanau 和 normed_bahdanau 中的一種作為 attention 的標記的值。這個attention 標記是指我們要使用哪種類型的 attention 機制。另外,我們為 attention 模型創建了一個新的目錄,所以,我們就不復用之前訓練的那個 NMT 模型了。

運行以下命令來開始訓練:

mkdir /tmp/nmt_attention_modelpython -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

訓練結束之後,我們使用相同的 inference 命令來進行預測,當然 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

Tips & Tricks

技巧和陷阱

Building Training, Eval and Inference Graphs

構建訓練、驗證和測試圖


當我們使用 TensorFlow 單間一個機器學習模型的時候,最好構建三個分開的 graph:

  • 訓練圖,包括:
  • Batches, buckets, 以及來自文件或外部輸入的數據集的部分採樣數據;
  • 前向和反向傳播的 ops;
  • 創建 optimizer,以及添加訓練 op;
  • 驗證圖,包括:
  • Batches, buckets, 以及來自文件或外部輸入的數據集輸入數據
  • 訓練時的前向傳播 op,以及要添加的 evaluation ops
  • 預測圖,包括:
  • 不需要批處理的輸入數據
  • 不需要 subsample 和 bucket 輸入數據
  • 從 placeholders 讀取輸入數據(數據可以通過 feed_dict 被圖讀取,或者使用 C++ TensorFlow serving binary)
  • 模型前向傳播的部分 ops,以及一些可能 session.run 函數調用的所需要的額外的為存儲狀態(state)所需要的 inputs/outputs。

原文:

  • The Training graph, which:
    • Batches, buckets, and possibly subsamples input data from a set of files/external inputs.
    • Includes the forward and backprop ops.
    • Constructs the optimizer, and adds the training op.
  • The Eval graph, which:
    • Batches and buckets input data from a set of files/external inputs.
    • Includes the training forward ops, and additional evaluation ops that arent used for training.
  • The Inference graph, which:
    • May not batch input data.
    • Does not subsample or bucket input data.
    • Reads input data from placeholders (data can be fed directly to the graph via feed_dict or from a C++ TensorFlow serving binary).
    • Includes a subset of the model forward ops, and possibly additional special inputs/outputs for storing state between session.run calls.

現在比較棘手的一點是 ,如何在一個機器上讓三個圖共享這些 variables,這可以通過為每個圖創建不同的 session 來解決。訓練過程的session 周期性的保存 checkpoints,然後 eval session 和 inference session 就可以讀取checkpoints。下面的例子展示了這兩種方法的不同。

前一種方法:三個模型都在一個圖裡,並且共享一個 session。

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

後一種方法:三個模型在三個圖裡,三個 sessions 共享同樣的 variables。

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.sun 調用時通過 feed_dicts 餵給數據,而是使用自帶狀態的 iterator 對象(stateful iterator objects)。不論在單機還是分散式集群上,這些 iterators 可以讓輸入管道(input pipeline)變得更容易。在下一小節,我們將使用新的數據輸入管道(input data pipeline)。

數據輸入管道 | Data Input Pipeline


在 TensorFlow 1.2版本之前,用戶有兩種把數據餵給 TensorFlow training 和 eval pipelines的方法:

  1. 在每次訓練調用 session.run 時,通過 feed_dict 直接餵給數據;
  2. 使用 tf.train(例如 tf.train.batch)和 tf.contrib.train 中的隊列機制(queueing machanisms);
  3. 使用來自 helper 層級框架比如 tf.contrib.learn 或 tf.contrib.slim 的 helpers (這種方法是使用更高效的方法利用第二種方法)。

第一種方法對不熟悉 TensorFlow 或需要做一些外部的數據修改(比如他們自己的 minibatch queueing)的用戶來說更簡單,這種方法只需用簡單的 Python 語法就可實現。第二種和第三種方法更標準但也不那麼靈活,他們需要開啟多個 Python 線程(queue runners)。更重要的是,如果操作不當會導致死鎖或難以查明的錯誤。儘管如此,隊列的方法仍要比 feed_dict 的方法高效很多,並且也是單機和分散式訓練的標準。

從TensorFlow 1.2開始,有一種新的數據讀取的方法可以使用: dataset iterators,其在 tf.data 模塊。Data iterators 非常靈活,易於使用和操作,並且利用 TensorFlow C++ runtime 實現了高效和多線程。

我們可以使用一個 batch data Tensor,一個文件名,或者包含多個文件名的 Tensor 來創建一個 dataset。下面是一些例子:

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

所有的數據都可以完成像數據預處理一樣的處理方式,包括數據的 reading 和 cleaning,bucketing(在 training 和 eval 的時候),filtering 以及 batching。

把每個句子轉換為單詞串的向量(vectors of word strings),那我們可以使用 dataset 的 map transformation:

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

我們也可以把每個句子向量轉換為包含向量與其動態長度的元組:

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

最後,我們可以對每個句子應用 vocabulary lookup。給定一個 lookup 的 table,此 map 函數可以把元組的第一個元素從串向量轉換為數字向量。(譯者註:不好翻譯,原文是: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))

合併兩個 datasets 也非常簡單,如果兩個文件有行行對應的翻譯,並且兩個文件分別被不同的 dataset 讀取,那麼可以通過下面這種方式生成一個新的 dataset,這個新的 dataset 的內容是兩種語言的翻譯一一對應的元組。

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

Batching 變長的句子實現起來也很直觀。下邊的代碼從 source_target_dataset 中 batch 了 batch_size 個元素,並且分別為每個 batch 的源向量和目標向量 padding 到最長的源向量和目標向量的長度。

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 拿到的數據會嵌套為元組,其 tensors 的最左邊的維度是 batch_size. 其結構如下:

  • 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.

最後,bucketing 多個 batch 的大小差不多的源句子也是可以的。更多的代碼實現詳見文件utils/iterator_utils.py。

從 dataset 中讀取數據需要三行的代碼:創建 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 每一次調用 source 和 target ,都會從dataset中自動提取下一個 minibatch 的數據。

讓 NMT 模型更完美的其他技巧

Other details for better NMT models


雙向 RNNs | Bidirectional RNNs

一般來講,encoder 的雙向 RNNs 可以讓模型表現更好(訓練速度會下降,因為有更多的層需要計算)。這裡,我們給出了構建一個單層雙向層的 encoder 的簡單代碼:

# Construct forward and backward cellsforward_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_outputsencoder_state 也可以使用 Encoder 小節的方法獲取到。需要注意的是,如果要創建多層雙向層,你需要修改一下 encoder_state,見 model.py 的_build_bidirectional_rnn() 方法。

Beam Search

Greedy decoding 可以給我們非常合理的翻譯結果,但是 beam search decoding 可以讓翻譯結果更好。Beam search 的思想是,考慮我們可以選擇的所有翻譯結果的排名最靠前的幾個候選的集合,我們探索其所有的可能翻譯結果(這裡解釋的不是很清楚,可以參考知乎的一個討論:誰能解釋下seq2seq中的beam search演算法過程?)。Beam 的這個 size 我們稱為 beam width,一個較小的 beam width 比如說 10,就已經足夠大了。我們推薦讀者閱讀 Neubig, (2017) 的 7.2.3 小節。這是 beam search 的一個例子:

# Replicate encoder infos beam_width timesdecoder_initial_state = tf.contrib.seq2seq.tile_batch( encoder_state, multiplier=hparams.beam_width)# Define a beam-search decoderdecoder = 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 decodingoutputs, _ = tf.contrib.seq2seq.dynamic_decode(decoder, ...)

在 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() 函數。

超參數 | Hyperparameters

有一些超參數也可以供我們調節。這裡,根據我們的實驗,我們列舉了幾個超參數【你可以表示不認同,保留自己的看法】。

  • optimizer:對於「不太常見」的網路結構,Adam 可能可以給出一個較合理的結果,如果你用 SGD 進行訓練,那麼 SGD 往往可以取得更好的結果。
  • Attention:Bahdanau 類型的 attention,encoder 需要雙向結構才能表現很好;同時 Luong 類型的 attention 需要其他的一些設置才能表現很好。在本教程中,我們推薦使用被改進的這兩個類型的 attention:scaled_luongnormed_bahdanau

多 GPU 訓練 | Multi-GPU training

訓練一個 NMT 模型可能需要幾天的時間,我們可以把不同的 RNN layers 放在不同的 GPUs 進行訓練可以加快訓練速度。這裡是使用多 GPUs 創建 RNN layers 的例子:

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 參數來同步梯度的計算。

你會發現,儘管我們使用了多個 GPUs,但是 attention-based NMT 模型的訓練速度提升不大。問題的關鍵在於,在標準的 attention 模型中,在每個時間步,我們都需要用最後一層的輸出去「查詢」attention,這就意味著,每一個解碼的時間步都需要等前面的時間步完全完成。因此,我們不能簡單的通過在多 GPUs 上部署 RNN layers 來同步解碼過程。

GNMT attention architecture 可以通過使用第一層的輸出來查詢 attention 的方法來同步 decoder 的計算。這樣,解碼器的每一步就可以在前一步的第一層和 attention 計算完成之後就可以進行解碼了。我們的 API 實現了這個結構 GNMTAttentionMultiCell,其是tf.contrib.rnn.MultiRNNCell 的子類。這裡是使用 GNMTAttentionMultiCell 創建一個 decoder 的例子:

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, # dont add an additional dense layer. output_attention=False,)cell = GNMTAttentionMultiCell(attention_cell, cells)

Benchmarks


這部分就不翻譯了,截個圖放這吧:

其他資源 | Other resources


想更深入的學習神經機器翻譯和 seq2seq 模型,我們強烈推薦這三個參考資料: Luong, Cho, Manning, (2016); Luong, (2016); and Neubig, (2017).

構建 seq2seq 模型有很多不同的工具,所以每種框架我們選了一種:

  • Matlab:Stanford NMT
  • TensorFlow:tf-seq2seq
  • Theano:Nemantus
  • Torch:OpenNMT
  • PyTorch:OpenNMT-py

參考資料 | References


  • Dzmitry Bahdanau, Kyunghyun Cho, and Yoshua Bengio. 2015. Neural machine translation by jointly learning to align and translate. ICLR.
  • Minh-Thang Luong, Hieu Pham, and Christopher D Manning. 2015. Effective approaches to attention-based neural machine translation. EMNLP.
  • Ilya Sutskever, Oriol Vinyals, and Quoc V. Le. 2014. Sequence to sequence learning with neural networks. NIPS.

@article{luong17, author = {Minh{-}Thang Luong and Eugene Brevdo and Rui Zhao}, title = {Neural Machine Translation (seq2seq) Tutorial}, journal = {


推薦閱讀:

seq2seq models in sub-quadratic time
手把手教你寫一個中文聊天機器人
基於Seq2Seq模型深度學習Github工單

TAG:教程 | seq2seq | TensorFlow |