前言:虽然我综述过至少4遍预训练语言模型,但每次看都依然可以发现一些新的东西。经典的突破性进展,值得让人深入反复讲。


一文看尽预训练语言模型_java

过去 NLP 领域是一个任务用标注数据训练一个模型,而现在我们可以在大量无标注的语料上预训练出一个在少量有监督数据上微调就能做很多个任务的模型。这其实也会比较接近我们学习语言的过程。测试英文能力的好坏,考个雅思,里面有各式各样的任务,有听说读写,有各式各样的题型,有填空有选择。但我们学习英文的方法并不是去做大量的选择题,而是背大量的英文单词,理解它的词性、意思,阅读大量的英文文章、掌握它在段落中的用法。你只需要做少量的考点题,就可以通过某个语言能力的测试。这便是 NLP 领域所追求的目标。我们期待可以训练一个模型,它真的了解人类的语言。在需要解各式各样的任务的时候,只需要稍微微调一下,它就知道怎么做了。


一文看尽预训练语言模型_java_02

预训练语言模型的缩写大多是芝麻街的人物。这显然是起名艺术大师们的有意为之。他们甚至都可以抛弃用首字母缩写的原则去硬凑出芝麻街人名。


一文看尽预训练语言模型_java_03

预训练语言模型希望做的是把输入的每个 token 表示成一个 embedding vector。这个向量应该包含了该 token 的语义。意思相近的 token,应该要有比较相近的嵌入。嵌入中的某些维度,应该能看出语义的关联性。在没有 Elmo 和 BERT 这一套预训练方法之前,模型通常是输入一个 token ,输出一个向量,就结束了。输入一个独热编码,输出一个有连续值的向量有很多种做法。常用的是 Word2vec 和 Glove。对于英文,如果只是把英文词汇当作 tok,容易出现新词不在词表内的 OOV 问题。我们可以把输入变成是英文单词的字符,输出是一个向量。我们期待模型可以通过读取词汇的字首和字根,判断一个没有看过的词汇的意思。其中的代表模型就是鼎鼎大名的 FastText。


一文看尽预训练语言模型_java_04

如果是中文,我们可以以中文字符作为输入单位,也可以把中文字形图像作为提取图像特征的 CNN 模型的输入单位。这样的模型可以通过认识部首偏旁来给词向量提供更丰富的语义信息。


一文看尽预训练语言模型_java_05

以上这种词向量会遇到相同字在不同语境下意思不同的问题。比如养只狗中的"狗"和单身狗中的"狗"是截然不同的意思。一种解决方案是对每个"狗"字用不同的下标去表示区分。但这又忽略了"狗"这个字在不同上下文意思不一样,但依然有相关。因此我们需要一种能基于上下文语义变化的动态词向量。


一文看尽预训练语言模型_java_06

过去的 Word2vec 是吃一个 token 吐出一个 embedding。而到了 ELMo 和 BERT 这些预训练语言模型,它们是吃一整个句子,再给每个 token embedding。而接纳句子输入的模型部分,可以是各种编码器,比如 CNN,LSTM,或者 Transformer。考虑到词汇和词汇之间可能会有一些文法关系,我们也可以用一些 Tree-based 模型去把文法信息编码到模型中。但这样的想法今天并没有非常地流行。可能是因为 LSTM 和 Transformer 在预训练过程中就已经学到了文法的信息。Tree-based 模型就显得不是那么必要了。Tree-based 模型没有流行起来的另一个原因是,很多研究者实验中发现,它做出来的效果并不比 LSTM 好。它只有在处理文法结构非常清楚的问题时,比如数学公式,才会明显好过 LSTM。而其他文本序列任务上,表现一般。


一文看尽预训练语言模型_java_07

