基於TensorFlow框架的Seq2Seq英法機器翻譯模型

基於TensorFlow框架的Seq2Seq英法機器翻譯模型

來自專欄機器不學習

前言

本篇文章的內容主要是基於英法平行語料(English-French Parallel Corpus)來實現一個簡單的英法翻譯模型,代碼框架採用TensorFlow 1.6。

本篇文章與去年我在知乎專欄《機器不學習》上發表的《從Encoder到Decoder實現Seq2Seq模型》文章類似。

天雨粟:從Encoder到Decoder實現Seq2Seq模型?

zhuanlan.zhihu.com圖標

那為什麼今天還要重新寫這篇文章,對這篇文章和代碼更新的原因有以下4個方面:

  • 去年文章介面實現採用TensorFlow 1.1實現,有些介面已經發生變化,導致代碼下載以後部分片段無法正常運行;
  • 文章部分寫作內容描述不夠清晰,本篇文章對一些表達不當的地方進行重構;
  • 之前的Seq2Seq模型是對單詞的字母進行排序,數據處理部分相對較為簡單。而此次將採用英法平行語料來構建翻譯模型,增加一些數據處理操作;
  • 專欄下一篇文章準備寫關於改進版本的Machine Translation模型,包括使用BiRNN和Attention機制的模型(將採用Keras實現),此篇文章可以來做些許鋪墊。

運行環境

寫專欄的目的主要在於通過代碼理解演算法,在之前的文章中,有很多同學會提一些介面的問題,恕平時較忙,無法一一回答,關於介面和代碼的基礎問題請自行百度或Google,基本都能得到解決。本篇文章與代碼所基於的環境:

  • 系統環境:Mac OS High Sierra
  • Python環境:Python 3.6
  • TensorFlow版本:1.6
  • Anaconda (Jupyter Notebook)

代碼完整地址:NELSONZHAO/zhihu

歡迎各位Star和Fork。

正文

本文主要包含兩個部分:數據處理與模型構建。在數據處理部分,我們將會把原始的平行語料轉化為我們模型所需要的數據。在模型構建部分,我們將會一步步基於TensorFlow 1.6構建最基本的Seq2Seq模型,並基於我們所擁有的平行語料進行訓練與預測。


第一部分 數據處理

在數據處理部分,我們主要包括以下四個步驟:

1. 載入數據。本篇文章使用的數據是English-French平行語料(parallel corpus)。

    • small_vocab_en文件包含了英文原始語料,其中每一行代表一個完整的句子
    • small_vocab_fr文件包含了法語原始語料,其中每一行代表一個完整的句子

2. 數據探索。即對語料文本的統計性描述。

3. 數據預處理。

    • 構造英文詞典(vocabulary),對單詞進行編碼
    • 構造法語詞典(vocabulary),對單詞進行編碼

4. 語料轉換。

    • 將原始英文文本轉換為機器可識別的編碼
    • 將原始法語文本轉換位機器可識別的編碼

1. 載入數據

由於本篇文章主要是來做英文到法語的翻譯,因此我們輸入的是英文,期望輸出的是法語。因此,我們稱英文是source,法語為target。後面文章中變數的命名也採用這種方式。

我們的數據為平行語料,即每一行為一個完整的句子,兩個文件的同一行對應著同一句話的英法兩種語言的表達。

2. 描述性統計

描述性統計主要在於幫助我們了解數據。

上面的代碼主要是對載入進來的source_text和target_text進行統計分析。我們的原始語料均採用了小寫處理。

  • 4-5行代碼統計了在source文本中的唯一字元串(包括單詞與標點)個數;
  • 9-13行代碼對source文本進行了分句與分詞,統計了其中的句子個數、平均句子長度和最大句子長度;
  • 16-17行代碼對target文本進行了分句與分詞,統計了其中的句子個數、平均句子長度和最大句子長度;
  • 24-28行代碼分別print了英法語料的前10個句子。

通過統計分析,我們可以看到我們的樣本共有13W多的句子,其中英文句子的平均長度為13.2,法語句子的平均長度為14.2。英文句子的最大長度為17,法語句子相對更長,其最大句子長度為23。

3. 數據預處理

機器翻譯模型的基本架構是Seq2Seq模型,在構造模型之前,我們需要先對語料進行處理。即將文本語料轉化為機器所能識別的數字。例如,對英文句子:I love machine learning and deep learning,編碼為數字[28, 29, 274, 873, 12, 983, 873]。因此本部分主要完成以下幾個任務:

  • 根據語料構造英文與法語的字典(vocabulary)
  • 構造英語與法語的映射,即將單詞轉換為數字的字典
  • 構造英語與法語的反向映射,即從數字轉換為單詞的字典

構造詞典(Vocabulary)

上述代碼分別對source和target文本進行單詞的統計,構造了詞典。由於我們文本較少,所以包含的唯一詞也較少。在英文語料中,我們的詞典大小為227,在法語語料中,詞典大小為354。

構造映射

有了詞典以後,我們就可以根據詞典來構造單詞的映射,將語料文本轉化為機器可識別的編碼。

首先,我們先定義了特殊字元<PAD>,<UNK>,<GO>,<EOS>。

  • <PAD>:由於翻譯問題的特殊性,我們的句子長度往往是不一致的,而在RNN處理batch數據時,我們需要保證batch中的句子長度一致,此時需要通過<PAD>對長度不足的句子進行補全;
  • <UNK>:Unknown字元,用來處理模型未見過的生僻單詞;
  • <GO>:翻譯句子時,用來告訴句子開始進行翻譯。僅在target中使用;
  • <EOS>:翻譯句子時,用來告訴句子結束翻譯。僅在target中使用。

第二部分代碼塊我們根據詞典和特殊字元分別構造了英法的單詞映射。可以看到,英文的映射詞典大小為229,法語的映射詞典大小為358。

4. 語料轉換

有了上述數據預處理的結果,我們就可以對原始語料進行文本轉換,即將文本轉換為數字。在轉換過程中,由於我們LSTM只能處理定長的數據,因此我們需要保證輸入語料的長度Tx與輸出語料的長度Ty保持固定。假設Tx=20,則對於不足20個單詞的句子進行PAD,對超過20個單詞的句子進行截斷。

例如,對於輸入句子」I love machine learning and deep learning",編碼後為[28, 29, 274, 873, 12, 983, 873, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]。

在這裡,我們實現一個函數來進行轉換,該函數接受一個完整的句子,並返回映射結果。

  • sentence參數是一個完整的句子;
  • map_dict是單詞到數字的映射,即我們上面生成的source_vocab_to_int和target_vocab_to_int;
  • max_length是指最大句子長度;
  • is_target是用來說明是否對target句子進行處理,因為在處理source和target中有個不同,在target中,我們需要在每個句子的最後添加<EOS>字元,而在source中不需要做這個操作。

函數構造後,我們就可以對英文語料和法語語料分別進行處理。

其中,我指定了英文句子的最大長度為20,也就是說,對不足20個單詞的句子進行補全,對超過20個單詞的句子進行截斷。法語句子相對較長,我選用25作為最大長度。

我們隨機選擇一個處理後的結果查看,如上所示,兩個句子分別對應了英法兩種表達,進行處理後就變為了數字編碼,並且不足長度的都用0進行了補全。

至此,我們的數據處理部分就基本完成,這一部分我們將文本語料通過構造詞典、映射等方式轉化為了機器可識別的數字編碼。下一部分我們將開始對模型進行構建。


第二部分 模型構建

在第一部分的數據構建中,我們已經將原始的語料文本轉化為機器能夠識別的數字編碼。在第二部分,我們將分三塊構建我們的模型。首先在基本的Seq2Seq模型中,我們輸入一個序列,通過Encoder端將序列轉換為一個固定長度的向量(Context Vector),這個向量包含了輸入序列的信息,將這個向量再作為輸入傳遞給Decoder端生成新的序列。我們以翻譯為例,如下圖所示:

我們輸入」我愛機器學習「,期望它能夠生成其對應的英文翻譯:」I love machine learning「。從輸入到輸出,共經歷了以下幾個步驟:

  1. 對」我愛機器學習「文本進行分詞得到四個詞」我「,」愛「,」機器「,」學習「;
  2. 將每個詞進行Embedding嵌入,即轉化為稠密向量;
  3. 輸入給LSTM進行學習;
  4. LSTM學習序列得到固定長度的Context Vector;
  5. Context Vector作為輸入傳輸給Decoder端,進行新詞生成;
  6. 得到翻譯結果」I love machine learning「。

在上圖中,左下角部分是Encoder端,右上角部分是Decoder端,因此,我們在函數和代碼的構件上也大題遵循模型結構。我們的模型代碼模塊主要分為以下四個部分:

  • 模型輸入 model_inputs
  • Encoder端 encoder_layer
  • Decoder端
    • Decoder輸入端 decoder_layer_inputs
    • Decoder訓練 decoder_layer_train
    • Decoder預測/推斷 decoder_layer_inference
  • Seq2Seq模型

1. 模型輸入

模型輸入主要是實現tensor的構造。

這個函數構造了inputs,targets等tensor。具體來說,inputs是一個二維tensor,第一個維度代表batch size,第二個維度是序列長度,targets同理。如下圖所示:

inputs是經過編碼以後的文本序列(圖中數字我瞎寫的,僅供說明圖),如果我們此時只給模型傳入一個batch,則inputs的shape為[1, 4],其中1對應batch的大小,4對應序列長度。targets同理。

2. Encoder端

Encoder端即編碼端,其目的是通過學習序列來將其映射為一個固定長度向量。具體實現代碼為:

首先,我們要對輸入的inputs進行詞向量嵌入,進而使用LSTM進行序列學習。通過tf.nn.dynamic_rnn方法返回LSTM的序列狀態,其中encoder_states即為我們所需的Context Vector。

具體而言,encoder_embed是經過嵌入的詞向量,如果指定encoder_embed_size為100,則我們得到的每個詞的嵌入向量就是100維,如上圖所示。LSTM最終的狀態結果就是Context Vector。

3. Decoder端

在Decoder端,我們分為三個模塊:

  • decoder_layer_inputs:主要用來處理Decoder的輸入;
  • decoder_layer_train:用於訓練過程中的Decoder;
  • decoder_layer_infer:用於測試過程中的Decoder。

( 1 ) Decoder輸入

Decoder端輸入需要特殊處理,如下圖所示:

在Decoder端,我們用Context Vector和上一輪的輸出結果來生成當前階段的單詞。當我們在最後一個階段時,上一輪輸入結果的learning,此時根據learning我們輸出了<EOS>,代表著翻譯的結束。我們可以發現,最後一的輸出詞並沒有再作為輸入來進行預測,也就是說<EOS>並不會作為下一階段的輸入。我們可以知道,由於Decoder端最後一個輸出詞並不參與到新一輪的輸入中,我們可以將輸出結果的最後一個單詞去掉。

另外,我們需要在Decoder端用「<GO>」告訴它翻譯的開始。因此,我們的函數就用來完成這兩個功能:

第10行代碼用來將整個batch中的每一個句子的最後一個字元去掉,第12行代碼則在每個句子前面添加一個起始符「<GO>」。

具體如下圖:

先刪除最後一個字元「<EOS>」,在加入起始詞「<GO>」。

( 2 ) Decoder訓練

完成了input的構建,我們需要再構造decoder端的訓練(train)與預測(infer)函數。為什麼這裡train和infer要分開呢?我們知道在LSTM中,會將上一輪的輸出結果作為下一輪的輸入,例如在預測「learning」的時候,我們會將上一輪的輸出「machine」作為輸入,但如果上一輪的輸入不夠準確,或者有誤差,那麼就意味著會影響到後續的所有預測結果。

那怎麼解決呢?具體來說,由於我們訓練過程中知道每一輪的真實輸入(ground truth)是啥,因此,我們可以強制用真實輸入來進行訓練,這種方式叫做Teacher Forcing,如下圖所示:

在第一行中,我們用普通的方式訓練,即用上一輪的輸出作為下一輪的輸入,當我們在某一輪出錯時,即love本身應該輸出machine,但卻輸出了apple,這種錯誤會不斷累積到後續的訓練中,將會導致翻譯結果完全錯誤;而採用Teacher Forcing時,由於我們知道訓練樣本真實的targets應該是I love machine learning,因此我們的每一輪輸入都用真實標記去訓練,這樣可以保證在訓練過程中緩解誤差對後續訓練的影響。

但是Teacher Forcing方法僅適用於訓練過程,因為在預測過程中,我們無法得知真實標記,只能將前一個的輸出作為當前的輸入。因此在這裡我們需要構建兩個函數來區分訓練和預測過程。

在TensorFlow中通過TrainingHelper構造一個helper對象,並傳入BasicDecoder中。

在dynamic_decode方法中,impute_finished為True時,當預測被標記為結束時,會自動複製前一輪狀態向量,並將輸出都置為0。

Python boolean. If True, then states for batch entries which are marked as finished get copied through and the corresponding outputs get zeroed out.

( 3 ) Decoder預測

在預測階段,構造GreedyEmbeddingHelper對象傳入BasicDecoder中。

TensorFlow的介面中對GreedyEmbeddingHelper的定義為:

A helper for use during inference.

Uses the argmax of the output (treated as logits) and passes the result through an embedding layer to get the next input.

( 4 ) Decoder層

Decoder層對上述函數進行了組裝。

上面函數主要定義了幾個內容:

  1. 第19行到20行代碼對Decoder端的輸入數據進行Embedding;
  2. 第26行代碼用來構造LSTM層;
  3. 第29行代碼用來構造全連接層;
  4. 第31-49行代碼用來調用train和infer獲取logits。

4. Seq2Seq模型

通過上面的函數,我們已經將Encoder和Decoder端的各個模塊都定義完成,下面再構造一個函數來將這些組件拼在一起。

上面的代碼主要分為三步:

  1. 第24行代碼來獲得Encoder端對輸入源序列的編碼結果;
  2. 第27行代碼用來處理Decoder端的輸入;
  3. 第29行代碼調用decoder端獲得train和infer的輸出。

