Github地址:https://github.com/1234560o/Bert-model-code-interpretation.git
Contents
- 前言
- 模型輸入
- Padding_Mask
- attention_layer
- transformer_model
- Bert_model class
- 后續
前言
關于Bert模型的基本內容這里就不講述了,可參考其它文章,這里有一個收集了很多講解bert文章的網址:
http://www.52nlp.cn/bert-paper-論文-文章-代碼資源匯總
與大多數文章不同的是,本文主要是對Bert模型部分的源碼進行詳細解讀,搞清楚數據從Bert模型輸入到輸出的每一步變化,這對于我們理解Bert模型、特別是改造Bert是具有極大幫助的。需要注意的是,閱讀本文之前,請先對Transformer、Bert有個大致的了解,本文直接講述源碼中的數據運算細節,并不會涉及一些基礎內容。當然,我們還是先來回顧下Bert模型結構:
Bert模型采用的是transformer的encoder部分(見上圖),不同的是輸入部分Bert增加了segment_embedding且模型細節方面有些微區別。下面直接進入Bert源碼解析。Bert模型部分源碼地址:
https://github.com/google-research/bert/blob/master/modeling.py。
模型輸入
Bert的輸入有三部分:token_embedding、segment_embedding、position_embedding,它們分別指得是詞的向量表示、詞位于哪句話中、詞的位置信息:
Bert輸入部分由下面兩個函數得到:
embedding_lookup得到token_embedding,embedding_postprocessor得到將這三個輸入向量相加的結果,注意embedding_postprocessor函數return最后結果之前有一個layer normalize和droupout處理:
Padding_Mask
由于輸入句子長度不一樣,Bert作了填充處理,將填充的部分標記為0,其余標記為1,這樣是為了在做attention時能將填充部分得到的attention權重很少,從而能盡可能忽略padding部分對模型的影響:
attention_layer
為了方便分析數據流,對張量的維度作如下簡記:
做了該簡記后,經過詞向量層輸入Bert的張量維度為[B, F, embedding_size],attention_mask維度為[B, F, T]。由于在Bert中是self-attention,F和T是相等的。接下來我詳細解讀一下attention_layer函數,該函數是Bert的Multi-Head Attention,也是模型最為復雜的部分。更詳細的代碼可以結合源碼看。在進入這部分之前,也建議先了解一下2017年谷歌提出的transformer模型,推薦Jay Alammar可視化地介紹Transformer的博客文章The Illustrated Transformer ,非常容易理解整個機制。而Bert采用的是transformer的encoding部分,attention只用到了self-attention,self-attention可以看成Q=K的特殊情況。所以attention_layer函數參數中才會有from_tensor,to_tensor這兩個變量,一個代表Q,另一個代表K及V(這里的Q,K,V含義不作介紹,可參考transformer模型講解相關文章)。
? atterntion_layer函數里面首先定義了函數transpose_for_scores:
該函數的作用是將attention層的輸入(Q,K,V)切割成維度為[B, N, F 或T, H]。了解transformer可以知道,Q、K、V是輸入的詞向量分別經過一個線性變換得到的。在做線性變換即MLP層時先將input_tensor(維度為[B, F, embedding_size])reshape成二維的(其實源碼在下一個函數transformer_model中使用這個函數傳進去的參數已經變成二維的了,這一點看下一個函數transformer_model可以看到):
接下來就是MLP層,即對輸入的詞向量input_tensor作三個不同的線性變換去得到Q、K、V,當然這一步后維度還需要轉換一下才能得到最終的Q、K、V:
MLP層將[B * F, embedding_size]變成[B * F, N * H]。但從后面的代碼(transformer_model函數)可以看到embedding_size等于hidden_size等于N * H,相當于這個MLP層沒有改變維度大小,這一點也是比較難理解的:
之后,代碼通過先前介紹的transpose_for_scores函數得到Q、K、V,維度分別為[B, N, F, H]、[B, N, T, H]、[B, N, T, H]。不解得是,后面的求V代碼并不是通過transpose_for_scores函數得到,而是又把transpose_for_scores函數體再寫了一遍。
到目前為止Q、K、V我們都已經得到了,我們再來回顧一下論文“Attention is all you need”中的attention公式:
下面這部分得到的attention_scores得到的是softmax里面的部分。這里簡單解釋下tf.matmul。這個函數實質上是對最后兩維進行普通的矩陣乘法,前面的維度都當做batch,因此這要求相乘的兩個張量前面的維度是一樣的,后面兩個維度滿足普通矩陣的乘法規則即可。細想一下attention的運算過程,這剛好是可以用這個矩陣乘法來得到結果的。得到的attention_scores的維度為[B, N, F, T]。只看后面兩個維度(即只考慮一個數據、一個attention),attention_scores其實就是一個attention中Q和K作用得到的權重系數(還未經過softmax),而Q和K長度分別是F和T,因此共有F * T個這樣的系數:
那么比較關鍵的一步來了——Mask,即將padding部分“mask”掉(這和Bert預測詞向量任務時的mask是完全不同的,詳情參考相關文章,這里只討論模型的詳細架構):
我們在前面步驟中得到的attention_mask的維度為[B, F, T],為了能實現矩陣加法,所以先在維度1上(指第二個維度,第一個維度axis=0)擴充一維,得到維度為[B, 1, F, T]。然后利用python里面的廣播機制就可以相加了,要mask的部分加上-10000.0,不mask的部分加上0。這個模型的mask是在softmax之前做的,至于具體原因我也不太清楚,還是繼續跟著數據流走吧。加上mask之后就是softmax,softmax之后又加了dropout:
再之后就是softmax之后的權重系數乘上后面的V,得到維度為[B, N, F, H],在維度為1和維度為2的位置轉置一下變成[B, F, N, H],該函數可以返回兩種維度的張量:
- [B * F, N * H](源碼中注釋H變成了V,這一點是錯誤嗎?還是我理解錯了?)
- [B, F, N * H]
至此,我將bert模型中最為復雜的Multi-Head Attention數據變化形式講解完了。下一個函數transformer_model搭建Bert整體模型。
transformer_model
下面我對transformer_model這個函數進行解析,該函數是將Transformer Encoded所有的組件結合在一起。 很多時候,結合圖形理解是非常有幫助的。下面我們先看一下下面這個盜的圖吧(我們把這個圖的結構叫做transformer block吧):
整個Bert模型其實就是num_hidden_layers個這樣的結構串連,相當于有num_hidden_layers個transformer_block。而self-attention部分在上個函數已經梳理得很清楚了,剩下的其實都是一些熟悉的組件(殘差、MLP、LN)。transformer_model先處理好輸入的詞向量,然后進入一個循壞,每個循壞就是一個block:
上面的截圖并未包括所有的循環代碼,我們一步步來走下去。顯然,代碼是將上一個transformer block的輸出作為下一個transformer block的輸入。那么第一個transformer block的輸入是什么呢?當然是我們前面所說的三個輸入向量相加得到的input_tensor。至于每個block維度是否對得上,計算是否準確,繼續看后面的代碼就知道了。該代碼中還用了變量all_layer_outputs來保存每一個block的輸出結果,設置參數do_return_all_layers可以選擇輸出每個block的結果或者最后一個block的結果。transformer_model中使用attention_layer函數的輸入數據維度為二維的([B * F或B * T, hidden_size])。詳細看attention_layer函數時是可以輸入二維張量數據的:
至于下面這部分為什么會有attention_heads這個變量,原因我也不知道,仿佛在這里是多此一舉,源碼中的解釋如下:
我們再回顧一下上一個函數attention_layer,return的結果維度為[B * F, N * H]或[B, F, N * H]。注意這里面使用的attention_layer函數do_return_2d_tensor參數設置為True,所以attention_output的維度為[B * F, N * H]。然后再做一層MLP(該層并沒改變維度,因為hidden_size=N * H)、dropout、layer_norm:
此時attention_output的維度還是[B * F, N * H或hidden_size]。由上面的圖可以接下來是繼續MLP層加dropout加layer_norm,只不過該層MLP的神經元數intermediate_size是一個超參數,可以人工指定:
由上面截圖的代碼可知接下來做了兩層MLP,維度變化[B * F, hidden_size]到[B * F, intermediate_size]再到[B * F, hidden_size],再經過dropout和layer_norm維度大小不變。至此,一個transformer block已經走完了。而此時得到的layer_out將作為下一個block的輸入,這個維度與該模型第一個block的的輸入是一樣的,然后就是這樣num_hidden_layers次循環下去得到最后一個block的輸出結果layer_output,維度依舊為[B * F, hidden_size]。
return的時候通過reshape_from_matrix函數把block的輸出變成維度和input_shape一樣的維度,即一開始詞向量輸入input_tensor的維度([batch_size, seq_length, hidden_size])
Bert_model class
為了方便訓練,模型的整個過程都封裝在Bert_model類中,通過該類的實例可以訪問模型中的結果。詳細的過程見代碼。上述幾個函數梳理之后便沒什么復雜的了,只是把內容整合在一起了。self.all_encoder_layers是經過transformer_model函數返回每個block的結果,self.sequence_output得到最后一個維度的結果,由上面的分析知維度為[Batch_szie, seq_length, hidden_size],這和一開始詞向量的維度是一樣的,只不過這個結果是經過Transformer Encoded提取特征之后的,包含重要的信息,也是Bert想得到的結果:
在這一步之后,該類用成員變量self.pooled_output保存第一個位置再經過一個MLP層的輸出結果。熟悉數據輸入形式的可以知道,這個位置是[CLS],該位置的輸出在Bert預訓練中是用來判斷句子上下文關系的:
這里保存該結果除了可以用于Bert預訓練,還可以微調Bert用于分類任務,詳細可參考:
http://www.lxweimin.com/p/22e462f01d8c
后續
文中可能存在不少筆誤或者理解不正確的表達不清晰地方敬請諒解,非常歡迎能提出來共同學習。