这样的融合了上下文信息的词向量,确实能做到不同语境意思不一样。上图是 BERT embedding 的可视化。上面五个句子指的都是苹果,而下面五个句子指的都是公司。我们把这十个句子中的"苹"的嵌入都抽出来,两两之间去计算余弦相似度。图中颜色越亮表示相关性越高。结果发现,前五个句子的"苹"字之间,和后五个句子的"苹"字之间,语义会比较像。


一文看尽预训练语言模型_java_08

预训练语言模型比来比去,变得越来越巨大,越来越臃肿,越来越玩不起,没有产出和落地,就越来越没意思。有钱人的生活就是这么索然无味,且枯燥。


一文看尽预训练语言模型_java_09

然而,让模型变小,就好比我们去到非洲,可以让人民币在一线城市的购买力变得更多。这就是穷人用的 BERT,拥有穷人的权威。在 Distill BERT,Tiny BERT,Mobile BERT,Q8BERT 和 ALBERT 中,最有名的是 ALBERT。它神奇在,和原版的 BERT 几乎是一样的。但不一样的地方是,原来 BERT 是12层,而它是只有4层。虽然要跑的计算没少,但参数占用的显存变少了。神奇的是它比 BERT 还小,却比 BERT 还好。


一文看尽预训练语言模型_java_10

要怎么把模型变小呢?可以参照李宏毅之前机器学习讲的模型压缩那一节。可以尝试的技术有网络剪枝,知识蒸馏,量化和架构设计。

一文看尽预训练语言模型_java_11

近年来比较火的尝试是架构设计。为了能让机器可以读非常长的序列,Transformer-XL 可以让机器读跨越片段的内容。为了让自注意力的计算复杂度变小,从 O(T²) 变成了 O(TlogT),甚至更低, Reformer 使用了局部敏感性的哈希注意力。为了让自注意力的空间复杂度也变小,从O(T²)变小,Longformer 用注意力模式来稀疏完整的注意力矩阵。


一文看尽预训练语言模型_java_12

预训练语言模型要如何做不同任务呢?如果输入是两个句子,中间用 [SEP] 分隔符分开,输出接一个MLP分类,端到端训练下来就结束了。如果输出是一个类别,有两种做法。一种是直接对 [CLS] 这个 token 的嵌入接 MLP 进行分类。另一种是把所有位置的嵌入接 MLP 进行分类。如果输出是每个位置一个类别,则对非 [CLS] 的 token 各接一个MLP,输出分类。


一文看尽预训练语言模型_java_13

如果输出需要复制部分的输入,比如抽取式机器阅读理解。则输出接两个 MLP,一个输出答案的开始位置,一个输出答案的结尾位置。


一文看尽预训练语言模型_java_14

如果我们要做 Seq2Seq 的模型,比如翻译。一种思路是,可以把预训练语言模型的输出经过一个注意力层后,丢给另一个解码器解码。


一文看尽预训练语言模型_java_15

另一种 Seq2Seq 的方式是自回归式的。每次让分类器生成一个 token 后,再把这个 token 与源输入拼接起来,再丢回给模型,用分类器生成下一个 token。以此类推,直至生成出 <EOS> 结尾符号。



一文看尽预训练语言模型_java_16

预训练语言模型要如何进行微调呢?一种方法是固定住预训练语言模型,让它作为一个特征提取器。训练的时候,只训练下面接的 MLP 的参数。另一种是不固定住预训练语言模型,对整个模型进行训练微调。第二种方法实践中会好很多。


一文看尽预训练语言模型_java_17

但是,如果我们现在采取的是对整个模型进行微调,这个模型在不同的任务里面会变得不同。每个任务我们都要存一个巨大的模型,显然是行不通的。所以我们需要一个 Adaptor 的结构,来让我们微调的时候,不是更新整个预训练语言模型的参数,而是只需要更新其中很小一部分的 Adaptor Layer 的参数,就可以获得和更新整个模型参数一样好的效果。这样,我们对不同任务模型储存的时候,就只需要把这一小部分 Adaptor 的参数给储存下来。用的时候,把它放到原版的 BERT 中就可以了。这样就会比每一个任务都要存一个很大的预训练语言模型要小得多。其实模型参数的储存并不是一个大问题。问题是当我们要做集成学习的时候,只储存 Adaptor 的参数会非常方便。


