10個問題看透Transformer

點擊上方視學算法」,選擇加"星標"或「置頂css

重磅乾貨,第一時間送達node

做者丨張春陽@知乎(已受權)
來源丨https://zhuanlan.zhihu.com/p/347709112
編輯丨極市平臺

Github 上有與文章配套的 jupyter notebook,和文章配合食用,效果更佳。python

https://github.com/BSlience/transformer-all-in-onegit

本文主要回答如下幾個問題:github

  • Attention 機制是用來作什麼的 ?web

  • Self-attention 是怎麼從 Attention 過分過來的 ?算法

  • Attention 和 self-attention 的區別是什麼 ?apache

  • Self-attention 爲何能 work ?json

  • 怎麼用 Pytorch 實現 self-attention ?ruby

  • Transformer 的做者對 self-attention 作了哪些 tricks ?

  • 怎麼用 Pytorch/Tensorflow2.0 實如今 Transfomer 中的 self-attention ?

  • 完整的 Transformer Block 是什麼樣的?

  • 怎麼捕獲序列中的順序信息呢 ?

  • 怎麼用 Pytorch 實現一個完整的 Transformer 模型?

Attention 機制是用來作什麼的 ?

Attention機制最先的提出是針對與序列模型的,出處是Bengio大神在2015年的這篇文章:

Neural Machine Translation by jointly learning to align and translate, Bengio et. al. ICLR 2015

https://arxiv.org/pdf/1409.0473.pdf

在這篇文章中其實並無常常性的提到attention(其實只有3次),這個詞流行起來實際上是在後來的一些工做中,被不少work說起到。在這篇文章中,我試圖在更common的視角下去理解attention的機制,而不是使用論文中在translation任務中的應用。

咱們要說attention機制實際上是借鑑了生物在觀察和學習行爲中的過程,也就是說咱們人來一般在觀察和學習的時候,都是經過快速的獲取全局的信息,創建起對於事物的須要重點觀察或者學習的區域,這些須要重點關注的目標區域,就是咱們注意力的焦點。然而,就和咱們平常生活中處理事情同樣,咱們沒有辦法同時處理全部的事情,咱們會給他們分出優先級。一樣的,注意力也會有一個權重值,從而更專一的聚焦在某些關鍵的信息上。

咱們把attention放到不一樣case裏,再去看看這種注意力不一樣的解釋。

首先咱們看看在視覺領域,咱們應該怎麼去理解attention:

當咱們去看上面這張照片的時候,咱們首先就是先去看總體,這裏有車、有街道、還有不少的廣告牌,不知道你們是否有感覺到,當我開始描述這些的時候,其實就是我把注意力放在了這些上面了。那當咱們想要跟深刻了解這張圖片的時候,我就要把注意力放的更聚焦。好比說,我想知道這是拍的哪裏,那我可能會試着去看看廣告牌上的文字,看這些文字是否是能給我一些啓示。

就像上面這張圖同樣,咱們可能會試着把注意力放到不一樣的區域,那咱們就可以獲得更多的關於不一樣角度的信息。這些信息,正是咱們但願在圖像處理的時候但願獲得的。

咱們再來看看再天然語言處理中,attention機制表示的又是什麼呢?

好比說上面的這句話,「她正在吃一個綠色的蘋果」,這裏咱們能夠比較清楚的看到,「吃」和「蘋果」有很強的聯繫,那咱們就但願在處理吃這個單詞的時候,可以在語義中,包含必定的蘋果的信息,這樣可以幫助咱們更好的理解「吃」這個動做。「綠色的」和「蘋果」也是同樣的,attention的機制可以幫助咱們在處理單個的token的時候,帶有必定的上下文信息。這就像是一種「軟性記憶」同樣,幫助咱們記住上下文中包含的信息。

當咱們看一篇文章的時候,其實也是相似的。咱們從拿到一篇文章開始,首先關注的也只是一些關鍵性的詞語,這些關鍵性的詞語,就可以幫助咱們快速的判斷文章的內容和結構。這些場景就是咱們在一些具體場景中對attention的應用。

那接下來,咱們再來看看,attention具體是怎麼工做的?

假設咱們的時間序列:   ,咱們把它放到座標軸上,就是上面這張圖的樣子。

這些點是咱們從總體數據中採樣出來的,這裏有不少的噪音(noisy),咱們想辦法能不能經過一些方法,獲得這些數據的更好的表示,從而可以使噪音減小。那這裏面咱們可使用re-weighting的操做,讓咱們的這些點都包含一些其餘點的信息,使得全部的數據可以更平滑一些。咱們定義這些re-weighting的參數爲   ,咱們使用這些weights就可以獲得一個點的新的表示   。

Self-attention是一個序列到序列的操做:一個向量的序列做爲輸入,一個向量的序列做爲輸出。咱們把輸入的序列定義爲   ,而且與它相關的輸出向量是   。這兩個向量的維度都是   。那麼對於每個點   ,咱們均可以經過一個不一樣的權重值,來將它轉化爲一個新的序列,這個新的序列就多是咱們原始序列的一個更好的表示,這些   就是一組attention的值。

來看一個動圖的例子:

以上就是attention機制。

