重溫Seq2Seq和Attention機制

1. 寫在前面

最近用深度學習做一些時間序列預測的實驗, 用到了一些循環神經網絡的知識, 而當初學這塊的時候,只是停留在了表面,並沒有深入的學習和研究,只知道大致的原理, 並不知道具體的細節,所以導致現在復現一些經典的神經網絡會有困難, 所以這次藉着這個機會又把RNN, GRU, LSTM以及Attention的一些東西複習了一遍,真的是每一遍學習都會有新的收穫,之前學習過也沒有整理, 所以這次也藉着這個機會把這一塊的基礎內容進行一個整理和總結, 順便了解一下這些結構底層的邏輯。

當然,這次的整理是查缺補漏, 類似於知識的串聯, 一些很基礎的內容可能不會涉及到, 這一部分由於篇幅很長,所以打算用三篇基礎文章來整理, 分別是重溫循環神經網絡RNN重溫LSTM和GRU和重溫Seq2Seq與Attention機制。 前面兩篇已經搞定, 今天是第三篇, 嘗試整理Seq2Seq和Attention機制,這次依然是基於前面兩篇的知識, 這裏的邏輯就是Seq2Seq是一種編碼器-解碼器的網絡結構, 而這裏的編碼器和解碼器就可以是RNN或者是它的變體, 所以如果想弄明白Seq2Seq的工作原理, 就需要先知道RNN或者變體的工作原理。 而Attention是在Seq2Seq的基礎上又做了一些改進, 依然是這個結構,只不過在計算的時候加了一些新的東西,使得神經網絡在計算的時候有了注意力或者說聚焦的地方,使得網絡的工作更高效。

所以,這篇文章首先會從Seq2Seq這個結構出發, 說一下Seq2Seq是什麼樣子的, 爲什麼要有這個結構, 然後說一下它的計算原理, 最後分析一下單純的Seq2Seq有什麼問題,從而能夠引出爲什麼需要加入Attention, Attention的計算過程如何運用到這個結構中去, 在最後我們可以通過代碼的方式看一下如何實現一個帶有Attention的Seq2Seq,這樣有利於更好的理解細節。 這篇文章的內容有些多, 主要聚焦Attention, 這裏的Attention整理不像Attention is all you need這篇論文中的那樣, 這次重在細節。

大綱如下

  • Seq2Seq初識
  • Seq2Seq的工作原理和計算細節
  • Seq2Seq存在的問題及Attention初識
  • Attention的計算細節
  • 帶有Attention機制的Seq2Seq的簡單代碼實現
  • 總結

Ok, let’s go!

2. Seq2Seq初識

我們知道, RNN是非常擅長處理序列數據的, 但是RNN能夠應對的序列數據場景很有限, 你要是給個句子: The cat, which …, was tired! , 假設我們讓RNN預測一句話中的某個詞這種的, RNN能夠輕鬆應對。

But, 如果是比較複雜的任務, 不是讓RNN預測詞語, 而是讓它做機器翻譯, 我輸入一個句子,讓它輸出一個句子了, 這時候RNN就表現的不是那麼出色了, 爲啥呢? 因爲這種情況, 我們的輸入和輸出都是不定長的序列,比如將一句中文翻譯成英文,那麼這句英文的長度有可能會比中文短,也有可能會比中文長, 輸出就不確定了。 這時候單獨用一個RNN網絡就不是那麼好了, 所以就想到了能不能用兩個RNN網絡把輸入的處理和輸出的處理分開進行呢, 就誕生了Seq2Seq模型。

Seq2Seq模型是輸出的長度不確定時採用的模型, 現在應用場景也非常普遍, 在機器翻譯, 人機對話等都會看到這種模型的身影。 這個模型長這個樣子:
在這裏插入圖片描述
這個就是Seq2Seq的一個結構, 在這裏我們會看到, 這種模型使用了兩個RNN網絡, 左邊那個叫做編碼器, 負責的是將我們的輸入進行編碼, 右邊的叫做解碼器, 負責的是基於左邊的編碼進行生成我們想要的結果。 這樣,就能把輸入和輸出分開處理, 解決了輸入和輸出都不定長的問題。 是不是也挺簡單的? 其實就是兩個RNN而已, 哈哈。

當然, 外表是挺簡單的, 但是卻有着豐富的內涵, 所以下面就看一看具體的細節, 編碼器和解碼器到底在幹啥。

3. Seq2Seq的工作原理及計算細節

3.1 Seq2Seq的宏觀工作原理

在具體介紹編碼器和解碼器之前, 我們先來宏觀的看一下編碼器和解碼器的工作過程, 這裏再放一張和上面不一樣的圖(不一樣的圖可以幫助我們更好的理解原理,而不是侷限於圖本身), 這個圖出自2014年的一篇經典論文Learning Phrase Representations using RNN Encoder–Decoderfor Statistical Machine Translation

在這裏插入圖片描述
我們根據這個圖走一遍Seq2Seq的工作流程:宏觀上來看,上面的這個過程就是我們輸入一個句子(假設做機器翻譯), 這個句子的每個單詞要通過embedding的方式進行向量表示[X1,X2,...,XT][X_1, X_2, ...,X_T], 然後輸入到一個RNN裏面, RNN會計算出每個時間步的一個隱藏狀態, 通過組合這些隱藏狀態,我們會得到一個上下文向量C(注意這裏的C可不是LSTM裏的cell), 這個上下文向量可以理解成綜合了前面的輸入信息, 這就是左邊的編碼過程, 輸入是一個句子, 輸出是含有這個句子信息的上下文向量。

有了這個綜合輸入句子信息的上下文向量C, 然後把這個作爲解碼器的輸入, 去進行句子的翻譯, 當然句子翻譯的時候, 可能還需要前面時間步的預測的結果, 所以解碼器工作是這樣: 在第一個時間步, 會接收一個初始化的y0, 這個表示句子的起始, 一般可以初始化爲<bos>, 標誌着我句子要開始翻譯, 還會接收一個初始化的s0, 這個表示的隱藏狀態(之所以用s是爲了和編碼器那裏的隱藏狀態h區分開), 然後就是這個輸入C, 基於這三個就可以計算出第一個時間步隱藏狀態s1和輸出y1, 也就是第一個單詞。 然後來到第二個時間步,接收第一個時間步的輸出y1, 接收第一個時間步的隱藏狀態s1和C三個計算出s2和y2, 這樣依次進行下去, 直到遇到終止符(一般用<Eos>表示), 也就是說當神經網絡預測的概率最大的單詞是EOS的時候, 神經網絡就終止輸出了。

所以, 這個就是Seq2Seq的宏觀工作過程, 編碼器是接收一個輸入, 然後輸出一個綜合輸入信息的上下文向量。 解碼器部分接收這個上下文向量和前面時間步的預測值得到後面時間步的預測值,也是我們想要的結果。

那麼具體細節呢? 我們下面就分開看看吧:

3.2 Seq2Seq的計算細節

3.2.1 編碼器

編碼器的作用是把一個不定長的輸入序列變換成一個定長的背景變量C ,並在該背景變量中編碼輸入序列信息。編碼器可以使用循環神經網絡。
在這裏插入圖片描述
假設輸入序列是x1,x2,..xtx_1, x_2, ..x_t, xix_i表示輸入句子中的第i個單詞。 在時間步tt中, RNN將輸入的特徵向量xtx_t和上個時間步的隱藏狀態ht1h_{t−1}變換爲當前時間步的隱藏狀態hth_t 。我們可以用函數ff表達循環神經網絡隱藏層的變換:
ht=f(xt,ht1)\boldsymbol{h}_{t}=f\left(\boldsymbol{x}_{t}, \boldsymbol{h}_{t-1}\right)
這裏的ff就相當於RNN的一個單元裏面的計算, 具體細節這裏不多說。 這樣我們就得到了每個時間步的隱藏狀態hth_t, 那麼就可以自定義函數qq將各個時間步的隱藏狀態變化爲背景變量:
C=q(h1,,ht)\boldsymbol{C}=q\left(\boldsymbol{h}_{1}, \ldots, \boldsymbol{h}_{t}\right)

這個C如果簡單的來算的話, 就直接可以將最後一個時間步的hth_t賦值給C。

上面就是編碼器的計算細節, 這裏用了一個單向的RNN, 每個時間步的隱藏狀態只取決於該時間步及之前的輸入子序列。我們也可以使用雙向循環神經網絡構造編碼器。在這種情況下,編碼器每個時間步的隱藏狀態同時取決於該時間步之前和之後的子序列(包括當前時間步的輸入),並編碼了整個序列的信息。 這個在下面的代碼實例中會看到這種方式。

3.2.2 解碼器

