9.3 機器翻譯-Seq2Seq
前言
上一節通過影評資料分類任務介紹了seq2cls的場景,本節介紹seq2seq的任務——機器翻譯。
機器翻譯是典型的seq2seq任務,本節將採用傳統基於RNN的seq2seq結構模型進行,seq2seq任務有一些特點,並且需要重點理解,包括
- 特殊token:, , , 。分別表示起始、結束、未知和填充。尤其是“起始”和"結束",它們是序列生成任務中重要的概念。
- 序列最大長度:序列最大長度直接影響模型性能,過長導致模型難以學習,過短導致無法完成任務,本案例選擇長度為20。
- Teacher forcing 教師強制學習:本概念也是序列生成任務中,在訓練階段所涉及的重要概念,表示decoder的輸入採用標籤,而非自回歸式。
- 推理代碼邏輯:模型推理輸出序列時,採用的是自回歸方式,因而代碼與訓練時要做修改。
任務介紹
機器翻譯是經典的seq2seq任務,日常生活中也常用到翻譯工具,因此任務比較好理解。例如以下幾個句子就是本節要處理的任務。
- That mountain is easy to climb. 那座山很容易爬。
- It's too soon. 太早了。
- Japan is smaller than Canada. 日本比加拿大小。
現在需要構建模型,接收英文句子序列,進行綜合理解,然後輸出中文句子序列,在神經網路中,通常採用encoder-decoder架構,例如《動手學》中的示意圖。
- 編碼器,對輸入進行處理,獲得對輸入句子的綜合理解資訊——狀態。
- 解碼器,根據狀態,以及解碼器的輸入,逐個token的生成輸出,直到輸出特殊token——,模型停止輸出。
- 解碼器的輸入,採用自回歸方式(推理時),含義是當前時刻的輸入,來自上一時刻的輸出,特別地,第0個時刻,是沒有上一個時刻,所以採用特殊token——作為輸入。
資料模組
數據下載
本案例資料集Tatoeba下載自https://www.manythings.org/anki/,該專案是説明不同語言的人學習英語,因此是英語與其它幾十種語言的翻譯文本。
其中就包括本案例使用的英中文本,共計29668條(Mandarin Chinese - English cmn-eng.zip (29668))
資料以txt形式存儲,一行是一對翻譯文本,例如長這樣:
1.That mountain is easy to climb. 那座山很容易爬。
2.It's too soon. 太早了。
3.Japan is smaller than Canada. 日本比加拿大小。
資料集劃分
對於29668條資料進行8:2劃分為訓練、驗證,這裡採用配套代碼a_data_split.py進行劃分,即可在統計目錄下獲得train.txt和text.txt。
詞表構建
文本任務首要任務是為文本構建詞表,這裡採用與上節一樣的方法,首先對文本進行分詞,然後統計語料庫中所有的詞,最後根據最大上限、最小詞頻等約束,構建詞表。本部分配套代碼是b_gen_vocabulary.py
詞表的構建過程中,涉及兩個知識點:中文分詞和特殊token。
1. 中文分詞
對於英文,分詞可以直接採用空格。而對於中文,就需要用特定的分詞方法,這裡採用的是jieba分詞工具,以下是英文和中文的分詞代碼。
source.append(parts[0].split(' '))
target.append(list(jieba.cut(parts[1]))) # 分詞
Copy
2. 特殊token
由於seq2seq任務的特殊性,在解碼器部分,通常需要一個token告訴模型,現在是開始,同時還需要有個token讓模型輸出,以此告訴人類,模型輸出完畢,不要再繼續生成了。
因此相較于文本分類,還多了,, 兩個特殊token,有的時候,開始token也會用表示。
PAD_TAG = "<pad>" # 用PAD補全句子長度
BOS_TAG = "<bos>" # 用BOS表示開始
EOS_TAG = "<eos>" # 用EOS表示結束
UNK_TAG = "<unk>" # 用EOS表示結束
PAD = 0 # PAD字元對應的數位
BOS = 1 # BOS字元對應的數位
EOS = 2 # EOS字元對應的數位
UNK = 3 # UNK字元對應的數位
Copy
運行代碼後,詞表字典保存到了result目錄下,並得到如下輸出,表明英文中有2518個詞,中文有3365,但經過最大長度3000的截斷後,只剩下2996,另外4個是特殊token。
100%|██████████| 23635/23635 [00:00<00:00, 732978.24it/s]
原始詞表長度:2518,截斷後長度:2518
2522
保存詞頻統計圖:vocab_en.npy_word_freq.jpg
100%|██████████| 23635/23635 [00:00<00:00, 587040.62it/s]
保存統計圖:vocab_en.npy_length_freq.jpg
原始詞表長度:3365,截斷後長度:2996
3000
Copy
Dataset編寫
NMTDataset的編寫邏輯與上一小節的Dataset類似,首先在類初始化的時候載入原始資料,並進行分詞;在getitem反覆運算時,再進行token轉index操作,這裡會涉及增加結束符、填充符、未知符。
核心代碼如下:
def __init__(self, path_txt, vocab_path_en, vocab_path_fra, max_len=32):
self.path_txt = path_txt
self.vocab_path_en = vocab_path_en
self.vocab_path_fra = vocab_path_fra
self.max_len = max_len
self.word2index = WordToIndex()
self._init_vocab()
self._get_file_info()
def __getitem__(self, item):
# 獲取切分好的句子list,一個元素是一個詞
sentence_src, sentence_trg = self.source_list[item], self.target_list[item]
# 進行填充, 增加結束符,索引轉換
token_idx_src = self.word2index.encode(sentence_src, self.vocab_en, self.max_len)
token_idx_trg = self.word2index.encode(sentence_trg, self.vocab_fra, self.max_len)
str_len, trg_len = len(sentence_src) + 1, len(sentence_trg) + 1 # 有效長度, +1是填充的結束符 <eos>.
return np.array(token_idx_src, dtype=np.int64), str_len, np.array(token_idx_trg, dtype=np.int64), trg_len
def _get_file_info(self):
text_raw = read_data_nmt(self.path_txt)
text_clean = text_preprocess(text_raw)
self.source_list, self.target_list = text_split(text_clean)
Copy
模型模組
seq2seq模型,由編碼器和解碼器兩部分構成。
對於編碼器,需要的是其對輸入句子的全文理解,因此可採用RNN中輸出的hidden state特徵來表示,在這裡均採用LSTM作為基礎模型。
對於解碼器,同樣是一個LSTM,它接收3個資料,一個是輸入,另外兩個是hidden state和cell state。解碼器的輸入就是自回歸式的,上一時刻輸出的單詞傳到當前時刻。
這裡借助《動手學》的示意圖,理解seq2seq模型的樣子。
首先構建一個EncoderLSTM,這個比較簡單,只看它的forward,對於輸入的句子x,輸出hidden_state, cell_state。
def forward(self, x):
# Shape -----------> (26, 32, 300) [Sequence_length , batch_size , embedding dims]
embedding = self.dropout(self.embedding(x))
# Shape --> outputs (26, 32, 1024) [Sequence_length , batch_size , hidden_size]
# Shape --> (hs, cs) (2, 32, 1024) , (2, 32, 1024) [num_layers, batch_size, hidden_size]
outputs, (hidden_state, cell_state) = self.LSTM(embedding)
return hidden_state, cell_state
Copy
然後構建一個DecoderLSTM,解碼器除了需要對資料進行提特徵,獲得hidden_state, cell_state,還需要進行當前時刻,單詞的輸出,即token級的分類任務。
所以,它的forward返回有三個資訊,包括輸出的token預測向量,LSTM的hidden_state, cell_state,這裡需要注意,在代碼實現時,解碼器輸出的隱狀態預設包括了來自編碼器的,因此後續時間步不再需要從編碼器拿隱狀態特徵了。更直觀的就是,解碼器返回的hidden_state, cell_state,會是下一次forward輸入的hidden_state, cell_state。
def forward(self, x, hidden_state, cell_state):
x = x.unsqueeze(0) # x.shape == [1, batch_size]
embedding = self.dropout(self.embedding(x))
outputs, (hidden_state, cell_state) = self.LSTM(embedding, (hidden_state, cell_state))
predictions = self.fc(outputs)
predictions = predictions.squeeze(0)
return predictions, hidden_state, cell_state
Copy
最後,編寫一個Seq2Seq類,將編碼器和解碼器有序的組合起來,在這裡,核心任務是解碼器中,如何有序的進行每個時刻的資料處理。
該Seq2Seq類用於訓練階段,會設計teacher forcing(強制學習)的概念,它表示在訓練階段,解碼器的輸入時自回歸,還是根據標籤進行輸入,採用標籤進行輸入的方法稱為teacher forcing。
這裡仔細研究一下forward的後半部分——解碼器部分,前半部分就是編碼器進行一次性的編碼,獲得隱狀態特徵。
對於解碼器,採用for迴圈,依次進行token輸出,最終存儲在outputs中,for迴圈需要設置最大步數,一般根據任務的資料長度而定,這裡設置為32。
接著,解碼器工作,解碼器會輸出:預測的token分類向量,隱狀態資訊,其中隱狀態資訊會在下一個for迴圈時,輸入到解碼器中。
再往下看,解碼器的輸入x,則是根據一個條件判斷,以一定的概率採用標籤,一定的概率採用自回歸方式。這裡概率通常設置為0.5,如果設置為1,表明採用強制學習,永遠採用標籤作為輸入,也就是強制學習機制(teacher forcing)。
# Shape of x (32 elements)
x = target[0] # <bos> token
for i in range(1, target_len):
# output.shape == (bs, vocab_length)
# hidden_state.shape == [num_layers, batch_size size, hidden_size]
output, hidden_state, cell_state = self.Decoder_LSTM(x, hidden_state, cell_state)
outputs[i] = output
best_guess = output.argmax(1) # 0th dimension is batch size, 1st dimension is word embedding
x = target[i] if random.random() < tfr else best_guess # Either pass the next word correctly from the dataset or use the earlier predicted word
# Shape --> outputs (14, 32, 5766)
return outputs
Copy
模型訓練
資料和模型準備好之後,可以運行train_seq2seq.py進行訓練,seq2seq的訓練還有些許不同,主要包括:
- 採用blue進行指標評價;
- 損失函數加入ignore_index
BLEU是IBM在2002提出的,用於機器翻譯任務的評價,發表在ACL,引用次數10000+,原文題目是“BLEU: a Method for Automatic Evaluation of Machine Translation”。
它的總體思想就是準確率,假如給定標準譯文reference,模型生成的句子是candidate,句子長度為n,candidate中有m個單詞出現在reference,m/n就是bleu的1-gram的計算公式。
當統計不再是一個單詞,而是連續的N個單詞時,就有了n-gram的概念,片語的概念稱為n-gram,片語長度通常選擇1, 2, 3, 4
舉一個例子來看看實際的計算:
candinate: the cat sat on the mat
reference: the cat is on the mat
BLEU-1: 5/6 = 0.83
BLEU-2: 3/5 = 0.6
BLEU-3: 1/4 = 0.25
BLEU-4: 0/3 = 0
分子表示candidate中預測到了的片語的次數,如BLEU-1中,5分別表示, the, cat, on, the, mat預測中了。BLEU-2中,3分別表示, the cat, on the, the mat預測中了。以此類推。
針對BLEU還有些改進計算方法,可參考BLEU詳解
由於句子長度不一致,而訓練又需要將資料構造成統一長度的句子來組batch,因此會加入很多特殊token——,對應的index是0,所以會在計算損失函數的時候,這部分的loss是不計算的,可以巧妙的通過設置ignore_index來實現。 nn.CrossEntropyLoss(ignore_index=0)
由於資料量少,以及模型參數未精心設計,模型存在過擬合,這裡僅作為seq2seq任務的學習和理解,不對模型性能做進一步提升,因為後續有更強大的解決方案——基於Transformer。
模型推理
運行配套代碼c_inference.py,可以看到模型的表現如下所示,整體上像一個模型翻譯的樣子,但效果遠不如意,這裡包含多方面原因。
- 資料量過少,訓練資料僅2萬多條,而中英文的詞表就3000多
- 模型過於簡單,傳統RNN構建的seq2seq在上下文理解能力上表現欠佳,後續可加入注意力機制,或是基於Transformer架構來提升。
- 輸入: he didn't answer the phone , so i sent him an email . <eos>
- 標籤:他 沒有 <unk> , 所以 我 給 他 發 了 <unk> <unk> 。 <eos>
- 翻譯:我 不 <unk> , 所以 我 他 他 他 <unk> 。 <eos>
- 輸入: just as he was going out , there was a great earthquake . <eos>
- 標籤:就 在 他 要 出門 的 時候 , 發生 了 <unk> 。 <eos>
- 翻譯:他 他 的 <unk> <unk> , 但 他 <unk> <unk> 。 <eos>
- 輸入: tom hugged mary . <eos>
- 標籤:湯姆 擁抱 了 瑪麗 。 <eos>
- 翻譯:<unk> 了 瑪麗 。 <eos>
- 輸入: there are many americans who can speak japanese . <eos>
- 標籤:有 很多 美國 <unk> 說 日語 。 <eos>
- 翻譯:有 <unk> <unk> 能 非常 <unk> 。 <eos>
- 輸入: i'm not good at <unk> . <eos>
- 標籤:我 不 擅長 <unk> 。 <eos>
- 翻譯:我 不 太 擅長 運動 。 <eos>
Copy
小結
本節通過機器翻譯瞭解seq2seq任務,主要涉及一些特殊的、新的知識點,包括:
- 特殊token:解碼器需要兩個特殊的token,起始token和結束token,用於控制生成序列的起始和結束。
- 強制學習:訓練時,解碼器的輸入採用標籤
- 編碼器-解碼器的自回歸推理邏輯:需要for迴圈的形式,串列的依次生成句子中的每個單詞
- 中文分詞工具jieba
留言列表