Self-attention 是怎麼從 Attention 過分過來的 ?

Self-attention就本質上是一種特殊的attention。它和attention的區別我會在下一個章節介紹,這裏先來介紹下self-attention,這種應用在transformer中最重要的結構之一。

上面咱們介紹了attention機制,它可以幫咱們找到子序列和全局的attention的關係,也就是找到權重值   。self-attention對於attention的變化,其實就是尋找權重值 的過程不一樣。下面,咱們來看看self-attention的運算過程。

爲了可以產生輸出的向量   ,self-attention實際上是對全部的輸入作了一個加權平均的操做,這個公式和上面的attention是一致的。

 表明整個序列的長度,而且   個權重的相加之和等於1。值得一提的是,這裏的   並非一個須要神經網絡學習的參數,它是來源於   和   的之間的計算的結果(這裏   的計算髮生了變化)。它們之間最簡單的一種計算方式,就是使用點積的方式。

 和   是一對輸入和輸出。對於下一個輸出的向量   ,咱們有一個全新的輸入序列和一個不一樣的權重值。

這個點積的輸出的取值範圍在負無窮和正無窮之間,因此咱們要使用一個softmax把它映射到   之間,而且要確保它們對於整個序列而言的和爲1。

以上這些就是self-attention最基本的操做,其餘的部分咱們須要完整的Trasnformer纔可以解釋,這些咱們會在後面的內容中說明。

self-attention的基礎操做,沒有包含softmax操做

Attention 和 self-attention 的區別是什麼 ?

這裏有幾個重要的區別,能夠幫助你們更好的區分在不一樣任務中的使用方法:

  1. 在神經網絡中,一般來講你會有輸入層(input),應用激活函數後的輸出層(output),在RNN當中你會有狀態(state)。若是attention (AT) 被應用在某一層的話,它更多的是被應用在輸出或者是狀態層上,而當咱們使用self-attention(SA),這種注意力的機制更多的實在關注input上。
  2. Attention (AT) 常常被應用在從編碼器(encoder)轉換到解碼器(decoder)。好比說,解碼器的神經元會接受一些AT從編碼層生成的輸入信息。在這種狀況下,AT鏈接的是兩個不一樣的組件(component),編碼器和解碼器。可是若是咱們用SA,它就不是關注的兩個組件,它只是在關注你應用的那一個組件。那這裏他就不會去關注解碼器了,就好比說在Bert中,使用的狀況,咱們就沒有解碼器。
  3. SA能夠在一個模型當中被屢次的、獨立的使用(好比說在Transformer中,使用了18次;在Bert當中使用12次)。可是,AT在一個模型當中常常只是被使用一次,而且起到鏈接兩個組件的做用。
  4. SA比較擅長在一個序列當中,尋找不一樣部分之間的關係。好比說,在詞法分析的過程當中,可以幫助去理解不一樣詞之間的關係。AT卻更擅長尋找兩個序列之間的關係,好比說在翻譯任務當中,原始的文本和翻譯後的文本。這裏也要注意,在翻譯任務重,SA也很擅長,好比說Transformer。
  5. AT能夠鏈接兩種不一樣的模態,好比說圖片和文字。SA更多的是被應用在同一種模態上,可是若是必定要使用SA來作的話,也能夠將不一樣的模態組合成一個序列,再使用SA。
  6. 對我來講,大部分狀況,SA這種結構更加的general,在不少任務做爲降維、特徵表示、特徵交叉等功能嘗試着應用,不少時候效果都不錯。

Self-attention 爲何能 work ?

上面描述的方法看起來彷佛很簡單,可是它爲何可以work呢?爲了可以創建起直觀的感覺,讓咱們來看看一種標準的推薦電影的方法,看看是否能獲得一些啓發。

假設你正在運營一家在線看電影的網站,你有一些電影和一些用戶,你想要把合適的電影推薦給你的用戶。你該怎麼辦呢?

一種方法是這樣的,給你的電影手動的建立一些特徵(feature),好比說這部電影關於愛情的部分有多少,關於動做的部分有多少;而後咱們再去對用戶的特徵進行分析,好比說用戶A對於愛情電影的喜好程度有多少,對動做電影的喜好程度有多少。若是咱們按照上述方式構建了用戶和電影的兩個矩陣,那麼它們的點積就會給你一個分數,這個分數就表明了用戶對於某種電影的喜好程度。

經過上面的這種計算方式,咱們就可以獲得一些score值。這些值有正數也有負數。好比說,電影是一部關於愛情的電影,而且用戶也很喜歡愛情電影,那麼這個分值就是一個正數;若是電影是關於愛情的,可是用戶卻不喜歡愛情電影,那麼這個分值就會是一個負值。

還有,這個分值的大小也表示了在某個屬性上,它的程度是多大:好比說某一部電影,可能它的內容中只有一點點是關於愛情的,那麼它的這個值就會很小;或者說有個用戶他不是很喜歡愛情電影,那麼這個值的絕對值就會很大,說明他對這種電影的偏見是很大的。

顯而易見,上面說的這種方法在現實中是很難實現的。咱們很難去人工標註上千萬的電影的特徵,和用戶喜歡哪一種類型的電影的分值。

