前言
前面我們介紹了【NLP】圖解從RNN到seq2seq+Attention,裏面提及了 Attention
,本篇就是來具體看看 Attention
的來源 Attention Is All You Need (Transformer
)。
自從 Attention
機制在提出之後,加入 Attention
的 Seq2Seq
模型在各個任務上都有了提升,所以現在的 seq2seq
模型指的都是結合 rnn
和 attention
的模型。
- 傳統的基於
RNN
的Seq2Seq
模型難以處理長序列的句子,無法實現並行,並且面臨對齊的問題
之後,研究人員也使用了多種方式來嘗試解決以上問題,但是依舊會存在一些問題,而 Transformer
拋棄了之前模型和RNN結合的固有模式,將其替換爲 Attention
,解決了長序列問題以及平行處理的問題,同時又可以引入 Multi-head
來關注序列的不同信息。下面我們就來看看它到底爲何如此強大。
Self-Attention
transformer
其實就是 Seq2Seq model with "Self-Attention"
,特別的地方就是在 Seq2Seq
中大量使用了 Self-Attention
通常我們要解決一個 sequence
問題時,我們最常想得到的就是使用 RNN
及其變種的架構,例如下圖:
- 上圖中的紅色塊中使用了雙向的RNN,我們在輸出每個的時候,都會將統統看一遍,如果使用簡單的RNN的話,那麼在輸出的時候,會將都看一篇,輸出的時候會將看一篇。
RNN經常被用在輸入是有序列的情況,但是RNN不容易被平行化,假設我們要計算 ,在簡單的RNN中需要依次看後才能算出來。那麼該怎麼辦呢?
- 接下來就有人提出將CNN拿來取代RNN的想法,如下圖:
雖然CNN能夠做到像RNN一樣輸出一個sequence輸出一個sequence
- 但是CNN只考慮局部信息,不像RNN一樣考慮整個序列
- 而CNN也不是說不能夠考慮更長的信息,只需要疊加更多CNN即可(上層的就會考慮更長的信息,如圖中上層CNN對第一層的前三個輸出進行計算,而這三個已經包含了的信息)
- 此外CNN的好處在於,我們無需等前面一個計算的結果來計算後一個,對於每層完全可以平行處理
那麼有沒有其他方法來解決以上兩個問題呢?先看圖:
這就是 Self-Attention
,它做的事情就是想取代原來RNN做的事情,而且可以解決RNN不能平行處理的問題。
在
Self-Attention
出來後,原本可以用RNN做的是,都可以用Self-Attention
來替換
Self-Attention
第一次出現在一篇名爲:Attention is all you need
的paper中(相信大家都知道,我就不給鏈接了),既然知道了 Self-Attention
是幹什麼的,自然就會想 Self-Attention
是怎麼做到的,不急,繼續往下看:
第一步
- 計算
- q:query(用來匹配其他的)
- k:key(用來被匹配的)
- v:value(用來抽取出來的)
現在我們每一個input都有 ,接下來要做的事就是:
第二步
- 拿每個query()去對每個key()做
Attention
,Attention
的方式有多種(輸入兩個向量輸出一個分數,判斷兩個向量有多匹配)
其中 是 和 的維度,這裏不好理解的是爲什麼要除以 ,一個直觀的理解是:
- 如果 和 的維度比較大,那麼的時候裏面的每個值相加的項就越多,值就比較大
第三步
- 接下來,就是做一次softmaxt
第四步
- 接着,我們用每個 去和對應的 相乘,然後相加:
我們可以發現,在計算 的時候,要使用到 ,也就是看到了所有的輸入,如果我們只需要局部的,那麼只需要將無需關注的部分的 設爲0即可
以上我們計算了 ,其實我們在計算 的時候,也可以同時計算
根據同樣地方式我們可以計算出 ,如果我們不關心怎麼計算的(或者沒看到上面的)那就暫時記住,我們的輸入經過一個 Self-Attention
後得到了 ,如下圖:
平行化處理(矩陣方式)
- 計算
- 計算
- 計算
整體上再看一遍
我們可以發現,
Self-Attention
內部就是做一系列的矩陣運算,而矩陣運算很容易用 GPU 加速
Multi-head Self-Attention
(多頭自注意力)
以兩個頭爲例:
那麼這個有什麼作用呢?
- 不同的head可能關注的點事不一樣的
這裏是以兩個head爲例,實際過程中,head的數目是一個參數,需要調的
圖解Transformer
具備了上面的知識,對理解 Transformer
就輕而易舉了
整體結構
這裏我們依然以機器翻譯爲例:
我們先將整個模型視爲黑盒,比如在機器翻譯中,接收一種語言的句子作爲輸入,然後將其翻譯成其他語言輸出。
細看下,其中由編碼組件、解碼組件和它們之間的連接層組成。
編碼組件是六層編碼器首位相連堆砌而成,解碼組件也是六層解碼器堆成的。
編碼器是完全結構相同的,但是並不共享參數,每一個編碼器都可以拆解成以下兩個字部分。
- 編碼器的輸入首先流過一個
self-attention
層,當它編碼某個詞時,該層幫助編碼器能夠看到輸入序列中的其他單詞 self-attention
的輸出流向一個前向網絡,每個輸入位置對應的前向網絡是獨立互不干擾的。
解碼器同樣也有這些子層,但是在兩個子層間增加了
attention層
,該層有助於解碼器能夠關注到輸入句子的相關部分,與seq2seq model
的Attention
作用相似。
輸入
現在,我們解析下模型最主要的組件,從向量/Tensor開始,然後是它們如何流經各個組件們並輸出的。正如NLP應用的常見例子,先將輸入單詞使用embedding algorithm
轉成向量。
每個詞映射到512維向量上,此處用box表示向量,只顯示4維
- 詞的向量化僅僅發生在最底層的編碼器的輸入時,這樣每個編碼器的都會接收到一個list(每個元素都是512維的詞向量),只不過其他編碼器的輸入是前個編碼器的輸出。
list的尺寸是可以設置的超參,通常是訓練集的最長句子的長度。
- 在對輸入序列做詞的向量化之後,它們流經編碼器的如下兩個子層。
- 這裏能看到
Transformer
的一個關鍵特性,每個位置的詞僅僅流過它自己的編碼器路徑。但是在self-attention
層中,這些路徑兩兩之間是相互依賴的(看完前部分內容的應該就知道了)- 而前向網絡層則沒有這些依賴性
- 這些路徑在流經前向網絡時可以並行執行
Encoder階段
正如之前所提,編碼器接收向量的list作輸入。然後將其送入self-attention處理,再之後送入前向網絡,最後將輸入傳入下一個編碼器。
每個位置的詞向量被送入
self-attention
模塊,然後是前向網絡(對每個向量都是完全相同的網絡結構)
不要被 self-attention
這個詞迷惑了(個人理解:就是加權,權重大的多關注,權重小的少關注),下面我們逐步分解下它是如何工作的:
以下面這句話爲例,作爲我們想要翻譯的輸入語句
The animal didn’t cross the street because it was too tired
。句子中it
指的是什麼呢?“it"指的是"street” 還是“animal”?對人來說很簡單的問題,但是對算法而言並不簡單。
當模型處理單詞“it”時,self-attention
允許將“it”和“animal”聯繫起來。當模型處理每個位置的詞時,self-attention
允許模型看到句子的其他位置信息作輔助線索來更好地編碼當前詞。如果你對RNN熟悉,就能想到RNN的隱狀態是如何允許之前的詞向量來解釋合成當前詞的解釋向量。
Transformer
使用self-attention
來將相關詞的理解編碼到當前詞中。
Self-Attention細節
一般計算
前面我們已經介紹了 Self-Attention
這裏我們帶入到 Transformer
再來鞏固一遍。
我們先看下如何計算 self-attention
的向量,再看下如何以矩陣方式計算。
- 第一步,根據編碼器的輸入向量,生成三個向量,比如,對每個詞向量,生成query-vec, key-vec, value-vec,生成方法爲分別乘以三個矩陣,這些矩陣在訓練過程中需要學習。
注意:不是每個詞向量獨享3個matrix,而是所有輸入共享3個轉換矩陣;權重矩陣是基於輸入位置的轉換矩陣;有個可以嘗試的點,如果每個詞獨享一個轉換矩陣,會不會效果更厲害呢?
注意到這些新向量的維度比輸入詞向量的維度要小(512–>64),並不是必須要小的,是爲了讓 Multi-head attention
的計算更穩定。
所謂的 query/key/value-vec
是什麼?
這種提取對計算和思考attention
是有益的,當讀完下面attention
是如何計算的之後,你將對這些向量的角色有更清晰的瞭解。
- 第二步,計算attention就是計算一個分值。對“Thinking Matchines”這句話,對“Thinking”(pos#1)計算attention 分值。我們需要計算每個詞與“Thinking”的評估分,這個分決定着編碼“Thinking”時(某個固定位置時),每個輸入詞需要集中多少關注度。
這個分,通過“Thing”對應query-vector與所有詞的key-vec依次做點積得到。所以當我們處理位置#1時,第一個分值是 和 的點積,第二個分值是 和 的點積。
第三步和第四步,除以8(),這樣梯度會更穩定。然後加上softmax操作,歸一化分值使得全爲正數且加和爲1。
softmax
分值決定着在這個位置,每個詞的表達程度(關注度)。很明顯,這個位置的詞應該有最高的歸一化分數,但大部分時候總是有助於關注該詞的相關的詞。
- 第五步,將
softmax
分值與value-vec
按位相乘。保留關注詞的value
值,削弱非相關詞的value
值。 - 第六步,將所有加權向量加和,產生該位置的
self-attention
的輸出結果。
上述就是self-attention
的計算過程,生成的向量流入前向網絡。在實際應用中,上述計算是以速度更快的矩陣形式進行的。下面我們看下在單詞級別的矩陣計算。
矩陣計算
第一步,計算query/key/value matrix
,將所有輸入詞向量合併成輸入矩陣 ,並且將其分別乘以權重矩陣
輸入矩陣X的每一行表示輸入句子的一個詞向量
最後,鑑於我們使用矩陣處理,將步驟2~6合併成一個計算self-attention層輸出的公式。
矩陣形式的
self-attention
計算
多頭機制
論文進一步增加了 multi-headed
的機制到 self-attention
上,在如下兩個方面提高了 attention
層的效果:
- 多頭機制擴展了模型集中於不同位置的能力。在上面的例子中,z1只包含了其他詞的很少信息,僅由實際自己詞決定。在其他情況下,比如翻譯
The animal didn’t cross the street because it was too tired
時,我們想知道單詞"it"指的是什麼。 - 多頭機制賦予attention多種子表達方式。像下面的例子所示,在多頭下有多組query/key/value-matrix,而非僅僅一組(論文中使用8-heads)。每一組都是隨機初始化,經過訓練之後,輸入向量可以被映射到不同的子表達空間中。
每個head都有一組Q/K/V matrix
如果我們計算 multi-headed self-attention
的,分別有八組不同的 Q/K/V matrix
,我們得到八個不同的矩陣。
這會帶來點麻煩,前向網絡並不能接收八個矩陣,而是希望輸入是一個矩陣,所以要有種方式處理下八個矩陣合併成一個矩陣。
上述就是多頭自注意機制的內容,我認爲還僅是一部分矩陣,下面嘗試着將它們放到一個圖上可視化如下。
現在加入 attention heads
之後,重新看下當編碼“it”時,哪些 attention head
會被集中。
編碼"it"時,一個
attention head
集中於"the animal",另一個head集中於“tired”,某種意義上講,模型對“it”的表達合成了的“animal”和“tired”兩者
如果我們將所有的 attention heads
都放入到圖中,就很難直觀地解釋了。
加入位置編碼
截止到目前爲止,我們還沒有討論如何理解輸入語句中詞的順序。
爲解決詞序的利用問題,Transformer
新增了一個向量對每個詞,這些向量遵循模型學習的指定模式,來決定詞的位置,或者序列中不同詞的舉例。對其理解,增加這些值來提供詞向量間的距離,當其映射到 Q/K/V
向量以及點乘的 attention
時。
爲了能夠給模型提供詞序的信息,新增位置emb向量,每個向量值都遵循指定模式
如果假設位置向量有4維,實際的位置向量將如下所示:
一個只有4維的位置向量表示例子
所謂的指定模式是什麼樣的呢?
在下圖中,每一行表示一個位置的pos-emb,所以第一行是我們將要加到句子第一個詞向量上的vector。每個行有512值,每個值範圍在[-1,1],我們將要塗色以便於能夠將模式可視化。
一個真實的例子有20個詞,每個詞512維。可以觀察中間顯著的分隔,那是因爲左側是用sin函數生成,右側是用cos函數生成。
殘差
編碼器結構中值得提出注意的一個細節是,在每個子層中(slef-attention+ffnn
),都有殘差連接,並且緊跟着 layer-normalization
。
如果我們可視化向量和layer-norm
操作,將如下所示:
在解碼器中也是如此,假設兩層編碼器+兩層解碼器組成Transformer,其結構如下:
Decoder階段
現在我們已經瞭解了編碼器側的大部分概念,也基本瞭解瞭解碼器的工作方式,下面看下他們是如何共同工作的。
編碼器從輸入序列的處理開始,最後的編碼器的輸出被轉換爲K和V,它倆被每個解碼器的 encoder-decoder atttention
層來使用,幫助解碼器集中於輸入序列的合適位置。
在編碼之後,是解碼過程;解碼的每一步輸出一個元素作輸出序列
下面的步驟一直重複直到一個特殊符號出現表示解碼器完成了翻譯輸出。每一步的輸出被喂到下一個解碼器中。正如編碼器的輸入所做的處理,對解碼器的輸入增加位置向量。
-
在解碼器中的
self attention
層與編碼器中的稍有不同,在解碼器中,self-attention 層僅僅允許關注早於當前輸出的位置。在softmax之前,通過遮擋未來位置(將它們設置爲-inf)來實現。 -
Encoder-Decoder Attention
層工作方式跟multi-headed self-attention
是一樣的,除了一點,它從前層獲取輸出轉成query矩陣,接收最後層編碼器的key和value矩陣做key和value矩陣。
最後的線性和Softmax層
解碼器最後輸出浮點向量,如何將它轉成詞?這是最後的線性層和softmax層的主要工作。
線性層是個簡單的全連接層,將解碼器的最後輸出映射到一個非常大的logits向量上。假設模型已知有1萬個單詞(輸出的詞表)從訓練集中學習得到。那麼,logits向量就有1萬維,每個值表示是某個詞的可能傾向值。
softmax層將這些分數轉換成概率值(都是正值,且加和爲1),最高值對應的維上的詞就是這一步的輸出單詞。
回顧訓練
現在我們已經瞭解了一個訓練完畢的Transformer的前向過程,順道看下訓練的概念也是非常有用的。
在訓練時,模型將經歷上述的前向過程,當我們在標記訓練集上訓練時,可以對比預測輸出與實際輸出。爲了可視化,假設輸出一共只有6個單詞(“a”, “am”, “i”, “thanks”, “student”, “”)
模型的詞表是在訓練之前的預處理中生成的
一旦定義了詞表,我們就能夠構造一個同維度的向量來表示每個單詞,比如one-hot編碼,下面舉例編碼“am”。
舉例採用one-hot編碼輸出詞表
下面讓我們討論下模型的loss損失,在訓練過程中用來優化的指標,指導學習得到一個非常準確的模型。
損失函數
我們用一個簡單的例子來示範訓練,比如翻譯“merci”爲“thanks”。那意味着輸出的概率分佈指向單詞“thanks”,但是由於模型未訓練是隨機初始化的,不太可能就是期望的輸出。
由於模型參數是隨機初始化的,未訓練的模型輸出隨機值。我們可以對比真實輸出,然後利用誤差後傳調整模型權重,使得輸出更接近與真實輸出
如何對比兩個概率分佈呢?簡單採用 cross-entropy或者Kullback-Leibler divergence中的一種。
鑑於這是個極其簡單的例子,更真實的情況是,使用一個句子作爲輸入。比如,輸入是“je suis étudiant”,期望輸出是“i am a student”。在這個例子下,我們期望模型輸出連續的概率分佈滿足如下條件:
- 每個概率分佈都與詞表同維度。
- 第一個概率分佈對“i”具有最高的預測概率值。
- 第二個概率分佈對“am”具有最高的預測概率值。
- 一直到第五個輸出指向"<eos>"標記。
在足夠大的訓練集上訓練足夠時間之後,我們期望產生的概率分佈如下所示:
訓練好之後,模型的輸出是我們期望的翻譯。當然,這並不意味着這一過程是來自訓練集。注意,每個位置都能有值,即便與輸出近乎無關,這也是softmax對訓練有幫助的地方。
現在,因爲模型每步只產生一組輸出,假設模型選擇最高概率,扔掉其他的部分,這是種產生預測結果的方法,叫做greedy 解碼。另外一種方法是beam search,每一步僅保留最頭部高概率的兩個輸出,根據這倆輸出再預測下一步,再保留頭部高概率的兩個輸出,重複直到預測結束。top_beams是超參可試驗調整。
總結
優點:
- 每層計算複雜度降低
- 計算可以被並行化
- CNN需要增加捲積層數來擴大視野,RNN需要從1到n逐個進行計算,而self-attention只需要一步矩陣計算就可以
self-attention
模型更可解釋,attention
結果的分佈表明了該模型學習到了一些語法和語義信息
缺點:
- 實踐上:有些
RNN
輕易可以解決的問題transformer
沒做到,比如複製string,或者推理時碰到的sequence
長度比訓練時更長(因爲碰到了沒見過的position embedding
) - 理論上:
transformers
非computationally universal
(圖靈完備)
Transformer是第一個用純attention搭建的模型,不僅計算速度更快,在翻譯任務上也獲得了更好的結果。
參考鏈接: