欢迎光临散文网 会员登陆 & 注册

从零实现BERT、GPT及Difussion类算法-2:Tokenizer

2023-04-23 00:07 作者:一代闲人  | 我要投稿


教程简介及目录见: 从零实现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

训练流程

  1. 将数据集拆成词,并统计词频

  2. 首先将数据集拆成词,并统计词频 (为了区分词的开始、结束,可以末尾添加</w>,或者在句中添加##等特殊符号)

    1. 举例如下:(本章节先以英文演示原理,在后续章节的Bert & GPT实战中,会有更详细的中文分词实现)

      1. {"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}

      2. 词典大小:{"l","o","w","e","r","n","s","t","i","d","</w>"}

  3. 从统计出的词频中,统计bigram最多的次数,组成新的单元

    1. lo: 5+2=7, ow: 5+2=7, w</w>: 5

    2. we: 2+6=8, er: 2, r</w>: 2

    3. ne: 6, ew: 6, es: 6+3=9, st: 6+3=9, t</w>: 6+3=9

    4. wi: 3, id: 3, de: 3

    5. 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}

    6. 词典大小:{"l","o","w","e","r","n","es","t","i","d", "</w>"}

  4. 继续第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概率相乘,为了方便计算,可以使用对数来将乘法转成加法,于是有

    logP(S)%20%3D%20logP(t_0)%2BlogP(t_1)%2B%5Ccdots%20%2BlogP(t_l)

  • 而WordPiece希望token合并后,新token能够最大化句子概率,及假定将t_it_j合并为t_k,则有

    maxium(logP(t_k)-(logP(t_i)%2BlogP(t_j)))%20%3D%20maximum(log(%5Cfrac%7BP(t_k)%7D%7BP(t_i)P(t_j)%7D))

  • 而因为合并前后词典大小几乎相同(实际可能相同、可能少1、可能多1),所以除法运算中约去公分母,得到

    maximum(log(%5Cfrac%7BP(t_k)%7D%7BP(t_i)P(t_j)%7D))%5Capprox%20maximum(log(%5Cfrac%7Bcount(t_k)%7D%7Bcount(t_i)count(t_j)%7D))

训练代码实现如下:

  • 从上面代码可以看出,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中所有句子的概率P(C)

    • 对每个词t_i,假设从vocab中去除t_i后,再次计算corpus中所有句子的概率P(C)_%7B%5Csim%20t_i%7D

    • 选出能够使P(C)-P(C)_%7B%5Csim%20t_i%7D最小化的t_i即为对概率影响最小的最不重要的词,然后从词表中移除

    • 如果觉得每次只选1个词,词典缩小速度太慢的话,也可以每次选出使得概率差值最小的k个词,并移除


从零实现BERT、GPT及Difussion类算法-2:Tokenizer的评论 (共 条)

分享到微博请遵守国家法律