那麼,咱們有沒有一種方法去經過問一小部分人,經過收集他們對電影的喜愛,來經過一些算法來優化找出用戶對於電影喜好程度這個模型的參數呢?固然是有的,那就是FM算法,這個不是調頻多少多少兆赫的那個FM,而是Factorization Machine。這個算法就是能經過左邊的這個用戶-電影矩陣,找到用戶對於不一樣特徵的喜愛程度。

上面右邊的矩陣是怎麼來的呢?咱們把上面的這個問題稍微的簡化如下,只當作是一個和用戶和物品兩個維度相關的task,那其實咱們就能夠經過估計兩個矩陣的點乘的形式,來對原有的矩陣表示。這兩個向量中表示的就分別是用戶的embedding和電影的embedding。咱們反過來思考下,這種辦法的核心思想就是是經過兩個低維小矩陣(一個表明用戶embedding矩陣,一個表明物品embedding矩陣)的點乘計算,來模擬真實用戶點擊或評分產生的大的協同信息稀疏矩陣,本質上是編碼了用戶和物品協同信息的降維模型。

當咱們想要看,某個用戶對於某個電影的喜愛程度時,只須要用他們的embedding相乘,就能獲得相應的score了。

雖然,咱們這裏的兩個embedding並無直接的告訴咱們,裏面每一個維度的參數的含義是什麼,可是當你按照這種方法求的最後的參數的時候,這些參數都可以描述某種有實際含義的特徵上。

上面的這個過程,就和咱們使用下面這個公式來表示attention的想法是一致的。

以上這些就是self-attention中爲何使用點乘的方法而且能work的緣由了。那再讓咱們看一個在天然語言處理中使用self-attention的例子。爲了應用self-attention,咱們給每個在詞表中的單詞   一個embeding向量   (這個是咱們經過一些NLP方法學習到的)。這也是咱們在序列模型中常見的embeding layer。它會把單詞  轉換成向量的形式

若是咱們對這些向量序列進行self-attention的處理,那麼就會生成一個新的向量序列

這其中   就是全部在第一個序列中的embedding向量的加權和,權重值就是   的點積。

上文中咱們也提到了,   是咱們學習到的embedding向量,它是   這個單詞向量化的表示。在大部分的場景中,   這個單詞和句子中的其餘單詞沒有很強的相關性,所以,咱們就會期待   和其餘單詞的點積結果應該比較小或者是一個負值。那再看  這個單詞,爲了可以解釋這個單詞,咱們但願可以知道是誰在   ,那在這句話當中,   和   的點積就應該有一個比較大的正的值。

以上這些,就是在self-attention背後一些直覺上的含義。點積操做很好的表示了輸入語句中兩個向量之間的相關性。

在咱們繼續下面的內容以前,很是有必要作一個小的總結。

  • 到目前爲止,咱們尚未用到須要學習的參數。基礎的self-attention實際上徹底取決於咱們建立的輸入序列,上游的embeding layer驅動着self-attention學習對於文本語義的向量表示。
  • Self-attention看到的序列只是一個集合(set),不是一個序列,它並無順序。若是咱們從新排列集合,輸出的序列也是同樣的。後面咱們要使用一些方法來緩和這種沒有順序所帶來的信息的缺失。可是值得一提的是,self-attention自己是忽略序列的天然輸入順序的。

怎麼用 Pytorch 實現self-attention ?

我不能實現的,也是我沒有理解的。
-- 費曼

因此,咱們將一塊兒從頭開始寫一個self-attention。咱們這裏將會使用Pytorch來實現。

一個簡單的實現方法就是循環全部的向量,去計算出權重和輸出,可是這樣的方法明顯太慢了。因此咱們要作的第一件事就是怎麼使用矩陣乘法的形式來表達self-attention。

咱們首先來表示輸入,一個   維的由   個向量組成的序列構成的矩陣   。包含一個batch的參數   ,咱們會得倒一個維度爲   的張量。

全部的點積的結果   也構成一個矩陣,咱們能夠簡單的使用   乘以它的轉置獲得。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
import torchimport torch.nn.functional as F
# assume we have some tensor x with size (b, t, k)x = ...
raw_weights = torch.bmm(x, x.transpose(1, 2))# - torch.bmm is a batched matrix multiplication. It # applies matrix multiplication over batches of # matrices.

而後咱們把權重值   轉換成正值而且確保它們的和爲1,咱們使用一個row-wise的softmax。

 
   
weights = F.softmax(raw_weights, dim=2)

最後,咱們計算輸出的序列,咱們只須要使用權重 乘以矩陣   。這樣咱們就獲得了維度爲 的矩陣   。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
y = torch.bmm(weights, x)以上,通過兩個簡單的矩陣乘法和一個softmax,咱們就獲得了self-attention。

Transformer 的做者對 Self-attention 作了哪些 tricks ?

實際在Transformer的實現過程當中,做者使用了三個tricks。下面就來一個個的聊一聊這幾個tricks。

1) Queries, keys and values

