1. 程式人生 > >TensorFlow從1到2(十)帶注意力機制的神經網路機器翻譯

TensorFlow從1到2(十)帶注意力機制的神經網路機器翻譯

基本概念

機器翻譯和語音識別是最早開展的兩項人工智慧研究。今天也取得了最顯著的商業成果。
早先的機器翻譯實際脫胎於電子詞典,能力更擅長於詞或者短語的翻譯。那時候的翻譯通常會將一句話打斷為一系列的片段,隨後通過複雜的程式邏輯對每一個片段進行翻譯,最終組合在一起。所得到的翻譯結果應當說似是而非,最大的問題是可讀性和連貫性非常差。
實際從機器學習的觀點來講,這種翻譯方式,也不符合人類在做語言翻譯時所做的動作。其實以神經網路為代表的機器學習,更多的都是在“模仿”人類的行為習慣。
一名職業翻譯通常是這樣做:首先完整聽懂要翻譯的語句,將語義充分理解,隨後把理解到的內容,用目標語言複述出來。
而現在的機器翻譯,也正是這樣做的,谷歌的seq2seq是這一模式的開創者。

如果用電腦科學的語言來說,這一過程很像一個編解碼過程。原始的語句進入編碼器,得到一組用於代表原始語句“內涵”的陣列。這些陣列中的數字就是原始語句所代表的含義,只是這個含義人類無法讀懂,是需要由神經網路模型去理解的。隨後解碼過程,將“有含義的數字”解碼為對應的目標語言。從而完成整個翻譯過程。這樣的得到的翻譯結果,非常流暢,具有更好的可讀性。

(圖片來自谷歌NMT文件)

注意力機制是人類特有的大腦思維方式,比如看到下面這幅照片:

(圖片來自網際網路)
照片的內容實際很多,甚至如果從數學上說,背景樹林的複雜度要高於前景。但看到照片的人,都會先注意到迎面而來的飛盤,隨後是投擲者,接著是影象右側的小孩子。其它的資訊都被忽略了。

這是人類在上萬年的進化中所形成的本能。對於快速向自己移動的物體首先會看到、識別危險、並且快速應對。接著是可能對自己造成威脅的同類或者生物。為了做到集註,不得不忽略看起來無關緊要的東西。
在機器學習中引入注意力模型,在影象處理、機器翻譯、策略博弈等各個領域中都有應用。這裡的注意力機制有兩個作用:一是降低模型的複雜度或者計算量,把主要資源分配給更重要的內容。二是對應把最相關的輸入匯出到相關的輸出,更有針對性的得到結果。

在機器翻譯領域,前面我們已經確定和解釋了編碼、解碼模型。那麼第二點的輸入輸出相關性就顯得更重要。
我們舉例來說明:比如英文“I love you”,翻譯為中文是“我愛你”。在一個編碼解碼模型中,首先由編碼器處理“I love you”,從而得到中間語義,比如我們稱為C:

    C = Encoder("I love you")  

解碼的時候,如果沒有注意力機制,那序列輸出則是:

    "我" = Decoder(C)  
    "愛" = Decoder(C)  
    "你" = Decoder(C)  

因為C相當於“I love you”三個單詞共同的作用。那麼解碼的時候,每一個字的輸出,都相當於3個單詞共同作用的結果。這顯然是不合理的,而且也不大可能得到一個理想、順暢的結果。
一個理想的解碼模型應當類似這樣的方式:

    "我" = Decoder(C+"I")  
    "愛" = Decoder(C+"love")  
    "你" = Decoder(C+"you")  

當然,機器學習不是人。人通過大量的學習、經驗的積累,一眼就能看出來“I”對應翻譯成“我”,“love”翻譯成“愛”。機器不可能提前知道這一切,所以我們比較切實的方法,只能是增加一套權重邏輯,在不同的翻譯處理中,對應不同的權重屬性。這就好像下面這樣的方式:

    "我" = Decoder(C+0.8x"I"+0.1x"love"+0.2x"you")  
    "愛" = Decoder(C+0.1x"I"+0.7x"love"+0.1x"you")  
    "你" = Decoder(C+0.2x"I"+0.1x"love"+0.8x"you")  

