基於PaddlePaddle的機器翻譯教程 | 深度學習基礎任務系列

機器翻譯(machine translation, MT)是用計算機來實現不同語言之間翻譯的技術。需要翻譯的語言通常稱爲源語言(source language),翻譯成的結果語言稱爲目標語言(target language)。機器翻譯即實現從源語言到目標語言轉換的過程,是自然語言處理的重要研究領域之一。

本文將帶領大家瞭解經典的端到端神經網絡機器翻譯Seq2Seq模型,以及如何用PaddlePaddle來訓練。如果您想要實踐效果更佳的翻譯模型,請參考GitHhub模型庫中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 Machine Translation, End-to-End NMT)(見圖1的右半部分),簡稱爲NMT模型。作爲經典模型的實現,可以幫助大家更好的理解機器翻譯。

圖1:基於神經網絡的機器翻譯系統

效果展示

以中英翻譯(中文翻譯到英文)的模型爲例,當模型訓練完畢時,如果輸入如下已分詞的中文句子:

這些 是 希望 的 曙光 和 解脫 的 跡象 .

如果設定顯示翻譯結果的條數爲3,生成的英語句子如下:

0 -5.36816 These are signs of hope and relief .
1 -6.23177 These are the light of hope and relief .
2 -7.7914 These are the light of hope and the relief of hope .

  • 左起第一列是生成句子的序號;左起第二列是該條句子的得分(從大到小),分值越高越好;左起第三列是生成的英語句子。
  • 另外有兩個特殊標誌:表示句子的結尾,表示未登錄詞(unknown word),即未在訓練字典中出現的詞。

模型概覽

本節依次介紹雙向循環神經網絡(Bi-directional Recurrent Neural Network),NMT模型中典型的編碼器-解碼器(Encoder-Decoder)框架以及柱搜索(beam search)算法。

雙向循環神經網絡

我們這裏介紹Bengio團隊在論文[2,4]中提出的另一種結構。該結構的目的是輸入一個序列,得到其在每個時刻的特徵表示,即輸出的每個時刻都用定長向量表示到該時刻的上下文語義信息。

具體來說,該雙向循環神經網絡分別在時間維以順序和逆序——即前向(forward)和後向(backward)——依次處理輸入序列,並將每個時間步RNN的輸出拼接成爲最終的輸出層。這樣每個時間步的輸出節點,都包含了輸入序列中當前時刻完整的過去和未來的上下文信息。下圖展示的是一個按時間步展開的雙向循環神經網絡。該網絡包含一個前向和一個後向RNN,其中有六個權重矩陣:輸入到前向隱層和後向隱層的權重矩陣(W_1,W_3),隱層到隱層自己的權重矩陣(W_2,W_5),前向隱層和後向隱層到輸出層的權重矩陣(W_4,W_6)。注意,該網絡的前向隱層和後向隱層之間沒有連接。

圖2:按時間步展開的雙向循環神經網絡

編碼器-解碼器框架

編碼器-解碼器(Encoder-Decoder)[2]框架用於解決由一個任意長度的源序列到另一個任意長度的目標序列的變換問題。即編碼階段將整個源序列編碼成一個向量,解碼階段通過最大化預測序列概率,從中解碼出整個目標序列。編碼和解碼的過程通常都使用RNN實現。

圖3:編碼器-解碼器框架

編碼器

編碼階段分爲三步:

  1. one-hot vector表示:將源語言句子x={x_1, x_2,…, x_t}的每個詞x_i表示成一個列向量W_iϵ〖{0,1}〗^(|v|),i=1,2,…,T。這個向量W_i的維度與詞彙表大小|V| 相同,並且只有一個維度上有值1(該位置對應該詞在詞彙表中的位置),其餘全是0。

  2. 映射到低維語義空間的詞向量:one-hot vector表示存在兩個問題,1)生成的向量維度往往很大,容易造成維數災難;2)難以刻畫詞與詞之間的關係(如語義相似性,也就是無法很好地表達語義)。因此,需再one-hot vector映射到低維的語義空間,由一個固定維度的稠密向量(稱爲詞向量)表示。記映射矩陣爲CϵR^(K*|V|),用S_i= C_(W_i )表示第i個詞的詞向量,K爲向量維度。

  3. 用RNN編碼源語言詞序列:這一過程的計算公式爲h_i = ∅_θ (h_(i-1), S_i),其中h_0是一個全零的向量,∅_θ是一個非線性激活函數,最後得到的h = {h_1,…, h_T}就是RNN依次讀入源語言T個詞的狀態編碼序列。整句話的向量表示可以採用h在最後一個時間步T的狀態編碼,或使用時間維上的池化(pooling)結果。

第3步也可以使用雙向循環神經網絡實現更復雜的句編碼表示,具體可以用雙向GRU實現。前向GRU按照詞序列(x_1,…, x_T)的順序依次編碼源語言端詞,並得到一系列隱層狀態 類似的,後向GRU按照(x_T,…, x_1)的順序依次編碼源語言端詞,得到 最後對於詞x_i,通過拼接兩個GRU的結果得到它的隱層狀態,即h_i =

