從零實(shí)現(xiàn)BERT、GPT及Difussion類算法-2:Tokenizer
教程簡(jiǎn)介及目錄見(jiàn):?從零實(shí)現(xiàn)BERT、GPT及Difussion類算法:文章簡(jiǎn)介及目錄
從Bert開(kāi)始,NLP任務(wù)都改用subword的分詞方法,主要包括:BPE、WordPiece、ULM。
BPE訓(xùn)練
參考:
https://huggingface.co/learn/nlp-course/chapter6/5?fw=pt
https://github.com/soaxelbrooke/python-bpe
訓(xùn)練流程
將數(shù)據(jù)集拆成詞,并統(tǒng)計(jì)詞頻
首先將數(shù)據(jù)集拆成詞,并統(tǒng)計(jì)詞頻 (為了區(qū)分詞的開(kāi)始、結(jié)束,可以末尾添加</w>,或者在句中添加##等特殊符號(hào))
舉例如下:(本章節(jié)先以英文演示原理,在后續(xù)章節(jié)的Bert & GPT實(shí)戰(zhàn)中,會(huì)有更詳細(xì)的中文分詞實(shí)現(xiàn))
{"l o w </w>":5, "l o w e r </w>":2, "n e w e s t </w>": 6, "w i d e s t </w>": 3}
詞典大?。簕"l","o","w","e","r","n","s","t","i","d","</w>"}
從統(tǒng)計(jì)出的詞頻中,統(tǒng)計(jì)bigram最多的次數(shù),組成新的單元
lo: 5+2=7, ow: 5+2=7, w</w>: 5
we: 2+6=8, er: 2, r</w>: 2
ne: 6, ew: 6, es: 6+3=9, st: 6+3=9, t</w>: 6+3=9
wi: 3, id: 3, de: 3
es、st、t</w>都為9,可任選一個(gè),變成{"l o w </w>":5, "l o w e r </w>":2, "n e w es t </w>": 6, "w i d es t </w>": 3}
詞典大?。簕"l","o","w","e","r","n","es","t","i","d", "</w>"}
繼續(xù)第3步,直到:達(dá)到預(yù)設(shè)的Subword詞表大小 或 下一個(gè)最高頻的字節(jié)對(duì)出現(xiàn)頻率為1
訓(xùn)練代碼實(shí)現(xiàn)如下:
首先我們需要一個(gè)簡(jiǎn)單的分詞器,將句子拆分成單詞(如根據(jù)空格、標(biāo)點(diǎn)進(jìn)行拆分)
詞典訓(xùn)練過(guò)程如下(BPETokenizer.fit())
統(tǒng)計(jì)初始詞典
首先如果設(shè)置了lowercase,會(huì)將大/小寫(xiě)同等對(duì)待(統(tǒng)一轉(zhuǎn)換成小寫(xiě))
然后通過(guò)對(duì)句子簡(jiǎn)單分詞,并數(shù)量統(tǒng)計(jì),得到Counter結(jié)構(gòu),如Counter({('n', 'e', 'w', 'e', 's', 't', '</w>'): 6, ('l', 'o', 'w', '</w>'): 5, ('w', 'i', 'd', 'e', 's', 't', '</w>'): 3, ('l', 'o', 'w', 'e', 'r', '</w>'): 2})
在_count_vocab中統(tǒng)計(jì)處目前的詞典為[('e', 17), ('w', 16), ('</w>', 16), ('s', 9), ('t', 9), ('l', 7), ('o', 7), ('n', 6), ('i', 3), ('d', 3), ('r', 2)]
持續(xù)迭代合并詞典中的高頻二元組(過(guò)程見(jiàn)_fit_step()),并更新vocab。直到 超過(guò)迭代步數(shù) 或 詞典大小滿足要求 或 已經(jīng)沒(méi)有可合并元素
在corpus的每個(gè)word中,以步長(zhǎng)1,窗口尺寸2,統(tǒng)計(jì)出所有二元組token的頻次
將最大二元組出現(xiàn)的地方合并成一個(gè)token
最后是添加一些特殊詞并導(dǎo)出詞典
BPE分詞
分詞是利用上一步訓(xùn)練好的vocab,將句子切分成詞典中的token,分詞是一個(gè)匹配最長(zhǎng)子串的過(guò)程
首先還是利用簡(jiǎn)單分詞器self.basic_tokenzier,將句子分成單詞序列
然后對(duì)每個(gè)單詞,從后往前,依次找到包含在vocab中的最長(zhǎng)sub_token
對(duì)于某個(gè)單詞,如果任何sub_token都不包含在vocab中,那么當(dāng)做未登錄詞"<UNK>"
分詞代碼如下:
重點(diǎn)關(guān)注tokenize、encode、decode
WordPiece訓(xùn)練
參考:https://huggingface.co/learn/nlp-course/chapter6/6?fw=pt
訓(xùn)練流程:
WordPiece原理和BPE基本一致,區(qū)別在于BPE每一步使用最大詞頻進(jìn)行合并
代碼實(shí)現(xiàn)為max_bigram = max(bigram_counter, key=bigram_counter.get)
而WordPiece使用最大概率,也即選擇合并后的詞頻/單個(gè)出現(xiàn)的詞頻的比值最大的詞進(jìn)行合并
代碼實(shí)現(xiàn)為max_bigram = max(bigram_counter, key=lambda x: bigram_counter.get(x) / (unigram_counter.get(x[0]) * unigram_counter.get(x[1])))
可選閱讀
之所以按如上方式,是因?yàn)榧俣╯entence中每個(gè)詞出現(xiàn)概率相互獨(dú)立,那么sentence出現(xiàn)概率就是組成sentence的每個(gè)token概率相乘,為了方便計(jì)算,可以使用對(duì)數(shù)來(lái)將乘法轉(zhuǎn)成加法,于是有
而WordPiece希望token合并后,新token能夠最大化句子概率,及假定將
、
合并為
,則有
而因?yàn)楹喜⑶昂笤~典大小幾乎相同(實(shí)際可能相同、可能少1、可能多1),所以除法運(yùn)算中約去公分母,得到
訓(xùn)練代碼實(shí)現(xiàn)如下:
從上面代碼可以看出,BPETokenizer和WordPieceTokenizer的差別只在每一步迭代的
進(jìn)一步對(duì)比代碼可以看出,BPETokenizer和WordPieceTokenizer只在"選出頻次最大的二元組" max_bigram這一行差別
注:上文講到為了區(qū)分詞的開(kāi)始、結(jié)束,可以word末尾添加</w>,或者在word中間添加##等特殊符號(hào),我這里只以末尾添加</w>解釋了分詞訓(xùn)練原理,中間添加##的方式大家可以可以自行修改實(shí)現(xiàn)代碼。
WordPiece分詞
分詞過(guò)程和BPE相同
ULM
參考:https://huggingface.co/course/chapter6/7?fw=pt
因?yàn)槠綍r(shí)工作中很少用到ULM,所以暫時(shí)沒(méi)有去實(shí)現(xiàn)代碼,要自己實(shí)現(xiàn)的同學(xué)可以參考huggingface文檔,這里只簡(jiǎn)單講一下訓(xùn)練原理:
BPE和WordPiece都是先有小詞表(如char級(jí)別的詞表),然后通過(guò)組合逐步擴(kuò)大此表
ULM是先有一個(gè)盡可能覆蓋全面的大詞表(可以想象先枚舉出所有可能得sub_token),然后在每一步的迭代中,刪除最不重要的詞,來(lái)將詞表縮小到符合要求的尺寸
從以上描述可以看出,ULM的重點(diǎn)是找到最不重要的詞。其基本思想是:
假設(shè)每個(gè)token獨(dú)立分布,基于當(dāng)前vocab計(jì)算出corpus中所有句子的概率
對(duì)每個(gè)詞
,假設(shè)從vocab中去除
后,再次計(jì)算corpus中所有句子的概率
選出能夠使
最小化的
即為對(duì)概率影響最小的最不重要的詞,然后從詞表中移除
如果覺(jué)得每次只選1個(gè)詞,詞典縮小速度太慢的話,也可以每次選出使得概率差值最小的k個(gè)詞,并移除