離開深度學習瞎折騰了一段時間后,我終于又回來了。
于是趕緊回顧了下18年之后NLP的發展,基本就是將遷移學習更廣泛的用于NLP領域,以及把17年年底的《Attention is all you need》里的思想給發揚光大了,ELMO彌補了傳統word2vec多義詞表示的不足,GPT使用更強大的特征提取器Transformer取代LSTM,Bert使用雙向Transformer進一步改進了GPT成為這兩年發展的集大成者。
從Bert模型所帶來的NLP界里程碑式的影響和所取得的成就來看,無疑Bert將會是未來兩三年NLP應用發展的基石,于是有必要仔細的看看其模型的結構,數據是如何流動的,訓練的和測試的。
不得不說現在的學習環境相對幾年前好太多了,本文主要參考了以下幾篇文章,然后加了點自己的理解:
Dissecting BERT Part 1: The Encoder
Dissecting BERT Appendix: The Decoder
它的總體框架同lstm時代的MNT或者是attention is all you need中的transformer一樣的encoder-decoder結構:
我們先來介紹一下Encoder部分。
Encoder
為了理解這個架構,我們使用一個簡單的具體的例子,來看一下輸入的數據是怎么通過encoder一步一步變化讓后到輸出的。
從詞到向量
bert的詞嵌入由三個嵌入token embedding、segment embedding,和position embedding疊加而成。
Token embedding
這個過程跟以往的RNNs沒什么區別,比如給定一個句子:
Hello, how are you?
第一步是先將其標記化:
“ Hello, how are you?” → [“Hello”, “,” , “how”, “are”, “you”, “?”]
然后是數字化,將每個標記映射到語料詞匯表中的唯一整數編號:
[“Hello”, “, “, “how”, “are”, “you”, “?”] → [34, 90, 15, 684, 55, 193]
接下來就是得到序列中每個詞的詞嵌入,也就是將整數映射到一個 維的向量,這個向量是模型在訓練時學習的,你可以將其視為一個查表的過程,這些向量的元素作為模型的參數,像其他權重一樣通過反向傳播進行了優化。
在論文中是使用WordPiece tokenization 來將英文單詞轉換成768()維的向量,轉化的過程類似這樣:
把每個詞的向量放到一起,就得到了一個句子長度x向量維度() 尺寸的矩陣Z:
說明一點,我們通常使用填充的方式來讓輸入序列具有相同的長度,比如通過添加"<pad>" 標記來增加某些序列的長度,還是前面的例子,填充后可能變為:
[“<pad>”, “<pad>”, “<pad>”, “Hello”, “, “, “how”, “are”, “you”, “?”] →
[5, 5, 5, 34, 90, 15, 684, 55, 193]
如果設定設定為9,那我們就把句子從5填充到了9。
Positional Encoding
但是,上面的embedding并沒有包含詞的位置信息。于是,我們的目標是能夠根據詞在句子中的位置適當調整這個向量,使它帶上位置信息。
作者選擇的方法是使用預定的(非學習的)正余弦函數將 之間的數字加到前面的embedding中,即通過正余弦函數將位置表示為彼此的線性組合,從而實現網絡學習中標記位置之間的相對關系。在Token embedding 獲得的矩陣
的基礎上加上位置矩陣
。
數學上,用 表示序列中標記的位置,用
表示token embedding特征向量中的位置:
具體來說,對于給定的句子 ,其位置嵌入矩陣為:
作者解釋說,使用這種確定性方法的結果和學習位置表示(就像我們對詞嵌入那樣)的結果差不多,因此這樣反而會有一些優勢:
- input_length 可以無限長,因為函數可以計算任意位置
- 需要學習的參數更好啊,訓練更快
因此,添加了位置信息之后的矩陣是:
它是第一個encoder塊的輸入,尺寸是
Encoder block
共有N個編碼器塊連接在一起直到生成編碼器的輸出,特定的塊負責查找輸入表示之間的關系并將編碼在其輸出中。
直觀地,通過這些塊的迭代過程將幫助神經網絡捕獲輸入序列中的詞之間的更加復雜的關系,你可以把它理解成一個整體用來捕捉輸入序列的語義。
Multi-Head Attention
encoder中使用Transformer的多頭注意力機制,這意味著它將計算 份不同權重矩陣的自注意力,然后將結果連接在一起。
這些并行注意力計算的結果稱之為Head,我們用下標 來表示一個特定的head和相關的權重矩陣。
如上圖所示,一旦計算了所有head,它們將被連接起來,得到一個尺寸為 的矩陣,然后將它乘以一個尺寸為
的權重矩陣
進行線性變換,就得到了一個尺寸為
的最終結果,用數學公式表示就是:
其中的 通過
乘以相應權重矩陣
獲得,我們通過一個簡單的例子來可視化的看一下這個過程。
這圖描繪了輸入標記通過 token embedding 和 positional encoding ,再輸入到Encoder:
接下來,我們再來看下Encoder中的操作過程,先看一下單頭的self-attention:
上圖描繪了一個Head的 是怎么來的,其中的
的尺寸是
, 因為Q和K需要計算相似性,所以維度應當是相同的,
的尺寸是
,
的維度可以相同也可以不同,在論文中
.
所謂的自注意力,就是 與
的點積進行
的縮放之后通過softmax獲得一個概率權重,然后用這些權重分別乘以各自的
即可:
為了加深理解,我們選擇其中一個頭,通過圖形繼續可視化的看一下這個變化過程:
然后計算self-attention,
多頭的話就是同時有多個上述計算過程在進行:
假設我們有8個Head,那么我們就獲得8個 :
但是,顯然前饋層只需要一個矩陣 ,怎么處理呢?類似多卷積核的處理,把這8個矩陣連起來,乘以一個權重矩陣
壓縮到一個矩陣。
為了有一個更加全面直觀的認識,我們把上面整個過程放到一個圖里,
顯然,第二個encoder塊是不需要embedding過程的,只要把第一個encoder塊的輸出作為輸入即可。
經過上面的介紹,你應該對這個過程已經有了足夠的了解,但是,為什么可以利用向量點積來計算注意力概率呢?
于是讓我們進一步深入來了解其中的原理。
這個結構體系的關鍵在于:
也就是每個詞的q向量與每個詞的k向量的點積,套用點積公式:
這意味著 和
的方向越相似,長度越大,點積就越大。詞與此之間關聯越大,對于理解這個詞時得到的關注越大,跟我們的本意是相同的。
Add & Norm
我們再看一下最開頭的結構示意圖,每個encoder塊在Multi-Head Attention之后經過一個 Add & Norm層才進入下一個塊。于是我們來看一下這一層做了些什么。
Add實際就是一個殘差連接,將輸出加上輸入,這個在每一塊的self-attenton以及FFN之后都會有,然后跟隨一個Layer Norm 。
Norm 是一個Layer Normlization,將 正則化,就是把它縮放到一個均值為0方差為1的域里。因為
不過一般在這一層之前,就會有一個dropout層。
Position-wise Feed-Forward Network
每個encoder塊都由 mulit-head atteion add & Norm
feed forword network
add & Norm 這樣一個過程,下面來介紹一下這個Feed-Forward Network。
這是一個全連接層,包含兩個線性變化和一個非線性函數(實際一般就是ReLu),
對于輸入的 (尺寸為
) ,通過權重矩陣
(尺寸為
)和偏置
線性變換到隱藏層 (尺寸為
) ,然后**ReLu **激活 ,記下來再用權重矩陣
(尺寸為
) 和偏置
的線性變換到輸出層(尺寸為
) ,表示成數學公式就是:
在最后一個encoder塊輸出之后連接到decoder。
Decoder
Decoder和Encoder的結構是類似的,但是因為可視信息的不同,又有所差別。
Transformer解決的是翻譯的問題,將一個句子翻譯成另一種語言,我們希望模型能夠捕捉到輸入句子中詞之間的關系,并且將輸入句子中包含的信息與每一步已翻譯的內容結合起來。繼續上面的例子,我們的目標是把一個句子從英文翻譯為西班牙文,這是我們獲得的序列標記:
X = [‘Hello’, ‘,’, ‘how’, ‘are’, ‘you’, ‘?’] (Input sequence)
Y = [‘Hola’, ‘,’, ‘como’, ‘estas’, ‘?’] (Target sequence)
我們同之前一樣來看看輸入到輸出數據是如何流動的。
這是我們的解碼器的輸入標記:
[‘<SS>’, ’Hola’, ‘,’, ‘ como’, ‘estas’, ‘?’]
然后這是解碼器的期望輸出:
[’Hola’, ‘,’, ‘ como’, ‘estas’, ‘?’,’<EOS>’]
但是,這里存在一個問題,比如輸入這邊我們已經看到了'como' 的后面是'estas', 然后再用它來預測'estas' ,這顯然是不合理的,因為模型在測試的時候是看不到后面的詞的。
因此,我們需要修改注意力層,防止模型可以看到預測詞右邊的信息,與此同時,它能利用已經預測的詞,即左邊的信息。
繼續上面的例子,我們將輸入標記轉換成矩陣的形式,并添加位置信息:
和encoder一樣,decoder塊的輸出也將是大小為 的矩陣,在逐行線性變換+softmax激活后,將生成一個舉證,其中每行的最大元素表示下一個單詞。也就是說,分配"<SS>" 的行負責預測“Hola”, 分配"Hola"的行負責預測"," ...以此類推。比如,為了預測"estas", 我們將允許該行直接和下圖中綠色區域互動,而不能和紅色區域互動:
但是,在我們使用多頭注意力機制的時候,所有的行都會產生交互,因此需要在輸入的時候添加遮罩,這個遮罩會在注意力計算之后進行:
這是self-attention的計算結果:
然后我們在此基礎上添加遮掩,就是把矩陣上三角的位置全部設置為 :
于是,在進行softmax激活之后,矩陣就變成了:
恰好達到了我們的要求,那些需要在訓練時忽略的右側的詞的注意力全部變成了0。
當將這個注意力矩陣與 相乘時,預測的詞就是模型可以訪問元素右邊的元素。注意,這里的多頭注意力輸出將是
維的,因為它的序列長度是
。
這個就是Decoder從target序列的輸入,并經過Masked Multi-Head Attention 的一個變化得到了,decoder的還有一部分輸入來自于源語句經過Encoder的最終輸出
(尺寸是
)。
接下來,就是與encoder一樣的 Multi-Head Attention Add and Layer Norm -> FFN 的過程。
只不過,現在的 來自于
,而
來自于
:
計算每個query相對于key的注意力之后,得到的是一個 的矩陣, 繼續咱們的例子,比如注意力矩陣為:
如上圖所見,這個注意力是當前Decoder輸入與Encoder輸出的每個詞之間的注意力,咱們用這個矩陣再乘以 ,就得到了一個
的矩陣,每一行代表了源語句相對于當前輸入詞匯的特征:
h個Head連接起來,尺寸變為 ,它通過
的權重矩陣
線性變換到一個
的輸出。
這在多個Decoder之后,最后輸出的矩陣通過乘以權重矩陣 (
) 進行線性變換,變換之后再對每一行的向量softmax, 其中選擇值最大位置對應詞表索引的詞就是預測的詞。
損失的話只需要用預測的每個詞向量與真實的詞的one-hot詞表示計算交叉熵即可。