我們使用基于GRU的編碼器和解碼器來在Keras中實現這一方法。選擇GRU而不是LSTM,會讓事情變得簡單一些,因為GRU只有一個狀態向量,而LSTM有多個狀態向量。首先是編碼器,如代碼清單11-28所示。
代碼清單11-28 基于GRU的編碼器
from tensorflow import keras
from tensorflow.keras import layersembed_dim = 256
latent_dim = 1024source = keras.Input(shape=(None,), dtype="int64", name="english") ←----不要忘記掩碼,它對這種方法來說很重要
x = layers.Embedding(vocab_size, embed_dim, mask_zero=True)(source) ←----這是英語源句子。指定輸入名稱,我們就可以用輸入組成的字典來擬合模型
encoded_source = layers.Bidirectional(layers.GRU(latent_dim), merge_mode="sum")(x) ←----編碼后的源句子即為雙向GRU的最后一個輸出
接下來,我們來添加解碼器——一個簡單的GRU層,其初始狀態為編碼后的源句子。我們再添加一個Dense層,為每個輸出時間步生成一個在西班牙語詞表上的概率分布,如代碼清單11-29所示。
代碼清單11-29 基于GRU的解碼器與端到端模型
past_target = keras.Input(shape=(None,), dtype="int64", name="spanish") ←----這是西班牙語目標句子
x = layers.Embedding(vocab_size, embed_dim, mask_zero=True)(past_target) ←----不要忘記使用掩碼
decoder_gru = layers.GRU(latent_dim, return_sequences=True)
x = decoder_gru(x, initial_state=encoded_source) ←----編碼后的源句子作為解碼器GRU的初始狀態
x = layers.Dropout(0.5)(x)
target_next_step = layers.Dense(vocab_size, activation="softmax")(x) ←----預測下一個詞元
seq2seq_rnn = keras.Model([source, past_target], target_next_step) ←----端到端模型:將源句子和目標句子映射為偏移一個時間步的目標句子
在訓練過程中,解碼器接收整個目標序列作為輸入,但由于RNN逐步處理的性質,它將僅通過查看輸入中第0~N個詞元來預測輸出的第N個詞元(對應于句子的下一個詞元,因為輸出需要偏移一個時間步)?。這意味著我們只能使用過去的信息來預測未來——我們也應該這樣做,否則就是在作弊,這樣生成模型在推斷過程中將不會生效。下面開始訓練模型,如代碼清單11-30所示。
代碼清單11-30 訓練序列到序列循環模型
seq2seq_rnn.compile(optimizer="rmsprop",loss="sparse_categorical_crossentropy",metrics=["accuracy"])
seq2seq_rnn.fit(train_ds, epochs=15, validation_data=val_ds)
我們選擇精度來粗略監控訓練過程中的驗證集性能。模型精度為64%,也就是說,平均而言,該模型在64%的時間里正確預測了西班牙語句子的下一個單詞。然而在實踐中,對于機器翻譯模型而言,下一個詞元精度并不是一個很好的指標,因為它會假設:在預測第N+1個詞元時,已經知道了從0到N的正確的目標詞元。實際上,在推斷過程中,你需要從頭開始生成目標句子,不能認為前面生成的詞元都是100%正確的。現實世界中的機器翻譯系統可能會使用“BLEU分數”來評估模型。這個指標會評估整個生成序列,并且看起來與人類對翻譯質量的評估密切相關。最后,我們使用模型進行推斷,如代碼清單11-31所示。我們從測試集中挑選幾個句子,并觀察模型如何翻譯它們。我們首先將種子詞元"[start]“與編碼后的英文源句子一起輸入解碼器模型。我們得到下一個詞元的預測結果,并不斷將其重新輸入解碼器,每次迭代都采樣一個新的目標詞元,直到遇到”[end]"或達到句子的最大長度。
代碼清單11-31 利用RNN編碼器和RNN解碼器來翻譯新句子
import numpy as np
spa_vocab = target_vectorization.get_vocabulary() ←---- (本行及以下1行)準備一個字典,將詞元索引預測值映射為字符串詞元
spa_index_lookup = dict(zip(range(len(spa_vocab)), spa_vocab))
max_decoded_sentence_length = 20def decode_sequence(input_sentence):tokenized_input_sentence = source_vectorization([input_sentence])decoded_sentence = "[start]" ←----種子詞元for i in range(max_decoded_sentence_length):tokenized_target_sentence = target_vectorization([decoded_sentence])next_token_predictions = seq2seq_rnn.predict( ←---- (本行及以下2行)對下一個詞元進行采樣[tokenized_input_sentence, tokenized_target_sentence])sampled_token_index = np.argmax(next_token_predictions[0, i, :])sampled_token = spa_index_lookup[sampled_token_index] ←---- (本行及以下1行)將下一個詞元預測值轉換為字符串,并添加到生成的句子中decoded_sentence += " " + sampled_tokenif sampled_token == "[end]": ←----退出條件:達到最大長度或遇到停止詞元breakreturn decoded_sentencetest_eng_texts = [pair[0] for pair in test_pairs]
for _ in range(20):input_sentence = random.choice(test_eng_texts)print("-")print(input_sentence)print(decode_sequence(input_sentence))
請注意,這種推斷方法雖然非常簡單,但效率很低,因為每次采樣新詞時,都需要重新處理整個源句子和生成的整個目標句子。在實際應用中,你會將編碼器和解碼器分成兩個獨立的模型,在每次采樣詞元時,解碼器只運行一步,并重新使用之前的內部狀態。翻譯結果如代碼清單11-32所示。對于一個玩具模型而言,這個模型的效果相當好,盡管它仍然會犯許多低級錯誤。
代碼清單11-32 循環翻譯模型的一些結果示例
Who is in this room?
[start] quién está en esta habitación [end]
-
That doesn't sound too dangerous.
[start] eso no es muy difícil [end]
-
No one will stop me.
[start] nadie me va a hacer [end]
-
Tom is friendly.
[start] tom es un buen [UNK] [end]
有很多方法可以改進這個玩具模型。編碼器和解碼器可以使用多個循環層堆疊(請注意,對于解碼器來說,這會使狀態管理變得更加復雜)?,我們還可以使用LSTM代替GRU,諸如此類。然而,除了這些調整,RNN序列到序列學習方法還受到一些根本性的限制。源序列表示必須完整保存在編碼器狀態向量中,這極大地限制了待翻譯句子的長度和復雜度。這有點像一個人完全憑記憶翻譯一句話,并且在翻譯時只能看一次源句子。RNN很難處理非常長的序列,因為它會逐漸忘記過去。等到處理序列中的第100個詞元時,模型關于序列開始的信息已經幾乎沒有了。這意味著基于RNN的模型無法保存長期上下文,而這對于翻譯長文檔而言至關重要。正是由于這些限制,機器學習領域才采用Transformer架構來解決序列到序列問題。我們來看一下。