解碼器負責根據語義向量生成指定的序列,
在這裏插入圖片描述
上面我們已經得到了編碼整個輸入序列信息的背景變量C, 那麼在給定訓練樣本中的輸出序列y1,y2,,yty_1,y_2,…,y_{t^′},對每個時間步 tt^′(符號與輸入序列或編碼器的時間步tt有區別),解碼器輸出yty_{t^′}的條件概率將基於之前的輸出序列y1,,yt1y_1,…,y_{t^′−1} 和背景變量C,即 P(yty1,,yt1,C)P\left(y_{t^{\prime}} | y_{1}, \ldots, y_{t^{\prime}-1}, \boldsymbol{C}\right)

所以,解碼器這部分我們用另一個RNN, 在輸出序列的時間步tt^′ ,解碼器將上一時間步的輸出yt1y_{t^′−1}以及背景變量 C作爲輸入,並將它們與上一時間步的隱藏狀態st1s_{t^′−1}變換爲當前時間步的隱藏狀態sts_{t^′} 。因此,我們可以用函數gg表達解碼器隱藏層的變換:
st=g(yt1,C,st1)\boldsymbol{s}_{t^{\prime}}=g\left(y_{t^{\prime}-1}, \boldsymbol{C}, \boldsymbol{s}_{t^{\prime}-1}\right)
有了解碼器的隱藏狀態後, 可以使用自定義的輸出層和softmax運算來計算P(yty1,,yt1,C)P\left(y_{t^{\prime}} | y_{1}, \ldots, y_{t^{\prime}-1}, \boldsymbol{C}\right)了。

那麼這個網絡訓練的時候怎麼訓練呢? 根據最大似然估計, 我們可以最大化序列基於輸入序列的條件概率:
P(y1,,yTx1,,xT)=t=1TP(yty1,,yt1,x1,,xT)=t=1TP(yty1,,yt1,st,c)\begin{aligned} P\left(y_{1}, \ldots, y_{T^{\prime}} | x_{1}, \ldots, x_{T}\right) &=\prod_{t^{\prime}=1}^{T^{\prime}} P\left(y_{t^{\prime}} | y_{1}, \ldots, y_{t^{\prime}-1}, x_{1}, \ldots, x_{T}\right) \\ &=\prod_{t^{\prime}=1}^{T^{\prime}} P\left(y_{t^{\prime}} | y_{1}, \ldots, y_{t^{\prime}-1}, \boldsymbol{s_{t^\prime}}, \boldsymbol{c}\right) \end{aligned}

而這個式子,我們可以取對數進行化簡併取負:
logP(y1,,yTx1,,xT)=t=1TlogP(yty1,,yt1,st,c)-\log P\left(y_{1}, \ldots, y_{T^{\prime}} | x_{1}, \ldots, x_{T}\right)=-\sum_{t^{\prime}=1}^{T^{\prime}} \log P\left(y_{t^{\prime}} | y_{1}, \ldots, y_{t^{\prime}-1},\boldsymbol{s_{t^\prime}}, \boldsymbol{c}\right)
這樣就相當於我們要最小化上面的這個函數。 這樣就可以選擇相應的損失函數(比如交叉熵)去訓練模型了。

這就是Seq2Seq的計算細節了, 下面來個圖再看一遍這個過程:

在這裏插入圖片描述
好吧, 這圖有點魔性, 但是應該能說明上面的過程了, 有個細節就是上面的tanh那個地方, 論文裏面的s0初識化成了這個, 原因是想翻譯第一個詞的時候更多的考慮一下輸入的第一個詞。 上面那個箭頭表示是先逆向運算得到的h1。

這就是Seq2Seq了, 應該挺好理解的吧, 那麼這個結構有沒有問題呢?

4. Seq2Seq存在的問題及Attention初識

上面的Seq2Seq其實存在一些問題的, 其中最顯眼的就是那個C, 上面的Seq2Seq是吧所有的輸入信息組合到了一個C上去,然後所有的輸出都是基於同樣的C去做翻譯。

那麼我們可以試想一下, 在機器翻譯的時候, 如果序列很長, 我翻譯第一個單詞相當於考慮了所有的輸入信息, 這顯然不符合我們翻譯的習慣(我們人翻譯一句話的時候也不是考慮所有的句子再進行翻譯吧), 我們做翻譯的時候,通常是先將長的句子分段, 翻譯的時候, 只聚焦於對某一小段進行翻譯, 這樣會比較準確。 比如翻譯“I love China, because it 巴拉巴拉”, 那麼我翻譯第一個詞的時候, 是不是更關注於" love China", 然後翻譯出“我”, 而不需要關注後面那一長串, 同理翻譯love的時候, 更關注I和China多一些?

這其實就是在說, 我們在做翻譯的時候,解碼器的每一時間步對輸入序列中不同時間步的表徵或編碼信息分配的注意力應該是不同的。 這也是注意力機制的由來。那麼如何做到這一點呢?

我們就可以在每個時間步得到不同的上下文向量C, 而這個C的由來, 是對編碼器隱藏層輸出的一個加權平均, 這個權重,就代表着我對於每個輸入所放上去的注意力大小。 這樣, 我們翻譯的時候, 就可以在不同的時刻只注重某一部分的區域。

所以, 加入注意力機制的Seq2Seq模型是下面的一個感覺:
在這裏插入圖片描述
這裏就會看到, 在每個時刻, 都會有一個單獨的上下文向量C, 這個C只聚焦於部分輸入, 且可以對關注的這部分輸入加入不同的權重表示關注度(應該放多少注意力在這個輸入上)

那麼, 上面這個過程就是這樣子的了: 我輸入下面的一段話進行翻譯, 當第一個時間步的時候, 我想輸出jane, 也就是翻譯輸入的第一個單詞, 這時候, 編碼器部分的工作是隻用到了輸入的前三個單詞, 進入了RNN, 然後計算出隱藏狀態h, 然後會給輸入的這三個h進行一個加權求和得到這一個時間步的上下文向量C, 這裏的C就是隻綜合了"jane visite I’Afrique" 這三個單詞的信息,並且會把注意力重點放在第一個單詞上, 也就是jane的權重會大一些。 這樣在解碼的時候, 就會更容易的翻譯出第一個單詞jane。 解碼器部分的工作細節和Seq2Seq的基本上一樣。 這就是加入Attention的Seq2Seq的宏觀工作過程。

如果還沒明白, 我找到了一個動畫解釋上面的宏觀過程, 具體鏈接會在下面給出, 把宏觀過程簡單總結, 其實就是六步: 輸入通過編碼器的RNN得到隱藏狀態信息、 給每一個隱藏狀態信息打分、 把分數進行softmax獲得每個隱藏狀態的權重、 把權重與隱藏狀態相乘、 把前面的相乘結果相加得到上下文向量C、把C放入到解碼器就可以進行翻譯。 下面採用動畫的方式看一下這六步:

  1. 輸入通過編碼器的RNN得到隱藏狀態信息
    我們首先準備第一個解碼器的隱藏狀態(紅色)和所有可用的編碼器隱藏狀態(綠色)。在下面的示例中,有4個編碼器隱藏狀態和當前解碼器隱藏狀態。
    在這裏插入圖片描述

  2. 給每一個隱藏狀態信息打分
    這個後面在計算的細節會比較詳細, 說白了,就是根據解碼器前一狀態的隱藏信息和當前輸入隱藏狀態進行一個計算得到每個輸入隱藏狀態的分數。 這個分數就代表着當前時刻的這些輸入對於當前的翻譯提供信息的多少(或者說對於當前時間步的翻譯的關鍵程度), 最簡單的打分方式就是每個輸入和解碼器前一隱藏狀態進行內積運算獲得。
    在這裏插入圖片描述
    比如, 我前一個隱藏狀態信息是[10, 5, 10], 我的四個輸入隱藏狀態分別是[0, 1, 1], [5, 0, 1], [1, 1, 0], [0, 5, 1], 那麼我分別用隱藏狀態的這個向量與後面的四個向量進行內積運算, 就會得到得分分別是: 15(10 * 0+5 * 1+10 * 1)、60, 15, 35. 會發現第二個輸入對當前翻譯的影響比較大。 當然這裏的打分方式有很多, 內積是比較簡單的,並且我們知道兩個向量的內積運算其實是比較這兩個向量的相似性, 相似性越高, 內積越大, 那麼這裏的第二個輸入說明和解碼器前一時刻的隱藏狀態挺相關, 而翻譯的時候上下文很重要。

  3. 把分數進行softmax獲得每個隱藏狀態的權重
    第三步的softmax無非就是把這些想辦法讓這些分數的加和爲1, 這樣就得出了每個隱藏狀態的權重,並且權重加和爲1。
    在這裏插入圖片描述

  4. 把權重與隱藏狀態相乘
    通過將每個編碼器的隱藏狀態與其softmax之後的分數(標量)相乘,我們就得到對其向量。
    在這裏插入圖片描述

  5. 把前面的相乘結果相加得到上下文向量C
    在這裏插入圖片描述

  6. 把C放入到解碼器就可以進行翻譯
    在這裏插入圖片描述

相信這個動畫,應該能把這個宏觀過程解釋的更加明白。 那麼下面就看看具體細節, 這個權重到底是啥? 要怎麼加? 最後的C應該怎麼計算了?

5. Attention的計算細節

關於計算的部分, 我們先從這個上下文向量開始, 先看看這個C現在是怎麼計算的, 還記得Seq2Seq的C是怎麼計算的嗎? 那裏我們說是綜合了所有的編碼器的隱藏狀態, 也就是這樣的一個公式:
C=q(h1,,ht)\boldsymbol{C}=q\left(\boldsymbol{h}_{1}, \ldots, \boldsymbol{h}_{t}\right)
而這裏加入attention之後, 每個h前面就會有一個權重, 而C就是帶有權重的這些h之和, 並且在解碼的每一個時刻tt^\prime,都有一個單獨的ctc_{t^\prime}所以公式變成了下面這樣:
ct=t=1Tαtthtc_{t^{\prime}}=\sum_{t=1}^{T} \alpha_{t^{\prime} t} h_{t}
這裏的αtt\alpha_{t^\prime t}指的就是在tt^\prime時刻的這個輸出應該放在第tt個輸入上的注意力大小。我們知道了這是一個概率分佈, 也就是softmax之後的值, 即:
αtt=exp(ett)k=1Texp(etk),t=1,,T\alpha_{t^{\prime} t}=\frac{\exp \left(e_{t^{\prime} t}\right)}{\sum_{k=1}^{T} \exp \left(e_{t^{\prime} k}\right)}, \quad t=1, \ldots, T

那麼我們也很容易能夠猜出, 這裏的ette_{t^{\prime} t}是啥了, 還記得上面的打分嗎? 這個東西就是每個h的分數, 然後進行了softmax就得到了權重α\alpha, 那麼就要看看這個東西又是咋算的?

根據上面的宏觀觀察, 這個東西要取決於解碼器的前一個時間步的隱藏狀態和編碼器在時間tt時刻的隱藏狀態, 所以是這樣算的:
ett=a(st1,ht)e_{t^{\prime} t}=a\left(s_{t^{\prime}-1}, \boldsymbol{h}_{t}\right)
這裏的aa函數有多種選擇, 上面動畫裏面選擇的是直接內積a(s,h)=sha(\boldsymbol{s}, \boldsymbol{h})=\boldsymbol{s}^{\top} \boldsymbol{h}。而這裏還可以採用一個多層感知機進行計算, 下面的代碼實現也是通過了一個多層感知機, 公式如下:
a(s,h)=vtanh(Wss+Whh)a(\boldsymbol{s}, \boldsymbol{h})=\boldsymbol{v}^{\top} \tanh \left(\boldsymbol{W}_{s} \boldsymbol{s}+\boldsymbol{W}_{h} \boldsymbol{h}\right)
其中這裏的v,Ws,Whv,W_s, W_h 都是可學習的模型參數。這個地方的vv的轉置加入是爲了讓這裏的ee變成一個標量,這樣才能進行後面的softmax。

這裏再拓展一下, 就是上面的這個過程還可以進行向量化計算,這樣計算會高效一些, 廣義上,注意力機制的輸入包括查詢項以及一一對應的鍵項和值項,其中值項是需要加權平均的一組項。在加權平均中,值項的權重來自查詢項以及與該值項對應的鍵項的計算。 這裏詳細的我在自然語言處理之Attention大詳解(Attention is all you need)整理過了, 這裏就說一下這個地方怎麼向量化。

這個例子中, 查詢器Q爲解碼器的隱藏狀態, 鍵K和值V都是編碼器的隱藏狀態。 讓我們考慮一個常見的簡單情形,即編碼器和解碼器的隱藏單元個數均爲 h ,且函數a(s,h)=sha(\boldsymbol{s}, \boldsymbol{h})=\boldsymbol{s}^{\top} \boldsymbol{h}。 假設我們希望根據解碼器單個隱藏狀態st1Rh\boldsymbol{s}_{t^{\prime}-1} \in \mathbb{R}^{h}和編碼器的所有隱藏狀態htRh,t=1,,T\boldsymbol{h}_{t} \in \mathbb{R}^{h}, t=1, \ldots, T來計算上下文向量ctRh\boldsymbol{c}_{t^{\prime}} \in \mathbb{R}^{h}。 那我們就可以將查詢項QR1×h\boldsymbol{Q} \in \mathbb{R}^{1 \times h}設爲st1\boldsymbol{s}_{t^{\prime}-1}^{\top}, 並令鍵項矩陣KRT×h\boldsymbol{K} \in \mathbb{R}^{T \times h}和值項矩陣VRT×h\boldsymbol{V} \in \mathbb{R}^{T \times h}相同且第tt行均爲ht\boldsymbol{h}_{t}^{\top}。 此時我們只需要通過矢量化計算:
softmax(QK)V\operatorname{softmax}\left(\boldsymbol{Q} \boldsymbol{K}^{\top}\right) \boldsymbol{V}
就可以算出轉置後的背景向量ct\boldsymbol{c}_{t^{\prime}}, 當查詢項矩陣QQ的行數爲nn時, 上面就會得到nn行的輸出矩陣。 輸出矩陣和查詢項矩陣在相同行上一一對應。 這樣到底算的是個什麼東西呢? 其實我們知道, 向量的內積操作就是在計算相似程度。 而QKQ與K轉置的乘積其實就是前一個隱藏狀態st1s_{t^\prime-1}與T個輸入隱藏狀態向量的相似性程度(那個分數),然後softmax就是每個 隱藏狀態的權重, 而V又是這T個隱藏狀態向量本身, 那麼這樣一乘正好就是帶上權重的隱藏狀態然後加和。 這個計算速度要比單純的循環遍歷要快得多。

這樣, 帶有注意力機制的編碼器部分就介紹完畢了, 還有一個細節就是解碼器這裏的隱藏狀態應該怎麼計算:這裏其實就是普通的LSTM或者是GRU的計算過程, 假設使用的解碼器是GRU, 那麼這裏的隱藏狀態公式應該是這樣:
st=ztst1+(1zt)s~t\boldsymbol{s}_{t^{\prime}}=\boldsymbol{z}_{t^{\prime}} \odot \boldsymbol{s}_{t^{\prime}-1}+\left(1-\boldsymbol{z}_{t^{\prime}}\right) \odot \tilde{\boldsymbol{s}}_{t}
而GRU我們知道有兩個門, 還有個候選隱藏狀態:
rt=σ(Wyryt1+Wsrst1+Wcrct+br)zt=σ(Wyzyt1+Wszst1+Wczct+bz)s~t=tanh(Wysyt1+Wss(st1rt)+Wcsct+bs)\begin{array}{l} \boldsymbol{r}_{t^{\prime}}=\sigma\left(\boldsymbol{W}_{y r} \boldsymbol{y}_{t^{\prime}-1}+\boldsymbol{W}_{s r} \boldsymbol{s}_{t^{\prime}-1}+\boldsymbol{W}_{c r} \boldsymbol{c}_{t^{\prime}}+\boldsymbol{b}_{r}\right) \\ \boldsymbol{z}_{t^{\prime}}=\sigma\left(\boldsymbol{W}_{y z} \boldsymbol{y}_{t^{\prime}-1}+\boldsymbol{W}_{s z} \boldsymbol{s}_{t^{\prime}-1}+\boldsymbol{W}_{c z} \boldsymbol{c}_{t^{\prime}}+\boldsymbol{b}_{z}\right) \\ \tilde{\boldsymbol{s}}_{t^{\prime}}=\tanh \left(\boldsymbol{W}_{y s} \boldsymbol{y}_{t^{\prime}-1}+\boldsymbol{W}_{s s}\left(\boldsymbol{s}_{t^{\prime}-1} \odot \boldsymbol{r}_{t^{\prime}}\right)+\boldsymbol{W}_{c s} \boldsymbol{c}_{t^{\prime}}+\boldsymbol{b}_{s}\right) \end{array}
會發現, 解碼器的輸入有三項: 上一時間步的輸出yt1y_{t^\prime-1}, 上一時間步隱藏狀態st1s_{t^\prime-1}和當前時間步tt^\prime的加入注意力機制的上下文向量ctc_{t^\prime}. 關於GRU的細節,這裏也是不多說, 之所以這裏給出一個GRU版本, 是因爲下面的代碼實踐中是一個雙向LSTM, 這樣使得知識點覆蓋的比較全面, 哈哈。

所以,關於Attention的計算細節, 也就這麼多了, 下面簡單的實現一個帶有Attention的Seq2Seq, 這個作業是來自吳恩達老師的課後作業, 這裏只拿出一部分來看, 有利於理解更多的細節。