咱們回顧如下上面所說道的self-attention的內容,上面咱們也提到了,在這樣一個模型當中,是沒有使用到能夠學習參數的,那咱們能不能使用一些參數,來讓整個結構更加的flexable。就是因爲這樣的想法,誕生了query,key和value這些參數。

爲了可以更清楚的說明,咱們使用圖片來稍微回顧下,以前講過的self-attention,以下圖。

在整個計算的過程當中,你們會發現,咱們使用了三次向量   這個文本的表示來作計算,那在Transformer中,就是把這幾個變量參數化,使用能夠學習的參數來替代,這裏咱們分別使用key、query和value三個可學習的向量來表示,這裏記爲  ,   ,  ,經過下面的計算,來獲得re-weighting的向量   。

經過圖形化的方法表示以下:

Linear層是一個沒有bias的全鏈接層,其實就是一個點乘。紅色的箭頭表示的是反向傳播的過程。經過方向傳播,key,query,value就可以學習到一個合理的表示。那麼這裏面key,queue,value分別學習到的是什麼呢?這個可能並無一個官方的解釋,可是經過這三個名稱的命名方式,咱們能夠大體的猜想。

這種命名的方式來源於搜索領域,假設咱們有一個key-value形式的數據集,就好比說是咱們知乎的文章,key就是文章的標題,value就是咱們文章的內容,那這個搜索系統就是但願,可以在咱們輸入一個query的時候,可以惟一返回一篇最咱們最想要的文章。那在self-attention中實際上是對這個task作了一些退化的處理,咱們優化並非返回一篇文章,而是返回全部的文章value,而且使用key和query計算出來的相關權重,來找到一篇得分最高的文章。

2) 縮放點積的值(Scaling the dot product)

Softmax 函數對很是大的輸入很敏感。這會使得梯度的傳播出現爲問題(kill the gradient),而且會致使學習的速度降低(slow down learning),甚至會致使學習的中止。那若是咱們使用   來對輸入的向量作縮放,就可以防止進入到softmax的函數增加過大:

這裏分母爲何要使用   呢?咱們想象一下,當咱們有一個全部的值都爲   的在   空間內的值。那它的歐式距離就爲   。除以 其實就是在除以向量平均的增加長度。

3) Multi-head attention

最後,咱們必需要知道的是,在真實的語言環境中,每個詞和不一樣的詞,都有不一樣的關係。咱們考慮下面這個例子,  。咱們能夠看到   和不一樣的部分有不一樣的關係。首先,   表示誰在進行   的動做,   表達被   的東西是什麼,   表示誰在接受東西。咱們就能夠用不一樣的self-attention mechanism來補貨這些不一樣的關係。以下圖:

若是咱們只進行single self-attention,全部的信息都會被加和到一塊兒。若是是   給     ,那麼咱們獲得的   就是同樣的了,可是其實意思應發生了改變。

因此,咱們能夠經過增長多個self-attention這樣的結構,來給self attention更強的辨別能力,咱們就有了更多個   的矩陣   ,那咱們把這些不一樣的self-attention就叫作attention head。

對於輸入   每個attention head都會生成一個向量   。咱們把這些向量進行concat操做,而且把concat的結果傳遞給一個全鏈接層,使得向量的維度從新回到k。這樣咱們就獲得了一個表示能力更強的向量。那應用了multi-head後的attention結構就變成了下圖這樣子:

有了這個結構,咱們就能夠把多個multi-head attention結構堆疊起來,從而獲得更增強大的能力。

Narrow and wide self-attention
一般,咱們有兩種方式來實現multi-head的self-attention。默認的作法是咱們會把embedding的向量   切割成塊,好比說咱們有一個256大小的embedding vector,而且咱們使用8個attention head,那麼咱們會把這vector切割成8個維度大小爲32的塊。對於每一塊,咱們生成它的queries,keys和values,它們每個的size都是32,那麼也就意味着咱們矩陣   的大小都是   。
那還有一種方法是,咱們可讓矩陣 的大小都是   ,而且把每個attention head都應用到所有的256維大小的向量上。第一種方法的速度會更快,而且可以更節省內存,第二種方法可以獲得更好的結果(同時也花費更多的時間和內存)。這兩種方法分別叫作narrow and wide self-attention。

怎麼用 Pytorch/Tensorflow2.0 實如今 Transfomer 中的self-attention ?

實現Transformer中的self-attention過程,咱們一共有8個步驟:

  1. 準備輸入
  2. 初始化參數
  3. 獲取key,query和value
  4. 給input1計算attention score
  5. 計算softmax
  6. 給value乘上score
  7. 給value加權求和獲取output1
  8. 重複步驟4-7,獲取output2,output3

1 準備輸入

爲了簡單起見,咱們使用3個輸入,每一個輸入都是一個4維的向量。

 
   
Input 1: [1, 0, 1, 0] Input 2: [0, 2, 0, 2]Input 3: [1, 1, 1, 1]
2 初始化參數

每個輸入都有三個表示,分別爲key(橙黃色)query(紅色)value(紫色)。好比說,每個表示咱們但願是一個3維的向量。因爲輸入是4維,因此咱們的參數矩陣爲   維。

後面咱們會看到,value的維度,一樣也是咱們輸出的維度。

