谷歌神經網路機器翻譯NMT:人人可利用TensorFlow快速建立翻譯模型

遇見更好的自己發表於2018-01-28

緒論

機器翻譯作為自動翻譯語言之間的任務,是機器學習社群中最活躍的研究領域之一。在機器翻譯的眾多方法中,序列到序列(“seq2seq”)模型最近取得了巨大的成功,並已成為大多數商業翻譯系統中的標準。然而,雖然seq2seq模型(如OpenNMT或tf-seq2seq)上有大量的材料,但是缺乏教學人員知識和技能的材料,可以輕鬆構建高質量的翻譯系統。

近日,TensorFlow在GitHub上宣佈一個新的神經機器翻譯(NMT)教程,讓讀者能夠充分了解seq2seq模型,並展示如何從零開始構建翻譯模型

一 、簡介

序列到序列(seq2seq)模型在諸如機器翻譯、語音識別和文字概括等各項任務中,取得了巨大的成功。本教程為讀者提供了對seq2seq模型的全面介紹,並展示瞭如何從頭構建一個seq2seq模型。我們專注於神經機器翻譯(NMT)的任務,這是第一個成功的seq2seq模型的測試平臺。包含的程式碼是輕量級的、高質量的、生產就緒的,並與最新的研究思想結合在一起。我們通過以下方式實現此目標:

  1. 使用最新的解碼器/注意力包裝器API,TensorFlow 1.2資料迭代器;
  2. 結合了我們在建立迴圈和seq2seq模型方面的專長;
  3. 提供提示和技巧,以構建最好的NMT模型,並複製Google的NMT(GNMT)系統;

我們認為重要的是,提供人們可以輕鬆複製的基準。因此,我們提供了完整的實驗結果,並對以下公開資料集的模型進行了預先訓練:

  1. 小規模:由IWSLT評估組織提供的平行語料庫,包含TED談話中的英語到越南語的133000個句子對;
  2. 大規模:WMT評估組織提供的德語到英語的平行語料庫(450萬個句子對);

我們首先介紹關於NMT的seq2seq模型的一些基本知識,說明如何構建並訓練vanilla NMT模型。第二部分將詳細介紹建立一個高效的NMT模式的注意力機制。然後,我們將討論提示和技巧,以構建最佳的NMT模型(包括速度和翻譯質量),例如TensorFlow最佳實踐(批處理、降級),雙向RNN和集束搜尋。

二、基礎

回到過去,傳統的基於短語的翻譯系統將源語句分解成多個組,然後逐句翻譯。這導致翻譯產品不一致性,而且翻譯的水平跟人類相比差異很大。人類通讀整個源句,理解它的含義,然後再翻譯。而神經機器翻譯(NMT)正是這麼模擬的!

這裡寫圖片描述

具體來說,首先,NMT系統使用編碼器,讀取源語句,以構建“思想”向量,表示句子意義的數字序列;然後,解碼器處理句子向量,以發出翻譯,如圖1所示。這通常被稱為編碼器-解碼器架構。以這種方式,NMT解決了傳統的、基於短語的方法中的本地翻譯問題:它可以捕獲語言的長期依賴性,例如語法結構等等,併產生更流暢的翻譯。

NMT模型因具體結構而有所不同。順序資料的自然選擇是大多數NMT模型使用的迴圈神經網路(RNN)。通常,RNN用於編碼器和解碼器。然而,RNN模型在以下方面會不同:
(a)方向性——單向或雙向;
(b)深度——單層或多層;
(c)型別——通常是vanilla RNN、長短期記憶網路(LSTM)或門控迴圈單元(GRU)。

在本教程中,我們將一個深度多層RNN視為單向,並將LSTM作為迴圈單元。我們在圖2中展示了一個模型的例子。在這個例子中,我們建立一個模型,將源句子“I am a student”翻譯成一個目標句子“Je suisétudiant”。NMT模型由兩個迴圈神經網路組成:編碼器RNN簡單地處理輸入源句子,而不進行任何預測;另一方面,解碼器在預測下一個單詞的同時,處理目標句子。

這裡寫圖片描述

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

我們首先需要了解構建一個 NMT 模型具體程式碼的核心,我們會在圖 2 中更詳細地講解。我們後面會介紹資料準備和全部的程式碼,這一部分是指 model.py 檔案。

