源碼剖析transformer、self-attention(自注意力機制)、bert原理!

首先給大家引入一個github博客,這份代碼是我在看了4份transformer的源碼後選出來的,這位作者的寫法非常易懂,代碼質量比較高。https://github.com/Separius/BERT-keras

這篇文章主要跟大家分享四個點:多頭機制(multi-head)、LN和GELU、位置編碼。

在這再給大家安利幾篇博客,便於大家更具體的理解自注意力的內在原理。

https://zhuanlan.zhihu.com/p/44121378

https://zhuanlan.zhihu.com/p/47282410(精華)

https://www.cnblogs.com/robert-dlut/p/8638283.html

 

transformer是self-attention的落地或者說擴展,多頭機制把自注意力機制發揮得淋漓盡致。transformer最亮眼的地方就是完全拋棄了常規的鏈式RNN結構(包括LSTM等其他變體),即:並行計算能力特別弱的計算方法。它應該會是早期NLP訓練技術跟當期技術的一個里程碑,畢竟人家BERT是吧,刷新了不造幾個記錄,雖然XLNET又刷新了BERT的記錄,但是這也正證實了這種設計理念的優秀!優秀啊。。。[斜眼笑]。。。

言歸正傳!

一、自注意力機制(self-attention)和多頭機制(mutil-head)

常規的語言生成模型長這樣

下一個字的生成,依靠且只依靠上一個字的輸出狀態和當前輸入的輸入狀態,也就是說,預測值在某一程度上說只跟上一個字關係大一些,而自注意力模型,差不多長下面這個樣子。

 

這個圖的意思是,每一個字的生成,會跟所有的字(encode的時候)都有關係,這就是所謂的“注意力”機制。整個文字的生成過程中,每一個字都可能會跟所有的字做加權,爲什麼是“可能”呢,因爲有mask嘛,隨機給屏蔽掉一些詞,屏蔽掉的那就沒辦法顧及了。這樣的好處有兩個:

一是能“照顧”所有的詞,也就是我們理解的“語境”,

比如,句子1:“優秀!這就很優秀了!我做夢都沒想到他這麼175的個子能在中場投籃投進了!”;

和句子2:“優秀!這就很優秀了!他這185的個子在籃下那麼好的位置還是沒進球!”。

同樣位置的一個詞“優秀!”,一模一樣的字,它的意思的完全相反的(中華文化博大精深),自注意力機制就需要在即使後面說了一堆廢話的基礎上,還是能學出這個詞是褒義還是貶義。換句話說,它在判斷優秀是褒義還是貶義時,甚至需要看到最後幾個關鍵語氣的詞,才能做出判斷,而這個功能正是RNN系列模型做不到的!數學意義上可以叫做“貢獻度”。

二是,這樣所以詞就能並行計算了,至少這一步是可以並行計算了!

OK,自注意力就是大致這樣個流程,多頭又是什麼鬼!很簡單,經過嵌入層後,每個詞有多個維度(代碼嵌入爲768列),把這些維度均分成n_head(12)份,每一份都去做這麼一件事,就是多頭機制。簡而言之,就是自注意力的模式,複製了幾次,這個“幾次”就是“多頭”,12次就是12頭。。。只不過,不是做複製,二是做拆分,均分成12次來進行注意力的計算。

原理懂了哈,咱們看下人家是怎麼實現的。

(1)funcs的multihead_attention函數

_q, _k, _v = x[:, :, :n_state], x[:, :, n_state:2 * n_state], x[:, :, -n_state:]

# [B, n_head, max_len, 768 // n_head]
q = split_heads(_q, n_head)  # B, H, L, C//H
# [B, n_head, 768 // n_head, max_len]
k = split_heads(_k, n_head, k=True)  # B, H, C//H, L
# [B, n_head, max_len, 768 // n_head]
v = split_heads(_v, n_head)

x是embedding後的輸入,經過3*768個1x1卷積,變成[B, max_len, 3*768]的特徵矩陣,q、k和v各佔1/3,即:[B, max_len, 768]

(2)funcs的split_heads函數