圖4:使用雙向GRU的編碼器

解碼器

機器翻譯任務的訓練過程中,解碼階段的目標是最大化下一個正確的目標語言詞的概率。思路是: 1. 每一個時刻,根據源語言句子的編碼信息(又叫上下文向量,context vector)c、真實目標語言序列的第i個詞u_i和i時刻RNN的隱層狀態z_i,計算出下一個隱層狀態z_(i+1)。計算公式如下:

其中∅_θ’是一個非線性激活函數;c是源語言句子的上下文向量,在不使用注意力機制時,如果編碼器的輸出是源語言句子編碼後的最後一個元素,則可以定義c = h_t ;u_i是目標語言序列的第i個單詞,u_0是目標語言序列的開始標記 < s > ,表示解碼開始;z_i是i時刻解碼RNN的隱層狀態,z_0是一個全零的向量。

  1. 將z_(i+1)通過softmax歸一化,得到目標語言序列的第i+1個單詞的概率分佈p_(i+1)。概率分佈公式如下:

其中w_(sZ_(i+1) )+b_z是對每個可能的輸出單詞進行打分,再softmax歸一化就可以得到第i+1個詞的概率p_(i+1)。
2. 根據p_(i+1)和u_(i+1)計算代價。
3. 重複步驟1~23,直到目標語言序列中的所有詞處理完畢。

機器翻譯任務的生成過程,通俗來講就是根據預先訓練的模型來翻譯源語言句子。生成過程中的解碼階段和上述訓練過程的有所差異,具體介紹請見柱搜索算法。

柱搜索算法

柱搜索(beam search)是一種啓發式圖搜索算法,用於在圖或樹中搜索有限集合中的最優擴展節點,通常用在解空間非常大的系統(如機器翻譯、語音識別)中,原因是內存無法裝下圖或樹中所有展開的解。如在機器翻譯任務中希望翻譯“< s >你好”,就算目標語言字典中只有3個詞(< s >, , hello),也可能生成無限句話(hello循環出現的次數不定),爲了找到其中較好的翻譯結果,我們可採用柱搜索算法。

柱搜索算法使用廣度優先策略建立搜索樹,在樹的每一層,按照啓發代價(heuristic cost)(本教程中,爲生成詞的log概率之和)對節點進行排序,然後僅留下預先確定的個數(文獻中通常稱爲beam width、beam size、柱寬度等)的節點。只有這些節點會在下一層繼續擴展,其他節點就被剪掉了,也就是說保留了質量較高的節點,剪枝了質量較差的節點。因此,搜索所佔用的空間和時間大幅減少,但缺點是無法保證一定獲得最優解。

使用柱搜索算法的解碼階段,目標是最大化生成序列的概率。思路是:
1. 每一個時刻,根據源語言句子的編碼信息cc、生成的第ii個目標語言序列單詞u_i和i時刻RNN的隱層狀態z_i,計算出下一個隱層狀態z_(i+1)。
2. 將z_(i+1)通過softmax歸一化,得到目標語言序列的第i+1個單詞的概率分佈p_(i+1)。
3. 根據p_(i+1)採樣出單詞u_(i+1)。
4. 重複步驟1~3,直到獲得句子結束標記或超過句子的最大生成長度爲止。

注意:z_(i+1)和p_(i+1)的計算公式同解碼器中的一樣。且由於生成時的每一步都是通過貪心法實現的,因此並不能保證得到全局最優解。

數據介紹

本教程使用WMT-14數據集中的bitexts(after selection)作爲訓練集,dev+test data作爲測試集和生成集。

數據預處理

我們的預處理流程包括兩步:

  • 將每個源語言到目標語言的平行語料庫文件合併爲一個文件:
  • 合併每個XXX.src和XXX.trg文件爲XXX。
  • XXX中的第i行內容爲XXX.src中的第i行和XXX.trg中的第i行連接,用’t’分隔。

創建訓練數據的“源字典”和“目標字典”。每個字典都有DICTSIZE個單詞,包括:語料中詞頻最高的(DICTSIZE - 3)個單詞,和3個特殊符號< s >(序列的開始)、(序列的結束)和(未登錄詞)。

示例數據

因爲完整的數據集數據量較大,爲了驗證訓練流程,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 translatio
n_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
plac
e = 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-decoder for statistical machine translation[C]//Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), 2014: 1724-1734.
3. Chung J, Gulcehre C, Cho K H, et al. Empirical evaluation of gated recurrent neural networks on sequence modeling[J]. arXiv preprint arXiv:1412.3555, 2014.
4. Bahdanau D, Cho K, Bengio Y. Neural machine 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 for computational linguistics. Association for Computational Linguistics, 2002: 311-318.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章