爲了可以獲取這些表示,每個輸入(綠色)要和key,query和value相乘,在咱們例子中,咱們使用以下的方式初始化這些參數。

key的參數:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[[0, 0, 1], [1, 1, 0], [0, 1, 0], [1, 1, 0]]query的參數:
   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[[1, 0, 1], [1, 0, 0], [0, 0, 1], [0, 1, 1]]value的參數:
 
   
[[0, 2, 0], [0, 3, 0], [1, 0, 3], [1, 1, 0]]

一般在神經網絡的初始化過程當中,這些參數都是比較小的,通常會在_Gaussian,

Xavier and Kaiming distributions隨機採樣完成。_

3 獲取key,query和value

如今咱們有了三個參數,如今就讓咱們來獲取實際上的key,query和value。

對於input1的key的表示爲:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 0, 1, 0] x [1, 1, 0] = [0, 1, 1] [0, 1, 0] [1, 1, 0]

使用相同的參數獲取input2的key的表示:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][0, 2, 0, 2] x [1, 1, 0] = [4, 4, 0] [0, 1, 0]               [1, 1, 0]

使用參數獲取input3的key的表示:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 1, 1, 1] x [1, 1, 0] = [2, 3, 1] [0, 1, 0]               [1, 1, 0]

那使用向量化的表示爲:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 0, 1][1, 0, 1, 0] [1, 1, 0] [0, 1, 1][0, 2, 0, 2] x [0, 1, 0] = [4, 4, 0][1, 1, 1, 1] [1, 1, 0] [2, 3, 1]

讓咱們對value作相同的事情。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0, 2, 0][1, 0, 1, 0] [0, 3, 0] [1, 2, 3] [0, 2, 0, 2] x [1, 0, 3] = [2, 8, 0][1, 1, 1, 1] [1, 1, 0] [2, 6, 3]

query也是同樣的。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[1, 0, 1][1, 0, 1, 0] [1, 0, 0] [1, 0, 2][0, 2, 0, 2] x [0, 0, 1] = [2, 2, 2][1, 1, 1, 1] [0, 1, 1] [2, 1, 3]

在咱們實際的應用中,有可能會在點乘後,加上一個bias的向量。

4 給input1計算attention score

爲了獲取input1的attention score,咱們使用點乘來處理全部的key和query,包括它本身的key和value。這樣咱們就可以獲得3個key的表示(由於咱們有3個輸入),咱們就得到了3個attention score(藍色)。

 
   
 [0, 4, 2][1, 0, 2] x [1, 4, 3] = [2, 4, 4]            [1, 0, 1]

這裏咱們須要注意一下,這裏咱們只有input1的例子。後面,咱們會對其餘的輸入的query作相同的操做。

5 計算softmax

給attention score應用softmax。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
softmax([2, 4, 4]) = [0.0, 0.5, 0.5]

6 給value乘上score

使用通過softmax後的attention score乘以它對應的value值(紫色),這樣咱們就獲得了3個weighted values(黃色)。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
1: 0.0 * [1, 2, 3] = [0.0, 0.0, 0.0]2: 0.5 * [2, 8, 0] = [1.0, 4.0, 0.0]3: 0.5 * [2, 6, 3] = [1.0, 3.0, 1.5]

7 給value加權求和獲取output1

把全部的weighted values(黃色)進行element-wise的相加。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
[0.0, 0.0, 0.0]+ [1.0, 4.0, 0.0]+ [1.0, 3.0, 1.5]-----------------= [2.0, 7.0, 1.5]

獲得結果向量[2.0, 7.0, 1.5](深綠色)就是ouput1的和其餘key交互的query representation。

8 重複步驟4-7,獲取output2,output3

如今,咱們已經完成output1的所有計算,咱們要對input2和input3也重複的完成步驟4~7的計算。這相信你們本身是能夠實現的。

實現的代碼,我給你們準備了jupyter notebook,你們能夠clone下面的repo,本身一步步的完成代碼的調試,加深對於self-attention的理解。

https://github.com/BSlience/transformer-all-in-onegithub.com

完整的 Transformer Block 是什麼樣的?

Transformer 模型來源於Google發表的一篇論文 「Attention Is All You Need」,截止到我查詢的時候,這篇文章已經有17000+的引用量,可見這篇文章的影響力。但願你們能在有一些瞭解的基礎上,可以本身讀一下這篇文章。

https://arxiv.org/pdf/1706.03762.pdfarxiv.org

上面這張圖片是論文原文中的圖片,我把他們放在了一塊兒。這幾個模型分別表明了 Transformer 在翻譯任務中的應用(左),Multi-Head Attention(中),self-attention(右)。在前面的文章中,咱們已經講解過 self-attetnion(右),這裏和咱們以前講解過的稍有不一樣的是多了一個粉色的方框 Mask(opt),這個是用來左Mask任務的,括號中的opt表示是一個可選項,本篇先不提,後面咱們再細說;也講解了 Multi-Head Attention(中),多頭的注意力機制;本篇文章,咱們把重點集中在最左側的圖片,來看看 Transformer 結構。

