【NLP】完全解析!Bert Transformer 阅读理解源码详解
接上一篇:
你所不知道的 Transformer!
超詳細(xì)的 Bert 文本分類源碼解讀 | 附源碼
中文情感分類單標(biāo)簽
參考論文:
https://arxiv.org/abs/1706.03762
https://arxiv.org/abs/1810.04805
在本文中,我將以run_squad.py以及SQuAD數(shù)據(jù)集為例介紹閱讀理解的源碼,官方代碼基于tensorflow-gpu 1.x,若為tensorflow 2.x版本,會(huì)有各種錯(cuò)誤,建議切換版本至1.14。?
當(dāng)然,注釋好的源代碼在這里:
https://github.com/sherlcok314159/ML/tree/main/nlp/code
章節(jié)
Demo傳參
數(shù)據(jù)篇
番外句子分類
創(chuàng)造實(shí)例
實(shí)例轉(zhuǎn)換
模型構(gòu)造
寫入預(yù)測(cè)
Demo傳參
python bert/run_squad.py \--vocab_file=uncased_L-12_H-768_A-12/vocab.txt \--bert_config_file=uncased_L-12_H-768_A-12/bert_config.json \--init_checkpoint=uncased_L-12_H-768_A-12/bert_model.ckpt \--do_train=True \--train_file=SQUAD_DIR/train-v2.0.json \--train_batch_size=8 \--learning_rate=3e-5 \--num_train_epochs=1.0 \--max_seq_length=384 \--doc_stride=128 \--output_dir=/tmp/squad2.0_base/ \--version_2_with_negative=True閱讀源碼最重要的一點(diǎn)不是拿到就讀,而是跑通源碼里面的小demo,因?yàn)槟闩芡╠emo就意味著你對(duì)代碼的一些基礎(chǔ)邏輯和參數(shù)有了一定的了解。
前面的參數(shù)都十分常規(guī),如果不懂,建議看我的文本分類的講解。這里講一下比較特殊的最后一個(gè)參數(shù),我們做的任務(wù)是閱讀理解,如果有答案缺失,在SQuAD1.0是不可以的,但是在SQuAD允許,這也就是True的意思。
需要注意,不同人的文件路徑都是不一樣的,你不能照搬我的,要改成自己的路徑。
數(shù)據(jù)篇
其實(shí)閱讀理解任務(wù)模型是跟文本分類幾乎是一樣的,大的差異在于兩者對(duì)于數(shù)據(jù)的處理,所以本篇文章重點(diǎn)在于如何將原生的數(shù)據(jù)轉(zhuǎn)換為閱讀理解任務(wù)所能接受的數(shù)據(jù),至于模型構(gòu)造篇,請(qǐng)看文本分類:
https://github.com/sherlcok314159/ML/blob/main/nlp/tasks/text.md
番外句子分類
想必很多人看到SquadExample類的_repr_方法都很疑惑,這里處理好一個(gè)example,為什么后面還要進(jìn)行處理?看英文注釋會(huì)發(fā)現(xiàn)這個(gè)類其實(shí)跟閱讀理解沒關(guān)系,它只是處理之后對(duì)于句子分類任務(wù)的,自然在run_squad.py里面沒被調(diào)用。_repr_方法只是在有start_position的時(shí)候進(jìn)行字符串的拼接。
創(chuàng)造實(shí)例
用于訓(xùn)練的數(shù)據(jù)集是json文件,需要用json庫讀入。
訓(xùn)練集的樣式如下,可見data是最外層的
{"data": [{"title": "University_of_Notre_Dame","paragraphs": [{"context": "Architecturally, the school has a Catholic character.","qas": [{"answers": [{"answer_start": 515,"text": "Saint Bernadette Soubirous"}],"question": "To whom did the Virgin Mary allegedly appear in 1858 in Lourdes France?","id": "5733be284776f41900661182"}]}]},{"title":"...","paragraphs":[{"context":"...","qas":[{"answers":[{"answer_start":..,"text":"...",}],"question":"...","id":"..."},]}]}] }
input_data是一個(gè)大列表,然后每一個(gè)元素樣式如下
is_whitespace方法是用來判斷是否是一個(gè)空格,在切分字符然后加入doc_tokens會(huì)用到。
然后我們層層剝開,然后遍歷context的內(nèi)容,它是一個(gè)字符串,所以遍歷的時(shí)候會(huì)遍歷每一個(gè)字母,字符會(huì)被進(jìn)行判斷,如果是空格,則加入doc_tokens,char_to_word_offset表示切分后的索引列表,每一個(gè)元素表示一個(gè)詞有幾個(gè)字符組成。
切分后的doc_tokens會(huì)去掉空白部分,同時(shí)會(huì)包括英文逗號(hào)。一個(gè)單詞會(huì)有很多字符,每個(gè)字符對(duì)應(yīng)的索引會(huì)存在char_to_word_offset,例如,前面都是0,代表這些字符都是第一個(gè)單詞的,所以都是0,換句話說就是第一個(gè)單詞很長(zhǎng)。
doc_tokens = ['Architecturally,', 'the', 'school', 'has', 'a', 'Catholic', 'character.', 'Atop', 'the',"..."]char_to_word_offset = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]接下來進(jìn)行qas內(nèi)容的遍歷,每個(gè)元素稱為qa,進(jìn)行id和question內(nèi)容的分配,后面都是初始化一些參數(shù)
qa里面還有一個(gè)is_impossible,用于判斷是否有答案
確保有答案之后,剛剛讀入了問題,現(xiàn)在讀入與答案相關(guān)的部分,讀入的時(shí)候注意start_position和end_position是相對(duì)于doc_tokens的
接下來對(duì)答案部分進(jìn)行雙重檢驗(yàn),actual_text是根據(jù)doc_tokens和始末位置拼接好的內(nèi)容,然后對(duì)orig_answer_text進(jìn)行空格切分,最后用find方法判斷orig_answer_text是否被包含在actual_text里面。
這個(gè)是針對(duì)is_impossible來說的,如果沒有答案,則把始末位置全部變成-1。
然后將example變成SquadExample的實(shí)例化對(duì)象,將example加入大列表——examples并返回,至此實(shí)例創(chuàng)建完成。
實(shí)例轉(zhuǎn)換
把json文件變成實(shí)例之后,我們還差一步便可以把數(shù)據(jù)塞進(jìn)模型進(jìn)行訓(xùn)練了,那就是將實(shí)例轉(zhuǎn)化為變量。
先對(duì)question_text進(jìn)行簡(jiǎn)單的空格切分變?yōu)閝uery_tokens
如果問題過長(zhǎng),就進(jìn)行截?cái)嗖僮?/p>
接下來對(duì)doc_tokens進(jìn)行空格切分以及詞切分,變成all_doc_tokens,需要注意的是orig_to_tok_index代表的是doc_tokens在all_doc_tokens的索引,取最近的一個(gè),而tok_to_orig_index代表的是all_doc_tokens在doc_tokens索引
對(duì)tok_start_position和tok_end_position進(jìn)行初始化,記住,這兩個(gè)是相對(duì)于all_doc_tokens來說的,一定要與start_position和end_position區(qū)分開來,它們是相對(duì)于doc_tokens來說的
接下來先介紹_improve_answer_span方法,這個(gè)方法是用來處理特殊的情況的,舉個(gè)例子,假如說你的文本是"The Japanese electronics industry is the lagest in the world.",你的問題是"What country is the top exporter of electornics?" 那答案其實(shí)應(yīng)該是Japan,可是呢,你用空格和詞切分的時(shí)候會(huì)發(fā)現(xiàn)Japanese已經(jīng)在詞表中可查,這意味著不會(huì)對(duì)它進(jìn)行再切分,會(huì)直接將它返回,這種情況下可能需要這個(gè)方法救場(chǎng)。
因?yàn)槭潜O(jiān)督學(xué)習(xí),答案已經(jīng)給出,所以呢,這個(gè)方法干的事情就是詞切分后的tokens進(jìn)行再一次切分,如果發(fā)現(xiàn)切分之后會(huì)有更好的答案,就返回新的始末點(diǎn),否則就返回原來的。
對(duì)tok_start_position和tok_end_position進(jìn)行進(jìn)一步賦值
計(jì)算max_tokens_for_doc,與文本分類類似,需要減去[CLS]和兩個(gè)[SEP]的位置,這里不同的是還要減去問題的長(zhǎng)度,因?yàn)檫@里算的是文本的長(zhǎng)度。?
tokens = [CLS] query tokens [SEP] context [SEP]
很多時(shí)候文章長(zhǎng)度大于maximum_sequence_length的時(shí)候,這個(gè)時(shí)候我們要對(duì)文章進(jìn)行切片處理,把它按照一定長(zhǎng)度進(jìn)行切分,每一個(gè)切片稱為一個(gè)doc_span,start代表從哪開始,length代表一個(gè)的長(zhǎng)度。
doc_spans儲(chǔ)存很多個(gè)doc_span。這里對(duì)窗口的長(zhǎng)度有所限制,規(guī)定了start_offset不能比doc_stride大,這是第二個(gè)窗口的起點(diǎn),從這個(gè)角度或許可以理解doc_stride代表平滑的長(zhǎng)度。
接下來的操作跟文本分類有些類似,添加[CLS],然后添加問題和[SEP],這些在segment_ids里面都為0。
下面講_check_is_max_context方法,這個(gè)方法是用來判斷某個(gè)詞是否具有完備的上下文關(guān)系,源代碼給了一個(gè)例子:?
Span A: the man went to the?
Span B: to the store and bought?
Span C: and bought a gallon of ...?
那么對(duì)于bought來說,它在Span B和Span C中都有出現(xiàn),那么,哪一個(gè)上下文關(guān)系最全呢?其實(shí)我們憑直覺應(yīng)該可以猜到應(yīng)該是Span C,因?yàn)镾pan B中bought出現(xiàn)在句末,沒有下文。當(dāng)然了,我們還是得用公式計(jì)算一下
score = min(num_left_context, num_right_context) + 0.01 * doc_span.lengthscore_B = min(4, 0) + 0.05 = 0.05?
score_C = min(1,3) + 0.05 = 1.05?
所以,在Span C中,bought的上下文語義最全,最終該方法會(huì)返回True or False,在滑動(dòng)窗口這個(gè)方法中,一個(gè)詞很可能出現(xiàn)在多個(gè)span里面,所以用這個(gè)方法判斷當(dāng)前這個(gè)詞在當(dāng)前span里面是否具有最完整的上下文
回到上面,token_to_orig_map是用來記錄文章部分在all_doc_tokens的索引,而token_is_max_context是記錄文章每一個(gè)詞在當(dāng)前span里面是否具有最完整的上下文關(guān)系,因?yàn)橐婚_始只有一個(gè)span,那么一開始每個(gè)詞肯定都是True。split_token_index用于切分成每一個(gè)token,這樣可以進(jìn)行上下文關(guān)系判斷,至于后面添[SEP]和segment_ids添1這種操作文本分類也有。
接下來將tokens(精細(xì)化切分后的)按照詞表轉(zhuǎn)化為id,另外若不足,則把0填充進(jìn)去這種操作也是很常見的。
前面是進(jìn)行判斷,如果切了之后答案并不在span里面就直接舍棄,若在里面,因?yàn)橐婚_始all_doc_tokens里面沒有問題和[CLS],[SEP]時(shí)正文的索引是tok_start_position,然后轉(zhuǎn)換為input_ids又有問題以及[CLS],[SEP],所以要得到正文索引需要跳過它們。
接下來大量的tf.logging只是寫入日志信息,同時(shí)也是你終端或輸出那里看到的。
最終用這些參數(shù)實(shí)例化InputFeatures對(duì)象,然后不斷重復(fù),每一個(gè)feature對(duì)應(yīng)著一個(gè)特殊的id,即為unique_id。
模型構(gòu)建
這里大致與文本分類差不多,只是文本分類在模型里面直接進(jìn)行了softmax處理,然后進(jìn)行最小交叉熵?fù)p失,而這次我們沒有直接這樣做,得到了開頭和結(jié)尾處的未歸一化的概率logits,之后我們直接返回。
然后這次我們是在model_fn_builder方法里面的子方法model_fn里定義compute_loss,其實(shí)這里也是經(jīng)過softmax進(jìn)行歸一化,然后再計(jì)算交叉熵?fù)p失,最終返回均方誤差。
然后我們計(jì)算開頭和結(jié)尾處的損失,總損失為二者和的平均。
最終我們進(jìn)行優(yōu)化。
寫入預(yù)測(cè)?
start_logit & end_logit 代表著未經(jīng)過softmax的概率,start_logit表示tokens里面以每一個(gè)token作為開頭的概率,后者類似的。還有一對(duì)null_start_logit & null_end_logit,它們兩個(gè)代表的是SQuAD2.0沒有答案的那些,默認(rèn)全為0。
首先,簡(jiǎn)單介紹一下_get_best_indexes,這個(gè)方法是用來輸出由高到低前n_best_size個(gè)的概率的索引。
遍歷start_indexes,end_indexes(都是分別經(jīng)過_get_best_indexes得到),對(duì)于答案未缺失的,以具體的logit填入,另外,feature_index代表第幾個(gè)feature。
如果答案缺失,則全都為0
接下來我們進(jìn)一步轉(zhuǎn)換為具體的文本
然后進(jìn)一步清洗數(shù)據(jù)
這樣還有個(gè)問題,詞切分會(huì)自動(dòng)小寫,與答案還存在一定的偏移,這里介紹get_final_text方法來解決這一問題,比如:?
pred_text = steve smith?
orig_text = Steve Smith's?
這個(gè)方法通俗來講就是獲得orig_text(未經(jīng)過詞切分)上正確的截取片段。?
然后將其添加到nbest中
同樣會(huì)存在沒有答案的情況
接下來會(huì)有一個(gè)total_scores,它的元素是start_logit和end_logit相加,注意,它們不是數(shù)值,是數(shù)組,之后就計(jì)算total_scores的交叉熵?fù)p失作為概率。
剩下的部分跟文本分類差不多,這里就此略過。
往期精彩回顧適合初學(xué)者入門人工智能的路線及資料下載機(jī)器學(xué)習(xí)及深度學(xué)習(xí)筆記等資料打印機(jī)器學(xué)習(xí)在線手冊(cè)深度學(xué)習(xí)筆記專輯《統(tǒng)計(jì)學(xué)習(xí)方法》的代碼復(fù)現(xiàn)專輯 AI基礎(chǔ)下載機(jī)器學(xué)習(xí)的數(shù)學(xué)基礎(chǔ)專輯溫州大學(xué)《機(jī)器學(xué)習(xí)課程》視頻 本站qq群851320808,加入微信群請(qǐng)掃碼:總結(jié)
以上是生活随笔為你收集整理的【NLP】完全解析!Bert Transformer 阅读理解源码详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vue cli3使用jQuery控件
- 下一篇: Python数据结构与算法(五)--链表