一文看尽预训练语言模型_java_18

Adaptor 是怎样运作的呢?相关的研究有好几篇,每篇的解法还都不一样。怎样解是好的,还是一个待研究的问题。这边只是举一个例子来给大家参考。这篇 ICML 19 的论文做法是把 Adaptor 插入在 Transformer 中的 Feed-forward 和 Lyaer Norm 层之间。它的架构也很简单,一个线性层+非线性单元再加一个线性层,输入输出之间用 ResNet 的思想相加。微调时,只调整 Adapter 的参数。


一文看尽预训练语言模型_java_19

这种方法的效果很不错。相比于要预训练整个模型,只训练 Adaptor 表现也差不多。把 Adaptor 加在哪里,模型结构怎么设计都是值得研究的话题。


一文看尽预训练语言模型_java_20

微调还有第四中不一样的做法。我们可以把不同层的输出都抽出来做加权求和再去分类。加权求和后的嵌入融合了浅层和深层的输出,并依据任务的不同来调整浅层特征和深层特征的权重。


一文看尽预训练语言模型_java_21

为什么我们需要这些预训练的模型呢?一是因为这些预训练语言模型,真的能给我们带来比较好的表现。预训练语言模型在 GLUE 上的表现逐年增高,到近年来超越人类水平。


一文看尽预训练语言模型_java_22

EMNLP 2019这篇文章表明,预训练语言模型可以大大加速损失的收敛。而不使用预训练语言模型,这个损失比较难下降。可以理解为,预训练语言模型提供了一种比随机初始化更好的初始化。


一文看尽预训练语言模型_java_23

另外一个结论是,预训练语言模型可以大大增加模型的泛化能力。如何看模型的泛化能力可以参照李宏毅之前讲过的深度学习理论的课程。上图表示给模型不同参数的时候,模型的损失。图中模型训练后结束点的损失会抵达一个 local minima 的位置。这个 local minima 越陡峭,则泛化能力越弱。因为输入稍微变化,它的损失就有变大的倾向。反之,这个 local minima 越平缓,则泛化能力越强。上图可以看到,使用了预训练语言模型微调后,结束点的损失平面 local minima 周边比较平缓,说明泛化能力强。