咱們來把這幅圖放大來看,這個模型結構分爲左右兩個部分,由於原文中是用Transtormer來作翻譯任務的,你們可能知道一般咱們作翻譯任務的時候,都使用 Encoder-Decoder 的架構來作。這裏面的左側對應着 Encoder ,右側就是 Decoder 。Encoder 本質的目的就是對 input 生成一種中間表示,Decoder目的就是對這種中間表示作解碼,生成目標語言的ouput。你們會發現兩邊的結構基本上是一致的,爲了着重的研究Transformer結構,咱們把視線聚焦在Encoder的部分。

你們會在圖中看到,這裏有個   的符號,這表示了右側的結構能夠被   次堆疊,這就像是咱們在使用神經網絡的時候,能夠   次堆疊 layer 同樣,一般咱們把這樣的一種由多個 layer 組成的模塊叫作 block,這種 block 就是一種比 layer 更大規模的可複用單元。那麼,接下來咱們把重點放到 Transformer Block 上。

在這樣一個block中,是由幾個重要的組件構成的:

  1. self-attention layer
  2. normalization layer
  3. feed forward layer
  4. another normalization layer

在這樣四個組件中的兩個 normalization layer 以前,使用了殘差網絡(Residula connections)進行了鏈接。實際上,這幾個組件之間的順序並無被徹底的定死,這裏面最重要的事情是,要聯合使用 self-attention 和 feed forward layer,而且要在它們之間增長normalization 和 residual connections。

Normaliztion 和 residual connections 是咱們常用的,幫助加快深度神經網絡訓練速度和準確率的 tricks。

這裏咱們能夠先看看使用 Pytorch 實現這樣一個 block 是什麼樣子的。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class TransformerBlock(nn.Module): def __init__(self, k, heads): super().__init__()
self.attention = SelfAttention(k, heads=heads)
self.norm1 = nn.LayerNorm(k) self.norm2 = nn.LayerNorm(k)
self.ff = nn.Sequential( nn.Linear(k, 4 * k), nn.ReLU(), nn.Linear(4 * k, k))
def forward(self, x): attended = self.attention(x) x = self.norm1(attended + x) fedforward = self.ff(x)    return self.norm2(fedforward + x)

咱們這裏主觀的選擇4倍輸入大小做爲咱們 feedforward 層的維度,這個值使用的越小就越節省內存,可是相應的表示性也會變弱;可是,最小也應該大於咱們輸入的維度。

怎麼捕獲序列中的順序信息呢 ?

經過使用 Transformer 咱們能夠獲得一個對於輸入信息的 embedding vector,可是這裏你們可能也會發現,咱們並無利用好序列的輸入順序。好比說   和   ,它們獲得的 vector 是同樣的。顯然,這並非但願看到的。因此,咱們要給模型增長捕獲序列順序的能力。咱們應該怎麼作呢?

辦法也很簡單,咱們建立一個和輸入序列等長的新序列,這個序列裏包含序列中的順序信息,咱們把這個序列和原有序列進行相加,從而獲得輸入到 Transformer 的序列。那應該怎樣表示序列中的位置信息呢?

這裏咱們有兩種方法:

  • position embeddings

咱們簡單的 embed 位置信息,就像咱們對待每個輸入同樣。好比說咱們以前對每一個單詞 建立一個 vector  ,那咱們也對每個位置生成一個向量   。而後咱們使用模型的學習能力來學習到這些位置的 vector。可是這種方法會存在一個問題,那就是咱們須要在訓練的過程當中讓模型見過全部的須要在預測階段使用的位置 vector,不然模型就不知道相應位置的 vector。

  • position encodings

position encoding的方法其實和 position embedding 的方法很類似,咱們都是但願可以經過一個位置的 vector 來表示位置的信息,讓模型學習到這個信息。可是,這裏稍有不一樣的是,encoding 的方法是由咱們選擇一個 function 來生成每一個位置的 vector 的,而且讓模型網絡去找出該如何去理解這些 encoding vector。這樣作的好處是,對於一個選擇的比較好的function,網絡模型可以處理那些在訓練階段沒有見過的序列位置 vector(雖然這也並非說這些沒見過的位置 vector 必定可以表現的很好,可是好在是咱們能夠有比較直接的方法來測試他們)。這種方法也是 Transformer 的做者選擇的方法,讓咱們看看做者是怎麼設計這個 function 的。

做者使用上面的兩個 functions 來生成一個2維的矩陣常量,   表示在序列中的順序,   表示序列中數據 vector 的維度,   表示輸出的維度大小,以下圖所示:

這裏我給出一個使用 Pytorch 實現的 PositionEncoder 的代碼:

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class PositionalEncoder(nn.Module): def __init__(self, d_model, max_seq_len = 80): super().__init__() self.d_model = d_model # 根據pos和i建立一個常量pe矩陣 pe = torch.zeros(max_seq_len, d_model) for pos in range(max_seq_len): for i in range(0, d_model, 2): pe[pos, i] = \ math.sin(pos / (10000 ** ((2 * i)/d_model))) pe[pos, i + 1] = \ math.cos(pos / (10000 ** ((2 * (i + 1))/d_model))) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): # 讓 embeddings vector 相對大一些 x = x * math.sqrt(self.d_model) # 增長位置常量到 embedding 中 seq_len = x.size(1) x = x + Variable(self.pe[:,:seq_len], \ requires_grad=False).cuda() return x