在網路的底層,編碼器和解碼器 RNN 接收到以下輸入:首先是原句子,然後是從編碼到解碼模式的過渡邊界符號「」,最後是目標語句。對於訓練來說,我們將為系統提供以下張量,它們是以時間為主(time-major)的格式,幷包括了單詞索引:

a. encoder_inputs [max_encoder_time, batch_size]:源輸入詞。
b. decoder_inputs [max_decoder_time, batch_size]:目標輸入詞。
c. decoder_outputs [max_decoder_time, batch_size]:目標輸出詞,這些是 decoder_inputs 按一個時間步向左移動,並且在右邊有句子結束符。

為了更高的效率,我們一次用多個句子(batch_size)進行訓練。測試略有不同,我們會在後面討論。

1.Embedding

給定單詞的分類屬性,模型首先必須查詢詞來源和目標嵌入以檢索相應的詞表徵。為了令該嵌入層能夠執行,我們首先需要為每一種語言選定一個詞彙表。通常,選定詞彙表大小 V,那麼頻率最高的 V 個詞將視為唯一的。而所有其他的詞將轉換並打上「unknown」標誌,因此所有的詞將有相同的嵌入。我們通常在訓練期間嵌入權重,並且每種語言都有一套。

# Embedding
embedding_encoder = variable_scope.get_variable(
    "embedding_encoder", [src_vocab_size, embedding_size], ...)
encoder_emb_inp = embedding_ops.embedding_lookup(
    embedding_encoder, encoder_inputs)

#   encoder_inputs: [max_time, batch_size]
#   encoder_emp_inp: [max_time, batch_size, embedding_size]

我們同樣可以構建 embedding_decoder 和 decoder_emb_inp。注意我們可以選擇預訓練的詞表徵如 word2vec 或 Glove vectors 初始化嵌入權重。通常給定大量的訓練資料,我們能從頭學習這些嵌入權重。

2 編碼器

一旦可以檢索到,詞嵌入就能作為輸入饋送到主神經網路中。該網路有兩個多層迴圈神經網路組成,一個是原語言的編碼器,另一個是目標語言的解碼器。這兩個 RNN 原則上可以共享相同的權重,然而在實踐中,我們通常使用兩組不同的迴圈神經網路引數(這些模型在擬合大型訓練資料集上做得更好)。解碼器 RNN 使用零向量作為它的初始狀態,並且可以使用如下程式碼構建:

# Build RNN cell
encoder_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_seqence_length, time_major=True)

注意語句有不同的長度以避免浪費計算力,因此我們會通過 source_seqence_length 告訴 dynamic_rnn 精確的句子長度。因為我們的輸入是以時間為主(time major)的,我們需要設定 time_major=True。現在我們暫時只需要構建單層 LSTM、encoder_cell。我們後面會詳細描述怎樣構建多層 LSTM、新增 dropout 並使用注意力機制。

3 解碼器

decoder 也需要訪問源資訊,一種簡單的方式是用編碼器最後的隱藏態 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_cell(類似於 encoder_cell) 的 decoder、helper 以及之前作為輸入的 encoder_state。

通過分離 decoders 和 helpers,我們能重複使用不同的程式碼庫,例如 TrainingHelper 可由 GreedyEmbeddingHelper 進行替換,來做貪婪解碼。

最後,我們從未提到過的 projection_layer 是一個密集矩陣,將頂部的隱藏態轉變為維度 V 的邏輯向量。我們在圖 2 的上部展示了此過程。

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

4 損失

給出以上的 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 是一個與 decoder_outputs 大小一樣的 0-1 矩陣。該矩陣將目標序列長度以外的其他位置填充為標量值 0。

我們需要指出來的是,訓練損失可以由 batch_size 分割,因此我們的超引數 batch_size 是「不變數」。也有些人將訓練損失按照 batch_size * num_time_steps 分割,這樣可以減少短句所造成的誤差。更巧妙的,我們的超引數(應用於前面的方法)不能用於後面的方法。例如,如果兩種方法都是用學習率為 1.0 的隨機梯度下降,後面的方法將更有效地利用一個較小的學習率,即 1 / num_time_steps。

5 梯度計算和優化

現在是時候定義我們的 NMT 模型的反向傳播了。計算反向傳播只需要寫幾行程式碼:

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

訓練 RNN 的一個重要步驟是梯度截斷(gradient clipping)。這裡,我們使用全域性範數進行截斷操作。最大值 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))

在我們的實驗中,我們使用標準的隨機梯度下降(tf.train.GradientDescentOptimizer),並採用了遞減的學習率方案,因此也就有更好的效能。