上面我们讲到了预训练语言模型可以怎么微调下游任务。这一次我们来说一说可以怎么预训练。
一文看尽预训练语言模型_java_24较早的时候,CoVe 使用的是基于翻译的方法来训练模型得到句子的嵌入。我们用翻译模型的 encoder 对输入语言A的句子编码经过 attention 之后再丢给解码器输出语言B的句子。我们用encoder最后的隐层作为这个句子的表征。这里用翻译会比用摘要好,因为翻译需要把句子的意思如实地呈现到输出句子中。因此输入句子中的每一个词汇都需要被考虑到。但 CoVe 这种方法的缺点在,它需要大量的成对数据,而成对数据缺乏。
一文看尽预训练语言模型_java_25但如果我们能够用自监督的方式去训练,则能利用海量的现有文本。
一文看尽预训练语言模型_java_26要怎么样从无标签的数据本身获得监督信息呢?一种方法是给定一个的句子,遮住最后一个 token,让模型去预测这个遮住的 token。这种预测下一个 token 的方式,是最早语言模型训练的方式,还没有 Transformer 之前,大家用的都是 LSTM 作为编码器,包括著名的 ULMFiT 和 ELMo。
一文看尽预训练语言模型_java_27如果我们把 LSTM 换成是 Self-attention 去做 PNT,就有了 GPT,GPT-2,GPT-3,Megatron,以及 Turing NLG 等模型。但使用自注意力有个要注意的点是,它不像 LSTM 那样会有先后顺序输入模型一个个进行编码。它一次性能看到上下文的每一个 token。因此我们需要设计好 MASK 矩阵来约束,好让做自注意力的时候看不到最后那个token。
一文看尽预训练语言模型_java_28为什么我们只通过让模型预测下一个 token 就可以得到我们想要的 embedding 呢?因为它符合语言学中,一个词汇的意思往往取决于它的上下文的准则。一文看尽预训练语言模型_java_29如果我们只是用 LSTM 从左往右过一遍句子,那预测下一个 token 所依赖的信息就只能取决于它左边的内容。为了能真的利用这个 token 的上下文。我们可以从右到左再过一遍句子,即用 BiLSTM 来做。这便是 ELMo。
一文看尽预训练语言模型_java_30而对于 Transformer 而言,自注意力能同时看上下文,每一个 token 两两之间都能交互,所以不需要像 LSTM 那样双向。只需要随机地把某个 token 用 MASK 遮住就可以了。
一文看尽预训练语言模型_java_31如果你回溯历史,回溯到7-8年前,Word2vec 刚刚掀起一波 NLP 革命的时候。你会发现 CBOW 的训练方式和 BERT 的训练方式,几乎一样。它们的一个关键性差别在,BERT 左右能关注的词的范围长度是灵活可变的,而 CBOW 是固定窗口。一文看尽预训练语言模型_java_32随机地 MASK 掉某个 token 是否真的是好方式呢?对于中文来说,词的粒度是由多个字组词。一个字为一个 token。如果我们随机 MASK 掉某个 token,模型可能不需要学很多语义依赖,就可以很容易地通过前面的字或后面的字来预测这个 token。为此我们可以把难度提升一点,盖住的不是某个 token,而是某个词 span,模型就需要用更多的语义去把遮住的 span 预测出来。这便是 BERT-wwm。同理,我们把词的 span 再延长一些,拓展成短语层面、实体层面。这些短语实体得用NER模型或知识图谱辅助识别出来。因此训练出来的模型能得到知识增强。一文看尽预训练语言模型_java_33还有一种增强方法叫 SpanBert。它每次会盖住一排 token 。每次要盖多长它是根据短语词频统计得到的一个分布。结果实验发现,这种基于 span 的预测方法能显著好于盖全词,盖短语或实体的方法。而且这种覆盖方法不需要 NER 或知识图谱辅助,非常方便。
一文看尽预训练语言模型_java_34SpanBert 中还提出了一种 SBO 的训练方法。一般我们训练只是把盖住的 MASK 的 tokens 给训练出来。而SBO做的是,希望被盖住范围的左右两边的嵌入,去预测被盖住的范围内,有什么样的东西。如图所示,SBO单元会输入 w3 和 w8 的嵌入,和一个指定数值。这个数值表示的是MASK span 中的第几个 token。如果输入的是3,就是要还原出 w5 的内容。为什么要这样呢?这样对共指解析非常有用。共指解析任务中,我们希望一个 span 前后两边字的嵌入包含整个 span 的内容。
一文看尽预训练语言模型_java_35XLNet 中的 XL 指的是 Transformer-XL。XL的好处是可以为跨越 segment 的信息编码,利用了相对位置编码。XLNet指出,原版的 BERT 会用 MASK 把 New York 这两个 token 一起盖住,这样我们就没有办法学到根据 New 去预测 York,或者根据 York 去预测 New。但如果在 BERT 中,对于同一个句子,我们是每次都随机去遮住 token,就可以有时盖住 New,有时盖住 York。这便是 RoBERTa。原版的 BERT 对于一个句子只会固定一开始随机要 MASK 的索引去预测。而 RoBERTa的这种方法可以解决 XLNet 提出的批判。XLNet 可以从两个观点去看。如果是从自回归语言模型去看,我们是要从左往右去一个个看句子中的词再预测下一个 token。
一文看尽预训练语言模型_java_36如果从 BERT 的观点来看,我们会把一些 token MASK 起来,然后根据整个句子的信息和 MASK 的位置信息去预测 MASK 的内容。在 XLNet 中,我们希望不是根据整个句子的信息,而是根据 MASK 左边的信息或 MASK 右边的信息。而且,与 BERT 不同在,XLNet 的输入没有 MASK 的存在。它给的只是 MASK token 所在的位置信息。即通过1、2个位置的token,去预测第三个位置的 token。
一文看尽预训练语言模型_java_37BERT 做生成任务并不是很好。因为我们要做生成,必须要有给定一段句子序列,让它能预测下一个 token 的能力。对 BERT 来说,并不是没有类似地训练过。比如它预训练时,会经历根据 w1-w3 预测第四个遮住的 token 的情况。但 BERT 并不是把遮住的 token 的右边或左边 token 的信息当作未知去预测。这种讨论都只局限于自回归模型。我们在说话、写字的时候,都是从左写到右。不会是左边写了一点,空出几格,再写右边,再把中间空出的格子补上。因此直觉上,正常产生句子的方式应该是一个自回归的过程。当然,非自回归的方式也可以产生句子。如何用更好的非自回归的方式去生成句子是当下值得研究的课题。
一文看尽预训练语言模型_java_38BERT 因为缺乏生成的能力。所以它不大适合做 Seq2Seq 的预训练语言模型。如果我们想要做 Seq2seq 的任务,BERT 只能当做编码器。解码器的部分,我们就没有训练到。我们有没有方法直接预训练一个 Seq2seq 的模型呢?答案是可以的。我们把一个句子输入编码器,解码时,希望它能重构回原来的句子。但我们必须把输入的句子做某种程度的破坏。因为如果没有任何破坏,模型可以直接复制粘贴输出,它可能学不到任何东西。所以我们要增加问题的难度。一文看尽预训练语言模型_java_39MASS 的做法是,把输入的一些部分随机用 MASK token 遮住。输出不一定要还原完整的句子序列,只要能把 MASK 的部分预测正确就结束了。在 BART 论文中,它又提出了各式各样的方法。除了给输入序列随机 MASK 以外,还可以直接删除某个 token。或者也可以随机排列组合,希望输出的句子能够把他们变成正确的语序。再或者用旋转的方式,把某些放在尾部的 tokens,放到前面去。还有一种方法叫 Text Infilling。我们会在句子中加入 MASK,可以在A 和 B 的 token 之间随机插入一个 MASK,做误导。也可以把一连串的 token ,比如 C 和 D 都 MASK。BART 的结论表示,排列组合和旋转的方式表现都不好,但文本填充的预训练方式能够稳定地好。
一文看尽预训练语言模型_java_40还有一个模型叫作 UniLM,它同时是编码器和解码器,还是Seq2seq 的模型。
一文看尽预训练语言模型_java_41UniLM 就是一个模型。它是很多 Transformer 堆叠起来的。它同时进行三种训练,包括 BERT 那样作为编码器的方式,GPT那样作为解码器的方式,以及 MASS/BART 那样作为 Seq2seq 的方式。它作为 Seq2seq 使用时,输入被分成两个片段,输入第一个片段的时候,该片段上的 token 之间可以互相注意,但第二个片段中,都只能看左边的 token。
一文看尽预训练语言模型_java_42到目前为止,我们都是要去预测一些序列的信息来实现自监督的预训练。ELECTRA 则采用了一种其它做法。预测一个东西需要的训练强度是很大的。ELECTRA 就想要避开预测需要生成这件事。ELECTRA 是一个希腊人物,恋母情结的意思。ELECTRA 预训练的时候,只做二分分类。它直接把输入随机替换成某个其它词汇,预测是该词汇是否被随机替换了。这样预训练起来重构就变得简单。而且每一个位置都有预测输出。一文看尽预训练语言模型_java_43但问题来了,怎样把一些词汇替换成其它词汇,可以产生文法上没有错,语义上不是怪怪的句子呢?因为如果被替换成了一些奇怪的 token,模型很容易就能发现。ELECTRA 就学不到什么厉害的东西。所以论文用另外一个比较小的 BERT 去产生被 MASK 的东西。这个看起来有点像 GAN,但它不是 GAN。因为生成器在训练的时候,要骗过判别器。而这里的小 BERT 是自己玩自己的。其实还有另外一个原因是,文本的 GAN 比较难训练。一文看尽预训练语言模型_java_44ELECTRA 的结果也很惊人。在相同的预训练量下,GLUE 上的分数显著比 BERT 要好。这样就会更加省资源。它只需要 1/4 的运算量,就能接近 XLNet 了。
一文看尽预训练语言模型_java_45
以上我们是想要给每一个 token 都有一个融合了上下文的局部嵌入。现在我们想给整个句子一个全局的嵌入。比如我们要给整个句子做分类的时候,希望有一个向量能代表这整个句子,而不是 N 个 token 的嵌入向量拼接在一起的矩阵。这种句嵌入要怎么做呢?
一文看尽预训练语言模型_java_46直觉上看,一个句子的句意取决于它上下文根哪些句子相邻。基于这样的想法,有一招叫 Skip Thought。通过训练一个 Seq2Seq 的模型,输入一个句子,预测它的下一个句子。如果有两个不同的句子,它们下一个接的句子都比较像,那么这两个句子就有类似的嵌入。但 Skip Thought 训练起来是比较困难的。因为生成任务的搜索空间巨大。由此我们有了 Quick Thought。它和 ELECTRA 的思路一样,想办法去避开做生成这一件事。它有两个编码器,分别把两个句子编码成向量,再做一个二分类预测它们是不是相邻顺接在一起的。如果是,则二者比较接近。
一文看尽预训练语言模型_java_47在原始的 BERT 之中,有一个叫作 CLS 的 token,我们希望它的嵌入代表了整个输入句子的嵌入。可是要怎么训练出这样的 embedding 呢?对 BERT 来说,它希望去解一个比较全局的任务。它输入是两个句子,用 SEP 分割开来。这两个句子是不是相邻接在一起的,CLS 这个 token 位置的分类器输出的就是 yes。这个任务叫作 NSP。后来这个任务在 RoBERTa 中正式发现,它不是很好。后来被一致证明用途不大。有另外一种方法叫作 SOP。我们给机器两个前后相邻的句子,BERT 要输出 Yes,但如果把二者顺序掉转过来,BERT 要输出 No。后来在 ALBERT 中有被用到。NSP 不起作用的根本原因是它这个任务太过简单,而 SOP 更加困难。还有一种方法是阿里提出的 strucBERT (Alice),里面也有用到类似 SOP 的想法,但是它有三个类别。相当于把 NSP 和 SOP 结合起来了。
一文看尽预训练语言模型_java_48预训练语言模型的预训练需要的资源太大,不是普通人随随便便就可以做的。谷歌有篇论文叫作 T5,它展现了谷歌强大的财力和运算资源。它把各式各样的预训练方法都尝试了一次,然后得到了一些结论。帮助日后研究这一领域的研究者踩坑,让别人没有研究可做。一文看尽预训练语言模型_java_49百度的 ERNIE 是希望在预训练的过程中,加入知识图谱的信息。日后会详细再讲。一文看尽预训练语言模型_java_50语音版的 BERT 叫作 Audio BERT。往后这篇论文的一作会有个解读。
后记:之后邱锡鹏还发了一篇预训练语言模型综述 PTMs,做了很全面的概述。但李宏毅的特点在知识之间串的非常好,能教人一步步去把一个问题理解得很透彻。

视频地址

https://www.youtube.com/watch?v=Bywo7m6ySlk&feature=youtu.be


Reference李宏毅 《人类语言处理 2020》BERT and its family



记得备注呦


一文看尽预训练语言模型_java_51