機器翻譯(machine translation, MT)是用計算機來實現不同語言之間翻譯的技術。需要翻譯的語言通常稱為源語言(source language),翻譯成的結果語言稱為目標語言(target language)。機器翻譯即實現從源語言到目標語言轉換的過程,是自然語言處理的重要研究領域之一。
本文將帶領大家瞭解經典的端到端神經網路機器翻譯Seq2Seq模型,以及如何用PaddlePaddle來訓練。如果您想要實踐效果更佳的翻譯模型,請參考GitHub模型庫中Transformer實現。
Seq2Seq專案地址:
https://github.com/PaddlePaddle/book/blob/develop/08.machine_translation/README.cn.md
Transformer專案地址:
https://github.com/PaddlePaddle/models/tree/develop/PaddleNLP/neural_machine_translation/transformer
背景介紹
早期機器翻譯系統多為基於規則的翻譯系統,需要由語言學家編寫兩種語言之間的轉換規則,再將這些規則錄入計算機。該方法對語言學家的要求非常高,而且我們幾乎無法總結一門語言會用到的所有規則,更何況兩種甚至更多的語言。因此統計機器翻譯(Statistical Machine Translation, SMT)技術應運而生。
在統計機器翻譯技術中,轉化規則是由機器自動從大規模的語料中學習得到的,而非我們人主動提供規則。因此,它克服了基於規則的翻譯系統所面臨的知識獲取瓶頸的問題,但仍然存在許多挑戰:1)人為設計許多特徵(feature),但永遠無法覆蓋所有的語言現象;2)難以利用全域性的特徵;3)依賴於許多預處理環節,如詞語對齊、分詞或符號化(tokenization)、規則抽取、句法分析等,而每個環節的錯誤會逐步累積,對翻譯的影響也越來越大。
近年來,深度學習技術的發展為解決上述挑戰提供了新的思路。將深度學習應用於機器翻譯任務的方法大致分為兩類:1)仍以統計機器翻譯系統為框架,只是利用神經網路來改進其中的關鍵模組,如語言模型、調序模型等(見圖1的左半部分);2)不再以統計機器翻譯系統為框架,而是直接用神經網路將源語言對映到目標語言,即端到端的神經網路機器翻譯(End-to-End Neural MachineTranslation, End-to-End NMT)(見圖1的右半部分),簡稱為NMT模型。作為經典模型的實現,可以幫助大家更好的理解機器翻譯。
圖1:基於神經網路的機器翻譯系統
效果展示
以中英翻譯(中文翻譯到英文)的模型為例,當模型訓練完畢時,如果輸入如下已分詞的中文句子:
這些 是 希望 的 曙光 和 解脫 的 跡象 .
如果設定顯示翻譯結果的條數為3,生成的英語句子如下:
0 -5.36816 These are signs of hope and relief . <e>
1 -6.23177 These are the light of hope and relief . <e>
2 -7.7914 These are the light of hope and the relief of hope . <e>
左起第一列是生成句子的序號;左起第二列是該條句子的得分(從大到小),分值越高越好;左起第三列是生成的英語句子。
另外有兩個特殊標誌:<e>表示句子的結尾,<unk>表示未登入詞(unknown word),即未在訓練字典中出現的詞。
模型概覽
本節依次介紹雙向迴圈神經網路(Bi-directional Recurrent Neural Network),NMT模型中典型的編碼器-解碼器(Encoder-Decoder)框架以及柱搜尋(beam search)演算法。
雙向迴圈神經網路
我們這裡介紹Bengio團隊在論文[2,4]中提出的另一種結構。該結構的目的是輸入一個序列,得到其在每個時刻的特徵表示,即輸出的每個時刻都用定長向量表示到該時刻的上下文語義資訊。
具體來說,該雙向迴圈神經網路分別在時間維以順序和逆序——即前向(forward)和後向(backward)——依次處理輸入序列,並將每個時間步RNN的輸出拼接成為最終的輸出層。這樣每個時間步的輸出節點,都包含了輸入序列中當前時刻完整的過去和未來的上下文資訊。下圖展示的是一個按時間步展開的雙向迴圈神經網路。該網路包含一個前向和一個後向RNN,其中有六個權重矩陣:輸入到前向隱層和後向隱層的權重矩陣,隱層到隱層自己的權重矩陣,前向隱層和後向隱層到輸出層的權重矩陣。注意,該網路的前向隱層和後向隱層之間沒有連線。
圖2:按時間步展開的雙向迴圈神經網路
編碼器-解碼器框架
編碼器-解碼器(Encoder-Decoder)[2]框架用於解決由一個任意長度的源序列到另一個任意長度的目標序列的變換問題。即編碼階段將整個源序列編碼成一個向量,解碼階段通過最大化預測序列概率,從中解碼出整個目標序列。編碼和解碼的過程通常都使用RNN實現。
圖3:編碼器-解碼器框架
編碼器
編碼階段分為三步:
1. one-hot vector表示:將源語言句子
的每個詞表示成一個列向量,i=1,2,...,T。這個向量的維度與詞彙表大小|V| 相同,並且只有一個維度上有值1(該位置對應該詞在詞彙表中的位置),其餘全是0。
2. 對映到低維語義空間的詞向量:one-hot vector表示存在兩個問題,1)生成的向量維度往往很大,容易造成維數災難;2)難以刻畫詞與詞之間的關係(如語義相似性,也就是無法很好地表達語義)。因此,需再one-hot vector對映到低維的語義空間,由一個固定維度的稠密向量(稱為詞向量)表示。記對映矩陣為,用表示第i個詞的詞向量,K為向量維度。
3.用RNN編碼源語言詞序列:這一過程的計算公式為
,其中是一個全零的向量,是一個非線性啟用函式,最後得到的
就是RNN依次讀入源語言T個詞的狀態編碼序列。整句話的向量表示可以採用h在最後一個時間步T的狀態編碼,或使用時間維上的池化(pooling)結果。
第3步也可以使用雙向迴圈神經網路實現更復雜的句編碼表示,具體可以用雙向GRU實現。前向GRU按照詞序列的順序依次編碼源語言端詞,並得到一系列隱層狀態。類似的,後向GRU按照(,...,)的順序依次編碼源語言端詞,得到。最後對於詞,通過拼接兩個GRU的結果得到它的隱層狀態,即=。
圖4:使用雙向GRU的編碼器
解碼器
機器翻譯任務的訓練過程中,解碼階段的目標是最大化下一個正確的目標語言詞的概率。思路是: 每一個時刻,根據源語言句子的編碼資訊(又叫上下文向量,context vector)c、真實目標語言序列的第i個和i時刻RNN的隱層狀態,計算出下一個隱層狀態。計算公式如下:
其中是一個非線性啟用函式;c是源語言句子的上下文向量,在不使用注意力機制時,如果編碼器的輸出是源語言句子編碼後的最後一個元素,則可以定義
是目標語言序列的第i個單詞,是目標語言序列的開始標記<s>,表示解碼開始;是i時刻解碼RNN的隱層狀態,是一個全零的向量。
1.將通過softmax歸一化,得到目標語言序列的第i+1個單詞的概率分佈。概率分佈公式如下:
其中是對每個可能的輸出單詞進行打分,再softmax歸一化就可以得到第i+1個詞的概率。
2.根據和計算代價。
3.重複步驟1~2,直到目標語言序列中的所有詞處理完畢。
機器翻譯任務的生成過程,通俗來講就是根據預先訓練的模型來翻譯源語言句子。生成過程中的解碼階段和上述訓練過程的有所差異,具體介紹請見柱搜尋算
柱搜尋演算法
柱搜尋(beam search)是一種啟發式圖搜尋演算法,用於在圖或樹中搜尋有限集合中的最優擴充套件節點,通常用在解空間非常大的系統(如機器翻譯、語音識別)中,原因是記憶體無法裝下圖或樹中所有展開的解。如在機器翻譯任務中希望翻譯“<s>你好<e>”,就算目標語言字典中只有3個詞(<s>
, <e>
, hello
),也可能生成無限句話(hello迴圈出現的次數不定),為了找到其中較好的翻譯結果,我們可採用柱搜尋演算法。
柱搜尋演算法使用廣度優先策略建立搜尋樹,在樹的每一層,按照啟發代價(heuristic cost)(本教程中,為生成詞的log概率之和)對節點進行排序,然後僅留下預先確定的個數(文獻中通常稱為beam width、beam size、柱寬度等)的節點。只有這些節點會在下一層繼續擴充套件,其他節點就被剪掉了,也就是說保留了質量較高的節點,剪枝了質量較差的節點。因此,搜尋所佔用的空間和時間大幅減少,但缺點是無法保證一定獲得最優解。
使用柱搜尋演算法的解碼階段,目標是最大化生成序列的概率。思路是:
1.每一個時刻,根據源語言句子的編碼資訊cc、生成的第ii個目標語言序列單詞和i時刻RNN的隱層狀態,計算出下一個隱層狀態。
2.將通過softmax歸一化,得到目標語言序列的第i+1個單詞的概率分佈。
3.根據取樣出單詞。
4.重複步驟1~3,直到獲得句子結束標記<e>或超過句子的最大生成長度為止。
注意:和的計算公式同解碼器中的一樣。且由於生成時的每一步都是通過貪心法實現的,因此並不能保證得到全域性最優解。
資料介紹
本教程使用WMT-14資料集中的bitexts(afterselection)作為訓練集,dev+test data作為測試集和生成集。
資料預處理
我們的預處理流程包括兩步:
將每個源語言到目標語言的平行語料庫檔案合併為一個檔案:
合併每個XXX.src和XXX.trg檔案為XXX。
XXX中的第i行內容為XXX.src中的第i行和XXX.trg中的第i行連線,用't'分隔。
建立訓練資料的“源字典”和“目標字典”。每個字典都有DICTSIZE個單詞,包括:語料中詞頻最高的(DICTSIZE - 3)個單詞,和3個特殊符號<s>(序列的開始)、<e>(序列的結束)和<unk>(未登入詞)。
示例資料
因為完整的資料集資料量較大,為了驗證訓練流程,PaddlePaddle介面paddle.dataset.wmt14中預設提供了一個經過預處理的較小規模的資料集。
該資料集有193319條訓練資料,6003條測試資料,詞典長度為30000。因為資料規模限制,使用該資料集訓練出來的模型效果無法保證。
模型配置說明
下面我們開始根據輸入資料的形式配置模型。首先引入所需的庫函式以及定義全域性變數。
from __future__ import print_function
import paddle
import paddle.fluid as fluid
import paddle.fluid.layers as pd
import os
import sys
try:
from paddle.fluid.contrib.trainer import *
from paddle.fluid.contrib.inferencer import *
except ImportError:
print(
"In the fluid 1.0, the trainer and inferencer are moving to paddle.fluid.contrib",
file=sys.stderr)
from paddle.fluid.trainer import *
from paddle.fluid.inferencer import *
dict_size = 30000 # 字典維度
source_dict_dim = target_dict_dim = dict_size # 源/目標語言字典維度
hidden_dim = 32 # 編碼器中的隱層大小
word_dim = 16 # 詞向量維度
batch_size = 2 # batch 中的樣本數
max_length = 8 # 生成句子的最大長度
topk_size = 50
beam_size = 2 # 柱寬度
is_sparse = True
decoder_size = hidden_dim # 解碼器中的隱層大小
model_save_dir = "machine_translation.inference.model"
然後如下實現編碼器框架:
def encoder(is_sparse):
# 定義源語言id序列的輸入資料
src_word_id = pd.data(
name="src_word_id", shape=[1], dtype='int64', lod_level=1)
# 將上述編碼對映到低維語言空間的詞向量
src_embedding = pd.embedding(
input=src_word_id,
size=[dict_size, word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))
# LSTM層:fc + dynamic_lstm
fc1 = pd.fc(input=src_embedding, size=hidden_dim * 4, act='tanh')
lstm_hidden0, lstm_0 = pd.dynamic_lstm(input=fc1, size=hidden_dim * 4)
# 取源語言序列編碼後的最後一個狀態
encoder_out = pd.sequence_last_step(input=lstm_hidden0)
return encoder_out
再實現訓練模式下的解碼器:
def train_decoder(context):
# 定義目標語言id序列的輸入資料,並對映到低維語言空間的詞向量
trg_language_word = pd.data(
name="target_language_word", shape=[1], dtype='int64', lod_level=1)
trg_embedding = pd.embedding(
input=trg_language_word,
size=[dict_size, word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))
rnn = pd.DynamicRNN()
with rnn.block(): # 使用 DynamicRNN 定義每一步的計算
# 獲取當前步目標語言輸入的詞向量
current_word = rnn.step_input(trg_embedding)
# 獲取隱層狀態
pre_state = rnn.memory(init=context)
# 解碼器計算單元:單層前饋網路
current_state = pd.fc(input=[current_word, pre_state],
size=decoder_size,
act='tanh')
# 計算歸一化的單詞預測概率
current_score = pd.fc(input=current_state,
size=target_dict_dim,
act='softmax')
# 更新RNN的隱層狀態
rnn.update_memory(pre_state, current_state)
# 輸出預測概率
rnn.output(current_score)
return rnn()
實現推測模式下的解碼器:
def decode(context):
init_state = context
# 定義解碼過程迴圈計數變數
array_len = pd.fill_constant(shape=[1], dtype='int64', value=max_length)
counter = pd.zeros(shape=[1], dtype='int64', force_cpu=True)
# 定義 tensor array 用以儲存各個時間步的內容,並寫入初始id,score和state
state_array = pd.create_array('float32')
pd.array_write(init_state, array=state_array, i=counter)
ids_array = pd.create_array('int64')
scores_array = pd.create_array('float32')
init_ids = pd.data(name="init_ids", shape=[1], dtype="int64", lod_level=2)
init_scores = pd.data(
name="init_scores", shape=[1], dtype="float32", lod_level=2)
pd.array_write(init_ids, array=ids_array, i=counter)
pd.array_write(init_scores, array=scores_array, i=counter)
# 定義迴圈終止條件變數
cond = pd.less_than(x=counter, y=array_len)
# 定義 while_op
while_op = pd.While(cond=cond)
with while_op.block(): # 定義每一步的計算
# 獲取解碼器在當前步的輸入,包括上一步選擇的id,對應的score和上一步的state
pre_ids = pd.array_read(array=ids_array, i=counter)
pre_state = pd.array_read(array=state_array, i=counter)
pre_score = pd.array_read(array=scores_array, i=counter)
# 更新輸入的state為上一步選擇id對應的state
pre_state_expanded = pd.sequence_expand(pre_state, pre_score)
# 同訓練模式下解碼器中的計算邏輯,包括獲取輸入向量,解碼器計算單元計算和
# 歸一化單詞預測概率的計算
pre_ids_emb = pd.embedding(
input=pre_ids,
size=[dict_size, word_dim],
dtype='float32',
is_sparse=is_sparse,
param_attr=fluid.ParamAttr(name='vemb'))
current_state = pd.fc(input=[pre_state_expanded, pre_ids_emb],
size=decoder_size,
act='tanh')
current_state_with_lod = pd.lod_reset(x=current_state, y=pre_score)
current_score = pd.fc(input=current_state_with_lod,
size=target_dict_dim,
act='softmax')
topk_scores, topk_indices = pd.topk(current_score, k=beam_size)
# 計算累計得分,進行beam search
accu_scores = pd.elementwise_add(
x=pd.log(topk_scores), y=pd.reshape(pre_score, shape=[-1]), axis=0)
selected_ids, selected_scores = pd.beam_search(
pre_ids,
pre_score,
topk_indices,
accu_scores,
beam_size,
end_id=10,
level=0)
with pd.Switch() as switch:
with switch.case(pd.is_empty(selected_ids)):
pd.fill_constant(
shape=[1], value=0, dtype='bool', force_cpu=True, out=cond)
with switch.default():
pd.increment(x=counter, value=1, in_place=True)
pd.array_write(current_state, array=state_array, i=counter)
pd.array_write(selected_ids, array=ids_array, i=counter)
pd.array_write(selected_scores, array=scores_array, i=counter)
length_cond = pd.less_than(x=counter, y=array_len)
finish_cond = pd.logical_not(pd.is_empty(x=selected_ids))
pd.logical_and(x=length_cond, y=finish_cond, out=cond)
translation_ids, translation_scores = pd.beam_search_decode(
ids=ids_array, scores=scores_array, beam_size=beam_size, end_id=10)
return translation_ids, translation_scores
進而,我們定義一個train_program來使用inference_program計算出的結果,在標記資料的幫助下來計算誤差。我們還定義了一個optimizer_func來定義優化器:
def train_program():
context = encoder()
rnn_out = train_decoder(context)
label = pd.data(
name="target_language_next_word", shape=[1], dtype='int64', lod_level=1)
cost = pd.cross_entropy(input=rnn_out, label=label)
avg_cost = pd.mean(cost)
return avg_cost
def optimizer_func():
return fluid.optimizer.Adagrad(
learning_rate=1e-4,
regularization=fluid.regularizer.L2DecayRegularizer(
regularization_coeff=0.1))
訓練模型
定義訓練環境
定義您的訓練環境,可以指定訓練是發生在CPU還是GPU上。
if use_cuda and not fluid.core.is_compiled_with_cuda():
return
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
定義資料提供器
下一步是為訓練和測試定義資料提供器。提供器讀入一個大小為 BATCH_SIZE的資料。paddle.dataset.wmt.train 每次會在亂序化後提供一個大小為BATCH_SIZE的資料,亂序化的大小為快取大小buf_size。
train_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.wmt14.train(dict_size), buf_size=1000),
batch_size=batch_size)
構造訓練器(trainer)
訓練器需要一個訓練程式和一個訓練優化函式。
trainer = Trainer(
train_func=train_program, place=place, optimizer_func=optimizer_func)
提供資料
feed_order用來定義每條產生的資料和paddle.layer.data之間的對映關係。比如,wmt14.train產生的第一列的資料對應的是src_word_id這個特徵。
feed_order = [
'src_word_id', 'target_language_word', 'target_language_next_word'
]
事件處理器
回撥函式event_handler在一個之前定義好的事件發生後會被呼叫。例如,我們可以在每步訓練結束後檢視誤差。
def event_handler(event):
if isinstance(event, EndStepEvent):
if event.step % 10 == 0:
print('pass_id=' + str(event.epoch) + ' batch=' + str(event.step))
if isinstance(event, EndEpochEvent):
trainer.save_params(model_save_dir)
開始訓練
最後,我們傳入訓練迴圈數(num_epoch)和一些別的引數,呼叫 trainer.train 來開始訓練
trainer = Trainer(
train_func=train_program, place=place, optimizer_func=optimizer_func)
trainer.train(
reader=train_reader,
num_epochs=EPOCH_NUM,
event_handler=event_handler,
feed_order=feed_order)
應用模型
定義解碼部分
使用上面定義的 encoder
和 decoder
函式來推測翻譯後的對應id和分數。
context = encoder()
translation_ids, translation_scores = decode(context)
定義資料
我們先初始化id和分數來生成tensors來作為輸入資料。在這個預測例子中,我們用wmt14.test
資料中的第一個記錄來做推測,最後我們用"源字典"和"目標字典"來列印對應的句子結果。
init_ids_data = np.array([1 for _ in range(batch_size)], dtype='int64')
init_scores_data = np.array(
[1. for _ in range(batch_size)], dtype='float32')
init_ids_data = init_ids_data.reshape((batch_size, 1))
init_scores_data = init_scores_data.reshape((batch_size, 1))
init_lod = [1] * batch_size
init_lod = [init_lod, init_lod]
init_ids = fluid.create_lod_tensor(init_ids_data, init_lod, place)
init_scores = fluid.create_lod_tensor(init_scores_data, init_lod, place)
test_data = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.wmt14.test(dict_size), buf_size=1000),
batch_size=batch_size)
feed_order = ['src_word_id']
feed_list = [
framework.default_main_program().global_block().var(var_name)
for var_name in feed_order
]
feeder = fluid.DataFeeder(feed_list, place)
src_dict, trg_dict = paddle.dataset.wmt14.get_dict(dict_size)
測試
現在我們可以進行預測了。我們要在feed_order
提供對應引數,放在executor
上執行以取得id和分數結果:
for data in test_data():
feed_data = map(lambda x: [x[0]], data)
feed_dict = feeder.feed(feed_data)
feed_dict['init_ids'] = init_ids
feed_dict['init_scores'] = init_scores
results = exe.run(
framework.default_main_program(),
feed=feed_dict,
fetch_list=[translation_ids, translation_scores],
return_numpy=False)
result_ids = np.array(results[0])
result_ids_lod = results[0].lod()
result_scores = np.array(results[1])
print("Original sentence:")
print(" ".join([src_dict[w] for w in feed_data[0][0][1:-1]]))
print("Translated score and sentence:")
for i in xrange(beam_size):
start_pos = result_ids_lod[1][i] + 1
end_pos = result_ids_lod[1][i+1]
print("%d\t%.4f\t%s\n" % (i+1, result_scores[end_pos-1],
" ".join([trg_dict[w] for w in result_ids[start_pos:end_pos]])))
break
總結
端到端的神經網路機器翻譯是近幾年興起的一種全新的機器翻譯方法。在本文中,我們介紹了NMT中典型的“編碼器-解碼器”框架。由於NMT是一個典型的Seq2Seq(Sequence to Sequence,序列到序列)學習問題,因此,Seq2Seq中的query改寫(query rewriting)、摘要、單輪對話等問題都可以用本教程的模型來解決。
參考文獻
1. Koehn P. Statistical machine translation[M]. Cambridge University Press, 2009.
2. Cho K, Van Merriënboer B, Gulcehre C, et al. Learning phrase representations using RNN encoder-decoderfor statistical machine translation[C]//Proceedings of the 2014 Conference on Empirical Methods inNatural Language Processing (EMNLP), 2014: 1724-1734.
3.Chung J, Gulcehre C, Cho K H, et al. Empiricalevaluation of gated recurrent neural networks on sequence modeling[J]. arXiv preprint arXiv:1412.3555, 2014.
4.Bahdanau D, Cho K, Bengio Y. Neuralmachine translation by jointly learning to align and translate[C]//Proceedings of ICLR 2015, 2015.
5.Papineni K, Roukos S, Ward T, et al. BLEU:a method for automatic evaluation of machine translation[C]//Proceedings of the 40th annual meeting on association forcomputational linguistics. Association for Computational Linguistics, 2002:311-318.