上面的這個模塊中,咱們在數據的 embedding vector 增長了 position encoding 的信息。

讓 embeddings vector 在增長 postion encoing 以前相對大一些的操做,主要是爲了讓position encoding 相對的小,這樣會讓原來的 embedding vector 中的信息在和 position encoding 的信息相加時不至於丟失掉。

怎麼用 Pytorch 實現一個完整的 Transformer 模型?

  1. Tokenize
  2. Input Embedding
  3. Positional Encoder
  4. Transformer Block
  5. Encoder
  6. Decoder
  7. Transformer

1 Tokenize

首先,咱們要對輸入的語句作分詞,這裏我使用 spacy 來完成這件事,你也能夠選擇你喜歡的工具來作。

 
   
class Tokenize(object):  def __init__(self, lang): self.nlp = importlib.import_module(lang).load()  def tokenizer(self, sentence): sentence = re.sub( r"[\*\"「」\n\\…\+\-\/\=\(\)‘•:\[\]\|’\!;]", " ", str(sentence)) sentence = re.sub(r"[ ]+", " ", sentence) sentence = re.sub(r"\!+", "!", sentence) sentence = re.sub(r"\,+", ",", sentence) sentence = re.sub(r"\?+", "?", sentence) sentence = sentence.lower()        return [tok.text for tok in self.nlp.tokenizer(sentence) if tok.text != " "]

2 Input Embedding

Token Embedding

給語句分詞後,咱們就獲得了一個個的 token,咱們以前有說過,要對這些token作向量化的表示,這裏咱們使用 pytorch 中torch.nn.Embedding 讓模型學習到這些向量。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class Embedding(nn.Module): def __init__(self, vocab_size, d_model): super().__init__() self.d_model = d_model self.embed = nn.Embedding(vocab_size, d_model) def forward(self, x): return self.embed(x)

Positional Encoder

前文中,咱們有說過,要把 token 在句子中的順序也加入到模型中,讓模型進行學習。這裏咱們使用的是 position encodings 的方法。

 
   
class PositionalEncoder(nn.Module):
def __init__(self, d_model, max_seq_len = 80): super().__init__() self.d_model = d_model # 根據pos和i建立一個常量pe矩陣 pe = torch.zeros(max_seq_len, d_model) for pos in range(max_seq_len): for i in range(0, d_model, 2): pe[pos, i] = \ math.sin(pos / (10000 ** ((2 * i)/d_model))) pe[pos, i + 1] = \ math.cos(pos / (10000 ** ((2 * (i + 1))/d_model))) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x): # 讓 embeddings vector 相對大一些 x = x * math.sqrt(self.d_model) # 增長位置常量到 embedding 中 seq_len = x.size(1) x = x + Variable(self.pe[:,:seq_len], \ requires_grad=False).cuda()        return x
3 Transformer Block

有了輸入,咱們接下來就要開始構建 Transformer Block 了,Transformer Block 主要是有如下4個部分構成的:

  1. self-attention layer
  2. normalization layer
  3. feed forward layer
  4. another normalization layer

它們之間使用殘差網絡進行鏈接,詳細在上文同一個圖下有描述,這裏就再也不贅述了。


Attention

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
def attention(q, k, v, d_k, mask=None, dropout=None): scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) # mask掉那些爲了padding長度增長的token,讓其經過softmax計算後爲0 if mask is not None: mask = mask.unsqueeze(1) scores = scores.masked_fill(mask == 0, -1e9) scores = F.softmax(scores, dim=-1) if dropout is not None: scores = dropout(scores) output = torch.matmul(scores, v) return output

這個 attention 的代碼中,使用 mask 的機制,這裏主要的意思是由於在去給文本作 batch化的過程當中,須要序列都是等長的,不足的部分須要 padding。可是這些 padding 的部分,咱們並不想在計算的過程當中起做用,因此使用 mask 機制,將這些值設置成一個很是大的負值,這樣才能讓 softmax 後的結果爲0。關於 mask 機制,在 Transformer 中有 attention、encoder 和 decoder 中,有不一樣的應用,我會在後面的文章中進行解釋。

MultiHead Attention

多頭的注意力機制,用來識別數據之間的不一樣聯繫,前文中的第二篇也已經聊過了。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class MultiHeadAttention(nn.Module): def __init__(self, heads, d_model, dropout = 0.1): super().__init__() self.d_model = d_model self.d_k = d_model // heads self.h = heads self.q_linear = nn.Linear(d_model, d_model) self.v_linear = nn.Linear(d_model, d_model) self.k_linear = nn.Linear(d_model, d_model) self.dropout = nn.Dropout(dropout) self.out = nn.Linear(d_model, d_model) def forward(self, q, k, v, mask=None): bs = q.size(0) # perform linear operation and split into N heads k = self.k_linear(k).view(bs, -1, self.h, self.d_k) q = self.q_linear(q).view(bs, -1, self.h, self.d_k) v = self.v_linear(v).view(bs, -1, self.h, self.d_k) # transpose to get dimensions bs * N * sl * d_model k = k.transpose(1,2) q = q.transpose(1,2) v = v.transpose(1,2) # calculate attention using function we will define next scores = attention(q, k, v, self.d_k, mask, self.dropout) # concatenate heads and put through final linear layer concat = scores.transpose(1,2).contiguous()\ .view(bs, -1, self.d_model) output = self.out(concat) return output