推理——如何生成翻譯

當你訓練你的 NMT 模型時(並且一旦你已經訓練了模型),可以在給定之前不可見的源語句的情況下獲得翻譯。這一過程被稱作推理。訓練與推理之間有一個明確的區分(測試):在推理時,我們只訪問源語句,即 encoder_inputs。解碼的方式有很多種,包括 greedy 解碼、取樣解碼和束搜尋解碼(beam-search)。下面我們討論一下 greedy 解碼策略。
其想法簡單,我們將在圖 3 中作說明:

a.在訓練獲取 encoder_state 的過程中,我們依然以相同方式編碼源語句,並且 encoder_state 用於初始化解碼器。

b.一旦解碼器接收到開始符 「《s》」(在我們的程式碼中指 tgt_sos_id),就開始解碼處理(翻譯)。

c.最大的單詞,其 id 與最大的 logit 值相關聯,正如被髮出的詞(這是 greedy 行為)。例如在圖 3 中,單詞 moi 在第一個解碼步中具有最高的翻譯概率。接著我們把這一單詞作為輸入饋送至下一個時間步。

d.這一過程會持續到這句話的終止符「《/s》」,然後輸出(在我們的程式碼中是 tgt_eos_id)。

這裡寫圖片描述

推理與訓練的區別在於步驟 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

我們在本文中使用了 GreedyEmbeddingHelper 而不是 TrainingHelper。由於無法提前知道目標語句的長度,我們使用 maximum_iterations 限制翻譯的長度。一個啟發是解碼最多兩倍的源語句長度。

注意力機制

注意力機制。該機制由 Bahdanau 等人在 2015 年首次提出(https://arxiv.org/abs/1409.0473),稍後 Luong 等人和其他人完善了它,其核心思想是當我們翻譯時通過「注意」相關的源內容,建立直接的短連線。注意力機制的一個很好副產品是源語句和目標語句之間的一個易於視覺化的對齊矩陣(如圖 4 所示)。
這裡寫圖片描述

請記住在 vanilla 序列到序列模型中,當開始編碼處理時,我們把最後的源狀態從編碼器傳遞到解碼器。這對短、中長度的語句效果很好;對於長句子,單一固定大小的隱狀態成為了資訊瓶頸。注意力機制沒有摒棄原RNN 中計算的所有隱狀態,而是提出了允許解碼器窺探它們的方法(把它們看作是源資訊的動態儲存)。如此,注意力機制提升了長句的翻譯質量。現在,注意力機制實至名歸,已成功應用於其他諸多工(比如語音識別)。

我們現在描述一下注意力機制的例項(Luong et al., 2015),它已經被應用到幾個最新型的系統當中了,包括開源工具,比如 OpenNMT(http://opennmt.net/about/)和此教程中的 TF seq2seq API。我們還將會提供注意力機制相關變體的內容。

這裡寫圖片描述

當前目標隱蔽狀態和所有源狀態(source state)進行比較,以匯出權重(weight),見圖 4。 基於注意力權重,我們計算了一個背景向量(context vector),作為源狀態的平均權值。 將背景向量與當前目標隱蔽態進行結合以生成最終的注意力向量。 此注意力向量將作為下一時序步驟的輸入。前三個步驟可以由下列公式總結:

這裡寫圖片描述

這裡,函式 score 用於將目標隱蔽狀態 ht 和每一個源狀態 hs 進行比較,結果會被標準化成生成式注意力權重(一個源位置的分佈)。其實有很多種關於評分函式(scoring function)的選擇;比較流行的評分函式包括公式(4)中給出的乘法與加法形式。一旦被計算,注意力向量 at 就會用於推導 softmax logit 和損失。這與 vanilla seq2seq 模型頂層的目標隱蔽態相似。函式 f 也可以利用其它形式。

這裡寫圖片描述

上述公式表明注意力機制有很多種變體。這些變體依賴於評分函式(scoring function)和注意力函式(attention function)的形式,也依賴於前一狀態 ht-1,而不依賴於開始建議的評分函式 ht(Bahdanau et al., 2015)。實際上我們發現的只有一些選擇上的注意事項。一,注意力的基本形式,例如,目標和源之間的直接聯絡需要被呈現。二,把注意力向量輸入給下一時間步驟,以把之前的注意力決策告知給網路(Luong et al., 2015)。最後,評分函式的選擇經常可以造成不同的效能表現。

相關文章