# [-1, max_len, 768]
x_shape = shape_list(x)
# 768
m = x_shape[-1]
# [-1, max_len, n_head,  768 // n_head]
new_x_shape = x_shape[:-1] + [n, m // n]
# [B, max_len, n_head,  768 // n_head]

new_x = K.reshape(x, new_x_shape)
# return [B, n_head, max_len, 768 // n_head] False
# return [B, n_head, 768 // n_head, max_len] True
return K.permute_dimensions(new_x, [0, 2, 3, 1] if k else [0, 2, 1, 3])

這裏對q和v拆分成[B, 12,  max_len, 768 // 12] = [B, 12, max_len, 64]的長度,k拆成[B, 12, 64, max_len]

也就是說,每個詞拆成了12等分,每一等分特徵由之前的768變成了64。整個分成了B個batch,12的小batch,每個小batch的句子是max_len個長度的詞組成,每個詞有64的特徵。

(3)這裏咱們細講一下這個permute,permute是自注意力計算邏輯最核心最抽象的地方之一。先上一張圖

permute跟numpy的transpose是異曲同工的,都可以理解爲轉置。只是我們在對張量做轉置的時候,用常規的二維矩陣的思維去理解比較難。但是!解釋還是得用二維矩陣的方法去解釋!

假設我們的q是一個2X6的矩陣,每一個字被嵌入成6個特徵。k跟q一毛一樣,但是我把k做一下轉置,變成6X2的矩陣。這樣的形式,我們用矩陣相乘的方法,把2x6的矩陣跟6x2的矩陣相乘,是不是變成了2x2,這個2x2是不是就可以理解爲2個字對自己的排列組合。換句話說,所有的字與字之間的加權值得出來了。如圖:優跟優的加權值是50(1x1+2x2+3x3+4x4+5x5),優跟秀的加權值是70(1x2+2x3+3x4+4x5+5x6),秀跟優的加權值是70(1x2+2x3+3x4+4x5+5x6),秀跟秀的加權值是90(2x2+3x3+4x4+5x5+6x6)。

 

(4)funcs的scaled_dot_product_attention_tf函數

先看這個函數:w = K.batch_dot(q, k)

這一步就是上一步所說的矩陣乘法。算法中,對每一組嵌入數據

[B, max_len, 768*3]通過均分,拆成3份[B, max_len, 768],分別作爲q、k和v的前身;通過reshape,都變成[B, max_len, 12, 64];在通過permute變成q=[B, 12, max_len, 64]、k=[B, 12, 64, max_len]和v=[B, 12, max_len, 64]。

其中,q和k進行矩陣乘法,把max_len個字彼此求加權值(q=[B, 12, max_len, 64] * k=[B, 12, max_len, 64]),變成w=[B, 12, max_len, max_len]。這裏的max_len放在“優秀”一組詞裏面就是2,即:當前句子的長度。

接着,繼續對w和v做矩陣乘法,再求一次加權值(至於這一步有什麼實際的原理,我不太確定),這一次矩陣乘法,在數學上把維度還原到[B, 12, max_len, 64],以方便後期繼續還原成輸入的shape。我揣測,這一步w*v的意義跟全連接層的意義是接近的。只不過在設計的出發點和可解釋性上,要稍強於全連接層的意義。

 

所以,我用我自己組織的方式,給大家解釋一下整個自注意力的過程

a、首先是split_heads,就是切分出qkv

 

b、矩陣乘法求權值,即:scaled_dot_product_attention_tf

c、merge_heads

經過上述兩步,O(output)=[B, 12, max_len, 64],在這個函數裏面,還原成[B, max_len, 64]

到此,多頭+自注意力機制暫告一段落!

接下來你想在這一步重複多少次就重複多少次,因爲輸入輸出都是一個shape。

 

二、LN(Layer Normalization)

https://blog.csdn.net/weixin_42078618/article/details/90730488

往這看!!!

 

三、GELU

一個公式說明一切(這個是近似函數,不是本徵函數)

0.5 * x * (1 + K.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * K.pow(x, 3))))

然後看看圖像

當我看到這個圖像的時候,我第一反應想起了Swish激活函數

他倆真的是異曲同工,幾乎長得都一毛一樣。從論文來看,GELU函數的收斂性會比RELU和ELU都有略好。

 

四、位置編碼

位置信息對於理解一句話來說,也是很重要的。比如:

a、難受!滑板壞了,水還灑身上了!

b、滑板壞了,難受!水還灑身上了!

對於句子b,其實表達出來的意思,難受的重心是滑板,水是附加的負面影響。對於a,可能整體對心情造成的負面影響是差不多的。這就是詞在不同位置可能對語境帶了影響的一種情況。

常用的位置編碼一般無外乎兩種:一種是詞嵌入,相當於先加一步全連接層,並且該層參數可學;另一種是自己設計位置編碼方法。比如我印象中bert是用正餘弦函數做編碼的,以後看到再跟大家分享;或者,做一個遞進的簡單累加也不是不行哇,哈哈。

這裏的Transformer階段的位置編碼只是使用了簡單的詞嵌入的方式,你也可以理解其實就是全連接層的一種應用方式。

 

結語:transformer以及後來的bert模型最核心的地方就是自注意力機制,大家能把自注意力的實現原理看懂,其核心思想也就一通百通了。

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