沒錯了,這個權重值,比如翻譯“我”的時候的權重序列:(0.8,0.1,0.2),就是注意力機制。在翻譯某個目標單詞輸出的時候,通過注意力機制,模型集註在對應的某個輸入單詞。
當然,注意力機制還包含上面示意性的表示式沒有顯示出來的一個重要操作:結合解碼器的當前狀態、和編碼器輸入內容之後的狀態,在每一次翻譯解碼操作中更新注意力的權重值。

翻譯模型

回到上面的編解碼模型示意圖。編碼器、解碼器在我們的機器學習中,實際都是神經網路模型。那麼把上面的示意圖展開,一個沒有注意力機制的編碼、解碼翻譯模型是這個樣子:

(圖片來自谷歌NMT文件)

隨後,我們為這個模型增加解碼時候的權重機制。模型在處理每個單詞輸出的時候,會在權重的幫助下,把重點放在對應的輸入單詞上。示意圖如下:

(圖片來自谷歌NMT文件)

最終,結合權重生成的過程,成為完整的注意力機制。注意力機制主要作用於解碼,在每一個輸出步驟中都要重新計算注意力權重,並更新到解碼模型從而對輸出產生影響。模型的示意圖如下:

(圖片來自谷歌NMT文件)
圖片中注意力權重的來源和去向箭頭,要注意看清楚,這對你下面閱讀實現的程式碼會很有幫助。

樣本及樣本預處理

前面的編解碼模型示意圖,還有模擬的表示式,當然都做了很多簡化。實際上中間還有很多工作要做,首先是翻譯樣本庫。

本例中使用http://www.manythings.org/anki/提供的英文對比西班牙文樣本庫,網站上還有很多其它語言的對比樣本可以下載,有興趣的讀者不妨在做完這個練習後嘗試一下其它語言的機器翻譯。
這個樣本是文字格式,包含很多行,每一行都是一個完整的句子,包含英文和西班牙文兩部分,兩種文字之間使用製表符隔開,比如:

May I borrow this book? ¿Puedo tomar prestado este libro?

對於樣本庫,我們要進行以下幾項預處理:

  • 讀取樣本庫,建立資料集。每一行的樣本按語言分為兩個部分。
  • 為每一句樣本,增加開始標誌<start>和結束標誌<end>。看過《從鍋爐工到AI專家(10)》的話,你應當理解這種做法。經過訓練後,模型會根據這兩個標誌作為翻譯的開始和結束。

做完上面的處理後,剛才的那行樣本看起來會是這個樣子:

<start> may i borrow this book ? <end>
<start> ¿ puedo tomar prestado este libro ? <end>

注意標點符號也是語言的組成部分,每個部分用空格隔開,都需要單獨數字化。所以你能看到,上面的兩行例句,標點符號之前也添加了空格。

  • 進行資料清洗,去掉不支援的字元。
  • 把單詞數字化,建立從單詞到數字和從數字到單詞的對照表。
  • 設定一個句子的最大長度,把每個句子按照最大長度在句子的後端補齊。

一行句子數字化之後,編碼同單詞之間的對照關係可能類似下面的樣子:

Input Language; index to word mapping
1 ----> <start>
8 ----> no
38 ----> puedo
804 ----> confiar
20 ----> en
1000 ----> vosotras
3 ----> .
2 ----> <end>

Target Language; index to word mapping
1 ----> <start>
4 ----> i
25 ----> can
12 ----> t
345 ----> trust
6 ----> you
3 ----> .
2 ----> <end>

你可能注意到了,“can't”中的單引號作為不支援的字元被過濾掉了,不過你放心,這並不會影響模型的訓練。當然在一個完善的翻譯系統中,這樣的字元都應當單獨處理,本例中就忽略了。

模型構建

本例中使用了編碼器、解碼器、注意力機制三個網路模型,都繼承自keras.Model,屬於三個自定義的Keras模型。
三個模型共同組成了完整的翻譯模型。完整模型的組裝,是在訓練過程和翻譯(預測)過程中,通過相應子程式把他們組裝在一起的。這是因為它們三者之間的邏輯機制相對比較複雜。無法用前面常用的keras.models.Sequential方法直接耦合在一起。
自定義Keras模型在本系列中是第一次遇到,所以著重講一下。實現自定義模型有三個基本要求:

  • 繼承自keras.Model類。
  • 實現__init__方法,用於實現類的初始化,同所有面向物件的語言一樣,這裡主要完成基類和類成員的初始化工作。
  • 實現call方法,這是主要的計算邏輯。模型接入到神經網路之後,訓練邏輯和預測邏輯,都通過逐層呼叫call方法來完成計算。方法中可以使用keras中原有的網路模型和自己的計算通過組合來完成工作。

