从Word Embedding到ELMO
Word Embedding的缺陷:无法处理多义词问题
在前文中,我们介绍了word2vec,但实际生产应用中,word2vec的效果并没有特别好。所以,Word Embedding存在什么问题?考虑下面两个句子:
● Sentence A:He got bit by Python.
● Sentence B:Python is my favorite programming language.
在句子A中,Python是蟒蛇的意思,而句子B中是一种编程语言。如果我们得到上面两个句子中单词Python的嵌入向量,那么像word2vec这种嵌入模型就会为这两个句子中的Python赋予相同的嵌入,因为它是上下文无关的。所以word embedding无法区分多义词的不同语义,问题的产生总是无情的推动技术的发展,因此ELMO出现了。
根据上下文动态调整的ELMO:预训练(双层双向LSTM) + 特征融合
ELMO是“Embedding from Language Models”的简称,提出ELMO的论文题目:“Deep contextualized word representation”更能体现其精髓,而精髓在哪里?在deep contextualized这个短语,一个是deep,一个是context,其中context更关键。它与之前的Word2Vec有何本质区别?
● 在此之前的Word Embedding本质上是个静态的方式,所谓静态指的是训练好之后每个单词的表达就固定住了,以后使用的时候,不论新句子上下文单词是什么,这个单词的Word Embedding不会跟着上下文场景的变化而改变
● 而ELMO的本质思想是:我事先用语言模型学好一个单词的Word Embedding,然后在我实际使用Word Embedding的时候,单词已经具备了特定的上下文了,这个时候我可以根据上下文单词的语义去调整单词的Word Embedding表示,这样经过调整后的Word Embedding更能表达在这个上下文中的具体含义,自然也就解决了多义词的问题了
所以ELMO本身是个根据当前上下文对Word Embedding动态调整的思路:
具体而言,ELMO采用了典型的两阶段过程:
- 第一个阶段是通过语言模型LSTM进行预训练所以,你看到:上图左端的前向双层LSTM代表正方向编码器,输入的是从左到右顺序的上文Context-before;右端的逆向双层LSTM代表反方向编码器,输入的是从右到左的逆序的下文Context-after
- 同时,每个编码器的深度都是两层LSTM叠加
- 第二个阶段是在做下游任务时,从预训练网络中提取对应单词的网络各层的Word Embedding作为新特征补充到下游任务中
上图展示的是其预训练过程,它的网络结构采用了双层双向LSTM,双向LSTM可以干啥?可以根据单词的上下文去预测单词,毕竟这比只通过上文去预测单词更准确。
那么问题来了,ELMO采用双向结构不就能看到需要预测的单词了么,你都能看到参考答案了,那岂不影响最终模型训练的效率与准确性,因为哪有做题时看着参考答案做题的。不过,巧就巧在ELMO虽然采用的双向结构,但两个方向是彼此独立训练的,从而避免了这个问题!
下面解释啥叫“ELMO虽然采用的双向结构,但两个方向是彼此独立训练的”,啥叫既然是双向的,又何来什么独立,然后避免see itself的问题呢?
好问题!虽然ELMO用双向LSTM来做encoding,但是这两个方向的LSTM其实是分开训练的(看到上图中那两个虚线框框没,分开看左边的双层LSTM和右边的双层LSTM,一个从左向右预测,一个从右向左预测,但在左边和右边的内部结构里,其本质不还是单向么,所以其实就是个伪双向,^_^),只是在最后在loss层做了个简单相加。换言之,两个关键点:
- 对于每个方向上的单词来说,因为两个方向彼此独立训练,故在一个方向被encoding的时候始终是看不到它另一侧的单词的,从而避免了see itself的问题;
- 而再考虑到句子中有的单词的语义会同时依赖于它左右两侧的某些词,仅仅从单方向做encoding是不能描述清楚的,所以再来一个反向encoding,故称双向。
看似完美!“伪双向”既解决了see itself的问题,又充分用上了上下文的语义。
然而,BERT的作者指出这种两个方向彼此独立训练即伪双向的情况下,即便双层的双向编码也可能没有发挥最好的效果,而且我们可能不仅需要真正的双向编码,还应该要加深网络的层数,但暂且不管加不加深,真正的双向编码网络还是会不得不面对这个老问题:导致模型最终可以间接地“窥探”到需要预测的词,即还是那个see itself的问题。此点,下文细谈。
ELMO这个网络结构其实在NLP中是很常用的。使用这个网络结构利用大量语料做语言模型任务就能预先训练好这个网络,如果训练好这个网络后,再输入一个新句子,句子中每个单词都能得到对应的三个Embedding:
- 第一个Embedding,是单词的Word Embedding
- 第二个Embedding,是双层双向LSTM中第一层LSTM对应单词位置的Embedding,这层编码单词的句法信息更多一些
- 第三个Embedding,是双层双向LSTM中第二层LSTM对应单词位置的Embedding,这层编码单词的语义信息更多一些
也就是说,ELMO的预训练过程不仅仅学会单词的Word Embedding,还学会了一个双层双向的LSTM网络结构,而这两者后面都有用。预训练好网络结构后,如何给下游任务使用呢?下图展示了下游任务的使用过程,比如我们的下游任务仍然是QA问题,此时对于问句X:
- 可以先将句子X作为预训练好的ELMO网络的输入
- 这样句子X中每个单词在ELMO网络中都能获得对应的三个Embedding
- 之后给予这三个Embedding中的每一个Embedding一个权重a,这个权重可以学习得来
- 根据各自权重累加求和,将三个Embedding整合成一个
- 然后将整合后的这个Embedding作为X句在自己任务的那个网络结构中对应单词的输入,以此作为补充的新特征给下游任务使用
对于上图所示下游任务QA中的回答句子Y来说也是如此处理。因为ELMO给下游提供的是每个单词的特征形式,所以这一类预训练的方法被称为“Feature-based Pre-Training”。
技术迭代的长河永不停歇,那么站在现在这个时间节点看,ELMO有什么值得改进的缺点呢?
● 首先,一个非常明显的缺点在特征抽取器选择方面,ELMO使用了LSTM而不是新贵Transformer,毕竟很多研究已经证明了Transformer提取特征的能力是要远强于LSTM
● 另外一点,ELMO采取双向拼接这种融合特征的能力可能比BERT一体化的融合特征方式弱
此外,不得不说除了以ELMO为代表的这种“Feature-based Pre-Training + 特征融合(将预训练的参数与特定任务的参数进行融合)”的方法外,NLP里还有一种典型做法,称为“预训练 + 微调(Fine-tuning)的模式”,而GPT就是这一模式的典型开创者!
微调(Fine-tuning):把在源数据集上训练的源模型的能力迁移到新数据新模型上
既然提到了Fine-tuning,则有必要详细解释一下什么是Fine-tuning。而要了解什么是Fine-tuning,就要先提到迁移学习概念。
所谓迁移学习(Transfer learning) ,就是把已训练好的模型(预训练模型)参数迁移到新的模型来帮助新模型训练。考虑到大部分数据或任务都是存在相关性的,所以通过迁移学习我们可以将已经学到的模型参数(也可理解为模型学到的知识),通过某种方式来分享给新模型从而加快并优化模型的学习效率,从而不用像大多数网络那样从零学习。
其中,实现迁移学习有以下三种手段:
● Transfer Learning:冻结预训练模型的全部卷积层,只训练自己定制的全连接层
● Extract Feature Vector:先计算出预训练模型的卷积层对所有训练和测试数据的特征向量,然后抛开预训练模型,只训练自己定制的简配版全连接网络
● Fine-tuning:冻结预训练模型的部分卷积层(通常是靠近输入的多数卷积层,因为这些层保留了大量底层信息)甚至不冻结任何网络层,训练剩下的卷积层(通常是靠近输出的部分卷积层)和全连接层
为了更好的说明什么是Fine-tuning,再引用下动手学深度学习一书上的例子,最后归纳Fine-tuning的步骤/流程。假设我们想从图像中识别出不同种类的椅子,然后将购买链接推荐给用户。一种可能的方法是:
- 先找出100种常见的椅子,为每种椅子拍摄1,000张不同角度的图像,然后在收集到的10万张图像数据集上训练一个分类模型这个椅子数据集虽然可能比一般的小数据集要庞大,但样本数仍然不及ImageNet数据集中样本数的十分之一;
- 所以如果把适用于ImageNet数据集的复杂模型用在这个椅子数据集上便会过拟合。同时,因为椅子数据量有限,最终训练得到的模型的精度也可能达不到实用的要求;
- 比较经济的解决办法便是应用迁移学习,将从ImageNet数据集学到的知识迁移到椅子数据集上毕竟虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等,这些类似的特征对于识别椅子也同样有效。
而这,就是所谓的迁移学习中一种常用技术-微调,如上图所示,微调由以下4步构成:
● 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型;
● 创建一个新的神经网络模型,即目标模型,它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层与源数据集的标签紧密相关,因此在目标模型中不予采用;
● 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数;
● 在目标数据集(如椅子数据集)上训练目标模型,我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。
最终,Fine-tuning意义在于以下三点:
- 站在巨人的肩膀上:前人花很大精力训练出来的模型在大概率上会比你自己从零开始搭的模型要强悍,没有必要重复造轮子;
- 训练成本可以很低:如果采用导出特征向量的方法进行迁移学习,后期的训练成本非常低,用CPU都完全无压力,没有深度学习机器也可以做;
- 适用于小数据集:对于数据集本身很小(几千张图片)的情况,从头开始训练具有几千万参数的大型神经网络是不现实的,因为越大的模型对数据量的要求越大,过拟合无法避免。这时候如果还想用上大型神经网络的超强特征提取能力,只能靠迁移学习。
从Word Embedding到GPT
生成式的预训练之GPT:预训练(单向Transformer) + Fine-tuning
GPT是“Generative Pre-Training”的简称,从名字看其含义是指的生成式的预训练。GPT也采用两阶段过程,第一个阶段是利用语言模型进行预训练,第二阶段通过Fine-tuning的模式解决下游任务。下图展示了GPT的预训练过程,其实和ELMO是类似的,主要不同在于两点:
● 首先,特征抽取器不是用的LSTM,而是用的Transformer,毕竟它的特征抽取能力要强于LSTM,这个选择很明显是很明智的;
● 其次,GPT的预训练虽然仍然是以语言模型作为目标任务,但是采用的是单向的语言模型。
生成式的预训练之GPT:为何采用单向Transformer
ELMO在做语言模型预训练的时候,预测单词可以同时使用上文和下文,用的双向LSTM结构,而GPT则只采用单词的上文来进行单词预测,而抛开了下文。说人话,就是让你根据提示造句,从左到右,是单向的。
什么是单向Transformer?在Transformer的文章中,提到了Encoder与Decoder使用的Transformer Block是不同的。怎么个不同?通过前文对Transformer的介绍可得知:
● Encoder因为要编码整个句子,所以每个词都需要考虑上下文的关系。所以每个词在计算的过程中都是可以看到句子中所有的词的;
● 但是Decoder与Seq2Seq中的解码器类似,每个词都只能看到前面词的状态,所以是一个单向的Self-Attention结构。
换言之,在解码Decoder Block中,使用了Masked Self-Attention(所谓Masked,即遮蔽的意思),即句子中的每个词,都只能对包括自己在内的前面所有词进行Attention,这就是单向Transformer。而GPT使用的Transformer结构就是将Encoder中的Self-Attention替换成了Masked Self-Attention,从而每个位置的词看不到后面的词。
既然我们已经确定通过上下文预测单词可以更准确,为何GPT还抛弃下文只通过上文预测单词呢?
● 首先,GPT把特征提取器从LSTM换成了更强的Transformer,此举已经很是创新了(GPT之后大部分模型都开始用Transformer做特征提取器)
● 而此时如果用Transformer的结构提取上下文去做单词预测,那就势必用上Transformer双向结构,而Transformer不像ELMO的双向结构各个方向独立训练而不会see itself,但双向Transformer会出现see itself 啊!这是万万不行的,因为咱们原本就要训练模型的预测能力,而如果你通过双向非独立的结构都能看到中间要被预测的单词,看到答案了,这是无法做预测的。(比如当你用前面的词逐个的预测下一个词的时候,结果你从另一个方向看到每个词)
● 最终两相权衡,才导致GPT放弃Transformer的双向结构,改用Transformer的单向结构,此举也决定了GPT更适合根据已有文本然后生成下文的任务,这也是它叫生成式模型的原因。
上面讲的是GPT如何进行第一阶段的预训练,那么假设预训练好了网络模型,后面下游任务怎么用?它有自己的个性,和ELMO的方式大有不同。
上图展示了GPT在第二阶段如何使用:
● 首先,对于不同的下游任务来说,本来你可以任意设计自己的网络结构,现在不行了,你要向GPT的网络结构看齐,把任务的网络结构改造成和GPT的网络结构是一样的;
● 然后,在做下游任务的时候,任务的网络结构的参数初始化为预训练好的GPT网络结构的参数,这样通过预训练学到的语言学知识就被引入到你手头的任务里来了;
● 再次,你可以用手头的任务去训练这个网络,对网络参数进行Fine-tuning,使得这个网络更适合解决手头的问题。
集大成者之BERT:双向Transformer版的GPT
BERT模型的架构:预训练(双向Transformer) + Fine-Tuning
GPT是使用「单向的Transformer Decoder模块」构建的,而 BERT则是通过「双向的Transformer Encoder 模块」构建的。至此,我们可以梳理下BERT、ELMO、GPT之间的演进关系:
可以看出,BERT综合了ELMO的双向优势与GPT的Transformer特征提取优势,即关键就两点
● 第一点是特征抽取器采用Transformer
● 第二点是预训练的时候采用双向语言模型
进一步,BERT采用和GPT完全相同的两阶段模型:
- 首先是预训练(通过不同的预训练任务在未标记的数据上进行模型训练),其次是使用Fine-Tuning模式解决下游任务
- 和GPT的最主要不同在于在预训练阶段采用了类似ELMO的双向语言模型,当然另外一点是语言模型的数据规模要比GPT大
BERT对输入、输出部分的处理
为了适配多任务下的迁移学习,BERT设计了更通用的输入层和输出层。具体而言,BERT的输入部分是个线性序列,两个句子之间通过分隔符「SEP」分割,最前面是起始标识「CLS」,每个单词有三个embedding:
- 单词embedding,这个就是我们之前一直提到的单词embedding,值得一提的是,有的单词会拆分成一组有限的公共子词单元,例如下图示例中‘playing’被拆分成了‘play’和‘ing’;
- 句子embedding,用于区分两个句子,例如B是否是A的下文(对话场景,问答场景等);
- 位置信息embedding,句子有前后顺序,组成句子的单词也有前后顺序,否则不通顺杂乱无章就是零散单词而非语句了,所以单词顺序是很重要的特征,要对位置信息进行编码。
把单词对应的三个embedding叠加(没错,直接相加),就形成了BERT的输入。
BERT的两个创新点(任务):Masked Language Model(MLM)与Next Sentence Prediction(NSP)
通过之前内容的多处示例,我们早已得知,预测一个单词只通过上文预测不一定准确,只有结合该词的上下文预测该词才能更准确。那么新问题来了:对于Transformer来说,怎样才能在这个结构上做双向语言模型任务呢?
很简单,它借鉴了Word2Vec的CBOW方法:根据需要预测单词的上文Context-Before和下文Context-after去预测单词。现在我们来总结、对比下ELMO、GPT、BERT预测中间词的不同:
● 如上文所述,ELMO采用双向LSTM结构,因为两个方向是彼此独立训练的,所以可以根据上下文预测中间词,尽管效果可能不是最佳
● 如上文所述,GPT由于采取了Transformer的单向结构,只能够看见当前以及之前的词,故只能根据上文预测下一个单词
● 而BERT没有像GPT一样完全放弃下文信息,而是采用了双向的Transformer。恩?!立马就有人要问了:用双向Transformer结构的话,不就导致上文提到的“see itself”的问题了么?
BERT原始论文里也提到了这点:“因为双向条件会让每个词间接地 ‘看到自己/see itself’,从而模型在多层上下文中可以十分容易地预测目标词”,而BERT用的双向Transformer结构,而非各个方向独立训练的双向LSTM结构,那咋办呢?BERT除了集众所长之外,还做了两个创新型任务:一个是论文中指出的Masked Language Model (MLM),一个是Next Sentence Prediction(NSP)。
首先来看第一个创新点,即什么是“Masked Language Model(MLM)呢?所谓MLM是指在训练的时候随即从输入预料上mask掉一些单词,然后通过的上下文预测该单词,该任务非常像训练一个中学生做完形填空的能力。也就是说:
- 为了让BERT具备通过上下文做完形填空的能力,自然就得让BERT不知道中间待预测词的信息,所以就干脆不要告诉模型这个中间待预测词的信息好了
- 即在输入的句子中,挖掉一些需要预测的词,然后通过上下文来分析句子,最终使用其相应位置的输出来预测被挖掉的词
但是,直接将大量的词替换为<mask>标签可能会造成一些问题,模型可能会认为只需要预测<mask>相应的输出就行,其他位置的输出就无所谓。同时Fine-Tuning阶段的输入数据中并没有<mask>标签,也有数据分布不同的问题。
为了减轻这样训练带来的不利影响,BERT采用了如下的方式:输入数据中随机选择15%的词用于预测,这15%的词中
● 80%的词向量输入时被替换为<mask>,比如my dog is hairy -> my dog is [mask]
● 10%的词的词向量在输入时被替换为其他词的词向量,比如my dog is hairy -> my dog is apple
● 另外10%保持不动,比如my dog is hairy -> my dog is hairy
这样一来就相当于告诉模型,我可能给你答案,也可能不给你答案,也可能给你错误的答案,有<mask>的地方我会检查你的答案,没<mask>的地方我也可能检查你的答案,所以<mask>标签对你来说没有什么特殊意义,所以无论如何,你都要好好预测所有位置的输出。
至于BERT的第二个创新点:“Next Sentence Prediction”,其任务是判断句子B是否是句子A的下文,如果是的话输出’IsNext‘,否则输出’NotNext‘,这个关系保存在BERT输入表示图中的[CLS]符号中。至于训练数据的生成方式是从平行语料中随机抽取的两句话:
● 其中50%是选择语料库中真正顺序相连的两个句子,符合IsNext关系;
● 另外50%是第二个句子从语料库中随机选择出一个拼到第一个句子后面,它们的关系是NotNext。
相当于我们要求模型除了做上述的Masked语言模型任务外,附带再做个句子关系预测,判断第二个句子是不是真的是第一个句子的后续句子。之所以这么做,是考虑到很多NLP任务是句子关系判断任务,单词预测粒度的训练到不了句子关系这个层级,增加这个任务有助于下游句子关系判断任务。所以可以看到,它的预训练是个多任务过程,即有上文提到的这两个训练目标:
- 一个Token级别或称为词级别,Token级别简言之就是完形填空,一个句子中间挖个词,让模型预测那个空的是哪个词与传统语言模型相比,通过上下文来预测中间某个缺失的单词,是否比从左往右(或者从右往左)的单词预测来的更直观和容易呢?与新一代ELMO相比,BERT作者Jacob的这种类似挖洞的做法,即每个单层内部都是双向的做法,是否比『ELMo从左往右的语言模型和从右往左的语言模型独立开来训练,共享embedding,然后把loss平均一下,最后用的时候再把两个方向的语言模型输出拼接在一起』,而更加符合你的直觉呢?
- 一个句子级别,即给两句句子,判断这两句是不是原文中的连在一起的互为上下文(句子级别的任务对于阅读理解,推理等任务提升较大)