- 原文地址:RECURRENT NEURAL NETWORK (RNN) – PART 4: ATTENTIONAL INTERFACES
- 原文做者:GokuMohandas
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:TobiasLee
- 校對者:changkun Brucexz
本系列文章彙總html
在這篇文章裏,咱們將嘗試使用帶有注意力機制的編碼器-解碼器(encoder-decoder)模型來解決序列到序列(seq-seq)問題,實現的原理主要是根據這篇論文,具體請參考這裏。前端
首先,讓咱們來一窺整個模型的架構而且討論其中一些有趣的部分,而後咱們會在先前實現的不帶有注意力機制的編碼器-解碼器模型基礎之上,添加註意力機制,先前模型的實現細節在這裏,咱們將慢慢引入注意力機制,並實現模型的推斷。。注意:這個模型並不是當下最好的模型,更況且這些數據仍是我在幾分鐘內草率地編寫的。這篇文章旨在幫助你理解使用注意力機制的模型,從而你可以運用到更大的數據集上,而且取得很是不錯的結果。react
這張圖片是第一張圖的更爲具體的版本,包含了更多細節。讓咱們從編碼器開始講起,直到最後解碼器的輸出。首先,咱們的輸入數據是通過填充(Padding)和詞嵌入(Embedding)處理的向量,咱們將這些向量交給帶有一系列 cell(上圖中藍色的 RNN 單元)的 RNN 網絡,這些 cell 的輸出稱爲隱藏狀態(hidden state,上圖中的h0,h1等),它們被初始化爲零,但在輸入數據以後,這些隱藏狀態會改變而且持有一些很是有價值的信息。若是你使用的是一個 LSTM 網絡(RNN 的一種),咱們會把 cell 的狀態 c 和隱藏狀態 h 一塊兒向前傳遞給下一個 cell。對於每個輸入(上圖中的 X0等),在每個 cell 上咱們都會獲得一個隱藏狀態的輸出,這個輸出也會做爲下一個 cell 輸入的一部分。咱們把每一個神經元的輸出記做 h1 到 hN,這些輸出將會成爲咱們注意力模型的輸入。android
在咱們深刻探討注意力機制以前,先來看看解碼器是怎麼處理它的輸入以及如何產生輸出的。目標語言通過一樣的詞嵌入處理後做爲解碼器的輸入,以 GO 標識開始,以 EOS 和其後的一些填充部分做爲結束。解碼器的 RNN cell 一樣有着隱藏狀態,而且和上面同樣,被初始化爲零且隨着數據的輸入而產生變化。這樣看來,解碼器和編碼器彷佛沒有什麼不一樣。事實上,它們的不一樣之處在於解碼器還會接收一個由注意力機制產生的上下文向量 ci做爲輸入。在接下來的部分裏,咱們將詳細地討論上下文向量是如何產生的,它是基於編碼器的全部輸入以及前面解碼器 cell 的隱藏狀態所產生的一個很是重要的成果:上下文向量可以指導咱們在編碼器產生的輸入上如何分配注意力,來更好地預測接下來的輸出。ios
解碼器的每個 cell 利用編碼器產生的輸入,和前一個 cell 的隱藏狀態以及注意力機制產生的上下文向量來計算,最後通過 softmax 函數產生最終的目標輸出。值得注意的是,在訓練的過程當中,每一個 RNN cell 只使用這三個輸出來得到目標的輸出,然而在推斷階段中,咱們並不知道解碼器的下一個輸入是什麼。所以咱們將使用解碼器以前的預測結果來做爲新的輸入。git
如今,讓咱們仔細看看注意力機制是怎麼產生上下文向量的。github
上圖是注意力機制的示意圖,讓咱們先關注注意力層的輸入和輸出部分:咱們利用編碼器產生的全部隱藏狀態以及上一個解碼器 cell 的輸出,來給每個解碼器 cell 生成對應的上下文向量。首先,這些輸入都會通過一層 tanh 函數來產生一個形狀爲 [N, H] 的輸出矩陣e,編碼器中每一個 cell 的輸出都會產生對應解碼器中第 i 個 cell 的一個 eij。接下來對矩陣 e 應用一次 softmax 函數,就能獲得一個關於各個隱藏狀態的機率,咱們把這個結果記做 alpha。而後再利用 alpha 和原來的隱藏狀態矩陣 h 相乘,使得每一個 h 中的每個隱藏狀態得到個權重,最後進行求和就獲得了形狀爲 [N, H] 的上下文向量 ci,實際上這就是編碼器產生的輸入的一個帶有權重分佈的表示。後端
在訓練開始,這個上下文向量可能會比較隨意,可是隨着訓練的進行,咱們的模型將會不斷地學習編碼器產生的輸入中哪一部分是重要的,從而幫助咱們在解碼器這一端產生更好的結果。bash
如今讓咱們來實現這個模型,其中最重要的部分就是注意力機制。咱們將使用一個單向的 GRU 編碼器和解碼器,和前面那篇文章裏使用的很是相似,區別在於這裏的解碼器將會額外地使用上下文向量(表示注意力分配)來做爲輸入。另外,咱們還將使用 Tensorflow 裏的 embedding_attention_decoder()
接口。網絡
首先,讓咱們來了解一下將要處理並傳遞給編碼器/解碼器的數據集。
我爲模型建立了一個很小的數據集:20 個英語和對應的西班牙語句子。這篇教程的重點是讓你瞭解如何創建一個帶有軟注意力機制的編碼器-解碼器模型,來解決像機器翻譯等的序列到序列問題。因此我寫了關於我本身的 20 個英文句子,而後把他們翻譯成對應的西班牙語,這就是咱們的數據。
首先,咱們把這些句子變成一系列 token,再把 token 轉換成對應的詞彙 id。在這個處理過程當中,咱們會創建一個詞彙詞典,使咱們可以從 token 和詞彙 id 之間完成轉換。對於咱們的目標語言(西班牙語),咱們會額外地添加一個 EOS 標識。接下來咱們將對源語言和目標語言轉換得來的一組 token 進行填充操做,將它們補齊至最大長度(分別是它們各自的數據集中的最長句子長度),這將成爲最終咱們要餵給咱們模型的數據。咱們把通過填充的源語言數據傳給編碼器,但咱們還會對目標語言的輸入作一些額外的操做以得到解碼器的輸入和輸出。
最後,輸入就長成下面這個樣子:
這只是數據集中的一個例子,向量裏的 0 都是填充的部分,1 是 GO 標識,2 則是一個 EOS 標識。下圖是數據處理過程更通常的表示,你能夠忽略掉 target weights 這一部分,由於咱們的實現中不會用到它。
咱們經過 encoder_inputs
來給編碼器輸入數據。輸入數據的是一個形狀爲 [N, max_len] 的矩陣,經過詞嵌入變成 [N, max_len, H]。編碼器是一個動態 RNN,通過它的處理以後,咱們獲得一個形狀爲 [N, max_len, H] 的輸出,以及一個狀態矩陣,形爲 [N, H](這就是全部句子經 RNN 網絡後最後一個 cell 相關的狀態)。這些都將做爲咱們編碼器的輸出。
在討論注意力機制以前,先來看看解碼器的輸入和輸出。解碼器的初始狀態就是由編碼器傳遞來的,每一個句子通過 RNN 網絡後最有一個 cell 的狀態(形爲 [N, H])。Tensorflow 的 embedding_attention_decoder()
函數要求解碼器的輸入是按前後順序排列的(句子中詞的前後)的列表,因此咱們把 [N, max_len] 的輸入轉換爲 max_len 長的列表 [N]。咱們還使用通過 softmax 做用的權重矩陣處理解碼器的輸出,來建立咱們的輸出投影權重。咱們將時序列表(即通過轉換的 [N, max_len])、初始狀態、注意力矩陣以及投影權重做爲參數傳遞給 embedding_attention_deocder()
函數,獲得輸出(形狀爲 [max_len, N, H] 的輸出以及狀態矩陣 [N, H])。咱們獲得的輸出也是按時間前後排列的,咱們將對它們進行 flatten 操做而且應用 softmax 函數獲得一個形爲 [N max_len, C] 的矩陣。而後咱們一樣對目標輸出進行 reshape 操做,從 [N, max_len] 變成 **[N max_len,]** ,再利用 sparse_softmax_cross_entropy_with_logits()
來計算 loss 。接下來咱們會對 loss 進行一些遮蔽操做,來避免填充操做對 loss 形成的影響。
最後,總算到了注意力機制這一部分。咱們已經知道了輸入和輸出,咱們把一系列參數(時序列表、初始狀態、注意力矩陣這些編碼器的輸出)交給了 embedded_attention_decoder()
函數,但在這其中究竟發生了什麼?首先, 咱們會建立一系列權重來對輸入進行嵌入操做,咱們把這些權重命名爲 W_embedding。在經過輸入生成解碼器的輸出以後,咱們會開始一個循環函數,來決定將哪一部分輸出交給下一個解碼器做爲輸入。在訓練過程當中,咱們一般不會把前一個解碼器單元的輸出傳遞給下一個,因此這裏的循環函數是 None。而在推理期間,咱們會這樣作,因此這裏的循環函數就會使用 _extract_argmax_and_embed()
,它的用處就如它的名字所言(提取參數而且嵌入)。獲得解碼器單元的輸出以後,讓它和 softmax 後的權重矩陣相乘(output_projection),並將它的形狀從 [N, H] 轉換成 [N, C],再使用一樣 W_embedding 來替代通過嵌入操做的輸出([N, H]),再將通過處理的輸出做爲下一個解碼器單元的輸入。
# 若是咱們須要預測下一個詞語的話,使用以下的循環函數
loop_function = _extract_argmax_and_embed(
W_embedding, output_projection,
update_embedding_for_previous) if feed_previous else None複製代碼
另一個關於循環函數可選的參數是 update_embedding_
,若是設置爲 False,那麼在咱們對解碼器的輸出(GO token 除外)進行嵌入操做的時候,就會中止在 W_embedding 權重上使用梯度更新。所以,雖然咱們在兩個地方使用了 W_embedding,但它的值只依賴於咱們在解碼器的輸入上使用的詞嵌入而不是在輸出上(GO token 除外)。而後,咱們就能夠把通過嵌入操做的時序解碼器輸入、初始狀態、注意力矩陣以及循環函數交給 attention_decoder()
函數了。
attention_decoder()
函數是注意力機制的核心,這其中有一些額外的操做是文章開頭那篇論文中沒有提到的。回憶一下,注意力機制將會使用咱們的注意力矩陣(編碼器的輸出)以及前一個解碼器單元的狀態,這些值將被傳入一個 tanh 層,經過隱藏狀態獲得一個 e_ij(用來衡量句子對齊的程度的變量)。而後,咱們將使用 softmax 函數將它轉換爲 alpha_ij 用於和與原始注意力矩陣相乘。咱們對這個相乘以後的向量進行求和,這就是咱們的新的上下文向量c_i。最終,這個上下文向量將被用來產生咱們新的解碼器的輸出。
主要的不一樣之處在於,咱們的注意力矩陣(編碼器的輸出)和前一解碼器單元的狀態不是簡簡單單經過一個 _linear()
函數可以處理,而且應用常規的 tanh 函數的。咱們須要一些額外的步驟來解決這個問題:首先,對注意力矩陣使用一個 1x1 的卷積,這可以幫助咱們在注意力矩陣中提取重要的 features,而不是直接處理原有的數據——你能夠回想一下卷積層在圖樣識別中重要的特徵提取做用。這一步可以讓咱們擁有更好的特徵,但帶來的一個問題就是咱們須要用一個 4 維的向量來表示注意力矩陣。
''' 形狀轉換: 初始的隱藏狀態: [N, max_len, H] reshape 成 4D 的向量: [N, max_len, 1, H] = N 張 [max_len, 1, H] 形狀的圖片 因此咱們能夠在上面應用濾波器 濾波器: [1, 1, H, H] = [height, width, depth, # num filters] 使用 stride 爲 1 和 padding 爲 1 的卷積: H = ((H - F + 2P) / S) + 1 = ((max_len - 1 + 2)/1) + 1 = height'
W = ((W - F + 2P) / S) + 1 = ((1 - 1 + 2)/1) + 1 = 3
K = K = H
結果就是把
[N, max_len, H] 變成了 [N, height', 3, H] '''
hidden = tf.reshape(attention_states,
[-1, attn_length, 1, attn_size]) # [N, max_len, 1, H]
hidden_features = []
attention_softmax_weights = []
for a in xrange(num_heads):
# 濾波器
k = tf.get_variable("AttnW_%d" % a,
[1, 1, attn_size, attn_size]) # [1, 1, H, H]
hidden_features.append(tf.nn.conv2d(hidden, k, [1,1,1,1], "SAME"))
attention_softmax_weights.append(tf.get_variable(
"W_attention_softmax_%d" % a, [attn_size]))複製代碼
這就意味着,爲了處理通過轉換的 4 維注意力矩陣和前一解碼器單元狀態,咱們須要把後者也轉換成 4 維的表示。這個操做很簡單,只要將前一解碼器單元的狀態經過一個 MLP 的處理,就能把它變成一個 4 維的 tensor,從而匹配注意力矩陣的轉換。
y = tf.nn.rnn_cell._linear(
args=query, output_size=attn_size, bias=True)
# reshape 成 4 D
y = tf.reshape(y, [-1, 1, 1, attn_size]) # [N, 1, 1, H]
# 計算 Alpha
s = tf.reduce_sum(
attention_softmax_weights[a] *
tf.nn.tanh(hidden_features[a] + y), [2, 3])
a = tf.nn.softmax(s)
# 計算上下文向量 c
c = tf.reduce_sum(tf.reshape(
a, [-1, attn_length, 1, 1])*hidden, [1,2])
cs.append(tf.reshape(c, [-1, attn_size]))複製代碼
將注意力矩陣和前一解碼器單元的狀態都進行過轉換以後,咱們就能夠進行 tanh 操做了。咱們將 tanh 後的結果和 softmax 獲得的權重進行相乘、求和,再應用一次 softmax 函數獲得 alpha_ij。最後,咱們將 alphas 通過 reshape 後和初始注意力矩陣相乘,進行求和以後獲得咱們的上下文向量 c_i。
接下來就能夠挨個地處理解碼器的輸入了。先討論訓練過程,咱們不在意解碼器的輸出由於輸入最終都會變成輸出,因此這裏的循環函數是 None。咱們將經過一個使用 _linear()
函數的 MLP 以及前一個上下文向量來處理解碼器輸入(初始化爲零),而後和前一個解碼器單元的狀態一塊兒交給 dynamic_rnn 單元獲得輸出。咱們一次處理全部樣本數據的同一時刻的 token,由於咱們須要從當時索引的最後一個 token 所對應的前一個狀態。按時序排列的輸入使咱們在一批數據中這樣作更爲高效,這就是爲何咱們須要輸入變成一個時序列表的緣由。
獲得動態 RNN 的輸出和狀態以後,咱們就可以根據新的狀態計算出新的上下文向量。cell 的輸出和新的上下文向量再經過一個 MLP,最終就能獲得咱們的解碼器輸出。這些額外的 MLP 並無在解碼器的示意圖中畫出,但他們是咱們獲得輸出必要的額外步驟。值得注意的是,cell 的輸出和 attention_decoder 的輸出的形狀都是[max_len, N, H]。
而當咱們在進行推斷的時候,循環函數再也不是 None,而是 _extract_argmax_and_append()
。這個函數會接收前一個解碼器單元的輸出,而咱們新的解碼器單元的輸入就是先前的輸出通過 softmax 以後的結果,接下來對它進行重嵌入操做。在利用注意力矩陣進行w完全部處理以後,將 prev 將被更新爲新預測的輸出。
# 依次處理解碼器的輸入
for i, inp in enumerate(decoder_inputs):
if i > 0:
tf.get_variable_scope().reuse_variables()
if loop_function is not None and prev is not None:
with tf.variable_scope("loop_function", reuse=True):
inp = loop_function(prev, i)
# 把輸入和注意力向量合併
input_size = inp.get_shape().with_rank(2)[1]
x = tf.nn.rnn_cell._linear(
args=[inp]+attns, output_size=input_size, bias=True)
# 解碼器 RNN
cell_outputs, state = cell(x, state) # our stacked cell
# 經過注意力拿到上下文向量
attns = attention(state)
with tf.variable_scope('attention_output_projection'):
output = tf.nn.rnn_cell._linear(
args=[cell_outputs]+attns, output_size=output_size,
bias=True)
if loop_function is not None:
prev = output
outputs.append(output)
return outputs, state複製代碼
而後,咱們處理從 attention_decoder 獲得的輸出:使用 softmax 函數、進行 flatten 操做,最後和目標輸出進行比較並計算 loss。
在機器翻譯這樣的序列對序列的任務上使用注意力機制模型的效果是很是出色的,但經常由於語料庫的巨大帶來問題。特別是在咱們訓練時,計算解碼器的輸出的 softmax 是很是耗費資源的,解決的辦法就是使用 sampled softmax,你能夠在個人這篇文章裏瞭解到更多爲何要這麼作以及如何實現。
下面是 sampled softmax 的代碼,注意這裏的權重和咱們在解碼器上使用的 output_projection 是同樣的,由於使用它們的目的都是相同的:把解碼器的輸出(長度爲 H 的向量)轉換成對應類別數量長度的向量。
def sampled_loss(inputs, labels):
labels = tf.reshape(labels, [-1, 1])
# We need to compute the sampled_softmax_loss using 32bit floats to
# avoid numerical instabilities.
# 咱們使用32位的浮點數來計算 sampled_softmax_loss ,以免數值不穩定
local_w_t = tf.cast(w_t, tf.float32)
local_b = tf.cast(b, tf.float32)
local_inputs = tf.cast(inputs, tf.float32)
return tf.cast(
tf.nn.sampled_softmax_loss(local_w_t, local_b,
local_inputs, labels,
num_samples, self.target_vocab_size),
dtype)
softmax_loss_function = sampled_loss複製代碼
接下來,咱們能夠利用 seq_loss 函數來計算 loss,其中的權重向量除了目標輸出爲 PAD token 的部分是 0,其餘都是 1。值得注意的是,咱們只會在訓練過程當中使用 sampled softmax,而在進行預測的過程當中,咱們會對整個語料庫進行採樣,使用常規的 softmax,而不只僅只是一部分最爲近似的語料。
else:
losses.append(sequence_loss(
outputs, targets, weights,
softmax_loss_function=softmax_loss_function))複製代碼
另一種常見的附加結構是使用 tf.nn.seq2seq.model_with_buckets()
函數,這也是 Tensorflow 官方的 NMT 教程所使用的模型,這種 buckets 模型的優勢在於縮短了注意力矩陣向量的長度。在先前的模型中,咱們會把注意力向量應用在 max_len 長度的 hidden states 上。而在這裏,咱們只要對相關的一部分應用注意力向量,由於 PAD token 是徹底能夠被忽略的。咱們能夠選擇對應的 buckets 使得句子中的 PAD token 儘量的少。
但我我的以爲這個方法有一點粗糙,並且若是真的想要避免處理 PAD token 的話,我會建議使用 seq_lens 這個屬性來過濾掉編碼器輸出中的 PAD token,或者當咱們在計算上下文向量的時候,咱們能夠把每一個句子中 PAD token 對應的 hidden state 置爲 0。這種方法有點複雜,因此咱們不在這裏實現它,但 buckets 對於 PAD token 帶來的噪音確實不是一種優雅的解決方法。
注意力機制是研究的一個熱門,而且也存在不少變種。不管在什麼狀況下,這種模型在序列對序列的任務上老是能有很是出色的表現,因此我很是喜歡使用它。請謹慎地分割訓練集和驗證集,由於這種模型很容易過擬合從而在驗證集上產生很是糟糕的表現。在接下來的文章裏,咱們會使用注意力機制來解決設計內存和邏輯推理的更爲複雜的任務。
編碼器的輸出形爲 [N, max_len],通過嵌入操做以後轉變爲 [N, max_len, H],而後交給編碼器 RNN。編碼器的輸出形爲 [N, max_len, H],狀態矩陣形爲 [N, H],其中包含了各個樣本最後的 cell 的狀態。
編碼器的輸出和注意力向量的形狀都是 [N, max_len, H]。
解碼器的輸出形爲 [N, max_len],會被轉換爲一個 max_len 長度的時序列表,其中每一個向量的形狀爲 N。解碼器的初始狀態就是編碼器形爲 [N, H] 的狀態矩陣。在將數據輸入解碼器 RNN 以前,數據會被進行嵌入操做,變成一個 max_len 長度的時序列表,其中的每一個向量形狀爲 [N, H]。輸入數據多是真實的解碼器輸入,或者在進行預測的時候,就是由前一個解碼器 cell 產生的輸出。前一個解碼器 cell 在前一刻產生的輸出形爲 [N, H],將通過一層 softmax 層(輸出投影)而變成 [N, C]。而後使用咱們在輸入上使用的權重向量,再一次進行嵌入操做變回 [N, H]。這些輸入將被餵給解碼器 RNN,從而產生解碼器形爲 [max_len, N, H] 的輸出以及狀態矩陣 [N, H]。輸出將被進行 flatten 操做而變成 [N* max_len, H] 而且和一樣通過 flatten 操做的目標輸出進行比較(一樣形爲 [N* max_len, H])。若是目標輸出中有 PAD token 的話,在計算 loss 的時候會進行一些遮蔽操做,接下來就是 backprop 了。
在解碼器 RNN 內部,一樣有一些形狀轉變的操做。首先注意力向量(編碼器輸出)形爲 [N, max_len, H],將被轉化爲一個 4 維的向量 [N, max_len, 1, H](這樣咱們就可使用卷積操做了)而且利用卷積來提取有用的特徵。這些隱藏特徵的形狀也是 4 維,[N, height , 3, H]。解碼器的前一隱藏狀態x向量,形爲 [N, H],一樣是注意力機制的一個輸入。這個隱藏狀態向量通過一個 MLP 變成 [N, H] (這麼作的緣由是爲了防止前一隱藏狀態的第二維(H)和 attention_size 不一樣,在這裏一樣是 H)。接下來這個隱藏狀態向量一樣被轉換成一個 4 維向量 [N, 1, 1, H],這樣咱們就能夠將它和隱藏特徵相結合。咱們對相加的結果使用 tanh 函數,再經過 softmax 函數獲得 alpha_ij,其形狀爲 [N, max_len, 1, 1] (這表明了每一個樣本中各個隱藏狀態的機率)。這個 alpha 和原始的隱藏狀態相乘,獲得形爲 [N, max_len, 1, H]的向量,再進行求和獲得形爲 [N, H] 的上下文向量。
上下文向量和解碼器的形爲 [N, H] 的輸入結合,不管這個輸入是來自解碼器的輸入數據(訓練時候)仍是來自前一個 cell 的預測(預測時候),這個輸入只是長度爲 max_len 列表中形爲 [N, H] 向量的其中一個。首先咱們讓它和前一個上下文向量相加(初始化爲全 0 的 [N, H] 矩陣),回想一下咱們的來自於解碼器輸入的數據是一個時序列表,長度爲 N,其中的向量形爲 [max_len, ],這就是爲何輸入的形狀都是 [N, H]。相加的結果將通過一層 MLP,獲得一個形爲 [N, H] 的輸出,這和狀態矩陣(形狀爲 [N, H])將被交給咱們的動態 RNN cell 。獲得的輸出 cell_outputs 形爲 [N, H],而且狀態矩陣一樣爲 [N, H]。這個新的狀態矩陣將會成爲們下一個解碼器的輸入。咱們對 max_len 個輸入進行這樣的操做,從而獲得了一個長度爲 max_len 的是列表,其中的向量都是 [N, H]。在從解碼器獲得這個輸出和狀態矩陣以後,咱們將新的狀態矩陣傳給 attention 函數獲得新的上下文向量,新的上下文向量形爲 [N, H],而且和形爲 [N, H] 的輸出相加,再一次應用 MLP,轉換成形爲 [N, H] 的向量。最後,若是咱們在進行預測,新的 prev 將會成爲咱們的最終輸出(prev 初始爲 none)。prev 將會成爲 loop_function 的輸入,來獲得下一個解碼器的輸出。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。