7. 帶有Attention機制的Seq2Seq的簡單代碼實現

這個大作業裏面, 編碼器用的是雙向的LSTM, 解碼器用的是單向的LSTM, 整個帶注意力機制的Seq2Seq結構如下:
在這裏插入圖片描述
這個圖其實解釋的也非常的清楚, 可以當做回顧看一遍, 下面就是簡單的實現這個網絡, 但是實現之前, 還想先解釋一下雙向的LSTM究竟是怎麼計算的? 編碼器這裏的隱藏狀態輸出會看到有一個正向的隱態, 有一個逆向的隱態然後兩者進行了一個堆疊, 那麼逆向的這個隱態究竟是怎麼算的呢? 這裏來個雙向RNN的圖:
在這裏插入圖片描述
其實看這個也挺明白的, 正向隱藏狀態的輸出我們知道是按照時間步從第一步開始計算,然後依次往後這樣計算即可, 那麼反向其實是同理的, 無非就是輸入的時候我們把輸入進行逆序一下就可以了,就相當於先從最後一個時間步開始往前進行計算了, 這樣每個時間步就會有一個正向隱藏狀態,一個反向隱藏狀態, 兩者進行一個拼接即可。

好了, 這個問題也說明白了,當然雙向RNN不用我們自己實現, 調用相應的包即可。 那麼怎麼實現上面的結構呢? 思路是這樣, 我們下面是一個雙向LSTM, 這個有包可以實現, 上面是單向LSTM, 也有包實現, 這麼多時間步無非就是一個循環。 但是我們知道, 每個時間步裏面都會有一個上下文向量C, 而這個注意力機制是我們要實現的關鍵, 只要實現了這個注意力機制, 那麼這個計算過程就很容易了, 邏輯就是我先根據一個雙向的LSTM, 把輸入轉成隱藏狀態, 然後對於每一個時間步的隱態, 我得計算一個上下文向量, 然後把上下文向量作爲解碼器的輸入去計算輸出。

所以我們得自己寫個函數,來獲取一個時間步的上下文向量, 看下面這個圖:
在這裏插入圖片描述
這就是一個時間步上下文向量的計算, 輸入是解碼器上一時刻的隱藏狀態, 和編碼器所有時間步的隱藏狀態(這裏考慮了所有時間步, 但是會根據當前位置對時間步的隱態加不同的權重), 而輸出就是當前時刻的上下文向量。 過程上面其實都說的挺清楚了, 計算得分, 然後softmax, 然後加和。 當然這裏也是採用了向量的方式, 開始寫代碼:

def one_step_attention(a, s_prev):  
	"""
		這裏的a表示的是編碼器所有的隱藏狀態信息, 維度是(m, Tx, 2*n_a)
		s_prev表示的是解碼器前一個隱藏狀態信息, 維度是(m, n_s)  後面這一維是隱藏單元個數
	"""
	# 首先, 我們會先給s_prev按照時間步方向擴充維度, 畢竟a裏面是所有的時間步, 後面要進行堆疊, 需要維度對應
	s_prev = RepeatVector(Tx)   # 複製Tx步, 這樣s_prev變成了(m, Tx, n_s)

	# 下一步就是把a和s_prev堆疊起來一塊計算
	concat = Concatenate(axis=-1)([a, s_prev])  # (m, Tx, 2*n_a+n_s)

	# 下面是計算每個隱藏狀態的得分, 加入兩個全連接層
	e = Dense(10, activation = "tanh")(concat)   # (m, Tx, 10)
	energies = Dense(1, activation = "relu")(e)  # (m, Tx, 1)
	
	# 接下來是softmax得到權重
	alpha = Activation(softmax)(energies)  # (m, Tx, 1)

	# 上下文向量   權重與a相乘然後加和
	context = Dot(axes=1)([alpha, y])   # (m, 1, 2*n_a)  
	# 這裏的上下文向量維度依然和當前時間步的編碼器輸出隱藏狀態的維度一致,只不過後者以不同的注意力融合了多個時間步輸入的信息。

	return context

有了這一步的上下文向量的計算, 上面的的seq2seq就比較容易實現了, 邏輯就是先計算出編碼器中的隱藏狀態, 然後對於每個解碼器的時間步進行遍歷, 每一步都是先根據編碼器的隱藏狀態和上一步的s求當前步的上下文向量c, 然後基於c, 前一步的s, 前一步的y得到當前步的輸出, 然後依次循環,直到結束。 代碼如下:

def seq2seq_att(Tx, Ty, n_a, n_s, input_dim, output_dim):
	"""
		Tx: 這個就是編碼器的時間步長度
		Ty: 這個是解碼器的時間步長度
		n_a: 編碼器隱藏單元個數
		n_s: 解碼器隱藏單元個數
		input_dim: 編碼器輸入的維度
		output_dim: 解碼器的輸出維度
	"""
	# 首先要定義輸入維度
	X = Input(shape=(Tx, input_dim))
	s0 = Input(shape=(n_s, ))
	c0 = Input(shape=(n_s,))
	s = s0
	c = c0

	# 弄一個列表存放輸出結果
	outputs = []

	# 編碼器計算每個時間步的隱藏狀態
	a = Bidirectional(LSTM(n_a, return_sequences=True), input_shape=(m, Tx, n_a*2))(X)   # 這裏的return_sequences一定要設置爲True, 要輸出所有的隱藏狀態, 如果是False, 就只輸出最後一個時間步的隱藏狀態

	# 開始計算每一步的輸出:
	for t in range(Ty):
		# 獲得當前時間步的上下文向量
		context = one_step_attention(a, s)
		
		# 通過解碼器的LSTM計算一步輸出, 這時候別忘了輸入是上下文向量, 還有前一個狀態的隱藏信息(注意,LSTM的隱藏狀態是兩個輸出h, c), 這裏不需要前一步的輸出y
		s, _, c = LSTM(n_s, return_state = True)((context, initial_state=[s, c])
        )  # return_state表示每一步要返回h, c
	
		# 得到輸出
		out = Dense(output_dim, activation=softmax)(s)

		# 保存結果
		outputs.append(out)
	
	# 建立最終模型
	model = Model(inputs=[X, s0, c0], outputs=outputs)
	return model

上面就是一個簡單的帶有注意力機制的seq2seq的實現過程, 通過代碼能更好的幫助理解一些細節,比如attention實現的時候一些向量的維度變化, 再比如LSTM單元的傳遞有h和c兩個向量。 千萬不要忘了這裏的c, 並且這個c和上下文的context可不一樣。

這樣, 關於Attention的內容就差不多介紹到了這裏。

8. 總結

這篇文章就總結到這裏吧, 通過重溫的這三篇文章, 又重新學習了一下RNN, LSTM, GRU和Seq2Seq, Attention, 這次學習收穫很多,之前都沒有學習的這麼細, 通過這次機會希望能整理的詳細一些, 這篇文章又是挺長的, 下面簡單回顧一下。

我們先從seq2seq模型開始說起的, 這個模型是爲了應對輸入和輸出都是不定長的那種任務 ,比如機器翻譯, 人機對話等。 這種模型分爲編碼和解碼兩部分, 一般由兩個RNN網絡或者變體組成, 原理就是先基於編碼器得到一個綜合所有輸入的上下文向量C, 然後進行解碼,解碼的時候, 將C作爲解碼RNN的輸入, 最後得到輸出結果。 這一塊從宏觀和計算細節兩方面進行展開。

然後分析了這種結構的弊端就是當前時刻的輸出要考慮所有的輸入, 這個是不符習慣的, 所以引入了注意力機制, 即給輸入進行加權, 每個時刻都會有一個上下文向量, 當時每個時刻的上下文向量對所有的輸入加了不同的注意力, 也就是當前時刻的輸出要重點關注某部分的輸入序列。 這個機制就保證了seq2seq模型能夠進行長序列的任務。 這一塊也是宏觀和計算細節兩方面展開。

最後,還利用keras簡單的實現了一個帶有注意力機制的Seq2Seq模型, 瞭解了一些細節。 總之, Attention機制現在用的非常廣泛也非常重要, 關於它的一些知識就介紹到這裏。 最後再宏觀上看一下帶有注意力機制的Seq2Seq的工作過程:
在這裏插入圖片描述
有了這些基礎的知識, 就可以進行一些實戰任務, 就像第一篇裏面說的後面會總結一篇用於時間序列預測非線性自迴歸模型的論文,這篇論文用的就是帶有雙階段注意力機制的LSTM(如果掌握了上面的這些知識點, 就會發現讀這篇論文無壓力了, 並且很清晰的感覺)。 後面也會使用keras嘗試復現並用於時間序列預測的任務,通過這樣的方式,可以把這些基礎知識從理論變成實踐。 這叫做首尾呼應 哈哈!😉

參考

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