从零实现BERT、GPT及Difussion类算法-2:Tokenizer
教程简介及目录见: 从零实现BERT、GPT及Difussion类算法:文章简介及目录
从Bert开始,NLP任务都改用subword的分词方法,主要包括:BPE、WordPiece、ULM。
BPE训练
参考:
https://huggingface.co/learn/nlp-course/chapter6/5?fw=pt
https://github.com/soaxelbrooke/python-bpe
训练流程
将数据集拆成词,并统计词频
首先将数据集拆成词,并统计词频 (为了区分词的开始、结束,可以末尾添加</w>,或者在句中添加##等特殊符号)
举例如下:(本章节先以英文演示原理,在后续章节的Bert & GPT实战中,会有更详细的中文分词实现)
{"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>"}
从统计出的词频中,统计bigram最多的次数,组成新的单元
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,可任选一个,变成{"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>"}
继续第3步,直到:达到预设的Subword词表大小 或 下一个最高频的字节对出现频率为1
训练代码实现如下:
首先我们需要一个简单的分词器,将句子拆分成单词(如根据空格、标点进行拆分)
词典训练过程如下(BPETokenizer.fit())
统计初始词典
首先如果设置了lowercase,会将大/小写同等对待(统一转换成小写)
然后通过对句子简单分词,并数量统计,得到Counter结构,如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中统计处目前的词典为[('e', 17), ('w', 16), ('</w>', 16), ('s', 9), ('t', 9), ('l', 7), ('o', 7), ('n', 6), ('i', 3), ('d', 3), ('r', 2)]
持续迭代合并词典中的高频二元组(过程见_fit_step()),并更新vocab。直到 超过迭代步数 或 词典大小满足要求 或 已经没有可合并元素
在corpus的每个word中,以步长1,窗口尺寸2,统计出所有二元组token的频次
将最大二元组出现的地方合并成一个token
最后是添加一些特殊词并导出词典
BPE分词
分词是利用上一步训练好的vocab,将句子切分成词典中的token,分词是一个匹配最长子串的过程
首先还是利用简单分词器self.basic_tokenzier,将句子分成单词序列
然后对每个单词,从后往前,依次找到包含在vocab中的最长sub_token
对于某个单词,如果任何sub_token都不包含在vocab中,那么当做未登录词"<UNK>"
分词代码如下:
重点关注tokenize、encode、decode
WordPiece训练
参考:https://huggingface.co/learn/nlp-course/chapter6/6?fw=pt
训练流程:
WordPiece原理和BPE基本一致,区别在于BPE每一步使用最大词频进行合并
代码实现为max_bigram = max(bigram_counter, key=bigram_counter.get)
而WordPiece使用最大概率,也即选择合并后的词频/单个出现的词频的比值最大的词进行合并
代码实现为max_bigram = max(bigram_counter, key=lambda x: bigram_counter.get(x) / (unigram_counter.get(x[0]) * unigram_counter.get(x[1])))
可选阅读
之所以按如上方式,是因为假定sentence中每个词出现概率相互独立,那么sentence出现概率就是组成sentence的每个token概率相乘,为了方便计算,可以使用对数来将乘法转成加法,于是有
而WordPiece希望token合并后,新token能够最大化句子概率,及假定将
、
合并为
,则有
而因为合并前后词典大小几乎相同(实际可能相同、可能少1、可能多1),所以除法运算中约去公分母,得到
训练代码实现如下:
从上面代码可以看出,BPETokenizer和WordPieceTokenizer的差别只在每一步迭代的
进一步对比代码可以看出,BPETokenizer和WordPieceTokenizer只在"选出频次最大的二元组" max_bigram这一行差别
注:上文讲到为了区分词的开始、结束,可以word末尾添加</w>,或者在word中间添加##等特殊符号,我这里只以末尾添加</w>解释了分词训练原理,中间添加##的方式大家可以可以自行修改实现代码。
WordPiece分词
分词过程和BPE相同
ULM
参考:https://huggingface.co/course/chapter6/7?fw=pt
因为平时工作中很少用到ULM,所以暂时没有去实现代码,要自己实现的同学可以参考huggingface文档,这里只简单讲一下训练原理:
BPE和WordPiece都是先有小词表(如char级别的词表),然后通过组合逐步扩大此表
ULM是先有一个尽可能覆盖全面的大词表(可以想象先枚举出所有可能得sub_token),然后在每一步的迭代中,删除最不重要的词,来将词表缩小到符合要求的尺寸
从以上描述可以看出,ULM的重点是找到最不重要的词。其基本思想是:
假设每个token独立分布,基于当前vocab计算出corpus中所有句子的概率
对每个词
,假设从vocab中去除
后,再次计算corpus中所有句子的概率
选出能够使
最小化的
即为对概率影响最小的最不重要的词,然后从词表中移除
如果觉得每次只选1个词,词典缩小速度太慢的话,也可以每次选出使得概率差值最小的k个词,并移除