本文約4400字,建議閱讀10 分鐘
本文對BERT模型預訓練任務的源代碼進行了詳細解讀,在Eclipse開發環境裡,對BERT 源代碼的各實現步驟分步解析。
BERT模型架構是一種基于多層雙向變換器(Transformers)的編碼器架構,在tensor2tensor庫框架下發布。由于在實現過程當中采用了Transformers,BERT模型的實現幾乎與Transformers一樣。
BERT預訓練模型沒有采用傳統的從左到右或從右到左的單向語言模型進行預訓練,而是采用從左到右和從右到左的雙向語言模型進行預訓練,本文對BERT模型預訓練任務的源代碼進行了詳細解讀,在Eclipse開發環境裡,對BERT 源代碼的各實現步驟分步解析。
BERT 模型的代碼量比較大,由于篇幅限制,不可能對每一行代碼展開解釋,在這裡,解釋一下其中每一個核心模塊的功能。
1) 數據讀取模塊
圖 1
模型訓練的第一步,是讀取數據,将數據從數據集中讀取進來,然後按照BERT 模型要求的數據格式,對數據進行處理,寫出具體數據處理的類以及實際要用到的數據集中數據處理的方法,如果任務中用到的數據集不是MRPC ,這部分的代碼需要依據特定的任務重新寫一下如何操作數據集的代碼,對于不同的任務,需要構造一個新的讀取數據的類,把數據一行一行地讀進來。
2) 數據預處理模塊
圖 2
利用tensorflow 對數據進行預處理,由于用TF-Record 讀數據的速度比較快,使用起來比較方便,在數據讀取層面,需要将數據轉換成TF-Record格式。首先,定義一個writer,利用writer函數将數據樣本寫入到TF-Record當中,這樣一來,在實際訓練過程中,不用每次都到原始數據中去讀取數據,直接到TF-Record當中讀取處理好的數據。
把每一個數據樣本都轉化成一個TF-Record格式的具體做法如下:首先,構建一個标簽,接下來對數據做一個判斷,判斷數據中由幾句話組成,拿到當前第一句話後,做一個分詞操作。分詞方法為wordpiece 方法。在英文文本中,由字母組成單詞,詞與詞之間利用空格來切分單詞,利用空格切分單詞往往還不充分,需要對單詞做進一步切分轉換,在BERT 模型中,通過調用wordpiece 方法将輸入的單詞再進一步切分,利用wordpiece的貪心匹配方法,将輸入單詞進一步切分成詞片,從而使得單詞表達的含義更加豐富。在這裡,利用wordpiece 方法将讀入的單詞進行再次切分,把輸入的單詞序列切分成更為基本的單元,從而更加便于模型學習。
在中文系統中,通常把句子切分成單個的字,切分完成之後,把輸入用wordpiece轉化成wordpiece結構之後,再做一個判斷,看是否有第二句話輸入,如果有第二句話輸入,則用wordpiece對第二句話做相同的處理。做完wordpiece轉換之後,再做一個判斷,判斷實際句子的長度是否超過max_seq_length 的值,如果輸入句子的長度超過max_seq_length規定的數值,則需要進行截斷操作。
3) tf-record 制作
對輸入句對進行編碼,遍曆wordpiece結構的每一個單詞以及每一個單詞的type_id ,加入句子分隔符【CLS】、【SEP】,為所有結果添加編碼信息;添加type_id,把所有單詞映射成索引功,對輸入詞的ID (标識符)進行編碼,以方便後續做詞嵌入時候進行查找;
Mask編碼:對于句子長度小于max_seq_length 的句子做一個補齊操作。在self_attention 的計算中,隻考慮句子中實際有的單詞,對輸入序列做input_mask 操作,對于不足128個單詞的位置加入額外的mask,目的是讓self_attention知道,隻對所有實際的單詞做計算,在後續self_attention計算中,忽略input_mask=0 的單詞,隻有input_mask=1 的單詞會實際參與到self_attention計算中。Mask編碼為後續的微調操作做了初始化,實現了任務數據的預處理。
圖 3
對input_Feature做初始化:構建 input_Feature并把結果返回給BERT。通過一個for 循環,遍曆每一個樣本,再對構造出來一些處理,把input_id、input_mask和segment_id均轉換成為int類型,方便後續tf-record的制作。之所以要做數據類型的轉換,是因為tensorflow 官方API要求這麼做,tensorflow對tf-record的格式做了硬性的規定,用戶無法自行對其修改。在後續具體項目任務中,在做tf-record時,隻要把原始代碼全部複制過去,按照原有的格式修改即可。構造好input_Feature之後,把它傳遞給tf_example,轉換成tf_train_features ,之後,直接寫入構建好的數據即可。
4) Embeding層的作用
在BERT 模型中有一個creat_model 函數,在creat_model 函數中一步一步把模型構建出來。首先,創建一個BERT 模型,該模型中包含了transformer的所有結構,具體操作過程如下:
圖 4
讀入配置文件,判斷是否需要進行訓練,讀入input_id、input_mask和segment_id等變量, one_hot_embedding變量 在利用TPU 訓練時才使用,在用CPU 訓練時不用考慮,默認值設為Faulse。
構建embedding層,即詞嵌入,詞嵌入操作将當前序列轉化為向量。BERT 的embedding層不光要考慮輸入的單詞序列,還需要考慮其它的額外信息和位置信息。BERT 構建出來的詞嵌入向量中包含以下三種信息:即輸入單詞序列信息、其它的額外信息和位置信息。為了實現向量間的計算,必須保持包含這三種信息的詞向量的維數一緻。
5) 加入額外編碼特征
接下來,進入到embedding_lookup 層,這個層的輸入是:input_id(輸入标識符)、vocab_size(詞彙表大小)、embedding_size(詞嵌入的維度)、initializer_range(初始化的取值範圍)。embedding_lookup的輸出是一個實際的向量編碼。
圖 5
首先,獲取embedding_table,然後到embedding_table裡查找每個單詞對應的詞向量,并将最終結果返回給output,這樣一來,輸入的單詞便成了詞向量。但這個操作隻是詞嵌入的一部分,完整的詞嵌入還應在詞嵌入中添加其它額外的信息,即:embedding_post_processor。
embedding_post_processor是詞嵌入操作必須添加進去的第二部分信息,embedding_post_processor的輸入有:input_tensor、use_token_type、token_type_id、token_type_vocab_size,返回的特征向量将包含這些額外的信息,其維度和輸入單詞的詞向量一緻。
6) 加入位置編碼特征
利用use_position_embedding 添加位置編碼信息。BERT 的Self_attention 中需要加入位置編碼信息,首先,利用full_position_embedding 初始化位置編碼,把每個單詞的位置編碼向量與詞嵌入向量相加,接着,根據當前的序列長度做一個計算,如果序列長度為128,則對這128個位置進行編碼。由于位置編碼僅包含位置信息,和句子的上下文語義無關,對于不同的輸入序列來說,雖然輸入序列的内容各不相同,但是它們的位置編碼卻是相同的,所以位置編碼的結果向量和實際句子中傳的什麼詞無關,無論傳的數據内容是什麼,它們的位置編碼均是一樣的。獲得位置編碼的輸出結果之後,在原詞嵌入輸出向量的基礎上,加上額外編碼獲得的特征向量和位置編碼向量,将三個向量求和,返回求和結果,到此為止,完成了BERT模型的輸入詞嵌入,得到了一個包含位置信息的詞向量,接下來,對這個向量進行深入的操作。
圖 6
7) mask機制
在完成詞嵌入之後,接下來便是Transformer結構了,在Transformer之前,先要對詞向量做一些轉換,即attention_mask ,創建一個mask矩陣:create_attention_mask_from_input_mask 。在前文提到的input_mask 中,隻有mask=1 的詞參與到attention的計算當中,現在需要把這個二維的mask轉換成為一個三維的mask,表示詞向量進入attention的時候,哪幾個向量會參與到實際計算過程當中。即在計算attention時,對輸入序列中128個單詞的哪些個單詞做attention計算,在這裡,又額外地加入了一個mask處理操作。
圖 7
完成mask處理之後,接下來是構建Transformer的Encode端,首先給Transformer傳入一些參數,如:input_tensor、attention_mask、hiden_size、head_num等等。這些參數在預訓練過程中已經設置好了,在進行微調操作時,均不得對這些參數随意更改。
在多頭attention機制中,每個頭生成一個特征向量,最終把各個頭生成的向量拼接在一起得到輸出的特征向量。
8) 構建QKV 矩陣
接下來,是attention機制的實現,BERT 的attention機制是一個多層的架構,在程序具體實現中,采用的是遍曆的操作,通過遍曆每一層,實現多層的堆疊。總共需要遍曆12層,當前層的輸入是前一層的輸出。attention機制中,有輸入兩個向量:from-tensor和to_tensor,而BERT 的attention機制采用的是self_attention,此時:
from-tensor=to_tensor=layer_input;
圖 8
在構建attention_layer過程中,需要構建K、Q、V 三個矩陣,K、Q、V矩陣是transformer中最為核心的部分。在構建K、Q、V矩陣時,會用到以下幾個縮略字符:
構建Query 矩陣:構建query_layer查詢矩陣,查詢矩陣由from-tensor構建而來,在多頭attention機制中,有多少個attention頭,便生成多少個Query 矩陣,每個頭生成的Query 矩陣輸出對應向量:
query_layer=【 B*F,N*H】, 即1024*768;
圖 9
構建Key 矩陣: Key 矩陣由to-tensor構建而來, 在多頭attention機制中,有多少個attention頭,便生成多少個Key 矩陣,每個頭生成的Key 矩陣輸出對應向量:
key_layer=【 B*T,N*H】, 即1024*768;
圖 10
構建Value矩陣: Value矩陣的構建和Key 矩陣的構建基本一樣,隻不過描述的層面不同而已:
value_layer=【 B*T,N*H】, 即1024*768;
構建QKV 矩陣完成之後,計算K矩陣和Q 矩陣的内積,之後進行一個Softmax操作。通過Value矩陣,幫助我們了解實際得到的特征是什麼,Value矩陣和Key矩陣完全對應,維數一模一樣。
圖 11
9) 完成Transformer 模塊構建
構建QKV 矩陣完成之後,接下來,需要計算K矩陣和Q 矩陣的内積,為了加速内積的計算,在這裡做了一個transpose轉換,目的是為了加速内積的計算,并不影響後續的操作。計算好K矩陣和Q 矩陣的内積之後,獲得了attention的分值:attention_score,最後需要利用Softmax操作将得到的attention的分值轉換成為一個概率:attention_prob。
在做Softmax操作之前,為了減少計算量,還需要加入attention_mask,将長度為128 的序列中不是實際有的單詞屏蔽掉,不讓它們參與到計算中來。在tensorflow中直接有現成的Softmax函數可以調用,把當前所有的attention分值往Softmax裡一傳,得到的結果便是一個概率值,這個概率值作為權重值,和Value矩陣結合在一起使用,即将attention_prob和Value矩陣進行乘法運算,便得到了上下文語義矩陣,即:
Context_layer=tf.matmul(attention_prob, value_layer);
圖 12
得到當前層上下文語義矩陣輸出之後,這個輸出作為下一層的輸入,參與到下一層attention的計算中去,多層attention通過一個for循環的多次叠代來實現,有多少層attention(在這裡是12層)就進行多少層叠代計算。
10) 訓練BERT 模型
做完self_attention之後,接下來是一個全連接層,在這裡,需要把全連接層考慮進來,利用tf.layer.dese 實現一個全連接層,最後要做一個殘差連接,注意:在全連接層的實現過程中,需要返回最終的結果,即将最後一層attention的輸出結果返回給BERT ,這便是整個Transformer 的結構。
圖 13
總結一下上述整個過程,即Transformer 的實現主要分為兩大部分:第一部分是embedding 層,embedding 層将wordpiece詞嵌入加上額外特定信息和位置編碼信息,三者之和構成embedding 層的輸出向量;第二部分是将embedding 層的輸出向量送入transformer結構,通過構建K、Q、V三種矩陣過,利用Softmax函數,得到上下文語義矩陣C , 上下文語義矩陣C不僅包含了輸入序列中各單詞的編碼特征,還包括了各單詞的位置編碼信息。
這就是BERT 模型的實現方式,理解了上述兩大部分的詳細過程,對BERT模型的理解便沒有什麼太大問題了。以上十大步驟基本涵蓋了BERT 模型中的重要操作。
經過BERT 模型之後,最終獲得的是一個特征向量,這個特征向量代表了最終結果。以上便是谷歌官方公布的開源MRPC 項目的全部過程。讀者在構建自己特定任務的項目時候,需要修改的是如何将數據讀入BERT 模型的部分代碼,實現數據預處理。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!