自定義模型之所以有這些要求,主要是為了自定義的模型,可以跟Keras原生層一樣,互相相容,支援多種模型的組合、互聯,從而共同形成更復雜的模型。

Encoder/Decoder主體都使用GRU網路,讀起來應當比較容易理解。有需要的話,複習一下《從鍋爐工到AI專家(10)》。
注意力機制的BahdanauAttention模型就很令人費解了,困惑的關鍵在於其中的演算法。演算法的計算部分只有兩行程式碼,程式碼本身都知道是在做什麼,但完全不明白組合在一起是什麼功能以及為什麼這樣做。其實閱讀由數學公式推導、轉換而來的程式程式碼都有這種感覺。所以現在很多的知識保護,根本不在於原始碼,而在於公式本身。沒有公式,很多原始碼非常難以讀懂。
這部分推薦閱讀Dzmitry Bahdanau的論文《Neural Machine Translation by Jointly Learning to Align and Translate》和之後Minh-Thang Luong改進的演算法《Effective Approaches to Attention-based Neural Machine Translation》。論文中對於理論做了詳盡解釋,也有公式的推導過程。
這裡的BahdanauAttention模型實際就是公式的程式實現。如果精力不夠的話,死記公式也算一種學習方法。

訓練和預測

我們以往碰到的模型,訓練和預測基本都是一行程式碼,幾乎沒有什麼需要解釋的。
今天的模型涉及了帶有注意力機制的自定義模型,主要的邏輯,是通過程式程式碼,在訓練和評估子程式中把模型組合起來完成的。
程式如果只是編碼器和解碼器串聯的邏輯,完全可以同以前一樣,一條keras.Sequential函式完成組裝,那就一點難度沒有了。而加上注意力機制,複雜度高了很多,也是最難理解的地方。做一個簡單的分析:

  • 編碼器Encoder是一次整句編碼,得到一個enc_output。enc_output相當於模型對整句語義的理解。
  • 解碼器Decoder是逐個單詞輸入,逐個單詞輸出的。訓練時,輸入序列由<start>起始標誌開始,到<end>標誌結束。預測時,沒有人知道這一句翻譯的結果是多少個單詞,就是逐個獲取Decoder的輸出,直到得到一個<end>標誌。
  • Encoder和Decoder都引出了隱藏層,用於計算注意力權重。keras.layers.GRU的state輸出其實就是隱藏層,平時這個引數我們是用不到的。
  • 對於每一個翻譯的輸出詞,注意力對其影響就是通過attention_weights * values,然後將結果跟前一個輸出詞一起作為Decoder的GRU輸入,values實際就是編碼器輸出enc_output。
  • Decoder輸出上一個詞時候的隱藏層,跟enc_output一起通過公式計算,得到下一個詞的注意力權重attention_weights。在第一次迴圈的時候Decoder還沒有輸出過隱藏層,這時候使用的是Encoder的隱藏層。
  • 注意力權重attention_weights從程式邏輯上並不需要引出,程式中在Decoder中輸出這個值是為了繪製注意力對映圖,幫助你更好的理解注意力機制。所以如果是在這個基礎上做翻譯系統,輸出權重值到模型外部是不需要的。
  • 為了匹配各個網路的不同維度和不同形狀,注意力機制的計算邏輯和注意力權重經過了各種維度變形。Decoder的輸入雖然是一個詞,但也需要擴充套件成一批詞的第一個元素(也是唯一一個元素),這個跟我們以前的模型在預測時所做的是完全一樣的。

完整原始碼

下面是完整的可執行原始碼,請參考註釋閱讀:

#!/usr/bin/env python3

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf

import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

import unicodedata
import re
import numpy as np
import os
import io
import time
import sys

# 如果命令列增加了引數'train'則進入訓練模式,否則按照翻譯模式執行
TRAIN = False
if len(sys.argv) == 2 and sys.argv[1] == 'train':
    TRAIN = True

