bert深入分析以及bert家族总结
- 2021 年 3 月 29 日
- AI
本文主要对bert以及各类bert based model做一个总结,在总结的过程中,对bert的各种细节(分词算法、相对\绝对为止编码、预训练任务等)进行整理,主要是因为在研究bert家族的过程中发现bert的各种变体基本都是从这些细节层面入手进行的魔改。所以其实bertology的models理解起来并不是非常复杂。
首先从input开始
tokenizer部分
特点:
1、rust实现,速度飞快,只需不到20秒即可用服务器CPU进行1GB文本处理;
2、用起来很简单,api平易近人;
3、截断、补0padding、特殊的token添加等一站式解决
目前huggingface实现了BPE、wordpeice和sentencepiece等分词方法。
常见而直观的英文或者中文分词的方式,往往是以word为基础的,例如:
"Don't you love Transformers? We sure do."
分词为
["Don", "'", "t", "you", "love", " ", "Transformers", "?", "We", "sure", "do", "."]
“我来自中国”
分词为:
“我”,“来”,“ 自 ”,“中国”
word-level
这些分词的方法都是将句子拆分为词,即word-level,这么做的优缺点是:
优点:能够保存较为完整的语义信息
缺点:
1、词汇表会非常大,大的词汇表对应模型需要使用很大的embedding层,这既增加了内存,又增加了时间复杂度。通常,transformer模型的词汇量很少会超过50,000,特别是如果仅使用一种语言进行预训练的话,而transformerxl使用了常规的分词方式,词汇表高达267735;
2、 word-level级别的分词略显粗糙,无法发现更加细节的语义信息,例如模型学到的“old”, “older”, and “oldest”之间的关系无法泛化到“smart”, “smarter”, and “smartest”。
3、word-level级别的分词对于拼写错误等情况的鲁棒性不好;
char-level
一个简单的方法就是将word-level的分词方法改成 char-level的分词方法,对于英文来说,就是字母界别的,比如 “China”拆分为”C”,”h”,”i”,”n”,”a”,对于中文来说,”中国”拆分为”中”,”国”,
优点:
1、这可以大大降低embedding部分计算的内存和时间复杂度,以英文为例,英文字母总共就26个。。。。,中文常用字也就几千个。
2、char-level的文本中蕴含了一些word-level的文本所难以描述的模式,因此一方面出现了可以学习到char-level特征的词向量FastText,另一方面在有监督任务中开始通过浅层CNN、HIghwayNet、RNN等网络引入char-level文本的表示;
缺点:
1、但是这样使得任务的难度大大增加了,毕竟使用字符大大扭曲了词的意义,一个字母或者一个单中文字实际上并没有任何语义意义,单纯使用char-level往往伴随着模型性能的下降;
2、增加了输入的计算压力,原本”I love you“是3个embedding进入后面的cnn、rnn之类的网络结构,而进行char-level拆分之后则变成 8个embedding进入后米娜的cnn或者rnn之类的网络结构,这样计算起来非常慢;
subword-level
为了两全其美,transformer使用了混合了char-level和word-level的分词方式,称之为subword-level的分词方式。
subword-level的分词方式遵循的原则是:尽量不分解常用词,而是将不常用此分解为常用的子词,例如,"annoyingly"
可能被认为是一个罕见的单词,并且可以分解为"annoying"
和"ly"
。"annoying"
并"ly"
作为独立的子词会更频繁地出现,同时,"annoyingly"
是由”annoying”和”ly”这两个子词的复合含义构成的复杂含义,这在诸如土耳其语之类的凝集性语言中特别有用,在该语言中,可以通过将子词串在一起来形成(几乎)任意长的复杂词。
subword-level的分词方式使模型相对合理的词汇量(不会太多也不会太少),同时能够学习有意义的与上下文无关的表示形式(另外,subword-level的分词方式通过将模型分解成已知的子词,使模型能够处理以前从未见过的词(oov)。
subword-level又分为不同的切法,这里就到huggingface的tokenizers的实现部分了,常规的char-level或者word-level的分词用spacy,nltk之类的工具就可以胜任了。
1、BPE;
GPT2和Roberta使用了这种分词的方法,思路也很简单,对应的是:
tk=kenizers.CharBPETokenizer()
Luke:深入理解NLP Subword算法:BPE、WordPiece、ULM
参考自上文:
以语料:
{'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}
为例,假设我们对原始的文本进行分词之后只有上面4个词,则:
1、对每个词进行词频统计,并且对每个词末尾的字符转化为 末尾字符和</w>两个字符,停止符”</w>”的意义在于表示subword是词后缀。举例来说:”st”字词不加”</w>”可以出现在词首如”st ar”,加了”</w>”表明改字词位于词尾,如”wide st</w>”,二者意义截然不同。
{'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”,”</w>”,”n”,”s”,”t”,”i”}
2、统计每一个连续字节对的出现频率,选择最高频者合并成新的subword
{'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}
最高频连续字节对”e”和”s”出现了6+3=9次,合并成”es”
需要注意的是,”es”生成后会消除”s”,因为上述语料中 “s”总和”es”共同出现,但是s除了”es”外就没有其它的字符组合出现了,所以”s”被消除,但是”e”在”lower”中出现过有”er”或”we”这样的组合,所以”e”没有被消除,此时词表变化为:
{“l”,”o”,’w’,”e”,”r”,”</w>”,”n”,”es”,”t”,”i”}
(补充另外两种情况:
如果es中的e和s都是和es一起出现,除此之外没有单独再出现其它的字符组合,则es一起雄消除;
如果es中的e和s都各自有和其它字符的组合,则e和s都不会被消除,但是多了个新词es)
3、继续上述过程:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}
此时最高频连续字节对”es”和”t”出现了6+3=9次, 合并成”est”。输出:
同理,上一步的词表中”t”被消除,”est”加入此表
。。。。。
继续迭代直到达到预设的subword词表大小或下一个最高频的字节对出现频率为1。
上述就完成了编码过程,得到了词表,然后要把原始的word-level的分词结果进行相应转化:
1、将此表按照其中token的长度,从长到短排列;
例如排序好之后的词表为:
[“errrr</w>”, “tain</w>”, “moun”, “est</w>”, “high”, “the</w>”, “a</w>”]
2、对原始的word-level的分词结果进行转化,例如原始的语料为:
[“the</w>”, “highest</w>”, “mountain</w>”]
则转化为:
"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]
这样就完成了BPE的分词了;
迭代的终止条件是subword词表大小或下一个最高频的字节对出现频率为1。
- 优点
- 可以有效地平衡词汇表大小和步数(编码句子所需的token数量)。
- 缺点
- 基于贪婪和确定的符号替换,不能提供带概率的多个分片结果,简单来说就是某个词可以被不同的词表中的token替换,也就是一个word可以用不同的token的替换方式,例如”apple”可以被替换为app和le/w,也可以被替换为ap,ple(这里的例子不是很恰当,理解即可)。
2、ByteLevelBPE
tk=tokenizers.ByteLevelBPETokenizer()
前面提到bpe以词频top-k数量建立的词典;但是针对字符相对杂乱的日文和字符较丰富的中文,往往他们的罕见词难以表示,就中文来说,字符级别就是到单个中文字,例如“窃位素餐”(指高级官员饱食终日,无所用心,贬义词)这样生僻的词语最后很可能都没法纳入词典之中,但是,举个例子,在情感分析中,这个词对于句子的情感分析又可能是决定性作用的。
为了理解这个算法,看了一下原论文:
//arxiv.org/pdf/1909.03341.pdf
BBPE整体和BPE的逻辑类似,不同的是,粒度更细致,BPE最多做到字符级别,但是BBPE是做到byte级别:
日文看不懂,看英文吧,“ask”这个单词变成字符是”a”,”s”,”k”,然后对”a”,”s”,”k”做utf8编码得到 “41 73 6B E2 96 81 71 75 65 73 74 69 6F 6E 73”。。。。这样得到了byte级别的初始的分词结果,这样,原来的一个”a“变成了”41 73 6B E2 96 “。。。然后以”41″这样的字节为单位,更加细致的做BPE了。。。wtf
然后使用BPE算法来处理,这样下来encode截断我们最终的词表就是一大堆的utf8编码的组合了。。。。encode部分到这里结束
比较麻烦的是decode部分,因为字节码的组合可能会得到无意义的组合,也就是得到的utf8的编码组合可能无法映射回一个有意义的字符,所以作者使用了:
下面的公式来进行矫正。
没查到啥模型用这种分词算法。。。。wtf
具体可见:
不想研究。。。
3、wordpiece
这个比较多模型用这种分词方式,包括bert,distilbert,electra
对应的接口为:
from tokenizers import BertWordPieceTokenizer
wordpiece是BPE算法的变种,整体的计算思路和BPE类似,仅仅在生成subword的时候不同,见下面的例子
wordpice整体上和BPE的计算思路类似,只不过下面的这个阶段不一样
2、(统计每一个连续字节对的出现频率,选择最高频者合并成新的subword)BPE使用的方法是k括号中的,而wordpiece则使用了另一种方法
最高频连续字节对”e”和”s”出现了6+3=9次,合并成”es”
{'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}
而wordpiece则是从整个句子的层面出发去确认subword的合并结果,假设有个句子是:
“see you next week”初始拆分为字符之后是
“s”,”e”,”e”……. “e”,”k”
则语言模型概率为:
n表示这个句子拆分成字符之后的长度(继续迭代的话就是拆分成subword的长度了),P(ti)表示”ti”这个字符或者subword在词表中占比的概率值,不过我们只需要计算下面的式子就可以:
可以看到,这里和决策树的分裂过层非常类似,两个两个相邻字符或subword之间进行分裂判断分裂增益是否增大,增大则合并。
从上面的公式,很容易发现,似然值的变化就是两个子词之间的互信息。简而言之,WordPiece每次选择合并的两个子词,他们具有最大的互信息值,也就是两子词在语言模型上具有较强的关联性,它们经常在语料中以相邻方式同时出现。
from
阿北:NLP三大Subword模型详解:BPE、WordPiece、ULM
感觉是个大工程。。算了先看看别的东西吧。。。