第三部分 模型訓練與預測

經過數據處理部分與模型構建部分,我們完成了訓練前的準備工作。接下來我們需要定義超參數,啟動我們的圖,並喂入數據進行訓練。

定義的超參數如下:

設置了10輪迭代,1層LSTM,encoder與decoder的嵌入詞向量維度均為100維,並指定每訓練50輪列印一次結果。

由於我們的訓練語料比較少,僅有13W條,而機器翻譯這種待學習參數規模較大的模型需要大量的訓練文本,因此我在這裡並沒有劃分train和validation,用了所有的13W數據進行了train。

經過模型訓練,我們可以看到訓練的Loss最終在0.01左右徘徊,大家也可以自己重新調整參數進行訓練。

預測部分我們將輸入一些訓練數據本身有的句子,來看看翻譯效果如何:

---------------------------------------【例子1】---------------------------------------

我們輸入「the united states is never beautiful during march , and it is usually relaxing in summer .」

模型給出的結果是「les états-unis est jamais belle en mars , et il est relaxant habituellement en hiver . <EOS>」

Google翻譯的結果是「les états - unis nest jamais beau en mars, et il est habituellement relaxant en été .」

不懂法語沒關係,我們將模型給出的法語翻譯結果再用Google翻譯到英文看看:「the united states is beautiful in march, and its relaxing, usually in the winter.」

可以看到我們模型將原來的英文Summer翻譯成了winter,說明模型捕捉到了這裡的季節單詞,但具體的季節卻捕捉錯誤。另外句子前半句沒有捕捉到否定詞「never」。

---------------------------------------【例子2】---------------------------------------

我們輸入「I dislike grapefruit , lemons , and peaches .」

模型給出的結果是「je naime pamplemousses , les citrons et les mangues . <EOS>」

Google翻譯的結果是「je déteste les pamplemousses, citrons, et les pêches.」

我們將模型給出的法語翻譯結果再用Google翻譯到英文看看:「i dont like grapefruit, lemon and mango.」

這次翻譯結果相對還可以,模型將「peaches」錯翻譯為「mango」,同樣它捕捉到了這是水果,但具體是什麼水果沒有很好地學習到。

總體來說,模型在訓練數據的擬合上還是可以的。讀者也可以自行嘗試其他訓練數據中的句子進行測試。但如果輸入訓練過程中沒出現的句子,翻譯的結果就會大打折扣。

總結

本篇文章基於TensorFlow 1.6版本構建了基礎的Seq2Seq模型,並通過模型實現了一個簡單的英法翻譯模型。即通過Encoder端對輸入序列進行學習,得到編碼以後的Context Vector,再將Context Vector傳入給Decoder端進行學習,生成翻譯結果。在Decoder端,train階段採用teacher forcing方式,用ground truth作為輸入;而預測階段則採用前一輪生成結果作為輸入。最終模型在訓練數據翻譯效果上還算不錯。

本篇文章主要目的是想通過代碼讓大家對基礎Seq2Seq模型和翻譯模型有一個大概了解,因此對於模型的一些實現上做了簡化。本模型主要有以下幾點不足:

  • 訓練語料過少。對於翻譯模型這種大規模依賴數據的模型不是很適合;
  • 未劃分訓練集與測試集。這也是由於訓練語料過少導致的,劃分train和validation將使得模型訓練數據更少;
  • 評估指標過於簡單。一般來說,翻譯模型的效果用BLEU(bilingual evaluation understudy)進行評估,文中僅簡單地使用了loss來觀察訓練的收斂;
  • 對於語料數據的補全過於簡單。一般來說,語料的句子長度不一致,大語料中不僅包括長句,還有一些短句。可以看出本文的語料中幾乎都是短句子,英文句子最大長度是17,法語句子最大長度是23。而實際中們的句子可能更長,此時將短句子按照最大句子長度補全就顯得不是那麼合適,在工程上效率很低,因此多採用bucket對不同長度的語料進行分組。

基本的Seq2Seq模型相對比較簡單,實際上,對於模型的改進還有很多方面,例如加入BiRNN,捕捉到更全面的輸入序列的信息;加入Attention,在翻譯每個單詞時會使用不同的context vector等。後續專欄將會基於Keras對翻譯模型進行更進一步的改進。

專欄:機器不學習

個人簡介:天雨粟

GitHub:NELSONZHAO

轉載請聯繫作者獲得授權。

推薦閱讀:

圖解線性代數:如何理解orthogonal matrix
機器學習 | 八大步驟解決90%的NLP問題
《機器學習實戰》學習總結(八)——Adaboost演算法
白話word2vec

TAG:自然語言處理 | 深度學習DeepLearning | 機器學習 |