# 下載樣本集,下載後自動解壓。資料儲存在路徑:~/.keras/datasets/
path_to_zip = tf.keras.utils.get_file(
    'spa-eng.zip',
    origin='http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip',
    extract=True)
# 指向解壓後的樣本檔案
path_to_file = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"

# 將文字從unicode編碼轉換為ascii編碼
def unicode_to_ascii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn')

# 對所有的句子做預處理
def preprocess_sentence(w):
    w = unicode_to_ascii(w.lower().strip())

    # 在單詞和標點之間增加空格
    # 比如: "he is a boy." => "he is a boy ."
    # 參考: https://stackoverflow.com/questions/3645931/python-padding-punctuation-with-white-spaces-keeping-punctuation
    w = re.sub(r"([?.!,¿])", r" \1 ", w)
    w = re.sub(r'[" "]+', " ", w)

    # 用空格替換掉除了大小寫字母和"."/ "?"/ "!"/ ","之外的字元
    w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)
    # 截斷兩端的空白
    w = w.rstrip().strip()

    # 在句子兩端增加開始和結束標誌
    # 這樣經過訓練後,模型知道什麼時候開始和什麼時候結束
    w = '<start> ' + w + ' <end>'
    return w

# 載入樣本集,對句子進行預處理
# 最終返回(英文,西班牙文)這樣的配對元組
def create_dataset(path, num_examples):
    lines = io.open(path, encoding='UTF-8').read().strip().split('\n')

    word_pairs = [[preprocess_sentence(w) for w in l.split('\t')]  for l in lines[:num_examples]]

    return zip(*word_pairs)
# 至此的輸出為:
# <start> go away ! <end>
# <start> salga de aqui ! <end>
# 這樣的形式。

# 獲取最長的句子長度
def max_length(tensor):
    return max(len(t) for t in tensor)

# 將單詞數字化之後的數字<->單詞雙向對照表
def tokenize(lang):
    lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(
        filters='')
    lang_tokenizer.fit_on_texts(lang)

    tensor = lang_tokenizer.texts_to_sequences(lang)

    tensor = tf.keras.preprocessing.sequence.pad_sequences(
        tensor,
        padding='post')

    return tensor, lang_tokenizer

def load_dataset(path, num_examples=None):
    # 載入樣本,兩種語言分別儲存到兩個陣列
    targ_lang, inp_lang = create_dataset(path, num_examples)
    # 把句子數字化,兩種語言是兩套對照編碼
    input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
    target_tensor, targ_lang_tokenizer = tokenize(targ_lang)

    return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer

# 訓練的樣本集數量,越大翻譯效果越好,但訓練耗時越長
num_examples = 80000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)
# 至此,input_tensor/target_tensor 是數字化之後的樣本(數字陣列)
# inp_lang/targ_lang 是數字<->單詞編碼對照表
# 計算兩種語言中最長句子的長度
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)

# 將樣本按照8:2分為訓練集和驗證集
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)

##############################################

BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 64
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256
units = 1024
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1

dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)

# 編碼器模型
class Encoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
        super(Encoder, self).__init__()
        self.batch_sz = batch_sz
        self.enc_units = enc_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(
                                    self.enc_units, 
                                    return_sequences=True, 
                                    return_state=True, 
                                    recurrent_initializer='glorot_uniform')

    def call(self, x, hidden):
        x = self.embedding(x)
        output, state = self.gru(x, initial_state=hidden)
        return output, state

    def initialize_hidden_state(self):
        return tf.zeros((self.batch_sz, self.enc_units))

encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)

# 注意力模型
class BahdanauAttention(tf.keras.Model):
    def __init__(self, units):
        super(BahdanauAttention, self).__init__()
        self.W1 = tf.keras.layers.Dense(units)
        self.W2 = tf.keras.layers.Dense(units)
        self.V = tf.keras.layers.Dense(1)

    def call(self, query, values):
        # query為上次的GRU隱藏層
        # values為編碼器的編碼結果enc_output
        hidden_with_time_axis = tf.expand_dims(query, 1)

        # 計算注意力權重值
        score = self.V(tf.nn.tanh(
            self.W1(values) + self.W2(hidden_with_time_axis)))

        attention_weights = tf.nn.softmax(score, axis=1)

        # 使用注意力權重*編碼器輸出作為返回值,將來會作為解碼器的輸入
        context_vector = attention_weights * values
        context_vector = tf.reduce_sum(context_vector, axis=1)

        return context_vector, attention_weights

