前言
这篇文章是两篇视频学习的笔记整合,分别是是Contextual Word Representations: BERT和Modeling contexts of use: Contextual Representations and Pretraining. ELMo and BERT.。这两个视频大致上分为四个部分做讲解,分别是交代背景(Representations for a word)、pre-Bert时代的预训练模型(ELMo、ULMfit)、Bert基本原理(Transformer models、BERT)以及post-Bert时代的各个模型的使用和对比。
Representations for a word
前面学习Word2Vec、GloVe和fastText可以直接生成一个词的词向量,但是它们生成的词向量在下游任务中存在两个局限:
- 无论单词标记出现在什么上下文中,单词类型的表示总是相同的,因为经过上面三个模型的输出的词是一个固定的词向量,无法根据句子的上下文做出相应的释义,比方说‘苹果’这个词,它可以在‘我爱吃苹果’句子中表达水果,也可以在‘我使用的是苹果手机’中表达手机。它们训练出来的词向量会包含这两种意思,成为一个‘杂糅’的向量。
- 难以表达单词的不同的语法和语义,有些词,它们既可以做动词也可以做名词,既可以做主语也可以做宾语,使用它们训练出来的词向量,这个词无论在句子充当什么角色,它们的向量依然是一样的,也是多种角色的‘杂糅’向量。
ELMo
2018年艾伦人工智能研究所提出了ELMo(Embeddings from Language Models),即从语言模型中动态学习词向量,ELMo很好的解决了上述两个问题,ELMo对一个词的表示由多个向量组成,并且每个向量的权重在具体的上下文中动态更新,因此不但粒度更细,而且能根据上下文动态调整一个词的最终词向量。
ELMo的特点
- 学习到的词向量可以根据上下文环境动态变换,不再是静态的词向量
- 使用很长的上下文进行学习,而不像word2vec一样使用较小的滑动窗口,所以ELMo能学到长距离词的依赖关系
- 使用双向的语言模型进行学习,并使用网络的所有隐藏层作为这个词的特征表示
ELMo的原理
ELMo通过RNN来建立更长的句子间依赖关系,而不是像word2vec一样使用n-gram的滑动窗口,并且它使用的是基于RNN的双向的语言模型(biLM),也就是说对于一个词‘退了’,它不仅可以通过上文学到自己的隐藏层特征,又可以通过下文学到相应的隐藏层特征,ELMo会两个方向的隐藏层特征拼起来,作为biLM学到的“退了”这个词的特征表示,做到‘瞻前顾后’。
为了学到更多的特征,ELMo对双向RNN进行堆叠,每增加一层就能多学习到2个特征表示(一正一反),ELMo文中使用了两层的双向LSTM抽取特征,所以对一个词能抽取到4个特征表示,即上图中的$h_1$和$h_2$(每个$h$包含一正一反特征向量组合)。
在使用ELMo词向量时,每个词的最终词向量是所有隐藏层特征向量$h$的加权求和,系数是$\alpha$。这个系数是根据词在不同的上下文中学习得来的。ELMo文章分析发现,不同的NLP任务学到的系数不尽相同,比如在Coref和SQuAD任务中,第一层的系数更大。有可能ELMo在第一层学到的是词的句法特征,第二次学到的是更高级的语义特征。有点类似于CNN中在浅层学到点、线、转角,在高层学到轮廓等高级特征。
结合上面学到的,ELMo可以通过上述的公式表示:
其中,$\overrightarrow{h}_{k,j}^{LM}$表示biLM抽取出来的前向特征,$\overleftarrow{h}_{k,j}^{LM}$表示抽取出来的后向特征,合并起来就是$h_{k,j}^{LM}$,也就是前图中的$h_i$。$x_k^{LM}$表示初始的biLM词向量输入,它是使用char-CNN抽取一个词的原始输入向量。所以,对于词$k$,ELMo得到的完整词向量是$R_k$,它其实是这个词的一系列特征向量的组合,它还不是这个词最终的词向量,因为最终的词向量要在具体的NLP任务中根据上下文来定。
$ELMo_k^{task}$表示词$k$在某个任务中的词向量,它等于对所有层的特征向量的加权求和,其中$s_j^{task}$(对应前图中的$\alpha$)根据当前任务动态学习得到,参数$\gamma^{task}$控制ELMo词向量对任务的贡献程度。
ELMo的使用
在命名实体识别中,使用方法如下图:
通过ELMo出来的隐藏层$h_{1,2}$和$h_{2,2}$接到全连接层之后输入到CRF中。
在性能方面,ELMo刷新了很多任务的SOTA。
ULMfit
ULMfit(Universal Language Model Fine-tuning),这个模型类似于CV领域的迁移学习,可以用于任意的NLP任务,它是一个早期的提出了应用预训练方法,fine-tuning到不同NLP任务的的模型。
如上图,ULMfit模型分为预训练阶段、语言模型的finetune阶段、分类任务的finetune阶段:
pretrain:使用了wikitext-103数据,先对语言模型进行预训练
语言模型fineutune:将通用语言模型finetune到目标领域的数据上来,作者提出来两个tricks:discriminative fine-tuning和anted triangular learning rates(STLR),前者是提到对不同层做finetune的时候,使用不同的学习率,后者是一个倒三角的学习率更新方式,让参数较快收敛到一个合适的区域,然后再慢慢调整。
- 分类任务finetune:将前面的语言模型的隐藏层输出的结果的进行concat,分别是时间上的maxpool及meanpool,得到$[h_T,maxpool(H),meanpool(H)]$,然后加上两个全连接层(带BN的ReLU激活函数)进行分类,此处使用了gradual unfreezing的方法,在finetune的时候,逐层解冻前面的层,避免一次性finetune所有层然后忘记了第一阶段预训练的参数。
在作者提出这个模型时候,在IMDb和TREC-6文本分类任务上的效果和其它模型对比如下:
From scratch表示完全从头开始训练,supervised表示仅用当前任务的数据进行LM的finetune,semi-supervised表示可以用所有task的数据进行LM的finetune。可以看出,用了较多数据进行finetune过后的LM,需要的训练样本更少,而且最终收敛效果也最好:
Transformer models
接下来要登场的就是Transformer,它是很多预训练LM的基础组件模型。
这部分直接从图解Transformer(完整版)中的内容中学习会更好,下文直接摘自该文章。
Transformer总体结构
和Attention模型一样,Transformer模型中也采用了 encoer-decoder 架构。但其结构相比于Attention更加复杂,论文中encoder层由6个encoder堆叠在一起,decoder层也一样。
每一个encoder和decoder的内部简版结构如下图:
对于encoder,包含两层,一个self-attention层和一个前馈神经网络,self-attention能帮助当前节点不仅仅只关注当前的词,从而能获取到上下文的语义。decoder也包含encoder提到的两层网络,但是在这两层中间还有一层attention层,帮助当前节点获取到当前需要关注的重点内容。
现在我们知道了模型的主要组件,接下来我们看下模型的内部细节。首先,模型需要对输入的数据进行一个embedding操作,(也可以理解为类似word2vec的操作),embedding结束之后,输入到encoder层,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,得到的输出会输入到下一个encoder。
Self-Attention
接下来我们详细看一下self-attention,其思想和attention类似,但是self-attention是Transformer用来将其他相关单词的“理解”转换成我们正常理解的单词的一种思路,我们看个例子:
The animal didn’t cross the street because it was too tired
这里的it到底代表的是animal还是street呢,对于我们来说能很简单的判断出来,但是对于机器来说,是很难判断的,self-attention就能够让机器把it和animal联系起来。
接下来我们看下详细的处理过程:
首先,self-attention会计算出三个新的向量,在论文中,向量的维度是512维,我们把这三个向量分别称为Query、Key、Value,这三个向量是用embedding向量与一个矩阵相乘得到的结果,这个矩阵是随机初始化的,维度为(64,512)注意第二个维度需要和embedding的维度一样,其值在BP的过程中会一直进行更新,得到的这三个向量的维度是64低于embedding维度的。
那么Query、Key、Value这三个向量又是什么呢?这三个向量对于attention来说很重要,当你理解了下文后,你将会明白这三个向量扮演者什么的角色。
计算self-attention的分数值,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2
接下来,把点成的结果除以一个常数,这里我们除以8,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大
下一步就是把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值。
在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵
这种通过query和key的相似性程度来确定value的权重分布的方法被称为scaled dot-product attention。其实scaled dot-Product attention就是我们常用的使用点积进行相似度计算的attention,只是多除了一个(为K的维度)起到调节作用,使得内积不至于太大。
Multi-Headed Attention
论文中针对self-attention加入了另外一个机制,被称为“multi-headed” attention,该机制理解起来很简单,就是说不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组,tranformer是使用了8组,所以最后得到的结果是8个矩阵,每个矩阵都被用来将输入词嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
在“多头”注意机制下,我们为每个头保持独立的查询/键/值权重矩阵,从而产生不同的查询/键/值矩阵。和之前一样,我们拿$X$乘以$W^Q$、$W^K$、$W^V$矩阵来产生查询/键/值矩阵。
如果我们做与上述相同的自注意力计算,只需八次不同的权重矩阵运算,我们就会得到八个不同的Z矩阵。
前馈层不需要8个矩阵,它只需要一个矩阵(由每一个单词的表示向量组成)。所以我们需要一种方法把这八个矩阵压缩成一个矩阵。那该怎么做?其实可以直接把这些矩阵拼接在一起,然后用一个附加的权重矩阵$W^O$与它们相乘。
这几乎就是多头自注意力的全部。这确实有好多矩阵,我们试着把它们集中在一个图片中,这样可以一眼看清。
既然我们已经摸到了注意力机制的这么多“头”,那么让我们重温之前的例子,看看我们在例句中编码“it”一词时,不同的注意力“头”集中在哪里:
当我们编码“it”一词时,一个注意力头集中在“animal”上,而另一个则集中在“tired”上,从某种意义上说,模型对“it”一词的表达在某种程度上是“animal”和“tired”的代表。
然而,如果我们把所有的attention都加到图示里,事情就更难解释了:
Positional Encoding
到目前为止,transformer模型中还缺少一种解释输入序列中单词顺序的方法。为了处理这个问题,transformer给encoder层和decoder层的输入添加了一个额外的向量Positional Encoding,维度和embedding的维度一样,这个向量采用了一种很独特的方法来让模型学习到这个值,这个向量能决定当前词的位置,或者说在一个句子中不同的词之间的距离。这个位置向量的具体计算方法有很多种,论文中的计算方法如下:
其中$pos$是指当前词在句子中的位置,$i$是指向量中每个值的index,可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。最后把这个Positional Encoding与embedding的值相加,作为输入送到下一层。
为了让模型理解单词的顺序,我们添加了位置编码向量,这些向量的值遵循特定的模式。
如果我们假设词嵌入的维数为4,则实际的位置编码如下:
那么生成位置向量需要遵循怎样的规则呢?
观察下面的图形,每一行都代表着对一个矢量的位置编码。因此第一行就是我们输入序列中第一个字的嵌入向量,每行都包含512个值,每个值介于1和-1之间。我们用颜色来表示1,-1之间的值,这样方便可视化的方式表现出来:
20字(行)的位置编码实例,词嵌入大小为512(列)。你可以看到它从中间分裂成两半。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们拼在一起而得到每一个位置编码向量。
原始论文里描述了位置编码的公式(第3.5节)。你可以在 get_timing_signal_1d()中看到生成位置编码的代码。这不是唯一可能的位置编码方法。然而,它的优点是能够扩展到未知的序列长度(例如,当我们训练出的模型需要翻译远比训练集里的句子更长的句子时)。
Layer normalization
在transformer中,每一个子层(self-attetion,ffnn)之后都会接一个残差模块,并且有一个Layer normalization。
在进一步探索其内部计算方式,我们可以将上面图层可视化为下图:
残差模块相信大家都很清楚了,这里不再讲解,主要讲解下Layer normalization。Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。
关于normalization,Batch Normalization是其中的一种方式。那么什么是Layer normalization呢?它也是归一化数据的一种方式,不过LN是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差:
下面看一下LN的公式:
到这里为止就是全部encoders的内容了,如果把两个encoders叠加在一起就是这样的结构,在self-attention需要强调的最后一点是其采用了残差网络中的short-cut结构,目的是解决深度学习中的退化问题。
Decoder层
上图是transformer的一个详细结构,相比本文一开始结束的结构图会更详细些,接下来,我们会按照这个结构图讲解下decoder部分。
可以看到decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion,这里的mask也是transformer一个很关键的技术,mask表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer模型里面涉及两种mask,分别是padding mask和sequence mask。
- Padding Mask:
因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过softmax,这些位置的概率就会接近0。padding mask实际上是一个张量,每个值都是一个Boolean,值为false的地方就是我们要进行处理的地方。 - Sequence mask:
sequence mask是为了使得decoder不能看见未来的信息。也就是对于一个序列,在$t$时刻,我们的解码输出应该只能依赖于$t$时刻之前的输出,而不能依赖$t$之后的输出。因此我们需要想一个办法,把$t$之后的信息给隐藏起来。
编码器通过处理输入序列启动。然后将顶部编码器的输出转换为一组注意向量k和v。每个解码器将在其“encoder-decoder attention”层中使用这些注意向量,这有助于解码器将注意力集中在输入序列中的适当位置:
完成编码阶段后,我们开始解码阶段。解码阶段的每个步骤从输出序列(本例中为英语翻译句)输出一个元素。
以下步骤重复此过程,一直到达到表示解码器已完成输出的符号。每一步的输出在下一个时间步被送入底部解码器,解码器像就像我们对编码器输入所做操作那样,我们将位置编码嵌入并添加到这些解码器输入中,以表示每个字的位置。
输出层
解码组件最后会输出一个实数向量。我们如何把浮点数变成一个单词?这便是线性变换层要做的工作,它之后就是Softmax层。
线性变换层是一个简单的全连接神经网络,它可以把解码组件产生的向量投射到一个比它大得多的、被称作对数几率(logits)的向量里。
不妨假设我们的模型从训练集中学习一万个不同的英语单词(我们模型的“输出词表”)。因此对数几率向量为一万个单元格长度的向量——每个单元格对应某一个单词的分数。
接下来的Softmax 层便会把那些分数变成概率(都为正数、上限1.0)。概率最高的单元格被选中,并且它对应的单词被作为这个时间步的输出。
BERT: Devlin, Chang, Lee, Toutanova(2018)
从创新的角度来看,bert其实并没有过多的结构方面的创新点,其和GPT一样均是采用的transformer的结构,相对于GPT来说,其是双向结构的,而GPT是单向的,如下图所示:
EMLo:将上下文当作特征,但是无监督的语料和我们真实的语料还是有区别的,不一定的符合我们特定的任务,是一种双向的特征提取。
GPT就做了一个改进,也是通过transformer学习出来一个语言模型,不是固定的,通过任务fine-tuning,用transfomer代替EMLo的lstm。
GPT其实就是缺少了encoder的transformer。当然也没了encoder与decoder之间的attention。
GPT虽然可以进行fine-tuning,但是有些特殊任务与pretraining输入有出入,单个句子与两个句子不一致的情况,很难解决,还有就是decoder只能看到前面的信息。
其次bert在多方面的nlp任务变现来看效果都较好,具备较强的泛化能力,对于特定的任务只需要添加一个输出层来进行fine-tuning即可。
结构
先看下bert的内部结构,官网最开始提供了两个版本,L表示的是transformer的层数,H表示输出的维度,A表示mutil-head attention的个数:
如今已经增加了多个模型,中文是其中唯一一个非英语的模型:
从模型的层数来说其实已经很大了,但是由于transformer的残差模块,层数并不会引起梯度消失等问题,但是并不代表层数越多效果越好,有论点认为低层偏向于语法特征学习,高层偏向于语义特征学习。
预训练
预训练的好处在于在特定场景使用时不需要用大量的语料来进行训练,节约时间效率高效,bert就是这样的一个泛化能力较强的预训练模型。BERT的预训练阶段包括两个任务,一个是Masked Language Model,还有一个是Next Sentence Prediction。
Masked Language Model
MLM可以理解为完形填空,作者会随机mask每一个句子中15%的词,用其上下文来做预测,例如:my dog is hairy → my dog is [MASK]
此处将hairy进行了mask处理,然后采用非监督学习的方法预测mask位置的词是什么,但是该方法有一个问题,因为是mask15%的词,其数量已经很高了,这样就会导致某些词在fine-tuning阶段从未见过,为了解决这个问题,作者做了如下的处理:
- 80%的时间是采用[mask],my dog is hairy → my dog is [MASK]
- 10%的时间是随机取一个词来代替mask的词,my dog is hairy -> my dog is apple
- 10%的时间保持不变,my dog is hairy -> my dog is hairy
那么为啥要以一定的概率使用随机词呢?这是因为transformer要保持对每个输入token分布式的表征,否则Transformer很可能会记住这个[MASK]就是”hairy”。至于使用随机词带来的负面影响,文章中解释说,所有其他的token(即非”hairy”的token)共享15%*10% = 1.5%的概率,其影响是可以忽略不计的。Transformer全局的可视,又增加了信息的获取,但是不让模型获取全量信息。
Next Sentence Prediction
选择一些句子对A与B,其中50%的数据B是A的下一条句子,剩余50%的数据B是语料库中随机选择的,学习其中的相关性,添加这样的预训练的原因是目前很多NLP的任务比如QA和NLI都需要理解两个句子之间的关系,预训练能让预训练的模型更好的适应这样的任务。
输入
bert的输入可以是单一的一个句子或者是句子对,实际的输入值是segment embedding与position embedding相加。
BERT的输入词向量是三个向量之和:
- Token Embedding:WordPiece tokenization subword词向量。
- Segment Embedding:表明这个词属于哪个句子(NSP需要两个句子)。
- Position Embedding:学习出来的embedding向量。这与Transformer不同,Transformer中是预先设定好的值。
Fine-tuning
Fine-tuning之前对模型的修改比较简单,例如针对sequence-level classification problem(例如情感分析),取第一个token的输出表示,然后给一个softmax层得到分类结果输出;对于token-level classification(例如NER),取所有token的最后层transformer输出,给softmax层做分类。
总之不同类型的任务需要对模型做不同的修改,但是修改都是比较简单的,最多加一层神经网络即可。如下图所示:
特点
总结下BERT的主要贡献:
- 引入了Masked LM,使用双向LM做模型预训练。
- 为预训练引入了新目标NSP,它可以学习句子与句子间的关系。
- 进一步验证了更大的模型效果更好: 12 —> 24 层。
- 为下游任务引入了很通用的求解框架,不再为任务做模型定制。
- 刷新了多项NLP任务的记录,引爆了NLP无监督预训练技术。
BERT优点
- Transformer Encoder因为有Self-attention机制,因此BERT自带双向功能
- 因为双向功能以及多层Self-attention机制的影响,使得BERT必须使用Cloze版的语言模型Masked-LM来完成token级别的预训练
- 为了获取比词更高级别的句子级别的语义表征,BERT加入了Next Sentence Prediction来和Masked-LM一起做联合训练
- 为了适配多任务下的迁移学习,BERT设计了更通用的输入层和输出层
- 微调成本小
BERT缺点
- MLM的随机遮挡策略略显粗犷
- [MASK]标记在实际预测中不会出现,训练时用过多[MASK]影响模型表现
- 每个batch只有15%的token被预测,所以BERT收敛得比left-to-right模型要慢(它们会预测每个token)
- BERT对硬件资源的消耗巨大(大模型需要16个tpu,历时四天;更大的模型需要64个tpu,历时四天。
Post-BERT Pre-training Advancements
RoBERTA:基于bert,它使⽤了更多的epoch和更多的数据训练。
XLNet:使用了相对位置的embeddings,使用了排列的语言建模,它也使用了更多的数据和更大的模型,但实验表明,正在同等数据规模和模型大小下,它确实比Bert好一点。
ALBERT:它有两个改进:(1)分解embedding矩阵,使⽤更⼩的embedding size和transformer的hidden做矩阵乘法:
(2)让参数在每一层的transformer进行共享。他的效果相比之前的模型略有提升。
速度不变的情况下,ALBERT的参数有所减少: