超详细的 Bert 文本分类源码解读 | 附源码
本文詳細的GitHub地址:
https://github.com/sherlcok314159/ML
接上一篇:
你所不知道的 Transformer!
參考論文
https://arxiv.org/abs/1706.03762
https://arxiv.org/abs/1810.04805
在本文中,我將以run_classifier.py以及MRPC數據集為例介紹關于bert以及transformer的源碼,官方代碼基于tensorflow-gpu 1.x,若為tensorflow 2.x版本,會有各種錯誤,建議切換版本至1.14。
當然,注釋好的源代碼在這里:
https://github.com/sherlcok314159/ML/tree/main/nlp/code
章節
Demo傳參
跑不動?
數據篇
數據讀入
數據處理
詞處理
切分
詞向量編碼
TFRecord文件構建
模型構建
詞向量拼接
詞向量編碼
句子類型編碼
位置編碼
多頭注意力
MASK機制
Q,K,V矩陣構建
損失優化
構建模型
其他注意點
Demo傳參
首先大家拿到這個模型,管他什么原理,肯定想跑起來看看結果,至于預訓練模型以及數據集下載。任何時候應該先看官方教程:
https://github.com/google-research/bert
官方代表著權威,更容易實現,如果遇到問題可以去issues和stackoverflow看看,再輔以中文教程,一般上手就不難了,這里就不再贅述了。
先從Flags參數講起,到如何跑通demo。
拿到源碼不要慌張,英文注釋往往起著最關鍵的作用,另外閱讀源碼詳細技巧可以看源碼技巧:
https://github.com/sherlcok314159/ML/blob/main/nlp/source_code.md
"Required Parameters"意思是必要參數,你等會執行時必須向程序里面傳的參數。
export BERT_BASE_DIR=/path/to/bert/uncased_L-12_H-768_A-12 export GLUE_DIR=/path/to/gluepython run_classifier.py \--task_name=MRPC \--do_train=true \--do_eval=true \--data_dir=$GLUE_DIR/MRPC \--vocab_file=$BERT_BASE_DIR/vocab.txt \--bert_config_file=$BERT_BASE_DIR/bert_config.json \--init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \--max_seq_length=128 \--train_batch_size=32 \--learning_rate=2e-5 \--num_train_epochs=3.0 \--output_dir=/tmp/mrpc_output/這是官方給的示例,這個將兩個文件夾加入了系統路徑,本人Ubuntu18.04加了好像也找不到,所以建議將那些文件路徑改為絕對路徑。
task_name --> 這次任務的名稱 do_train --> 是否做fine-tune do_eval --> 是否交叉驗證 do_predict --> 是否做預測 data_dir --> 數據集的位置 vocab_dir --> 詞表的位置(一般bert模型下好就能找到) bert_config --> bert模型參數設置 init_checkpoint --> 預訓練好的模型 max_seq_length --> 一個序列的最大長度 output_dir --> 結果輸出文件(包括日志文件) do_lower_case --> 是否小寫處理(針對英文)其他的字面意思跑不動??
有些時候發現跑demo的時候會出現各種問題,這里簡單匯總一下?
1. No such file or directory! 這個意思是沒找到,你需要確保你上面模型和數據文件的路徑填正確就可解決
2.?Memory Limit
因為bert參數量巨大,模型復雜,如果GPU顯存不夠是帶不動的,就會出現上圖的情形不斷跳出。
解決方法
把batch_size,max_seq_length,num_epochs改小一點
把do_train直接false掉
使用優化bert模型,如Albert,FastTransformer
經過本人實證,把參數適當改小參數,如果還是不行直接不做fine-tune就好,這對迅速跑通demo的人來說最有效。
數據篇
這是很多時候我們自己跑別的任務最為重要的一章,因為很多時候模型并不需要你大改,人家都已經給你訓練好了,你在它的基礎上進行優化就好了。而數據如何讀入以及進行處理,讓模型可以訓練是至關重要的一步。
數據讀入
簡單介紹一下我們的數據,第一列為Quality,意思是前后兩個句子能不能匹配得起來,如果可以即為1,反之為0。第二,三兩列為ID,沒什么意義,最后兩列分別代表兩個句子。
接下來我們看到DataProcessor類,(有些類的作用僅僅是初始化參數,本文不作講解)。這個類是父類(超類),后面不同任務數據處理類都會繼承自它。它里面定義了一個讀取tsv文件的方法。
首先會將每一列的內容讀取到一個列表里面,然后將每一行的內容作為一個小列表作為元素加到大列表里面。
數據處理
因為我們的數據集為MRPC,我們直接跳到MrpcProcessor類就好,它是繼承自DataProcessor。
這里簡要介紹一下os.path.join。
我們不是一共有三個數據集,train,dev以及test嘛,data_dir我們給的是它們的父目錄,我們如何能讀取到它們呢?以train為例,是不是得"path/train.tsv",這個時候,os.path.join就可以把兩者拼接起來。
這個意思是任務的標簽,我們的任務是二分類,自然為0&1。
examples最終是列表,第一個元素為列表,內容圖中已有。
詞處理
讀取數據之后,接下來我們需要對詞進行切分以及簡單的編碼處理
切分
label_list前面對數據進行處理的類里有get_labels參數,返回的是一個列表,如["0","1"]。
想要切分數據,首先得讀取詞表吧,代碼里面一開始創造一個OrderedDict,這個是為什么呢??
在python 3.5的時候,當你想要遍歷鍵值對的時候它是任意返回的,換句話說它并不關心鍵值對的儲存順序,而只是跟蹤鍵和值的關聯程度,會出現無序情況。而OrderedDict可以解決無序情況,它內部維護著一個根據插入順序排序的雙向鏈表,另外,對一個已經存在的鍵的重復復制不會改變鍵的順序。?
需要注意,OrderedDict的大小為一般字典的兩倍,尤其當儲存的東西大了起來的時候,需要慎重權衡。?
但是到了python 3.6,字典已經就變成有序的了,為什么還用OrderedDict,我就有些疑惑了。如果說OrderedDict排序用得到,可是普通dict也能勝任,為什么非要用OrderedDict呢?
在tokenization.py文件中提供了三種切分,分別是BasicTokenizer,WordpieceTokenizer和FullTokenizer,下面具體介紹一下這三者。
在tokenization.py文件中遍布convert_to_unicode,這是用來轉換為unicode編碼,一般來說,輸入輸出不會有變化。
這個方法是用來替換不合法字符以及多余的空格,比如\t,\n會被替換為兩個標準空格。接下來會有一個_tokenize_chinese_chars方法,這個是對中文進行編碼,我們首先要判斷一下是否是中文字符吧,_is_chinese_char方法會進行一個判斷。
如果是中文字符,_tokenize_chinese_chars會將中文字符旁邊都加上空格,圖中我也有引例注釋。
whitespace_tokenize會進行按空格切分。
_run_strip_accents會將變音字符替換掉,如résumé中的é會被替換為e。
接下來進行標點字符切分,前提是判斷是否是標點吧,_is_punctuation履行了這個職責,這里不再多說。
以上便是BasicTokenizer的內容了。
接下來是WordpieceTokenizer了,其實這個詞切分是針對英文單詞的,因為漢字每個字已經是最小的結構,不能進行切分了。而英文還可以進行切分,英文有不同語態,如loved,loves,loving等等,這個時候WordpieceTokenizer就能發揮作用了。
遍歷一個英文單詞里面的小結構,如果發現在詞表里找到,就把這個切掉
對未被切分的部分繼續進行步驟一,直至所有都被切分干凈,注意除了第一個,其他的前面都要加上"##"
下面有個gif可以直觀顯示,來源:
https://alanlee.fun/2019/10/16/bert-tokenizer/
最后是FullTokenizer,這個是兩者的集成版,先進行BasicTokenizer,后進行WordpieceTokenizer。當然了,對于中文,就沒必要跑WordpieceTokenizer。
下面簡單提一下convert_by_vocab,這里是將具體的內容轉換為索引。
以上就是切分了。
詞向量編碼
剛剛對數據進行了切分,接下來我們跳到函數convert_single_example,進一步進行詞向量編碼。
這里是初始化一個例子。input_ids 是等會把一個一個詞轉換為詞表的索引;segment_ids代表是前一句話(0)還是后一句話(1),因為這還未實例化,所以is_real_example為false。
此處tokenizer.tokenize是FullTokenizer的方法。
不同的任務可能含有的句子不一樣,上面代碼的意思就是若b不為空,那么max_length = 總長度 - 3,原因注釋已有;若b為空,則就需要減去2即可。
_truncate_seq_pair進行一個截斷操作,里面用了pop(),這個是列表方法,把列表最后一個取出來,英文注釋也說了為什么沒有按照比例截斷,若一個序列很短,那按比例截斷會流失信息較多,因為比例是長短序列通用的。同時,_truncate_seq_pair還保證了a,b長度一致。若b為空,a則不需要調用這個方法,直接列表方法取就好。
我們不是說需要在開頭添加[CLS],句子分割處和結尾添加[SEP]嘛(本次任務a,b均不為空),剛剛只是進行了一個切分和截斷操作。
tokens是我們用來放序列轉換為編碼的新列表,segment_ids用來區別是第一句還是第二句。這段代碼大意就是在開頭和結尾處加入[CLS],[SEP],因為是a所以都是第一句,segment_ids就都為0,同時[CLS]和[SEP]也都被當做是a的部分,編碼為0。下面關于b的同理。
接下來再把具體內容轉換為索引。
我們一開始的參數不是有max_seq_length嘛,這個代表一整個序列的最大長度(a,b拼接的),但是很多時候我們的總序列長度不會達到最大長度,但是我們又要保證所有輸入序列長度一致,即為最大序列長度。所以我們需要對剩下的部分,即沒有內容的部分進行填充(Padding),但填充的時候有個問題,一般我們都會添0,但做self-attention的時候(如果還不了解自注意力,可以去主頁看看我寫的Transformer的論文解讀),每一個詞要跟句子里面所有的詞做內積,但是0是我們人為填充進去的,它不代表任何意義,然而,做自注意力的時候還是要跟它做內積,是不是不太合理呀?
于是就有了MASK機制,什么意思呢?我們把機器需要看,需要做自注意力的保留,不要看的MASK掉,這樣做自注意力的時候就不會出岔子。
同時,只要沒達到最大長度,就全部補零。
這個的剩余部分tf.logging是日志,不用管,這個convert_single_example最終返回的是feature,feature包含什么已經具體闡述過了。
TFRecord文件構建
因為用TFRecord讀取文件比較方便快捷,需要轉換一下文件格式。
前半部分是examples寫入,examples是來自上圖方法。features是來自上面剛講過的convert_single_example方法。
需要注意的是這份run_classifier.py人家谷歌是用TPU跑的,所以會有TPU部分代碼,一般我們只用GPU,所以TPU部分不需要關注,一般TPU都會出現TPUEstimator。
模型構建
接下來,是構建模型篇,是整個代碼中最重要的一部分。接下來我將用代碼介紹一下transformer模型的架構。
找到modeling.py文件,這是模型文件。
首先是BertConfig的類,這里自定義了一些參數及數值。
vocab_size --> 詞表的大小,用別人的詞表,這個參數已經固定 hidden_size --> 隱層神經元個數 num_hidden_layers --> encoder的層數 num_attention_heads -->注意力頭的個數 intermediate_size --> 中間層神經元個數 hidden_act --> 隱層激活函數 hidden_dropout_prob --> 在全連接層中實施Dropout,被去掉的概率 attention_probs_dropout_prob --> 注意力層dropout比例 max_position_embeddings --> 最大位置數目 initializer_range --> truncated_normal_initializer的stdev,用來初始化權重參數,從普通正態分布中標準差為0.02的分布中取樣出一部分參數,作為初始化權重后面 batch_size x seq_length 會經常出現,這里是原始定義 這里還有個初始化,如果MASK和token_type_ids我們前面沒有,這里就默認全為1和0。這是為了后面詞嵌入(embedding)做準備。
詞向量拼接?
接下來正式進入Embedding層的操作,最終傳到注意力層的其實是原始token_ids,token_type_ids以及positional embedding拼接起來的。?
token_ids編碼?
首先是token_ids的操作,先來看一下embedding_lookup方法。
這是它的參數,大部分英文注釋已有,需要注意的一點是input_ids的shape必須為[batch_size,max_seq_length]。
接下來進行擴維。
等會我們需要在embedding_table里面查找,這里先構建一個[vocab_size,embedding_size]的table。需要注意的是vocab_size 和 embedding_size 都是固定好的,訓練的時候不能亂改。
之后我們對input_ids進行降維,貌似這樣可以加速。one_hot_embedding一般為false,這是對TPU加速用的。接下來在embedding_table里面進行查找。
然后我們把output reshape一下。
這就是token的編碼了。
句子類型編碼
進行位置編碼之前,我們首先進行對token_type_ids的編碼(判斷是哪一句)。
首先創建token_type_table。
然后進行一個token_type_embedding,matul是矩陣相乘
做好相乘之后,我們需要把token_type_embedding的shape還原,因為等會要將token_type_ids與詞編碼相加。
位置編碼
首先我們先創造大量的位置,max_position_embeddings是官方給定的參數,不能修改。
我們創造了這么多的位置,最終不一定用的完,為了更快速的訓練,我們一般做切片處理,只要到我的max_seq_length還有位置就好,后面都可以不要。
前面要把token_type_embeddings加到input_ids的編碼中,進行了同維度處理,這里對于位置編碼也一樣,不然最后相加不了。
至此,Embedding層就結束了。Transformer論文不是說了嘛,在加入位置編碼之前會進行一個Dropout操作
多頭機制
接下來來到整個transformer模型的精華部分,即為多頭注意力機制。
MASK機制
首先來到create_attention_mask_from_input_mask方法,from_seq_length和to_seq_length分別指的是a和b,前面講關于切分的時候已經說了,切分處理會讓a,b長度一致為max_seq_length。所以這里兩者長度相等。最后創建了一個shape為(batch_size,from_seq_length,to_seq_length)的MASK。又擴充了一個維度,那這個維度用來干什么呢?我們一開始不是說了嗎?自注意的時候需要將填充的部分遮掉,那么多余的維度干的就是這個事。比如我們設置最大長度為8,句子長度為6,那么有一個維度是[1,1,1,1,1,1,0,0]。
Q,K,V矩陣
構建首先來到attention_layer方法,q,k,v矩陣的激活函數均為None。
在進入構建之前,最好先熟悉這5個字母的含義。
開始構建q矩陣,注意q是由from_tensor,即第一個句子構建的。
接著構建k和v矩陣,都是從to_tensor構建的。
接下來會對q,k矩陣進行加速內積處理,不做深入探討。
記得我們在transformer里面需要除以d的維度開根號。
attention_mask即為上節我們說的MASK,這里進行拓展一個維度。
這里再簡要介紹一下adder。tf.cast方法只是轉換數據類型,這里用x代表attention_mask,(1-x)* (-1000)的目的是當attention為1時,即要關注這個,那么(1-x)就越趨近于0,那么做softmax,值就越接近于0,類似地,如果attention為0,那么進過softmax后的值就更接近-1。最后把這個adder加到剛剛我們得到注意力的值,估計這里會有人搞不懂為什么怎么做。?
果關聯度很高,那么attention_scores就越接近1,越低,越接近0,但是,很可能是我們補零的部分,所以我們需要對這個進行處理,這里有兩種思路,既然是補零的,我們直接去掉就好;或者這里谷歌的做法是如果不需要,直接-1,是不是注意力值就趨近于0了,如果需要,加了0本身值不會發生變化。經過谷歌驗證,后者效率更高。
接下來進行transformer模型構建,不難發現這里from_tensor和to_tensor一致,所以是做自注意力。
損失優化
在bert里面說過,最后拿出開頭的[CLS]就可以了。這既是get_pooled_output方法的作用。
最后再連接一個全連接層,最后就是二分類的任務w * x + b
模型構建
model_fn方法是構建的函數之一,一定一定要小心,雖然上面寫著返回給TPUEstimator,可如果你運行過demo的話,輸出的很多東西都來源于這個方法。
進入main(_)主方法,需要注意的是,以后我們需要fine-tune,需要把我們自己定義的processor添加進processors。
確認要訓練之后,會計算需要一共多少步完成,這里還有個warm-up,意思是一開始呢讓learning rate低一下,等到了warm-up proportion之后再還原。
終于我們開始構建模型了
最終我們構建了estimator用于后期訓練,評估和預測
其他注意點
這是殘差相連的部分
還有一點就是記得在transformer中講過我們會連兩層全連接層,一層升維,另一層降維。
接下來進行降維
覺得寫的好,不妨去github上給我star,里面有很多比這還要棒的解析:
https://github.com/sherlcok314159/ML
推薦閱讀
(點擊標題可跳轉閱讀)
干貨 | 公眾號歷史文章精選
我的深度學習入門路線
我的機器學習入門路線圖
重磅!
AI有道年度技術文章電子版PDF來啦!
掃描下方二維碼,添加?AI有道小助手微信,可申請入群,并獲得2020完整技術文章合集PDF(一定要備注:入群?+ 地點 + 學校/公司。例如:入群+上海+復旦。?
長按掃碼,申請入群
(添加人數較多,請耐心等待)
感謝你的分享,點贊,在看三連??
總結
以上是生活随笔為你收集整理的超详细的 Bert 文本分类源码解读 | 附源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你所不知道的 Transformer!
- 下一篇: 随便写写有关微软2004软件架构师峰会的