# 解碼器模型
class Decoder(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
        super(Decoder, self).__init__()
        self.batch_sz = batch_sz
        self.dec_units = dec_units
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.gru = tf.keras.layers.GRU(
            self.dec_units, 
            return_sequences=True,
            return_state=True, 
            recurrent_initializer='glorot_uniform')
        self.fc = tf.keras.layers.Dense(vocab_size)

        self.attention = BahdanauAttention(self.dec_units)

    def call(self, x, hidden, enc_output):
        # 使用上次的隱藏層(第一次使用編碼器隱藏層)、編碼器輸出計算注意力權重
        context_vector, attention_weights = self.attention(hidden, enc_output)

        x = self.embedding(x)

        # 將上一迴圈的預測結果跟注意力權重值結合在一起作為本次的GRU網路輸入
        x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)

        # state實際是GRU的隱藏層
        output, state = self.gru(x)

        output = tf.reshape(output, (-1, output.shape[2]))

        x = self.fc(output)

        return x, state, attention_weights

decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)


optimizer = tf.keras.optimizers.Adam()
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# 損失函式
def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)

    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask

    return tf.reduce_mean(loss_)

# 儲存中間訓練結果
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(optimizer=optimizer,
                                 encoder=encoder,
                                 decoder=decoder)

# 一次訓練
@tf.function
def train_step(inp, targ, enc_hidden):
    loss = 0

    with tf.GradientTape() as tape:
        # 輸入源語言句子進行編碼
        enc_output, enc_hidden = encoder(inp, enc_hidden)
        # 保留編碼器隱藏層用於第一次的注意力權重計算
        dec_hidden = enc_hidden

        # 解碼器第一次的輸入必定是<start>,targ_lang.word_index['<start>']是轉換為對應的數字編碼
        dec_input = tf.expand_dims([targ_lang.word_index['<start>']] * BATCH_SIZE, 1)       

        # 迴圈整個目標句子(用於對比每一次解碼器輸出同樣本的對比)
        for t in range(1, targ.shape[1]):
            # 使用本單詞、隱藏層、編碼器輸出共同預測下一個單詞,同事保留本次的隱藏層作為下一次輸入
            predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)
            # 計算損失值,最終的損失值是整個句子所有單詞損失值的合計
            loss += loss_function(targ[:, t], predictions)

            # 在訓練時,每次解碼器的輸入並不是上次解碼器的輸出,而是樣本目標語言對應單詞
            # 這稱為teach forcing
            dec_input = tf.expand_dims(targ[:, t], 1)

    # 所有單詞的平均損失值
    batch_loss = (loss / int(targ.shape[1]))
    # 最終的訓練參量是編碼器和解碼的集合
    variables = encoder.trainable_variables + decoder.trainable_variables
    # 根據代價值計算下一次的參量值
    gradients = tape.gradient(loss, variables)
    # 將新的參量應用到模型
    optimizer.apply_gradients(zip(gradients, variables))

    return batch_loss

def training():
    EPOCHS = 10

    for epoch in range(EPOCHS):
        start = time.time()
        # 初始化隱藏層和損失值
        enc_hidden = encoder.initialize_hidden_state()
        total_loss = 0

        # 一個批次的訓練
        for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
            batch_loss = train_step(inp, targ, enc_hidden)
            total_loss += batch_loss

        # 每100次顯示一下模型損失值
        if batch % 100 == 0:
            print('Epoch {} Batch {} Loss {:.4f}'.format(
                                                        epoch + 1,
                                                        batch,
                                                        batch_loss.numpy()))
        # 每兩次迭代儲存一次資料
        if (epoch + 1) % 2 == 0:
            checkpoint.save(file_prefix=checkpoint_prefix)
        # 顯示每次迭代的損失值和消耗時間
        print('Epoch {} Loss {:.4f}'.format(epoch + 1,
                                            total_loss / steps_per_epoch))
        print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))

# 根據命令列引數選擇本次是否進行訓練
if TRAIN:
    training()
