從Transformer到Bert(一): self-attention機制
透過標題Transformers變形金剛,大家應該可以猜到我們今天要講變形金剛。 哦,no,其實是要講一個非常流行的架構,叫做transformer。由於最近一年,Bert模型非常popular,大部分人都知道Bert,但是確不明白是什麼,那麼可能你需要先從tranformer瞭解清楚。
Transformer最先用於NLP的問題上,很多工上實現了SOTA的表現,並且已經運用到很多其他領域。這個系列文章,就是想用最簡單的方式來帶大家瞭解transformer架構是如何工作的,如何從transformer演變到Bert的,而你,只需要一點大學基礎的線性代數知識就可以。
在這裡的第一篇,會著重介紹self-attention機制。(第二期請看這裡)
Self-attention
Transformer架構最基礎和最重要的操作就是self-attention。
Self-attention是一種sequence2sequence操作:通常將一個sequence轉換成另外一個sequence。我們可以假設輸入 vector是[x1,x2,。。。xt], 以及輸出 vector是[y1,y2,。。。,yt]。 這裡vector的維度都是k。
為了產生輸出vector
, self-attention操作會對所有輸入的vector進行加權平均
這裡 j 是對整個sequence進行index(從1到t) 並且 weight的和在所有j上相加等於1(因為我們使用softmax,後面會提到)。這裡的weight
不是我們通常學習機器學習裡看到的引數,而是透過
和
的函式得到。最簡單我們可以透過點乘法得到:
這樣的點積可能會產生任意一個數,所以我們再採用softmax,把值投射到[0,1]的區間裡,來保證在整個sequence上,他們的和為1。
以上就是self attention的基本操作,下面附上一張圖。
self-attention基本操作,這裡softmax沒有顯示出來
還有一些其他成分來構成整個transformer,我們待會再說,但是self-attention是最基礎的操作。更重要的是,這是整個transformer的架構裡唯一保持vector之間資訊流通的操作。所有其他的操作都是不在vector之間進行互動的。
掌握到這裡,你已經掌握了精華的一部分,如果有興趣,接著往下看:
理解為什麼self-attention有效果
儘管上面的內容看起來很簡單,但是為什麼self-attention效果特別好,這一點並不是很容易理解。為了建立一些直觀的理解,我們首先看看推薦系統中電影推薦一般的步驟。
假設你有一個電影網站,對於一些使用者,你想推薦可能會喜歡的電影給他們。
一種解決這類問題的方法是,先搜尋一些電影的特徵值:比如這個電影的浪漫程度,動作片程度之類,然後設計相應的使用者的特徵:使用者有多喜歡浪漫電影,有多喜歡動作片電影。如果這樣做,兩個vector(使用者和電影)的點乘就會給你一個分數顯示這個電影和使用者的匹配程度,換句話說,就是使用者可能喜歡這個電影的程度。
如果使用者喜歡浪漫的電影,正好這個電影又是浪漫主題的,那麼點乘score的結果就是正數positive,如果使用者不喜歡浪漫電影,但是電影是浪漫主題,那麼點乘的結果就是負數negative。
另外,這裡的特徵值本身的大小,也表明,他們自己對整體score的貢獻是多大:一個電影可能很小一部分講述動作武打,但並不是主要情節,或者使用者本身只是輕微喜歡武打動作片,那麼這一部分對於整個的結果影響就會比較小。
當然,或者這樣的特徵值本身實際情況下也許不太現實,或者很難獲得,尤其是當你有上百萬個電影的時候,標註起來就很困難,並且標註大量使用者的喜歡和不喜歡也很困難。
取而代之的是,我們把電影的特徵和使用者的特徵當做模型的引數,然後我們問使用者他們喜歡的一些電影,然後我們根據這些來最佳化使用者的特徵和電影的特徵使得他們的點乘值和這些已知的喜歡能夠匹配。(知識點:這是大部分推薦系統的核心思想)
即使我們不能直接告訴模型每個特徵值意味著什麼,但在實際工程中,經過訓練之後,特徵值往往都能準確的反應電影內容的資訊。很神奇是不是
這是從使用者與電影之間的關係學習出來的電影特徵的表達,投射在二維的空間裡,完全沒有用到電影內容資訊。這表明僅僅透過這樣的互動資訊,實際上我們可以推測出電影本身的特徵表達
這裡的ppt可以幫助你理解更多關於推薦系統的基礎知識。這裡只是給大家解釋點乘為什麼能夠幫我們來表達一個物件或者物件之間的關係。(這裡的物件是object,不是lover :) )
再回到self-attention,上面講的就是self-attention的intuition。我們再回到sequence of words上。為了使用self-attention,我們給每個word t一個embedding vector,我們叫他
(有很多word embedding的技術,後面會稍微提一些)。這個通常在模型裡叫做embedding層,它將每個word sequence:
the, cat, walks, on, the, street
投射成vector序列:
如果我們把這樣的序列放進self-attention層,那麼output就是另外一個vector序列:
這裡
是對第一個sequence的所有embedding vectors的加權求和,權重分別是他們的和
點乘。
因為
也是我們訓練出來的值,所以任意兩個詞之間有多接近,完全由這個訓練任務決定的。在大多數情況下,‘
the
‘ 的含義和其他單詞的含義並沒有非常有關聯;因此,我們很可能得到一個embedding使得
和其他單詞的embedding點積的絕對值比較小(不會影響最終結果)。從另一個方面來說,為了解釋’
walk
‘在句中的含義,去找出是誰在walking就比較重要了。這有可能被一個名詞(noun)表示,所以對於名詞’
cat
‘ 和動詞’
walks
‘, 我們就有可能會學到一個
和
使得他們的點積是一個比較大的正數。
這是self-attention背後的intuition。點積表達了兩個vectors之間有多相關,’相關性‘是由具體任務定義的,輸出vectors是輸入sequence的加權求和,這裡的權重取決於點乘的結果。
在我們往後面說之前,有幾點值得注意,這些並不是普通的sequence2sequence模型裡有的操作:
self-attention把它的input認為是一個set,而不是一個sequence。如果我們變換整個input序列順序,output的序列仍然會是一樣。在真正做transformer的時候,我們會有一些改變,但是self-attention操作本身是忽略input的順序的。
目前為止,我們還沒有提到任何引數parameters。單純self-attention是沒有涉及任何引數的。當然在embedding layer是有引數需要學習,另外我們在後面還會新增一些parameters。(請接著往下看)
來用Pytorch實現一個self-attention
”所有我不能創造的,都不是真正理解“。 所以讓我們一起來做一個簡單的transformer。我們從建立基礎的self-attention開始。如果你熟悉pytorch,或者不介意讀一些程式碼,這一部分可以幫助你更深入理解,如果不是,也沒有關係,直接跳過這一部分,隨時都可以回過來看。
第一件事我們要做的就是如何用矩陣乘法表達self attention。一個簡單的實現方法是對所有的vectors迴圈來計算權重,但是計算起來會特別慢。那麼我們該怎麼做?
我們來用一個t*k的矩陣
X
來表示一個長度為 t ,維度為 k 的vectors序列。再加上minibatch的維度/大小 b, 構成我們input tensor的維度是(b,t,k)。
所有的點積結果
形成了一個矩陣,我們可以透過矩陣相乘 X 和
:
import
torch
import
torch。nn。functional
as
F
# 假設我們有input tesnor x with shape (b,t,k)
x
=
。。。
# x 和 x轉置 相乘得到 weights
raw_weights
=
torch
。
bmm
(
x
,
x
。
transpose
(
1
,
2
))
# torch。bmm代表batched matrix multiplication
然後,為了將
轉變成postive的數值,並且和為1,我們需要採取 row-wise softmax(每一行進行softmax):
weights = F。softmax(raw_weights, dim = 2)
最後,為了計算output sequence,我們只需要把weights和X相乘。最後我們的output矩陣
Y
的shape就是(b, t, k)
y = torch。bmm(weights, x)
Great,兩個矩陣乘法和一個softmax,就給我們一個基礎的self-attention的操作,並不是很複雜是不是。看到這你已經知道了一大半。
還有一些小訣竅
真正現代的transformer模型裡用到的self-attention實際上還用到了三個小技巧:
1)Queries, keys and values
每個input vector
都在self attention裡被用作三種不同的操作:
query,用來和其他每個key vector進行互動,得到當前vector和其他vector的關聯性,或者我們說的weights,用於計算自己的output
key,用來和其他query vector進行互動, 幫助其他vector j 產生他的output
value,將query和其他key產生得到的權重,跟自身的value進行加權求和,得到自己的output
這就是我們說的query,key和value。在我們看到的基礎的self-attention中,每個input vector都必須扮演者三個角色。為了讓我們任務簡單點,我們可以對input vector進行簡單的線性轉換,就可以得到這三個新的vectors。換句話說,我們新增三個 k*k的權重矩陣
,
,
然後計算三個線性轉換:
,
,
這給了self-attention一些可以控制的引數,並且讓它改變了input vectors使得他們能夠扮演這三個角色。下面的圖更能說明整個流程,self-attention以及key,query,value的變化。
2)調整點積的大小
softmax本身會對非常大的值很敏感,這會造成vanish gradient從而減緩訓練速度,或者停止訓練。因為點積的值會隨著embedding dimension k的增大而增大,所以如果能按照dimension的大小,normalize一下,就可以防止softmax的結果變得太大:
也許你會問,為什麼是
?你可以想象一個長為k 的vector,裡面所有的值都是c,那麼歐氏距離就為
。因此,我們為了防止這部分由維度增加導致的距離增大,需要除以
。
3)Multi-head attention
最後,我們需要考慮,一個單詞可能在不同的語境下有不同的語義,看下面這個例子:
mary, gave, roses, to, susan
我們看到單詞
gave
和句子中不同部分是有不一樣的語義的意思。 ’
mary
’ 表示who在giving, ‘
roses
‘表明的what被given,’
susan
‘ 表明who是被given的人。 (突然化身英語老師)
在一個self-attention的操作裡,所有的這些資訊都加在一起。如果’susan’和’mary‘換過來:‘susan gave roses to mary’,那麼output
還是會相同,即使語義是不一樣的。
我們可以給self-attention更多區分這樣不同語義的能力,透過使用多個self attention(用r表示index),每個attention去關注不同的部分,我們稱為attention heads。。每個權重用
,
,
。
對於input
,每個attention head都會產生不同的output
。我們把它們連線起來,一起透過一個線性的轉換來把維度再降低為k。
用pytorch實現完整的self-attention
在又掌握一些訣竅之後,我們來實現一個完整的self-attention模組,我們把它包裝到一個python的一個module裡,這樣可以複用。
import torch
from torch import nn
import torch。nn。functional as F
class SelfAttention(nn。Module):
def __init__(self, k, heads = 8):
super()。__init__()
self。k, self。heads = k, heads
我們可以用h個不一樣的attention heads矩陣
,
,
來表示heads,但是更有效的方法是把所有的heads結合到三個k*hk的矩陣裡,這樣我們可以在一個大的矩陣裡進行矩陣乘法運算。
# input 維度為k(embedding結果),map成一個k*heads維度的矩陣
self。tokeys = nn。Linear(k, k * heads, bias = False)
self。toqueries = nn。Linear(k, k * heads, bias = False)
self。tovalues = nn。Linear(k, k * heads, bias = False)
# 在透過線性轉換把維度壓縮到 k
self。unifyheads = nn。Linear(heads * k, k)
我們現在來實現self-attention的計算(透過forward 方法)。首先,我們計算queries, keys, values:
def forward(self, x):
b, t, k = x。size()
h = self。heads
queries = self。toqueries(x)。view(b, t, h, k)
keys = self。tokeys(x)。view(b, t, h, k)
values = self。tovalues(x)。view(b, t, h, k)
每個Linear的輸出都是(b, t, h*k)的維度,我們可以reshape到(b,t,h,k)的維度,這樣可以給每個head他們自己的dimension。
下一步,我們需要計算點積(dot product)。這在每個head上的操作都是一樣的,所以我們每次對一整個batch進行操作。這可以確保我們可以跟之前一樣使用torch。bmm(),只是整個q,k,v的集合要更大一些,僅此而已。
因為head 和 batch的維度不是緊挨在一起,我們需要做矩陣的轉置。(這看起來很費時間,但是不可避免)
# 把 head 壓縮排 batch的dimension
queries = queries。transpose(1, 2)。contiguous()。view(b * h, t, k)
keys = keys。transpose(1, 2)。contiguous()。view(b * h, t, k)
values = values。transpose(1, 2)。contiguous()。view(b * h, t, k)
# 如果不明白contiguous()的意思,這篇部落格講的很清楚:
https://
zhuanlan。zhihu。com/p/64
551412
跟之前一樣,點積的結果可以被一個矩陣乘法搞定,只不過現在是queries和keys之間。
別忘了還要做一件事,就是把點積的結果需要normalize一下,除以
。其實更巧妙的做法是在queries和keys相乘前,normalize by
, 這可以對長的序列減少記憶體消耗:
# 這等效於對點積進行normalize
queries = queries / (k ** (1/4))
keys = keys / (k ** (1/4))
# 矩陣相乘
dot = torch。bmm(queries, keys。transpose(1,2))
# 進行softmax歸一化
dot = F。softmax(dot, dim=2)
我們在把self attention採用到values 上,產生出最後每個head的output:
out = torch。bmm(dot, values)。view(b, h, t, k)
為了再把multi-head結合到一起,我們再轉置一次,是的head的維度和embedding的維度貼到一起,然後reshape到合成的 k*h維度,再透過一個unifyheads 的線性轉換,迴歸到k維。
# swap h, t back, unify heads
out = out。transpose(1, 2)。contiguous()。view(b, t, h*k)
return self。unifyheads(out)
好啦,寫到這,你就有了multi-head的self-attention了,這是transformer裡最為關鍵的一部分了。
Transformer本身當然不只是self-attention這一層,想了解transformer的全貌?
看第二期:從Transformer到Bert(二):構建transformer
上一篇:怡情逸興得快樂