接上一篇: 參考論文 https:///abs/1706.03762 https:///abs/1810.04805 在本文中,我將以run_classifier.py以及MRPC數(shù)據(jù)集為例介紹關(guān)于bert以及transformer的源碼,官方代碼基于tensorflow-gpu 1.x,若為tensorflow 2.x版本,會(huì)有各種錯(cuò)誤,建議切換版本至1.14。 當(dāng)然,注釋好的源代碼在這里: https://github.com/sherlcok314159/ML/tree/main/nlp/code 章節(jié)
Demo傳參首先大家拿到這個(gè)模型,管他什么原理,肯定想跑起來看看結(jié)果,至于預(yù)訓(xùn)練模型以及數(shù)據(jù)集下載。任何時(shí)候應(yīng)該先看官方教程: https://github.com/google-research/bert 官方代表著權(quán)威,更容易實(shí)現(xiàn),如果遇到問題可以去issues和stackoverflow看看,再輔以中文教程,一般上手就不難了,這里就不再贅述了。 先從Flags參數(shù)講起,到如何跑通demo。 拿到源碼不要慌張,英文注釋往往起著最關(guān)鍵的作用,另外閱讀源碼詳細(xì)技巧可以看源碼技巧: https://github.com/sherlcok314159/ML/blob/main/nlp/source_code.md 'Required Parameters'意思是必要參數(shù),你等會(huì)執(zhí)行時(shí)必須向程序里面?zhèn)鞯膮?shù)。 export BERT_BASE_DIR=/path/to/bert/uncased_L-12_H-768_A-12 export GLUE_DIR=/path/to/glue
python 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/ 這是官方給的示例,這個(gè)將兩個(gè)文件夾加入了系統(tǒng)路徑,本人Ubuntu18.04加了好像也找不到,所以建議將那些文件路徑改為絕對(duì)路徑。
跑不動(dòng)? 有些時(shí)候發(fā)現(xiàn)跑demo的時(shí)候會(huì)出現(xiàn)各種問題,這里簡(jiǎn)單匯總一下 1. No such file or directory! 這個(gè)意思是沒找到,你需要確保你上面模型和數(shù)據(jù)文件的路徑填正確就可解決 2. Memory Limit 因?yàn)閎ert參數(shù)量巨大,模型復(fù)雜,如果GPU顯存不夠是帶不動(dòng)的,就會(huì)出現(xiàn)上圖的情形不斷跳出。 解決方法
經(jīng)過本人實(shí)證,把參數(shù)適當(dāng)改小參數(shù),如果還是不行直接不做fine-tune就好,這對(duì)迅速跑通demo的人來說最有效。 數(shù)據(jù)篇這是很多時(shí)候我們自己跑別的任務(wù)最為重要的一章,因?yàn)楹芏鄷r(shí)候模型并不需要你大改,人家都已經(jīng)給你訓(xùn)練好了,你在它的基礎(chǔ)上進(jìn)行優(yōu)化就好了。而數(shù)據(jù)如何讀入以及進(jìn)行處理,讓模型可以訓(xùn)練是至關(guān)重要的一步。 數(shù)據(jù)讀入 簡(jiǎn)單介紹一下我們的數(shù)據(jù),第一列為Quality,意思是前后兩個(gè)句子能不能匹配得起來,如果可以即為1,反之為0。第二,三兩列為ID,沒什么意義,最后兩列分別代表兩個(gè)句子。 接下來我們看到DataProcessor類,(有些類的作用僅僅是初始化參數(shù),本文不作講解)。這個(gè)類是父類(超類),后面不同任務(wù)數(shù)據(jù)處理類都會(huì)繼承自它。它里面定義了一個(gè)讀取tsv文件的方法。 首先會(huì)將每一列的內(nèi)容讀取到一個(gè)列表里面,然后將每一行的內(nèi)容作為一個(gè)小列表作為元素加到大列表里面。 數(shù)據(jù)處理 因?yàn)槲覀兊臄?shù)據(jù)集為MRPC,我們直接跳到MrpcProcessor類就好,它是繼承自DataProcessor。 這里簡(jiǎn)要介紹一下os.path.join。 我們不是一共有三個(gè)數(shù)據(jù)集,train,dev以及test嘛,data_dir我們給的是它們的父目錄,我們?nèi)绾文茏x取到它們呢?以train為例,是不是得'path/train.tsv',這個(gè)時(shí)候,os.path.join就可以把兩者拼接起來。 這個(gè)意思是任務(wù)的標(biāo)簽,我們的任務(wù)是二分類,自然為0&1。 examples最終是列表,第一個(gè)元素為列表,內(nèi)容圖中已有。 詞處理讀取數(shù)據(jù)之后,接下來我們需要對(duì)詞進(jìn)行切分以及簡(jiǎn)單的編碼處理 切分 label_list前面對(duì)數(shù)據(jù)進(jìn)行處理的類里有g(shù)et_labels參數(shù),返回的是一個(gè)列表,如['0','1']。 想要切分?jǐn)?shù)據(jù),首先得讀取詞表吧,代碼里面一開始創(chuàng)造一個(gè)OrderedDict,這個(gè)是為什么呢? 在python 3.5的時(shí)候,當(dāng)你想要遍歷鍵值對(duì)的時(shí)候它是任意返回的,換句話說它并不關(guān)心鍵值對(duì)的儲(chǔ)存順序,而只是跟蹤鍵和值的關(guān)聯(lián)程度,會(huì)出現(xiàn)無序情況。而OrderedDict可以解決無序情況,它內(nèi)部維護(hù)著一個(gè)根據(jù)插入順序排序的雙向鏈表,另外,對(duì)一個(gè)已經(jīng)存在的鍵的重復(fù)復(fù)制不會(huì)改變鍵的順序。 需要注意,OrderedDict的大小為一般字典的兩倍,尤其當(dāng)儲(chǔ)存的東西大了起來的時(shí)候,需要慎重權(quán)衡。 但是到了python 3.6,字典已經(jīng)就變成有序的了,為什么還用OrderedDict,我就有些疑惑了。如果說OrderedDict排序用得到,可是普通dict也能勝任,為什么非要用OrderedDict呢? 在tokenization.py文件中提供了三種切分,分別是BasicTokenizer,WordpieceTokenizer和FullTokenizer,下面具體介紹一下這三者。 在tokenization.py文件中遍布convert_to_unicode,這是用來轉(zhuǎn)換為unicode編碼,一般來說,輸入輸出不會(huì)有變化。 這個(gè)方法是用來替換不合法字符以及多余的空格,比如\t,\n會(huì)被替換為兩個(gè)標(biāo)準(zhǔn)空格。接下來會(huì)有一個(gè)_tokenize_chinese_chars方法,這個(gè)是對(duì)中文進(jìn)行編碼,我們首先要判斷一下是否是中文字符吧,_is_chinese_char方法會(huì)進(jìn)行一個(gè)判斷。 如果是中文字符,_tokenize_chinese_chars會(huì)將中文字符旁邊都加上空格,圖中我也有引例注釋。 whitespace_tokenize會(huì)進(jìn)行按空格切分。 _run_strip_accents會(huì)將變音字符替換掉,如résumé中的é會(huì)被替換為e。 接下來進(jìn)行標(biāo)點(diǎn)字符切分,前提是判斷是否是標(biāo)點(diǎn)吧,_is_punctuation履行了這個(gè)職責(zé),這里不再多說。 以上便是BasicTokenizer的內(nèi)容了。 接下來是WordpieceTokenizer了,其實(shí)這個(gè)詞切分是針對(duì)英文單詞的,因?yàn)闈h字每個(gè)字已經(jīng)是最小的結(jié)構(gòu),不能進(jìn)行切分了。而英文還可以進(jìn)行切分,英文有不同語態(tài),如loved,loves,loving等等,這個(gè)時(shí)候WordpieceTokenizer就能發(fā)揮作用了。
下面有個(gè)gif可以直觀顯示,來源: https:///2019/10/16/bert-tokenizer/ 最后是FullTokenizer,這個(gè)是兩者的集成版,先進(jìn)行BasicTokenizer,后進(jìn)行WordpieceTokenizer。當(dāng)然了,對(duì)于中文,就沒必要跑WordpieceTokenizer。 下面簡(jiǎn)單提一下convert_by_vocab,這里是將具體的內(nèi)容轉(zhuǎn)換為索引。 以上就是切分了。 詞向量編碼 剛剛對(duì)數(shù)據(jù)進(jìn)行了切分,接下來我們跳到函數(shù)convert_single_example,進(jìn)一步進(jìn)行詞向量編碼。 這里是初始化一個(gè)例子。input_ids 是等會(huì)把一個(gè)一個(gè)詞轉(zhuǎn)換為詞表的索引;segment_ids代表是前一句話(0)還是后一句話(1),因?yàn)檫@還未實(shí)例化,所以is_real_example為false。 此處tokenizer.tokenize是FullTokenizer的方法。 不同的任務(wù)可能含有的句子不一樣,上面代碼的意思就是若b不為空,那么max_length = 總長(zhǎng)度 - 3,原因注釋已有;若b為空,則就需要減去2即可。 _truncate_seq_pair進(jìn)行一個(gè)截?cái)嗖僮?,里面用了pop(),這個(gè)是列表方法,把列表最后一個(gè)取出來,英文注釋也說了為什么沒有按照比例截?cái)?,若一個(gè)序列很短,那按比例截?cái)鄷?huì)流失信息較多,因?yàn)楸壤情L(zhǎng)短序列通用的。同時(shí),_truncate_seq_pair還保證了a,b長(zhǎng)度一致。若b為空,a則不需要調(diào)用這個(gè)方法,直接列表方法取就好。 我們不是說需要在開頭添加[CLS],句子分割處和結(jié)尾添加[SEP]嘛(本次任務(wù)a,b均不為空),剛剛只是進(jìn)行了一個(gè)切分和截?cái)嗖僮鳌?/span> tokens是我們用來放序列轉(zhuǎn)換為編碼的新列表,segment_ids用來區(qū)別是第一句還是第二句。這段代碼大意就是在開頭和結(jié)尾處加入[CLS],[SEP],因?yàn)槭莂所以都是第一句,segment_ids就都為0,同時(shí)[CLS]和[SEP]也都被當(dāng)做是a的部分,編碼為0。下面關(guān)于b的同理。 接下來再把具體內(nèi)容轉(zhuǎn)換為索引。 我們一開始的參數(shù)不是有max_seq_length嘛,這個(gè)代表一整個(gè)序列的最大長(zhǎng)度(a,b拼接的),但是很多時(shí)候我們的總序列長(zhǎng)度不會(huì)達(dá)到最大長(zhǎng)度,但是我們又要保證所有輸入序列長(zhǎng)度一致,即為最大序列長(zhǎng)度。所以我們需要對(duì)剩下的部分,即沒有內(nèi)容的部分進(jìn)行填充(Padding),但填充的時(shí)候有個(gè)問題,一般我們都會(huì)添0,但做self-attention的時(shí)候(如果還不了解自注意力,可以去主頁看看我寫的Transformer的論文解讀),每一個(gè)詞要跟句子里面所有的詞做內(nèi)積,但是0是我們?nèi)藶樘畛溥M(jìn)去的,它不代表任何意義,然而,做自注意力的時(shí)候還是要跟它做內(nèi)積,是不是不太合理呀? 于是就有了MASK機(jī)制,什么意思呢?我們把機(jī)器需要看,需要做自注意力的保留,不要看的MASK掉,這樣做自注意力的時(shí)候就不會(huì)出岔子。 同時(shí),只要沒達(dá)到最大長(zhǎng)度,就全部補(bǔ)零。 這個(gè)的剩余部分tf.logging是日志,不用管,這個(gè)convert_single_example最終返回的是feature,feature包含什么已經(jīng)具體闡述過了。 TFRecord文件構(gòu)建因?yàn)橛肨FRecord讀取文件比較方便快捷,需要轉(zhuǎn)換一下文件格式。 前半部分是examples寫入,examples是來自上圖方法。features是來自上面剛講過的convert_single_example方法。 需要注意的是這份run_classifier.py人家谷歌是用TPU跑的,所以會(huì)有TPU部分代碼,一般我們只用GPU,所以TPU部分不需要關(guān)注,一般TPU都會(huì)出現(xiàn)TPUEstimator。 模型構(gòu)建接下來,是構(gòu)建模型篇,是整個(gè)代碼中最重要的一部分。接下來我將用代碼介紹一下transformer模型的架構(gòu)。 找到modeling.py文件,這是模型文件。 首先是BertConfig的類,這里自定義了一些參數(shù)及數(shù)值。 vocab_size --> 詞表的大小,用別人的詞表,這個(gè)參數(shù)已經(jīng)固定 hidden_size --> 隱層神經(jīng)元個(gè)數(shù) num_hidden_layers --> encoder的層數(shù) num_attention_heads -->注意力頭的個(gè)數(shù) intermediate_size --> 中間層神經(jīng)元個(gè)數(shù) hidden_act --> 隱層激活函數(shù) hidden_dropout_prob --> 在全連接層中實(shí)施Dropout,被去掉的概率 attention_probs_dropout_prob --> 注意力層dropout比例 max_position_embeddings --> 最大位置數(shù)目 initializer_range --> truncated_normal_initializer的stdev,用來初始化權(quán)重參數(shù),從普通正態(tài)分布中標(biāo)準(zhǔn)差為0.02的分布中取樣出一部分參數(shù),作為初始化權(quán)重 后面 batch_size x seq_length 會(huì)經(jīng)常出現(xiàn),這里是原始定義 這里還有個(gè)初始化,如果MASK和token_type_ids我們前面沒有,這里就默認(rèn)全為1和0。這是為了后面詞嵌入(embedding)做準(zhǔn)備。 詞向量拼接 接下來正式進(jìn)入Embedding層的操作,最終傳到注意力層的其實(shí)是原始token_ids,token_type_ids以及positional embedding拼接起來的。 token_ids編碼 首先是token_ids的操作,先來看一下embedding_lookup方法。 這是它的參數(shù),大部分英文注釋已有,需要注意的一點(diǎn)是input_ids的shape必須為[batch_size,max_seq_length]。 接下來進(jìn)行擴(kuò)維。 等會(huì)我們需要在embedding_table里面查找,這里先構(gòu)建一個(gè)[vocab_size,embedding_size]的table。需要注意的是vocab_size 和 embedding_size 都是固定好的,訓(xùn)練的時(shí)候不能亂改。 之后我們對(duì)input_ids進(jìn)行降維,貌似這樣可以加速。one_hot_embedding一般為false,這是對(duì)TPU加速用的。接下來在embedding_table里面進(jìn)行查找。 然后我們把output reshape一下。 這就是token的編碼了。 句子類型編碼 進(jìn)行位置編碼之前,我們首先進(jìn)行對(duì)token_type_ids的編碼(判斷是哪一句)。 首先創(chuàng)建token_type_table。 然后進(jìn)行一個(gè)token_type_embedding,matul是矩陣相乘 做好相乘之后,我們需要把token_type_embedding的shape還原,因?yàn)榈葧?huì)要將token_type_ids與詞編碼相加。 位置編碼 首先我們先創(chuàng)造大量的位置,max_position_embeddings是官方給定的參數(shù),不能修改。 我們創(chuàng)造了這么多的位置,最終不一定用的完,為了更快速的訓(xùn)練,我們一般做切片處理,只要到我的max_seq_length還有位置就好,后面都可以不要。 前面要把token_type_embeddings加到input_ids的編碼中,進(jìn)行了同維度處理,這里對(duì)于位置編碼也一樣,不然最后相加不了。 至此,Embedding層就結(jié)束了。Transformer論文不是說了嘛,在加入位置編碼之前會(huì)進(jìn)行一個(gè)Dropout操作 多頭機(jī)制 接下來來到整個(gè)transformer模型的精華部分,即為多頭注意力機(jī)制。 MASK機(jī)制 首先來到create_attention_mask_from_input_mask方法,from_seq_length和to_seq_length分別指的是a和b,前面講關(guān)于切分的時(shí)候已經(jīng)說了,切分處理會(huì)讓a,b長(zhǎng)度一致為max_seq_length。所以這里兩者長(zhǎng)度相等。最后創(chuàng)建了一個(gè)shape為(batch_size,from_seq_length,to_seq_length)的MASK。又?jǐn)U充了一個(gè)維度,那這個(gè)維度用來干什么呢?我們一開始不是說了嗎?自注意的時(shí)候需要將填充的部分遮掉,那么多余的維度干的就是這個(gè)事。比如我們?cè)O(shè)置最大長(zhǎng)度為8,句子長(zhǎng)度為6,那么有一個(gè)維度是[1,1,1,1,1,1,0,0]。 Q,K,V矩陣 構(gòu)建首先來到attention_layer方法,q,k,v矩陣的激活函數(shù)均為None。 在進(jìn)入構(gòu)建之前,最好先熟悉這5個(gè)字母的含義。 開始構(gòu)建q矩陣,注意q是由from_tensor,即第一個(gè)句子構(gòu)建的。 接著構(gòu)建k和v矩陣,都是從to_tensor構(gòu)建的。 接下來會(huì)對(duì)q,k矩陣進(jìn)行加速內(nèi)積處理,不做深入探討。 記得我們?cè)趖ransformer里面需要除以d的維度開根號(hào)。 attention_mask即為上節(jié)我們說的MASK,這里進(jìn)行拓展一個(gè)維度。 這里再簡(jiǎn)要介紹一下adder。tf.cast方法只是轉(zhuǎn)換數(shù)據(jù)類型,這里用x代表attention_mask,(1-x)* (-1000)的目的是當(dāng)attention為1時(shí),即要關(guān)注這個(gè),那么(1-x)就越趨近于0,那么做softmax,值就越接近于0,類似地,如果attention為0,那么進(jìn)過softmax后的值就更接近-1。最后把這個(gè)adder加到剛剛我們得到注意力的值,估計(jì)這里會(huì)有人搞不懂為什么怎么做。 果關(guān)聯(lián)度很高,那么attention_scores就越接近1,越低,越接近0,但是,很可能是我們補(bǔ)零的部分,所以我們需要對(duì)這個(gè)進(jìn)行處理,這里有兩種思路,既然是補(bǔ)零的,我們直接去掉就好;或者這里谷歌的做法是如果不需要,直接-1,是不是注意力值就趨近于0了,如果需要,加了0本身值不會(huì)發(fā)生變化。經(jīng)過谷歌驗(yàn)證,后者效率更高。 接下來進(jìn)行transformer模型構(gòu)建,不難發(fā)現(xiàn)這里from_tensor和to_tensor一致,所以是做自注意力。 損失優(yōu)化 在bert里面說過,最后拿出開頭的[CLS]就可以了。這既是get_pooled_output方法的作用。 最后再連接一個(gè)全連接層,最后就是二分類的任務(wù)w * x + b 模型構(gòu)建 model_fn方法是構(gòu)建的函數(shù)之一,一定一定要小心,雖然上面寫著返回給TPUEstimator,可如果你運(yùn)行過demo的話,輸出的很多東西都來源于這個(gè)方法。 進(jìn)入main(_)主方法,需要注意的是,以后我們需要fine-tune,需要把我們自己定義的processor添加進(jìn)processors。 確認(rèn)要訓(xùn)練之后,會(huì)計(jì)算需要一共多少步完成,這里還有個(gè)warm-up,意思是一開始呢讓learning rate低一下,等到了warm-up proportion之后再還原。 終于我們開始構(gòu)建模型了 最終我們構(gòu)建了estimator用于后期訓(xùn)練,評(píng)估和預(yù)測(cè) 其他注意點(diǎn) 這是殘差相連的部分 還有一點(diǎn)就是記得在transformer中講過我們會(huì)連兩層全連接層,一層升維,另一層降維。 接下來進(jìn)行降維 覺得寫的好,不妨去github上給我star,里面有很多比這還要棒的解析: https://github.com/sherlcok314159/ML ![]() AINLP 一個(gè)有趣有AI的自然語言處理公眾號(hào):關(guān)注AI、NLP、機(jī)器學(xué)習(xí)、推薦系統(tǒng)、計(jì)算廣告等相關(guān)技術(shù)。公眾號(hào)可直接對(duì)話雙語聊天機(jī)器人,嘗試自動(dòng)對(duì)聯(lián)、作詩機(jī)、藏頭詩生成器,調(diào)戲夸夸機(jī)器人、彩虹屁生成器,使用中英翻譯,查詢相似詞,測(cè)試NLP相關(guān)工具包。 333篇原創(chuàng)內(nèi)容 公眾號(hào) |
|