bert深入分析以及bert家族总结

  • 2021 年 3 月 29 日
  • AI

本文主要对bert以及各类bert based model做一个总结,在总结的过程中,对bert的各种细节(分词算法、相对\绝对为止编码、预训练任务等)进行整理,主要是因为在研究bert家族的过程中发现bert的各种变体基本都是从这些细节层面入手进行的魔改。所以其实bertology的models理解起来并不是非常复杂。

首先从input开始

tokenizer部分

Python/latest/” data-draft-node=”block” data-draft-type=”link-card” class=”LinkCard old LinkCard–noImage”>Tokenizers – tokenizers documentationhuggingface.co

特点:

1、rust实现,速度飞快,只需不到20秒即可用服务器CPU进行1GB文本处理;

2、用起来很简单,api平易近人;

3、截断、补0padding、特殊的token添加等一站式解决

Summary of the tokenizershuggingface.co

目前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、ULMzhuanlan.zhihu.com图标

参考自上文:

以语料:

{'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.pdfarxiv.org

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

具体可见:

CaesarEX:浅谈Byte-Level BPEzhuanlan.zhihu.com图标

不想研究。。。

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、ULMzhuanlan.zhihu.com图标


感觉是个大工程。。算了先看看别的东西吧。。。