0. 背景
继上一篇博客,这篇主要讲一下BERT以及BERT衍生的模型,如RoBERTa ALBERT ERINE等模型的改进与优化效果。
不过首先还是我们先看下BERT。
1. BERT
BERT的全称叫Bidirectional Encoder Representations from Transformers,从论文题目和BERT英文全称,可以看到BERT做的是一个上下文的信息编码。整篇论文的主要比较对象是ELMo和GPT,ELMo和GPT的最大问题在于「不是真正的双向编码」
对比OpenAI GPT(Generative pre-trained transformer),BERT是双向的Transformer block连接;就像单向RNN和双向RNN的区别,直觉上来讲效果会好一些。
)P(w_i|w_1..w_{i-1}) 和 P(w_i|w_{i 1}..w_n) 作为目标函数,独立训练处两个representation然后拼接,而BERT则是以P(w_i|w1..w_{i-1},w_{i 1}..w_n) 作为目标函数训练LM。
- GPT利用的是Transformer结构的Decoder,所以肯定不是双向的,
- ELMo虽然把LSTM的正向向量和反向向量拼接在一起,但并不是真正的双向(想想,拼接但是并没有发生交互),这样的网络结构对下游任务非常不利的,举个例子,做问答任务的时候,从两个方向编码上下文是非常重要的。
BERT用到的是transformer的encoder部分,编码每个token的时候考虑了所有input token的交互,「所以BERT是真正的双向编码模型」
这里简要说明一下向量模型中的feature-based与fine-tunning范式:
- feature-based范式(特征抽取)
代表作如EMLo,在17年之前,Transformer没出的时候,大家最常解决NLP任务的方法就是「用别人训练好的词向量作为embedding」,然后后面接各种全新初始化的RNN/LSTM/CNN等网络结构,也就是预训练只提供了feature-based的embedding。
使用feature-based将不会对模型权重进行更新。
- fine-tunning范式(微调)
代表作如GPT,用于下游任务时,不仅仅保留了输入的embedding,Transformer里面的参数(如attention层、全连接层)也同样可以保留,在fine-tuning的时候只需在原来的Transfomer上加一些简单的层,就可以应用于具体的下游任务。
BERT当然也是属于fine-tuning范式。
使用feature-based将会对模型权重进行更新。
1.1 模型架构
BERT提供了一种解决各种下游任务的统一结构。当我们要对具体的任务做微调时,我们只需要在原来的结构上面增加一些网络层就OK了,「这样预训练的网络结构和具体下游任务的网络结构差别很小,有助于把BERT预训练时学习到的特征尽可能保留下来」。
模型可以简单的归纳为三个部分,分别是输入层,中间层,以及输出层。这些都和transformer的encoder一致,除了输入层有略微变化
输入层
为了使得BERT模型适应下游的任务(比如说分类任务,以及句子关系QA的任务),输入将被改造成CLS 片段A SEP (片段B SEP)
其中
- CLS: 代表的是分类任务的特殊token,它的输出就是模型的pooler output
- SEP:分隔符
- 片段A以及句子B是模型的输入文本,其中片段B可以为空,则输入变为CLS 片段ASEP
因为trasnformer无法获得字的位置信息,BERT和transformer一样也加入了 绝对位置 position encoding,但是和transformer不同的是,BERT使用的是不是transformer对应的函数型(functional)的encoding方式,而是直接采用类似word embedding的方式(Parametric),直接获得position embedding。
因为我们对输入进行了改造,使得模型可能有多个句子Segment的输入,所以我们也需要加入segment的embedding,例如[CLS], A_1, A_2, A_3,[SEP], B_1, B_2, B_3, [SEP]
对应的segment的输入是[0,0,0,0,1,1,1,1]
, 然后在根据segment id进行embedding_lookup得到segment embedding。 code snippet如下。
tokens.append("[CLS]")
segment_ids.append(0)
for token in tokens_a:
tokens.append(token)
segment_ids.append(0)
tokens.append("[SEP]")
segment_ids.append(0)
for token in tokens_b:
tokens.append(token)
segment_ids.append(1)
tokens.append("[SEP]")
segment_ids.append(1)
输入层为三个embedding相加(position embedding segment embedding token embedding)这是为什么?
首先我们简单地假设我们有一个token,我们假设我们的字典大小(vocabulary_size) = 5, 对应的的token_id 是2,这个token所在的位置是第0个位置,我们最大的位置长度为max_position_size = 6,以及我们可以有两种segment,这个token是属于segment = 0的情况。
首先我们分别对三种不同类型的分别进行 embedding lookup的操作,下面的代码中我们,固定了三种类型的embedding matrix,分别是token_embedding,position_embedding,segment_embedding。首先我们要清楚,正常的embedding lookup就是embedding id 进行onehot之后,然后在和embedding matrix 进行矩阵相乘,具体看例子中的 embd_embd_onehot_impl 和 embd_token,这两个的结果是一致的。
我们分别得到了三个类别数据的embedding之后(embd_token, embd_position, embd_sum),再将它们进行相加,得到embd_sum。
其结果跟,将三个类别进行onehot之后的结果concat起来,再进行embedding lookup的结果是一致的。比如下面,我们将token_id_onehot, position_id_onehot, segment_id_onehot 这三个onehot后的结果concat起来得到concat_id_onehot, 与三者的embedding matrix的concat后的结果concat_embedding,进行矩阵相乘,得到的结果 embd_cat。
可以发现 embd_sum == embd_cat。 具体参照下面代码。
代码语言:txt复制import tensorflow as tf
token_id = 2
vocabulary_size = 5
position = 0
max_position_size = 6
segment_id = 0
segment_size = 2
embedding_size = 4
token_embedding = tf.constant([[-3.,-2,-1, 0],[1,2,3,4], [5,6,7,8], [9,10, 11,12], [13,14,15,16]]) #size: (vocabulary_size, embedding_size)
position_embedding = tf.constant([[17., 18, 19, 20], [21,22,23,24], [25,26,27,28], [29,30,31,32], [33,34,35,36], [37,38,39,40]]) #size:(max_position_size, embedding_size)
segment_embedding = tf.constant([[41.,42,43,44], [45,46,47,48]]) #size:(segment_size, embedding_size)
token_id_onehot = tf.one_hot(token_id, vocabulary_size)
position_id_onehot = tf.one_hot(position, max_position_size)
segment_id_onehot = tf.one_hot(segment_id, segment_size)
embd_embd_onehot_impl = tf.matmul([token_id_onehot], token_embedding)
embd_token = tf.nn.embedding_lookup(token_embedding, token_id)
embd_position = tf.nn.embedding_lookup(position_embedding, position)
embd_segment = tf.nn.embedding_lookup(segment_embedding, segment_id)
embd_sum = tf.reduce_sum([embd_token, embd_position, embd_segment], axis=0)
concat_id_onehot = tf.concat([token_id_onehot, position_id_onehot, segment_id_onehot], axis=0)
concat_embedding = tf.concat([token_embedding, position_embedding, segment_embedding], axis=0)
embd_cat = tf.matmul([concat_id_onehot], concat_embedding)
with tf.Session() as sess:
print(sess.run(embd_embd_onehot_impl)) # [[5. 6. 7. 8.]]
print(sess.run(embd_token)) # [5. 6. 7. 8.]
print(sess.run(embd_position)) # [17. 18. 19. 20.]
print(sess.run(embd_segment)) # [41. 42. 43. 44.]
print(sess.run(embd_sum)) # [63. 66. 69. 72.]
print(sess.run(concat_embedding))
'''
[[-3. -2. -1. 0.]
[ 1. 2. 3. 4.]
[ 5. 6. 7. 8.]
[ 9. 10. 11. 12.]
[13. 14. 15. 16.]
[17. 18. 19. 20.]
[21. 22. 23. 24.]
[25. 26. 27. 28.]
[29. 30. 31. 32.]
[33. 34. 35. 36.]
[37. 38. 39. 40.]
[41. 42. 43. 44.]
[45. 46. 47. 48.]]
[[63. 66. 69. 72.]]
'''
print(sess.run(concat_id_onehot)) # [0. 0. 1. 0. 0. 1. 0. 0. 0. 0. 0. 1. 0.]
print(sess.run(embd_cat)) # [[63. 66. 69. 72.]]
中间层
模型的中间层和transformer的encoder一样,都是由self-attention layer ADD&BatchNorm layer FFN 层组成的。
输出层
模型的每一个输入都对应这一个输出,根据不同的任务我们可以选择不同的输出,主要有两类输出
- pooler output:对应的是CLS的输出。
- sequence output:对应的是所有其他的输入字的最后输出。
1.2 模型输入
「WordPiece」
在模型输入的时候,并非是具体的单词,而是WordPiece,具体的,我们看谷歌发布的原生BERT的vocab词表,有一些英文单词是带有##前缀的,例如##bed等等,如embedding这个单词,通过WordPiece会拆分成em、##bed、##d、##ing,带有##前缀的单词表示它是单词的一部分,而不是完成的单词(所以在词表里面bed、##bed,它们的含义是完全不一样的),具体的可以搜索一下BPE,引入WordPiece作为输入可以有效缓解OOV问题。
至于中文,个人认为还是单字作为输入,因为中文很难像英文一样,再进行拆分下去。
「Segment Pairs输入」
BERT引入了句子对作为输入,为什么要引入句子对作为输入,是为了让BERT能应对更多的下游任务(例如句子相似度任务,问答任务等都是多句输入)。注意!这里的""句子"是「广义的,表示的并非是单句,而是一段文章的连续片段,可以包含一个句子或多句句子」,所以输入的时候,其实是可能不止两个句子的。
感觉原文就不应该用Sentence pairs来表达,而应该用Segment Pairs。在后面的RoBERTa实验里验证,假如用单句拼接作为句子对相对于用连续片段拼接作为句子对,其实是损害性能的。
1.3 预训练任务
1.3.1 Masked LM(MLM)
该任务类似于高中生做的英语完形填空,把输入的句子对进行WordPiece处理后,将语料中句子的15%的token进行遮盖,使用 [MASK]
作为屏蔽符号,然后预测被遮盖词是什么。例如对于原始句子 my dog is hairy
,屏蔽后 my dog is [MASK]
。该任务中,隐层最后一层的 [MASK]
标记对应的向量会被喂给一个对应词汇表的 softmax 层,进行单词分类预测。
Q:为什么选中的15%的wordpiece token不能 全部 用 MASK代替,而要用 10% 的 random token 和 10% 的原 token
MASK 是以一种显式的方式告诉模型『这个词我不告诉你,你自己从上下文里猜』,从而防止信息泄露。如果 MASK 以外的部分全部都用原 token,模型会学到『如果当前词是 MASK,就根据其他词的信息推断这个词;如果当前词是一个正常的单词,就直接抄输入』。这样一来,在 finetune 阶段,所有词都是正常单词,模型就照抄所有词,不提取单词间的依赖关系了。
以一定的概率填入 random token,就是让模型时刻堤防着,在任意 token 的位置都需要把当前 token 的信息和上下文推断出的信息相结合。这样一来,在 finetune 阶段的正常句子上,模型也会同时提取这两方面的信息,因为它不知道它所看到的『正常单词』到底有没有被动过手脚的。
Q:最后怎么利用MASK token做的预测?
最终的损失函数只计算被mask掉的token的,每个句子里 MASK 的个数是不定的。实际代码实现是每个句子有一个 maximum number of predictions,取所有 MASK 的位置以及一些 PADDING 位置的向量拿出来做预测(总共凑成 maximum number of predictions 这么多个预测,是定长的),然后再用掩码把 PADDING 盖掉,只计算MASK部分的损失。
但这会引起一个问题:「预训练和下游任务,输入不一致,因为下游任务的时候,输入基本上是不带【MASK】的,这种不一致会损害BERT的性能」,这也是后面研究的改善方向之一),当然BERT自身也做出了一点缓解,就是并非15%的token都用【MASK】代替,而是15%的80%用【MASK】代替,10%用随机的词代替,10%用原来的词保持不变。
1.3.2 Next Sentence Prediction(NSP)
判断句子对是否是真正连续的句子对。
语料中的句子都是有序邻接的,我们使用 [SEP]
作为句子的分隔符号,[CLS]
作为句子的分类符号,现在对语料中的部分句子进行打乱并拼接,形成如下这样的训练样本:
Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP]
Label = IsNext
Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP]
Label = NotNext
上图下方3个Embedding具体含义如下:
Token Embedding
就是正常的词向量,即 PyTorch 中的nn.Embedding()
Segment Embedding
的作用是用 embedding 的信息让模型分开上下句,我们给上句的 token 全 0,下句的 token 全 1,让模型得以判断上下句的起止位置,例如
[CLS]我的狗很可爱[SEP]企鹅不擅长飞行[SEP]
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
Position Embedding
和 Transformer 中的不一样,不是三角函数,而是学习出来的
输入网络后,针对隐层最后一层 [CLS]
符号的词嵌入做 softmax 二分类,做一个预测两个句子是否是相邻的二分类任务。
可以看出,这两种任务都在训练过程中学习输入标记符号的 embedding,再基于最后一层的 embedding 仅添加一个输出层即可完成任务。该过程还可能引入一些特殊词汇符号,通过学习特殊符号譬如 [CLS]
的 embedding 来完成具体任务。
1.3.3 消融实验
对比去掉NLP任务和把原来的MLM任务改成LTR(Left-to-Right)任务,实验结果如下表,表明原来的MLM和NSP任务缺一不可。
模型规模越大,性能越好
「把BERT作为feature-based范式,而不是fine-funing范式」。具体的做法是把BERT的某些层的向量拿出来,作为token的embedding(这些embedding在后面的fine-tuning任务中不更新),可类比下,用word2vec作为token的特征,然后后面接具体的任务层,只不过这里的word2vec向量用BERT的某些层的输出作为代替,假如直接用BERT embeddings作为Feature,自然每个token的feature都是固定的(这就有点像用预训练好的的word2vec向量作为特征),如果取后面的层(每个token的feature不一样,有点像ELMo),实验证明,BERT无论是作为feature-based还是fine-tuning方法都是非常有效的。
1.4 Fine-tuning
首先对原有模型做适当构造,一般仅需加一输出层完成任务。Bert 的论文对于若干种常见任务做了模型构造的示例,如下:
图 a 和 b 是序列级别的任务,c 和 d 是词级别的任务。a 做句子对分类任务,b 做单句分类任务,构造非常简单,将图中红色箭头指的 [CLS]
对应的隐层输出接一个 softmax 输出层。c 做的是阅读理解问题,d 做的是命名实体识别(NER),模型构造也类似,取图中箭头指出的部分词对应的隐层输出分别接一个分类输出层完成任务。
类似以上这些任务的设计,可以将预训练模型 fine-tuning 到各类任务上,但也不是总是适用的,有些 NLP 任务并不适合被 Transformer encoder 架构表示,而是需要适合特定任务的模型架构。因此基于特征的方法就有用武之地了。
1.4.1 分类任务classification
在输入句子的开头加一个代表分类的符号[CLS]
,然后将该位置的 output,丢给 Linear Classifier,让其 predict 一个 class 即可。整个过程中 Linear Classifier 的参数是需要从头开始学习的,而 BERT 中的参数微调就可以了。
为什么要用第一个位置输出作为分类依据呢?
因为 BERT 内部是 Transformer,而 Transformer 内部又是 Self-Attention,所以[CLS]
的 output 里面肯定含有整句话的完整信息,这是毋庸置疑的。但是 Self-Attention 向量中,自己和自己的值其实是占大头的,现在假设使用w1的 output 做分类,那么这个 output 中实际上会更加看重w1,而w1又是一个有实际意义的字或词,这样难免会影响到最终的结果。但是[CLS]
是没有任何实际意义的,只是一个占位符而已,所以就算[CLS]
的 output 中自己的值占大头也无所谓。
当然也可以将所有词的 output 进行 concat,作为最终的 output。
1.4.2 槽填充 slot filling
将句子中各个字对应位置的 output 分别送入不同的 Linear,预测出该字的标签。其实这本质上还是个分类问题,只不过是对每个字都要预测一个类别
1.4.3 自然语言推理NLI
即给定一个前提,然后给出一个假设,模型要判断出这个假设是 正确、错误还是不知道。这本质上是一个三分类的问题,和 Case 1 差不多,对[CLS]
的 output 进行预测即可
1.4.4 问答QA
将一篇文章,和一个问题(这里的例子比较简单,答案一定会出现在文章中)送入模型中,模型会输出两个数 s,e,这两个数表示,这个问题的答案,落在文章的第 s 个词到第 e 个词。
首先将问题和文章通过[SEP]
分隔,送入 BERT 之后,得到上图中黄色的输出。此时我们还要训练两个 vector,即上图中橙色和黄色的向量。首先将橙色和所有的黄色向量进行 dot product,然后通过 softmax,看哪一个输出的值最大,例如上图中d2对应的输出概率最大,那我们就认为 s=2
同样地,我们用蓝色的向量和所有黄色向量进行 dot product,最终预测得 d3 的概率最大,因此 e=3。最终,答案就是 s=2,e=3
最终的输出 s>e 为没有答案。
1.5 结果-9项GLUE任务
General Language Understanding Evaluation
包含了很多自然语言理解的任务。
1. MNLI
Multi-Genre Natural Language Inference是一个众包大规模的文本蕴含任务。
给2个句子,判断第二个句子与第一个句子之间的关系。蕴含、矛盾、中立的
2. QQP
Quora Question Pairs
给2个问题,判断是否语义相同
3. QNLI
Question Natural Language Inference 是一个二分类任务,由SQuAD数据变成。
给1个(问题,句子)对,判断句子是否包含正确答案
4. SST-2
Stanford Sentiment Treebank,二分类任务,从电影评论中提取。
给1个评论句子,判断情感
5. CoLA
The Corpus of Linguistic Acceptablity,二分类任务,判断一个英语句子是否符合语法的
给1个英语句子,判断是否符合语法
6. STS-B
The Semantic Textual Similarity Benchmark,多分类任务,判断两个句子的相似性,0-5。由新闻标题和其他组成
给2个句子,看相似性
7. MRPC
Microsoft Research Paraphrase Corpus,2分类任务,判断两个句子是否语义相等,由网上新闻组成。05年的,3600条训练数据。
给1个句子对,判断2个句子语义是否相同
8. RTE
Recognizing Textual Entailment,二分类任务,类似于MNLI,但是只是蕴含或者不蕴含。训练数据更少
9. WNLI
Winograd NLI一个小数据集的NLI。据说官网评测有问题。所以评测后面的评测没有加入这个
GLUE评测结果
对于sentence-level
的分类任务,只用CLS
位置的输出向量来进行分类。
SQuAD-v1.1
SQuAD属于token-level
的任务,不是用CLS位置,而是用所有的文章位置的向量去计算开始和结束位置。
Finetune了3轮,学习率为5∗10−55∗10−5,batchsize为32。取得了最好的效果
NER
SWAG
The Situations With Adversarial Generations 是一个常识性推理数据集,是一个四分类问题。给一个背景,选择一个最有可能会发生的情景。
Finetune了3轮,学习率为2∗10−52∗10−5,batchsize=16
1.6 BERT不足与后序改进方向
- 模型太大,训练太慢 对于模型太大:BERT的模型压缩,各种DistilBERT,BERT-PKD以及华为的TinyBERT,都提出了基于知识蒸馏的方式进行模型压缩,并且达到了非常好的效果。同时Albert也进行了模型结构的改造,使用了parameter sharing以及embedding的factorization,使得模型参数减少,模型效果更好。 对于训练太慢:
- 可以使用LAMB 优化器,使得模型可以使用更大的batch size
- 使用Nvidia的Mixed Precision Training,可以在不降低模型的效果的基础上,大大降低参数的储存空间
- 此外由于transformer的限制,google最新提出的REFORMER : THE EFFICIENT TRANSFORMER,将时间和空间复杂度降低至$O(LlogL)$, 相信会是最新的研究前景。
- 分布式训练
- tensorRT加速预测(GPU),CPU加速参考(bert-as-service)。
- vocabulary size非常大,对于中文推荐使用CLUE Benchmark 团队的预训练模型的vocabulary,能大大提升速度,词汇表大小变小,但是精度不变。
- 完全训练了? RoBERTa 提出,BERT并没有完全训练,只要使用更多的数据,训练更多的轮次,就可以得到超过XLNET的效果。同时Albert也提出了,模型在大数据集上并没有overfitting,去掉了dropout之后,效果更好了。
- 训练的效率高吗? 在预训练中,我们只通过15%的masked tokens去更新参数,而85%的token对参数更新是没有起到作用的,ELECTRA论文中发现,采用100%的tokens能有效的提高模型效果。
- Position encoding的方法好吗? bert 使用的是绝对的参数型的position encoding,华为基于bert提出的中文预训练模型NEZHA中提出一种新的函数型的相对位置的position encoding的方法,并且说比目前的bert的方式更优。同时Self-Attention with Relative Position Representations这篇文章也提出了一种relative position encoding的方式,并且在实作上比transformer更好。
- MASK的机制好吗? BERT采用的是MASK wordpiece的方式,百度的ERNIE,BERT-WWM,以及SpanBERT都证明了mask连续一段的词效果比mask wordpiece更优。 此外,RoBERT采用的了一种Dynamic Masking的方式,每一次训练的时候动态生成MASK。 此外,MASK的机制在finetuning阶段没有,这个是模型的一个比较大的问题。
- MASK token在预训练中出现,但是在finetuning中没有出现。 这个问题XLNET结果的办法是使用Auto-regression,而在ELECTRA中,采用的是通过一个小的generator去生成replaced token,但是ELECTRA 论文中指出,这个discrepancy对效果的影响是小的。
- Loss有用吗? XLNET,SpanBERT,RoBERTa,和ALbert都分析发现NSP loss对模型的下游任务起到了反作用,Albert给出了具体的解析。
- Loss够吗? 对于后面的研究,不同的模型都或多或少加了自己的定义的loss objectives,例如Albert的SOP等,微软的MT-DNN甚至直接把下游的任务当作预训练的多任务目标,ERNIE2.0提出了多种不同的训练目标,这些都证明,语言模型的强大,并且多个不同的loss是对模型训练有效果的。
- 不能够做自然语言生成NLG XLNET以及GPT都是auto regressive 的模型,可以生成语言,但是BERT的机制限制了它的这种能力,但是目前的研究发现经过改变mask的机制,可以让BERT模型具备NLG的能力,统一了NLU和NLG的榜单,参见UniLM
2. RoBERTa
RoBERTa的全称叫做Robustly optimized BERT approach。RoBERTa之于BERT的改动很简单,主要是用了更多的数据,训练上,采用动态【MASK】、去掉下一句预测的NSP任务、更大的batch_size、文本编码。
最终效果:
下面简单阐述一下不同点;
2.1 动态MASK
预训练的每一个step,是重新挑选15%token进行【MASK】的,而BERT是固定的,就是对于同一个输入样本,在不同的epoch,输入是一样的,实验结果见下图,有很微弱的提升吧。
2.2 去NSP任务
FULL-SENTENCES和DOC-SENTENCES都是去掉NSP任务的,可以看到去掉NSP任务表现都比原来的要好,FULL-SENTENCES是可以跨文档来采样句子。DOC-SENTENCES是保证采样的句子都在同一个文档里面,可以看到DOC-SENTENCES表现稍微好一点。
最后的RoBERTa是采用去掉NSP而且一个样本是从同一个文档里面进行采样。
2.3 更大batch_size
BERT的batch_size是256,一共训练了1M步,实验证明,采用更大的batch_size以及训练更多步,可以提高性能,所以最后的RoBERTa采用的batch_size是8K。
3. ALBERT
ALBERT的全程是A Lite BERT,提出一种减少参数的方法同时可以增加模型规模,还提出SOP训练任务。
实质上,ALBERT-large版本的性能是比BERT-large版本的性能差的,大家所说的性能好的ALBERT版本是xlarge和xxlarge版本,而这两者模型,虽然都比BERT-large参数量少,但由于模型规模变大了,所以训练时间是变慢的,推断速度也变慢了。
所以ALBERT也不是如名字说的,属于轻量级模型。
由于模型的参数变少了,所以,我们可以训练规模更大的网络,具体的ALBERT-xxlarge版本也是12层,但是hidden_size为4096!控制BERT-large和ALBERT-xxlarge的训练时间一样,可以看到ALBERT-xxlarge版本的训练速度时间只有BERT-large的1/3左右,慢了不少,这是模型规则变大的副作用。但由于模型规则变大了,所以模型性能也得到了一定的提升,大家常说刷榜的ALBERT,其实是xxlarge版本,普通的large版本性能是比BERT的large版本要差的。
下面看看ALBERT与BERT 优化点;
3.1 减少参数
3.1.1 矩阵分解
这是从输入的embedding维度去减少参数,BERT采用的是WordPiece,大概有3K个token,然后原生BERT采用的embedding size是跟hidden size一样的,都为768,所以参数量约为3000 * 768 = 2304000。假如我们通过一个矩阵分解去代替本来的embedding矩阵,如上图所示,E取为128,则参数量变为3000 * 128 128 * 768=482304,参数量变为原来的20%!
思考一个问题:这样的分解会影响模型的性能吗?作者给出的角度是,WordPiece embedding是跟上下文独立的,hidden-layer embedding(即Transformer结构每一个encoder的输出)是跟上下文有关的,而BERT的强大主要是attention机制,即根据上下文给出token的表示,所以WordPiece embedding size不需要太大,因为WordPiece embedding不是BERT这么强的主要原因。
3.1.2 参数共享
思想就是,BERT的Transformer共用了12层的encoder,让这12层的attention层和全连接层层共享参数,作者还发现这样做对稳定网络参数有一定的作用。其实看下表的实验结果,全共享(attention层和全连接层都共享)是比单纯共享attention层的效果要差的,但是全共享d减少的参数实在太多了,所以作者采用的为全共享。
3.2 SOP代替NSP
后面的研究者发现,NSP给BERT带来不好的影响,主要原因是跟MLM任务相比,任务难度太小了。
具体的,把NSP分别topic prediction(主题预测)和coherence prediction(一致性预测),很明显NSP是比较偏向主题预测的(预测句子对是否是同一文档的连续片段),而topic prediction相对clherence prediction是比较简单的。
SOP将负样本换成了同一篇文章中的两个逆序的句子,从而消除topic prediction,让模型学习更难得coherence prediction。
3.3 n-gram MASK
预测n-gram片段,包含更完整的语义信息。每个片段的长度取值n(论文里取最大为3)。根据公式取1-gram、2-gram、3-gram的概率分别为6/11,3/11,2/11。越长概率越小。
4. ERINE 1.0
ERNIE1.0采用与BERT一样的架构,与BERT有所区别的是,在于训练任务的不同。
下面简述一下改进点
4.1 Knowledge Integration
具体的,把MASK分成三部分
- Basic-level Masking:与BERT一样
- Entity-level Masking:把实体作为一个整体MASK,例如J.K.Rowling这个词作为一个实体,被一起【MASK】
- Phrase-Level Masking:把短语作为一个整体MASK,如a series of作为一个短语整体,被一起【MASK】
4.2 Dialogue Language Model(DLM)
增加了对话数据的任务,如下图所示,数据不是单轮问答的形式(即问题 答案),而是多轮问答的数据,即可以是QQR、QRQ等等。同上面一样,也是把里面的单token、实体、短语【MASK】掉,然后预测它们,另外在生成数据的时,有一定几率用另外的句子替代里面的问题和答案,所以模型还要预测是否是真实的问答对。论文提到DLM任务能让ERNIE学习到对话中的隐含关系,增加模型的语义表达能力。
注意看Segment Embedding被Dialogue Embedding代替了,但其它结构跟MLM模型是一样的,所以可以和MLM任务联合训练。
5. ERINE 2.0
ERNIE2.0的结构与 ERNIE1.0 、BERT 的结构一样,ERNIE2.0 主要是从修改预训练的学习任务来提升效果。从BERT推出,到现在被广泛使用也有近三年的时间,这几年也有不少其它预训练模型的出现,它们大部分干的一件事就是「提出难度更大、更多样化的预训练任务,从而增加模型的学习难度,让模型有更好的词语、语法、语义的表征能力」ERNIE2.0正是如此,构建了三种类型的无监督任务。为了完成多任务的训练,又提出了连续多任务学习,整体框架见下图。
5.1 改进1:连续多任务学习
假如让模型同时学3个任务(就是目前比较火的联合训练)这里提供三种策略:
- 策略一,Multi-task Learning,就是让模型同时学这3个任务,具体的让这3个任务的损失函数权重双加,然后一起反向传播;
- 策略二,先训练任务1,再训练任务2,再训练任务3,这种策略的缺点是容易遗忘前面任务的训练结果,如最后训练出的模型容易对任务3过拟合;
- 策略三:连续多任务学习,即第一轮的时候,先训练任务1,但不完全让他收敛训练完,第二轮,一起训练任务1和任务2,同样不让模型收敛完,第三轮,一起训练三个任务,直到模型收敛完。
论文里采用的就是策略三的思想。
具体的,如下图所示,每个任务有独立的损失函数,句子级别的任务可以和词级别的任务一起训练
5.2 改进2:更多的无监督预训练任务
模型的结构如下图所示,由于是多任务学习,模型输入的时候额外多了一个Task embedding。
具体的三种类型的无监督训练任务是哪三种呢?每种里面又包括什么任务呢?
- 任务一:词法级别预训练任务
- Knowledge Masking Task:这任务同ERNIE 1.0一样,把一些字、短语、实体【MASK】掉,预测【MASK】词语。
- Capitalization Prediction Task:预测单词是大写还是小写,大写出现在实体识别等,小写可用于其他任务。
- Token-Document Relation Prediction Task:在段落A中出现的token,是否在文档的段落B中出现。
- 任务二:语言结构级别预训练任务
- Sentence Reordering Task:把文档中的句子打乱,识别正确顺序。
- Sentence Distance Task:分类句子间的距离(0:相连的句子,1:同一文档中不相连的句子,2:两篇文档间的句子)。
- 任务三:语句级别预训练任务
- Discourse Relation Task:计算两句间的语义和修辞关系。
- IR Relevance Task:短文本信息检索关系,搜索数据(0:搜索并点击,1:搜素并展现,2:无关)。
这些任务全是「无监督的预训练任务」!
6. ELECTRA
ELECTRA是这几年一个比较创新的模型,从模型架构和预训练任务都和BERT有一定程度的不同。ELECTRA的全称是Efficiently Learning an Encoder that Classifies Token Replacements Auucrately,在论文的开始指出了BERT训练的一个缺点,就是「学习效率太慢」,因为模型从一个样本中只能学习到15%的token信息,所以作者提出了一种新的架构让模型能学习到所有输入token的信息,而不仅仅是被【MASK】掉的tioken,这样模型学习效率会更好。作者指出,ELECTRA用相同的数据,达到和BERT、RoBERTa、XLNET相同效果所需要的训练轮数更少,假如使用相同的训练轮数,将超越上面所说的模型。
「但看哈工大发布的中文ELECTRA模型来看,发现并没有比BERT等要好,甚至在一些中文任务上表现反而要差了,对于这个模型,相信大家目前还是有很多争议的。」
6.1 新的架构:Generator-Discriminator的架构
ELECTRA的结构很简单,由一个Generator生成器和一个DIscriminator判别器组成,如下图所示。首先对一句话里面的token进行随机的【MASK】,然后训练一个生成器,对【MASK】掉的token进行预测,通常生成器不需要很大(原因在后面的实验部分有论证),生成器对【MASK】掉的token预测完后,得到一句新的话,然后输入判别器,判别器判断每个token,是否是原样本的,还是被替换过的。
注意的是,假如生成器预测出的token为原来的token,那这个token在判别器的输出标签里还是算原样本,而不是被替换过的(如下图的the,生成器预测为the,则the在判别器中的真实标签就为original,而不是replaced)。自此,整个模型架构的思想就介绍完了,是否很简单?
6.2 权重共享
假如生成器和判别器采用同样架构的话,则两个模型可以权重共享,假如不是同样架构的话,也可以共享embedding层。所以作者分别对以下三种情况做了实验:
- 生成器和判别器的参数独立,完全不共享;
- 生成器和判别器的embedding参数共享,而且生成器input层和output层的embedding参数共享(想想为什么可以这样?因为生成器最后是一个全词表的分类,所以可以跟输入时embedding矩阵的维度一致,而判别器最后是一个二分类,所以不能共享输入时的embedding矩阵),其他参数不共享;
- 生成器和判别器的参数共享。
第一种方案GLUE score为83.6,第二种方案GLUE score为84.3,第三种方案GLUE score为84.4,从结果上,首先肯定的是共享参数能带来效果的提升,作者给出的理由是,假如不共享参数,判别器只会对【MASK】的token的embedding进行更新,而生成器则会对全词表进行权重更新(这里有疑惑的可以想想,生成器最后可是做了一个全词表的分类哦),「所以共享参数肯定是必要的」,至于为什么作者最后采用方案二是不是方案三呢,是因为假如采用方案三的话,限定了生成器和判别器的模型结构要一样,极大影响了训练的效率。
7. XLNET
首先介绍两种无监督目标函数:
- AR(autoregressive):自回归,假设序列数据存在线性关系,用 x0..xt-1 预测 xt 。以前传统的单向语言模型(ELMo、GPT)都是以AR作为目标。
- AE(autoencoding):自编码,将输入复制到输出。BERT的MLM就是AE的一种。
AR是以前常用的方法,但缺点是不能进行双向的编码。因此BERT采用了AE,获取到序列全局的信息。但本文作者指出了BERT采用AE方法带来的两个问题:
- BERT有个不符合真实情况的假设:即被mask掉的token是相互独立的。比如预训练时输入:“自然Mask处理”,目标函数其实是 p(语|自然处理) p(言|自然处理),而如果使用AR则应该是 p(语|自然) p(言|自然语)。这样下来BERT得到的概率分布也是基于这个假设的,忽略了这些token之间的联系。
- BERT在预训练和精调阶段存在差异:因为在预训练阶段大部分输入都包含Mask,引入了噪声,即使在小部分情况下使用了其他token,但仍与真实数据存在差异。
以上就是BERT采用AE方法存在的痛点,接下来请看XLNet如何解决这些问题。
其实作者最后的实验也不是太充分,没有和BERT做充分的平等比较。不过新的想法还是难得的:
- Permutation Language Modeling:先给我们统一了之前语言模型的思想框架(AR or AE),再一个permutation把两者的优点结合起来,而且整体框架又回归到了AR,感觉生成模型的新SOTA指日可待。
- Transformer-XL Relative segment encoding:这个不是作者重点强调的,但却让我觉得很有用处,目前短文本的任务还好,文本一长难度就会上去,段落级甚至文章级,这两个操作让我看到了NLU在长文本上取得更大成果的可能。
7.1 PLM Permutation Language Modeling
与其说XLNet解决了BERT的问题,不如说它基于AR采用了一种新的方法实现双向编码,因为AR方法不存在上述两个痛点。
- 理论上
对于长度为T的序列x,存在T!种排列方法,如果把 x1,x2,x3,x4重新排列成 x2,x1,x4,x3 ,再采用AR为目标函数,则优化的似然为
因为对于不同的排列方式,模型参数是共享的,所以模型最终可以学习到如何聚集所有位置的信息。
- 操作上
由于计算复杂度的限制,不可能计算所有的序列排列,因此对于每个序列输入只采样一个排列方式。而且在实际训练时,不会打乱序列,而是通过mask矩阵实现permutation。作者特意强调,这样可以保持与finetune输入顺序的一致,不会存在pretrain-finetune差异。
7.1.1 Two-Stream Self-Attention
解决了核心问题,接下来就是实现的细节问题了。其实上面打乱顺序后有一个很大的问题,就是在预测第三个x的时候模型预测的是 P(x4|x2,x1) ,如果把排列方式换成x2,x1,x3,x4 ,则应该预测 P(x3|x2,x1) ,但模型是不知道当前要预测的是哪一个,因此输出的值是一样的,即 P(x3|x2,x1) ,这就不对了。所以说要加入位置信息,即 P(x4|x2,x1,4)和 P(x3|x2,x1,3) ,让模型知道目前是预测哪个位置的token。
那下一个问题又来了,传统的attention只带有token编码,位置信息都在编码里了,而AR目标是不允许模型看到当前token编码的,因此要把position embedding拆出来。怎么拆呢?作者就提出了Two-Stream Self-Attention。
Query stream:只能看到当前的位置信息,不能看到当前token的编码
Content stream:传统self-attention,像GPT一样对当前token进行编码
预训练阶段最终预测只使用query stream,因为content stream已经见过当前token了。在精调阶段使用content stream,又回到了传统的self-attention结构。
下面的图起码看3遍~看懂为止,图比我讲的明白。。
另外,因为不像MLM只用预测部分token,还需要计算permutation,XLNet的计算量更大了,因此作者提出了partial prediction进行简化,即只预测后面1/K个token。
7.2 借鉴Transformer-XL
7.2.1 Segment-level recurrence with state reuse
由于内存和算力的限制,目前长文本都需要进行截断处理,比如BERT的长度是512,无法直接处理长篇文章。作者参考RNN的隐藏记忆单元,提出Transformer-XL,试图把之前的信息记录下来,让之后的文本片段可以读到之前片段的信息。如果读取前4个token的信息,看起来就是下图的样子:
从图b可以看到,右上角的token比之前截断获取了更多的信息。感觉有点像CNN的感受野,随着深度加大视野也加大。
思想上是通过“hidden state”把之前片段的信息传给下一段,但具体实现上有一个问题,就是绝对的positional embedding,那这样两个片段都对自己但token进行1,2,3,4...位置的编码,模型就无法区分某个位置是片段1的还是片段2的。所以作者在实现上提出了relative positional encoding,即使用两个token的相对距离代替之前的绝对位置。具体的细节请参考原文,大致做法是在计算attention weight的时候把涉及到位置的矩阵单独拿出来改一下。
7.2.2 Multiple Segments建模
BERT还有一个Next Sentence Prediction的优化目标,有助于finetune阶段直接适应各种类型的下游任务。XLNet也可以使用这种结构,只不过最后研究结论是NSP任务对它没什么帮助。
XLNet提出了Relative Segment Encoding,因为以前BERT是直接分A、B句,每个句子有个segment embedding,XLNet借鉴了relative position的思想,只判断两个token是否在一个segment中,而不是判断他们各自属于哪个segment。
具体实现是在计算attention weight的时候,给query额外操作一波,算出一个额外的权重加到原本的权重上去,跟relative positional encoding差不多。
这样做的优点是之后可以处理更多的segments,而不是像BERT只能处理两个。
8. T5
T5的语料进一步扩大,使用了750G的语料。它最核心的贡献是将NLU和NLG、也就是自然语言理解和自然语言生成统一了。虽然这方面的工作很早就有,但是T5的试验做的非常全面,最终发现encoder/decoder是一个非常好的结构,T5最终的参数数量也是达到了110亿个。右边的图展示了T5的预训练目标,输入inputs是一些单词被遮罩的句子,进入到encoder;在decoder部分,模型需要生成被遮罩的单词。
当T5用于下游任务的时候,文本作为encoder端的输入,decoder负责标签的输出。事实上,不管是NLU还是NLG任务,都可以使用text文本用来表示他们的正确答案。因此不管是分类还是翻译还是回归任务,都可以使用一样的seq2seq模型结构和一样的训练/推理策略。
9. 总结
预训练能带来以下一些优势:
- 通过利用大量无标注文本,预训练有助于模型学习通用语言表征。
- 只需增加一两个特定的层,预训练模型可以适应下游任务。因此这能提供很好的初始化,从而避免从头开始训练下游模型(只需训练特定于任务的层)。
- 让模型只需小型数据集就能获得更好的表现,因此可以降低对大量有标注实例的需求。
- 深度学习模型由于参数数量大,因此在使用小型数据集训练时,容易过拟合。而预训练可以提供很好的初始化,从而可避免在小型数据集上过拟合,因此可将预训练视为某种形式的正则化。
9.1 预训练的步骤
预训练一个模型涉及以下五个步骤:
- 准备预训练语料库
- 生成词汇库
- 设计预训练任务
- 选择预训练方法
- 选择预训练动态
9.2 预训练任务
- 闲聊语言建模(CLM)
- 掩码语言建模(MLM)
- 替代 token 检测(RTD)
- 混洗 token 检测(STD)
- 随机 token 替换(RTS)
- 互换语言建模(SLM)
- 翻译语言建模(TLM)
- 替代语言建模(ALM)
- 句子边界目标(SBO)
- 下一句子预测(NSP)
- 句子顺序预测(SOP)
- 序列到序列语言模型(Seq2SeqLM)
- 去噪自动编码器(DAE)
Ref
- https://mp.weixin.qq.com/s/jE8IQARcztl8p9DMtlCy0A
- https://cloud.tencent.com/developer/article/1555590
- https://cloud.tencent.com/developer/article/1855316 BERT文本分类实战
- https://cloud.tencent.com/developer/article/1780054
- https://cloud.tencent.com/developer/article/1789826 w2v GPT ELMO发展
- https://wmathor.com/index.php/archives/1456/
- 李宏毅机器学习课程2021
- https://blog.csdn.net/yizhen_nlp/article/details/106560907 BERT微调策略
- https://zhuanlan.zhihu.com/p/132554155 BERT Transformer知识点
- https://blog.csdn.net/u011412768/article/details/108015783 https://plmsmile.github.io/2018/12/15/52-bert/#基于finetune BERT详解
- https://cloud.tencent.com/developer/article/1702065 BERT代码详解
- https://zhuanlan.zhihu.com/p/154527264 基于BERT的预训练模型浅析
- https://towardsdatascience.com/masked-language-modelling-with-bert-7d49793e5d2c MLM详解
- https://zhuanlan.zhihu.com/p/70218096 XLNET
- https://zhuanlan.zhihu.com/p/409867119 腾讯预训练模型
- https://mp.weixin.qq.com/s/EZciiZEVCn45Hm1Cqz8PqQ 预训练模型综述