################################################

# 評估(翻譯)一行句子
def evaluate(sentence):
    # 清空注意力圖
    attention_plot = np.zeros((max_length_targ, max_length_inp))
    # 句子預處理
    sentence = preprocess_sentence(sentence)
    # 句子數字化
    inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
    # 按照最長句子長度補齊
    inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs], 
                                                           maxlen=max_length_inp, 
                                                           padding='post')
    inputs = tf.convert_to_tensor(inputs)

    result = ''

    # 句子做編碼
    hidden = [tf.zeros((1, units))]
    enc_out, enc_hidden = encoder(inputs, hidden)

    # 編碼器隱藏層作為第一次解碼器的隱藏層值
    dec_hidden = enc_hidden
    # 解碼第一個單詞必然是<start>,表示啟動解碼
    dec_input = tf.expand_dims([targ_lang.word_index['<start>']], 0)

    # 假設翻譯結果不超過最長的樣本句子
    for t in range(max_length_targ):
        # 逐個單詞翻譯
        predictions, dec_hidden, attention_weights = decoder(dec_input,
                                                             dec_hidden,
                                                             enc_out)

        # 保留注意力權重用於繪製注意力圖
        # 注意每次迴圈的每個單詞注意力權重是不同的
        attention_weights = tf.reshape(attention_weights, (-1, ))
        attention_plot[t] = attention_weights.numpy()

        # 得到預測值
        predicted_id = tf.argmax(predictions[0]).numpy()

        # 從數字查錶轉換為對應單詞,累加到上一次結果,最終組成句子
        result += targ_lang.index_word[predicted_id] + ' '

        # 如果是<end>表示翻譯結束
        if targ_lang.index_word[predicted_id] == '<end>':
            return result, sentence, attention_plot

        # 上次的預測值,將作為下次解碼器的輸入
        dec_input = tf.expand_dims([predicted_id], 0)
    # 如果超過樣本中最長的句子仍然沒有翻譯結束標誌,則返回當前所有翻譯結果
    return result, sentence, attention_plot

# 繪製注意力圖
def plot_attention(attention, sentence, predicted_sentence):
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(1, 1, 1)
    ax.matshow(attention, cmap='viridis')

    fontdict = {'fontsize': 14}

    ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
    ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)

    plt.show()

# 翻譯一句文字
def translate(sentence):
    result, sentence, attention_plot = evaluate(sentence)

    print('Input: %s' % (sentence))
    print('Predicted translation: {}'.format(result))

    attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
    plot_attention(attention_plot, sentence.split(' '), result.split(' '))

# 恢復儲存的訓練結果
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))

# 測試以下翻譯
translate(u'hace mucho frio aqui.')
translate(u'esta es mi vida.')
translate(u'¿todavia estan en casa?')
# 據說這句話的翻譯結果不對,不懂西班牙文,不做評論
translate(u'trata de averiguarlo.')

第一次執行的時候要加引數tain:

$ ./translate_spa2en.py train
Epoch 1 Batch 0 Loss 4.5296
Epoch 1 Batch 100 Loss 2.2811
Epoch 1 Batch 200 Loss 1.7985
Epoch 1 Batch 300 Loss 1.6724
Epoch 1 Loss 2.0235
Time taken for 1 epoch 149.3063322815 sec
    ...訓練過程略...
    
Input: <start> hace mucho frio aqui . <end>
Predicted translation: it s very cold here . <end> 
Input: <start> esta es mi vida . <end>
Predicted translation: this is my life . <end> 
Input: <start> ¿ todavia estan en casa ? <end>
Predicted translation: are you still at home ? <end> 
Input: <start> trata de averiguarlo . <end>
Predicted translation: try to figure it out . <end> 

以後如果只是想測試翻譯效果,可以不帶train引數執行,直接看翻譯結果。
對於每一個翻譯句子,程式都會繪製注意力矩陣圖:

通常語法不是很複雜的句子,基本是順序對應關係,所以注意力亮點基本落在對角線上。
圖中X座標是西班牙文單詞,Y座標是英文單詞。每個英文單詞,沿X軸看,亮點對應的X軸單詞,表示對於翻譯出這個英文單詞,是哪一個西班牙文單詞權重最大。

(待續...