Layer Norm

這裏使用 Layer Norm 來使得梯度更加的平穩,關於爲何選擇 Layer Norm 而不是選擇其餘的方法,有篇論文對此作了一些研究,Rethinking Batch Normalization in Transformers,對這個有興趣的能夠看看這篇文章。

https://arxiv.org/pdf/2003.07845.pdfarxiv.org

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class NormLayer(nn.Module): def __init__(self, d_model, eps = 1e-6): super().__init__() self.size = d_model # 使用兩個能夠學習的參數來進行 normalisation self.alpha = nn.Parameter(torch.ones(self.size)) self.bias = nn.Parameter(torch.zeros(self.size)) self.eps = eps def forward(self, x): norm = self.alpha * (x - x.mean(dim=-1, keepdim=True)) \ / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias return norm

Feed Forward Layer

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class FeedForward(nn.Module): def __init__(self, d_model, d_ff=2048, dropout = 0.1): super().__init__() # We set d_ff as a default to 2048 self.linear_1 = nn.Linear(d_model, d_ff) self.dropout = nn.Dropout(dropout) self.linear_2 = nn.Linear(d_ff, d_model) def forward(self, x): x = self.dropout(F.relu(self.linear_1(x))) x = self.linear_2(x)

5 Encoder

Encoder 就是將上面講解的內容,按照下圖堆疊起來,完成將源編碼到中間編碼的轉換。

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class EncoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1): super().__init__() self.norm_1 = Norm(d_model) self.norm_2 = Norm(d_model) self.attn = MultiHeadAttention(heads, d_model, dropout=dropout) self.ff = FeedForward(d_model, dropout=dropout) self.dropout_1 = nn.Dropout(dropout) self.dropout_2 = nn.Dropout(dropout) def forward(self, x, mask): x2 = self.norm_1(x) x = x + self.dropout_1(self.attn(x2,x2,x2,mask)) x2 = self.norm_2(x) x = x + self.dropout_2(self.ff(x2)) return x

class Encoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout): super().__init__() self.N = N self.embed = Embedder(vocab_size, d_model) self.pe = PositionalEncoder(d_model, dropout=dropout) self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N) self.norm = Norm(d_model)
def forward(self, src, mask): x = self.embed(src) x = self.pe(x) for i in range(self.N): x = self.layers[i](x, mask) return self.norm(x)

6 Decoder

Decoder部分和 Encoder 的部分很是的類似,它主要是把 Encoder 生成的中間編碼,轉換爲目標編碼。後面我會在具體的任務中,來分析它和 Encoder 的不一樣。


   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class DecoderLayer(nn.Module):
def __init__(self, d_model, heads, dropout=0.1): super().__init__() self.norm_1 = Norm(d_model) self.norm_2 = Norm(d_model) self.norm_3 = Norm(d_model) self.dropout_1 = nn.Dropout(dropout) self.dropout_2 = nn.Dropout(dropout) self.dropout_3 = nn.Dropout(dropout) self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout) self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout) self.ff = FeedForward(d_model, dropout=dropout)
def forward(self, x, e_outputs, src_mask, trg_mask): x2 = self.norm_1(x) x = x + self.dropout_1(self.attn_1(x2, x2, x2, trg_mask)) x2 = self.norm_2(x) x = x + self.dropout_2(self.attn_2(x2, e_outputs, e_outputs, \ src_mask)) x2 = self.norm_3(x) x = x + self.dropout_3(self.ff(x2)) return x

class Decoder(nn.Module):
def __init__(self, vocab_size, d_model, N, heads, dropout): super().__init__() self.N = N self.embed = Embedder(vocab_size, d_model) self.pe = PositionalEncoder(d_model, dropout=dropout) self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N) self.norm = Norm(d_model)
def forward(self, trg, e_outputs, src_mask, trg_mask): x = self.embed(trg) x = self.pe(x) for i in range(self.N): x = self.layers[i](x, e_outputs, src_mask, trg_mask) return self.norm(x)

7 Transformer

   
   
   
    
    
             
    
    

   
   
   
    
    
             
    
    
class Transformer(nn.Module): def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout): super().__init__() self.encoder = Encoder(src_vocab, d_model, N, heads, dropout) self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout) self.out = nn.Linear(d_model, trg_vocab) def forward(self, src, trg, src_mask, trg_mask): e_outputs = self.encoder(src, src_mask) d_output = self.decoder(trg, e_outputs, src_mask, trg_mask) output = self.out(d_output) return output

以上,就是 Transformer 實現的全過程,配套着 jupyter notebook 食用, 效果更加。

https://github.com/BSlience/transformer-all-in-onegithub.com

實現了上述這些,咱們就獲得了一個 Transformer 中的結構。


點個在看 paper不斷!

本文分享自微信公衆號 - 視學算法(visualAlgorithm)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索