【转】NLP 的巨人肩膀(中)

8 篇文章 0 订阅

3. 梯子的一级半

除了在word级别的embedding方法上有大量模型和算法的涌现,同样地,在char级别、句子级别和段落级别同样有大量模型提出。

word2vec开源随后的第一年,也就是在2014年,还是Mikolov,在他和另一位作者合作的一篇论文《Distributed Representations of Sentences and Documents》中,提出了可以借鉴word2vec思想的两种结构:PV-DM和PV-DBOW,分别对应word2vec中的CBOW和Skip-gram.

3.1 PV-DM和PV-DBOW

PV-DM的全称是Distributed Memory Model of Paragraph Vectors,和CBOW类似,也是通过上下文预测下一个词,不过在输入层的时候,同时也维护了一个文档ID映射到一个向量的look-up table,模型的目的便是将当前文档的向量以及上下文向量联合输入模型,并让模型预测下一个词,训练结束后,对于现有的文档,便可以直接通过查表的方式快速得到该文档的向量,而对于新的一篇文档,那么则需要将已有的look-up table添加相应的列,然后重新走一遍训练流程,只不过此时固定好其他的参数,只调整look-up table,收敛后便可以得到新文档对应的向量了。PV-DBOW的全称则是Distributed Bag of Words version of Paragraph Vector,和Skip-gram类似,通过文档来预测文档内的词,训练的时候,随机采样一些文本片段,然后再从这个片段中采样一个词,让PV-DBOW模型来预测这个词,以此分类任务作为训练方法,说白了,本质上和Skip-gram是一样的。这个方法有个致命的弱点,就是为了获取新文档的向量,还得继续走一遍训练流程,并且由于模型主要是针对文档向量预测词向量的过程进行建模,其实很难去表征词语之间的更丰富的语义结构,所以这两种获取文档向量的方法都未能大规模应用开来。

pv-dm-dbow.png

2015年,多伦多大学的Kiros等人提出了一个很有意思的方法叫Skip-thoughts,同样也是借鉴了Skip-gram的思想,但是和PV-DBOW中利用文档来预测词的做法不一样的是,Skip-thoughts直接在句子间进行预测,也就是将Skip-gram中以词为基本单位,替换成了以句子为基本单位,具体做法就是选定一个窗口,遍历其中的句子,然后分别利用当前句子去预测和输出它的上一句和下一句。对于句子的建模利用的RNN的sequence结构,预测上一个和下一个句子时候,也是利用的一个sequence的RNN来生成句子中的每一个词,所以这个结构本质上就是一个Encoder-Decoder框架,只不过和普通框架不一样的是,Skip-thoughts有两个Decoder。在今天看来,这个框架还有很多不完善或者可以改进的地方(作者也在论文中分别提到了这些future works),比如输入的Encoder可以引入attention机制,从而让Decoder的输入不再只是依赖Encoder最后一个时刻的输出;Encoder和Decoder可以利用更深层的结构;Decoder也可以继续扩大,可以预测上下文中更多的句子;RNN也不是唯一的选择,诸如CNN以及2017年谷歌提出的Transformer的结构也可以利用进来,后来果不其然谷歌的BERT便借鉴了这一思路,当然这是后话了,留下暂且不表。

3.2 Skip-thoughts

skip-thoughts.png

2018年的时候,在Skip-thoughts的基础上,Google Brain的Logeswaran等人将这一思想做了进一步改进,他们认为Skip-thoughts的Decoder效率太低,且无法在大规模语料上很好的训练(这是RNN结构的通病)。所以他们把Skip-thoughts的生成任务改进成为了一个分类任务,具体说来就是把同一个上下文窗口中的句子对标记为正例,把不是出现在同一个上下文窗口中的句子对标记为负例,并将这些句子对输入模型,让模型判断这些句子对是否是同一个上下文窗口中,很明显,这是一个分类任务。可以说,仅仅几个月之后的BERT正是利用的这种思路。而这些方法都和Skip-thoughts一脉相承。

3.3 Quick-thoughts

quick-thoughts.PNG

除了Skip-thoughts和Quick-thoughts这两种不需要人工标记数据的模型之外,还有一些从监督数据中学习句子表示的方法,比如2017年Facebook的研究人员Conneau等人提出的InferSent框架,它的思想特别简单,先设计一个模型在斯坦福的SNLI(Stanford Natural Language Inference)数据集上训练,尔后将训练好的模型当做特征提取器,以此来获得一个句子的向量表示,再将这个句子的表示应用在新的分类任务上,来评估句子向量的优劣。框架结构如下图所示

3.4 InferSent

infersent_snli.PNG

这个框架最底层是一个Encoder,也就是最终要获取的句子向量提取器,然后将得到的句子向量通过一些向量操作后得到句子对的混合语义特征,最后接上全连接层并做SNLI上的三分类任务,做过句子匹配任务的一定知道,这个框架是一个最基本(甚至也是最简陋)的句子匹配框架。对于底层的Encoder来说,论文作者分别尝试了7种模型,然后分别以这些模型作为底层的Encoder结构,然后在SNLI上进行监督训练。训练完成后,在新的分类任务上进行评估,最后发现当Encoder使用BiLSTM with max pooling结构时,对于句子的表征性能最好,对于具体细节感兴趣的可以参考他们的论文《Supervised Learning of Universal Sentence Representations from Natural Language Inference Data》。

3.5 General Purpose Sentence Representation

此外,除了InferSent这种单个任务的监督学习外,最新的工作逐渐将多任务的联合学习应用到获取句子的表征中,例如Subramanian等人发表在2018年ICLR上的一篇文章《Learning General Purpose Distributed Sentence Representations via Large Scale Multi-task Learning》中就提出了利用四种不同的监督任务来联合学习句子的表征,这四种任务分别是:Natural Language Inference, Skip-thougts, Neural Machine Translation以及Constituency Parsing等,作者的出发点也特别简单,通用的句子表征应该通过侧重点不同的任务来联合学习到,而不是只有一个特定任务来学习句子表征,后来作者在论文中的实验也确实证明了这点。实验的具体做法是,先用联合学习的方法在上述四个任务上进行训练,训练结束后,将模型的输出作为句子的表征(或者把这个联合学习的模型作为特征提取器),然后直接在这个表征上接上非常简单的全连接层做分类器,并且同时保证最底层的特征提取器中参数不动(也就是只把它当做特征提取器),然后在新的分类任务上做训练(只训练最后接上的全连接层分类器),最后根据训练出来的简单分类器在各自分类任务的测试集上做评估。最后作者惊喜的发现很多任务上他们的简单分类器都要超过当时的最好结果,并且他们还发现联合训练中不同的任务对于句子表征中的不同方面有不同的贡献。

3.6 Universal Sentence Encoder

同样在2018年,谷歌的Daniel Cer等人在论文《Universal Sentence Encoder》中提出的思路基本和General Purpose Sentence Representation的工作一样,只不过作者提出了利用Transformer和DAN(上文提到过的和CBOW与fastText都神似的《Deep Unordered Composition Rivals Syntactic Methods for Text Classification》)两种框架作为句子的Encoder,Transformer结构更为复杂,参数更多,训练也相对比较耗时,但是一般来说效果会更好一些;对应的,DAN结构简单,只有两个隐藏层(甚至可以减小为只需要一个隐藏层),参数比较少,训练相对比较省时省资源,但是一般来说效果会差一些(并不是绝对,论文中也发现某些场景下DAN的效果甚至更好)。然后作者既在无标记数据上训练,也在监督数据上训练,最后在十个分类任务上进行迁移学习的评估。最后作者还放出了他们预训练好的Encoder,可以供迁移学习的句子特征提取器使用(见引用16)。

example-classification.png

example-similarity.png

4. 拨开迷雾——第二级梯子若隐若现

4.1 戈多会来吗?

上面我们介绍了好几种获取句子表征的方法,然而值得注意的是,我们并不是只对如何获取更好的句子表征感兴趣,其实更有趣的是,这些方法在评估他们各自模型性能的时候所采取的方法,回过头去进行梳理,我们发现,无论是稍早些的InferSent,还是2018年提出的Quick-thoughts和Multi-task Learning获取通用句子表征的方法,他们无一例外的都使用了同一种思路:将得到的句子表征,在新的分类任务上进行训练,而此时的模型一般都只用一个全连接层,然后接上softmax进行分类,分类器足够简单,足够浅层,相比那些在这些分类任务上设计的足够复杂的模型来说简直不值一提,然而令人大跌眼镜的是,结果无一例外的这些简单的分类器都能够比肩甚至超越他们各自时代的最好结果,这不能不说是个惊喜。而创造这些惊喜的背后功臣,就是迁移学习。更进一步地,迁移学习的本质,就是给爬上“巨人的肩膀”提供了一架结实的梯子。

具体的,在这些句子级别的任务中,属于InferSent和Quick-thoughts这些模型的“巨人肩膀”便是他们各自使用的训练数据,迁移学习最后给他们搭了一个梯子,然而这个梯子并没有很好上,磕磕绊绊,人类AI算是站在第一级梯子上,试探性的伸出了一只腿,另一只腿即将跨出,只可惜并不知道是否有他们苦苦等待了五年之久的戈多?

4.2 一丝曙光

2017年,Salesforce的Bryan McCann和其他一些人,发表了一篇文章《Learned in Translation: Contextualized Word Vectors》,在这篇文章中,他们首先用一个Encoder-Decoder框架在机器翻译的训练语料上进行预训练,尔后用训练好的模型,只取其中的Embedding层和Encoder层,同时在一个新的任务上设计一个task-specific模型,然后将原先预训练好的Embedding层和Encoder层的输出作为这个task-specific模型的输入,最终在新的任务场景下进行训练。他们尝试了很多不同的任务,包括文本分类,Question Answering,Natural Language Inference和SQuAD等等,并在这些任务中,与GloVe作为模型的输入时候的效果进行比较,实验结果表明他们提出的Context Vectors在不同任务中不同程度的都带来了效果的提升。

cove.png

和上文中提到的诸如Skip-thoughts方法有所不同的是,CoVe更侧重于如何将现有数据上预训练得到的表征迁移到新任务场景中,而之前的句子级任务中大多数都只把迁移过程当做一个评估他们表征效果的手段,因此观念上有所不同。

那么,CoVe似乎通过监督数据上的预训练,取得了让人眼前一亮的结果,是否可以进一步地,撇去监督数据的依赖,直接在无标记数据上预训练呢?

4.3 ELMo

2018年的早些时候,AllenNLP的Matthew E. Peters等人在论文《Deep contextualized word representations》(该论文同时被ICLR和NAACL接受,并且还获得了NAACL最佳论文奖,可见这篇论文的含金量)中首次提出了ELMo,它的全称是Embeddings from Language Models,从名称上可以看出,ELMo为了利用无标记数据,使用了语言模型,我们先来看看它是如何利用语言模型的。

webpuploading.4e448015.gif转存失败重新上传取消

biLM_with_residual_ELMo.png

基本框架是一个双层的Bi-LSTM,不过在第一层和第二层之间加入了一个残差结构(一般来说,残差结构能让训练过程更稳定)。做预训练的时候,ELMo的训练目标函数为

math?formula=%5Csum_%7Bk%3D1%7D%5EN%20%5Clog%20p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%2B%20%5Clog%20p(t_k%7Ct_%7Bk%2B1%7D%2C...%2Ct_N)uploading.4e448015.gif转存失败重新上传取消math?formula=%5Csum_%7Bk%3D1%7D%5EN%20%5Clog%20p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%2B%20%5Clog%20p(t_k%7Ct_%7Bk%2B1%7D%2C...%2Ct_N)uploading.4e448015.gif转存失败重新上传取消math?formula=%5Csum_%7Bk%3D1%7D%5EN%20%5Clog%20p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%2B%20%5Clog%20p(t_k%7Ct_%7Bk%2B1%7D%2C...%2Ct_N)uploading.4e448015.gif转存失败重新上传取消\sum_{k=1}^N \log p(t_k|t_1,...,t_{k-1}) + \log p(t_k|t_{k+1},...,t_N)

这个式子很清晰,前后有两个概率,第一个概率是来自于正向的由左到右的RNN结构,在每一个时刻上的RNN输出(也就是这里的第二层LSTM输出),然后再接一个Softmax层将其变为概率含义,就自然得到了math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消p(t_k|t_1,...,t_{k-1});与此类似,第二个概率来自反向的由右到左的RNN结构,每一个时刻RNN的输出经过Softmax层后也能得到一个概率大小,表示从某个词的下文推断该词的概率大小。

ELMo的基本框架便是2-stacked biLSTM + Residual的结构,不过和普通RNN结构的不同之处在于,ELMo借鉴了2016年Google Brain的Rafal Jozefowicz等人发表的一篇论文《Exploring the Limits of Language Modeling》,其主要改进在于输入层和输出层不再是word,而是变为了一个char-based CNN结构,ELMo在输入层和输出层考虑了使用同样的这种结构,该结构如下图示

modified_input_embedding-ELMo.png

这样做有什么好处呢?因为输入层和输出层都使用了这种CNN结构,我们先来看看输出层使用这种结构怎么用,以及有什么优势。我们都知道,在CBOW中的普通Softmax方法中,为了计算每个词的概率大小,使用的如下公式的计算方法

math?formula=P(w_t%7Cc_t)%20%3D%20%5Cfrac%7Bexp(e%5E%7B%5Cprime%7D(w_t)%5ETx)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(e%5E%7B%5Cprime%7D(w_i)%5ETx)%7D%2C%20x%20%3D%20%5Csum_%7Bi%5Cin%20c%7De(w_i)uploading.4e448015.gif转存失败重新上传取消math?formula=P(w_t%7Cc_t)%20%3D%20%5Cfrac%7Bexp(e%5E%7B%5Cprime%7D(w_t)%5ETx)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(e%5E%7B%5Cprime%7D(w_i)%5ETx)%7D%2C%20x%20%3D%20%5Csum_%7Bi%5Cin%20c%7De(w_i)uploading.4e448015.gif转存失败重新上传取消math?formula=P(w_t%7Cc_t)%20%3D%20%5Cfrac%7Bexp(e%5E%7B%5Cprime%7D(w_t)%5ETx)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(e%5E%7B%5Cprime%7D(w_i)%5ETx)%7D%2C%20x%20%3D%20%5Csum_%7Bi%5Cin%20c%7De(w_i)uploading.4e448015.gif转存失败重新上传取消P(w_t|c_t) = \frac{exp(e^{\prime}(w_t)^Tx)}{\sum_{i=1}^{|V|}exp(e^{\prime}(w_i)^Tx)}, x = \sum_{i\in c}e(w_i)

说白了,也就是先通过向量点乘的形式计算得到logits,然后再通过softmax变成概率意义,这本质上和普通的分类没有什么区别,只不过是一个较大的|V|分类问题。现在我们假定char-based CNN模型是现成已有的,对于任意一个目标词都可以得到一个向量表示math?formula=CNN(t_k)uploading.4e448015.gif转存失败重新上传取消math?formula=CNN(t_k)uploading.4e448015.gif转存失败重新上传取消math?formula=CNN(t_k)uploading.4e448015.gif转存失败重新上传取消CNN(t_k),相应的当前时刻的LSTM的输出向量为math?formula=huploading.4e448015.gif转存失败重新上传取消math?formula=huploading.4e448015.gif转存失败重新上传取消math?formula=huploading.4e448015.gif转存失败重新上传取消h,那么便可以通过同样的方法得到目标词的概率大小

math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%3D%20%5Cfrac%7Bexp(CNN(t_k)%5ETh)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(CNN(t_i)%5ETh)%7D%2C%20h%20%3D%20LSTM(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%3D%20%5Cfrac%7Bexp(CNN(t_k)%5ETh)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(CNN(t_i)%5ETh)%7D%2C%20h%20%3D%20LSTM(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消math?formula=p(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)%20%3D%20%5Cfrac%7Bexp(CNN(t_k)%5ETh)%7D%7B%5Csum_%7Bi%3D1%7D%5E%7B%7CV%7C%7Dexp(CNN(t_i)%5ETh)%7D%2C%20h%20%3D%20LSTM(t_k%7Ct_1%2C...%2Ct_%7Bk-1%7D)uploading.4e448015.gif转存失败重新上传取消p(t_k|t_1,...,t_{k-1}) = \frac{exp(CNN(t_k)^Th)}{\sum_{i=1}^{|V|}exp(CNN(t_i)^Th)}, h = LSTM(t_k|t_1,...,t_{k-1})

在原论文中,把这种先经过CNN得到词向量,然后再计算Softmax的方法叫做CNN Softmax,而利用CNN解决有三点优势值得注意,第一是,CNN能够减少普通做Softmax时全连接层中的必须要有的|V|h的参数规模,只需要保持CNN内部的参数大小即可,而一般来说,CNN中的参数规模都要比|V|h的参数规模小得多;另一方面,CNN可以解决OOV(Out-of-Vocabulary)问题,这个在翻译问题中尤其头疼;最后一方面,在预测阶段,CNN对于每一个词向量的计算可以预先做好,更能够减轻inference阶段的计算压力。补充一句:普通Softmax在大词典上的计算压力,都是因为来自于这种方法需要把一个神经网络的输出通过全连接层映射为单个值(而每个类别需要一个映射一次,就是一次h大小的计算规模,|V|次映射便需要总共|V|*h这么多次的映射规模),对于每个类别的映射参数都不同,而CNN Softmax的好处就在于能够做到对于不同的词,映射参数都是共享的,这个共享便体现在使用的CNN中的参数都是同一套,从而大大减少参数的规模。

同样的,对于输入层,ELMo也是用了一样的CNN结构,只不过参数不一样而已,和输出层中的分析类似,输入层中CNN的引入同样可以减少参数规模(不过《Exploring the Limits of Language Modeling》文中也指出了训练时间会略微增加,因为原来的look-up操作可以做到更快一些),对OOV问题也能够比较好的应对,从而把词典大小不再限定在一个固定的词典大小上。最终ELMo的主要结构便如下图(b)所示,可见输入层和输出层都是一个CNN,中间使用Bi-LSTM框架,至于具体细节便如上两张图中所示。

elmo_cnn_softmax.PNG

最后,在大规模语料上训练完成的这种CNN-BIG-LSTM模型(原文如此叫法),怎么用呢?其实,如果把每一层的输出结果拿出来,这里大概有三层的词向量可以利用:输入层CNN的输出,即是LSTM的输入向量,第一层LSTM的输出和第二层的输出向量。又因为LSTM是双向的,因此对于任意一个词,如果LSTM的层数为L的话,总共可获得的向量个数为math?formula=2L%2B1uploading.4e448015.gif转存失败重新上传取消math?formula=2L%2B1uploading.4e448015.gif转存失败重新上传取消math?formula=2L%2B1uploading.4e448015.gif转存失败重新上传取消2L+1,表示如下

math?formula=R_k%20%3D%20%5C%7Bx_k%2C%20%5Coverrightarrow%20%7B%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%2C%20%5Coverleftarrow%20%7B%20%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%20%5C%7D%2C%20j%20%3D%20%5B1%2C2%2C...%2CL%5Duploading.4e448015.gif转存失败重新上传取消math?formula=R_k%20%3D%20%5C%7Bx_k%2C%20%5Coverrightarrow%20%7B%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%2C%20%5Coverleftarrow%20%7B%20%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%20%5C%7D%2C%20j%20%3D%20%5B1%2C2%2C...%2CL%5Duploading.4e448015.gif转存失败重新上传取消math?formula=R_k%20%3D%20%5C%7Bx_k%2C%20%5Coverrightarrow%20%7B%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%2C%20%5Coverleftarrow%20%7B%20%5Ctextbf%7Bh%7D%7D_%7Bk%2Cj%7D%20%5C%7D%2C%20j%20%3D%20%5B1%2C2%2C...%2CL%5Duploading.4e448015.gif转存失败重新上传取消R_k = \{x_k, \overrightarrow {\textbf{h}}_{k,j}, \overleftarrow { \textbf{h}}_{k,j} \}, j = [1,2,...,L]

到这里还只是把ELMo的向量给抽取出来了,具体用的话,对于每一个词,可以根据下面的式子得到它的向量,其中math?formula=%5Cgammauploading.4e448015.gif转存失败重新上传取消math?formula=%5Cgammauploading.4e448015.gif转存失败重新上传取消math?formula=%5Cgammauploading.4e448015.gif转存失败重新上传取消\gamma是一个scale因子,加入这个因子主要是想要将ELMo的向量于具体任务的向量分布拉平到同一个分布水平,这个时候便需要这么一个缩放因子了。另外,math?formula=s_juploading.4e448015.gif转存失败重新上传取消math?formula=s_juploading.4e448015.gif转存失败重新上传取消math?formula=s_juploading.4e448015.gif转存失败重新上传取消s_j便是针对每一层的输出向量,利用一个softmax的参数来学习不同层的权值参数,因为不同的任务需要的词语意义的粒度也不一致,一般认为浅层的表征比较倾向于句法,而高层输出的向量比较倾向于语义信息,因此通过一个softmax的结构让任务自动去学习各层之间的权重,自然也是比较合理的做法。

math?formula=%5Ctextbf%7BELMo%7D_k%5E%7Btask%7D%20%3D%20%5Cgamma%5E%7Btask%7D%20%5Csum_%7Bj%3D0%7D%5E%7BL%7Ds_j%5E%7Btask%7D%5Ctextbf%7Bh%7D_%7Bk%2Cj%7Duploading.4e448015.gif转存失败重新上传取消math?formula=%5Ctextbf%7BELMo%7D_k%5E%7Btask%7D%20%3D%20%5Cgamma%5E%7Btask%7D%20%5Csum_%7Bj%3D0%7D%5E%7BL%7Ds_j%5E%7Btask%7D%5Ctextbf%7Bh%7D_%7Bk%2Cj%7Duploading.4e448015.gif转存失败重新上传取消math?formula=%5Ctextbf%7BELMo%7D_k%5E%7Btask%7D%20%3D%20%5Cgamma%5E%7Btask%7D%20%5Csum_%7Bj%3D0%7D%5E%7BL%7Ds_j%5E%7Btask%7D%5Ctextbf%7Bh%7D_%7Bk%2Cj%7Duploading.4e448015.gif转存失败重新上传取消\textbf{ELMo}_k^{task} = \gamma^{task} \sum_{j=0}^{L}s_j^{task}\textbf{h}_{k,j}

elmo_combination.png

前面我们说过,无论是基于传统统计的N-gram还是普通神经网络的NNLM结构,都会有一个很严重的问题,那就是计算复杂度随着上下文窗口N大小的增大急剧上升(其中N-gram是指数上升,NNLM是以math?formula=%7Cd%7C%5Ctimes%20Nuploading.4e448015.gif转存失败重新上传取消math?formula=%7Cd%7C%5Ctimes%20Nuploading.4e448015.gif转存失败重新上传取消math?formula=%7Cd%7C%5Ctimes%20Nuploading.4e448015.gif转存失败重新上传取消|d|\times N的形式增加,math?formula=%7Cd%7Cuploading.4e448015.gif转存失败重新上传取消math?formula=%7Cd%7Cuploading.4e448015.gif转存失败重新上传取消math?formula=%7Cd%7Cuploading.4e448015.gif转存失败重新上传取消|d|是词向量的维度,虽然NNLM已经改观了很多,但依然是一个斜率很大的线性增加关系),后来CBOW和Skip-gram以及再后来的GloVe等等终于做到了计算复杂度与所选窗口大小无关,只与词典大小和词向量维度相关(不过需要指出的是,这里讨论的计算复杂度只是预测单个词的计算时间复杂度,如果是求整个输入序列的话,还是避免不了要与序列长度相关,在这一点上和下面要分析的RNN在横向的时间序列上有一个时间复杂度,其原因是一致的),并且近些年得益于硬件持续的摩尔定律发挥威力,机器的计算能力也有长足的进步,因此在这两方面因素的作用下,以word2vec为代表的方法大放光彩,引领了一波NLP的发展浪潮。

然而,在今天看来,无论word2vec中的模型,还是GloVe的模型,模型都过于简单,它们都受限于所使用的模型表征能力,某种意义上都只能得到比较偏上下文共现意义上的词向量,并且也很少考虑过词序对于词的意义的影响(比如CBOW从其名称来看就是一个bag-of-words,在模型的输入中没有词序的概念)。理论上,RNN结构的计算复杂度,跟两个方向上都有关系,一方面是纵向上,另一方面是横向上,纵向上主要是RNN结构本身的时间复杂度,这个复杂度只与RNN结构内部的hidden state维度以及模型结构的复杂度,在ELMo中的话还跟词典大小相关(因为最后一层还是一个词典大小上的分类问题,以及输入也需要维护一个词典大小的loop up操作);在横向上的计算复杂度,就主要是受制于输入序列的长度,而RNN结构本身因为在时间序列上共享参数,RNN本身的计算复杂度这一部分不变,因而总的ELMo结构计算复杂度主要有词典大小、隐藏层输出维度大小、模型的结构复杂度以及最后的输入序列长度,前三者可以认为和之前的模型保持一致,最后的输入序列长度,也只是与其保持线性关系,虽然系数是单个RNN单元的计算复杂度,斜率依然很大(通常RNN结构的训练都比较费时),但是在机器性能提升的情况下,这一部分至少不是阻碍词向量技术发展的最关键的因素了。

因此,在新的时代下,机器性能得到更进一步提升的背景下,算法人员都急需一种能够揭示无论词还是句子更深层语义的方法出现,我想ELMo正是顺应了这种时代的需要而华丽诞生。

ELMo的思想足够简单,相比它的前辈们,可以说ELMo并没有本质上的创新,连模型也基本是引用和拼接别人的工作(这似乎从反面证明了真正漂亮的工作从来不是突出各自的模型有多么绚丽,只有无其他亮点的论文,才需要依靠描摹了高清足够喜人眼球的图片去吸引评审人的注意力,因此从这个角度去看,似乎可以得出一个啼笑皆非的结论:论文的漂亮程度与论文图的漂亮程度呈反比),它的思想在很多年前就已经有人在用,并没有特别新奇的地方。

但同时它的效果又足够惊艳,它的出现,在2018年年初,这个也许在NLP历史上并没有多么显眼的年头,掀起了一阵不小的波澜,至少在6项NLP任务上横扫当时的最好结果,包括question answering(SQuAD), textual entailment(SNLI), semantic role labelling(SRL), named entity extraction(NER), coreference resolution(Coref), and sentiment analysis(SST-5)。

而后来的故事以及可预见的将来里,这或许仅仅只是一个开始,就如山洪海啸前的一朵清秀的涟漪。

4.4 ULMFit

差不多和ELMo同期,另一个同样非常惊艳的工作也被提出来,这个团队是致力于将深度学习普及和易用的Fast AI,而论文的两位共同作者之一的Jeremy Howard,其实就是Fast AI的创始人,是Kaggle之前的president和首席科学家,并且亲自参与过Kaggle上的很多比赛,长期排在排行榜的第一。在他们的论文《Universal Language Model Fine-tuning for Text Classification》中,他们提出了ULMFit结构,其实这本质上他们提出的是一个方法,而不是具体的某种结构或模型,只不过正如论文标题所言,他们主要把它应用在了文本分类的问题中。和ELMo相同的地方在于,ULMFit同样使用了语言模型,并且预训练的模型主要也是LSTM,基本的思路也是预训练完成后去具体任务上进行finetune,但是不同的地方也有很多,分别来讲讲。

首先,ULMFit的预训练和finetune过程主要可以分为三个阶段,分别是在大规模语料集上(比如Wikitext 103,有103million个词)先预训练,然后再将预训练好的模型在具体任务的数据上重新利用语言模型来finetune一下(这是第一次finetune,叫做LM finetune),尔后再根据具体任务设计的一个模型上,将预训练好的模型当做这个任务模型的多层,再一次finetune(这是第二次finetune,如果是分类问题的话可以叫做Classifier finetune),整个过程如下所示:

ulmfit_approach.png

ULMFit.PNG

其次,所使用的模型来自于2017年salesforce发表的一篇论文《Regularizing and Optimizing LSTM Language Models》(这是一篇很好的文章),在这篇文章中,他们提出了AWD-LSTM(Averaged SGD and Weight-Dropped LSTM),正如名字中所揭示的,这个框架更多的是一种训练方法,主要思想分为两大块,其中Averaged SGD是指先将模型训练到一定epoch,然后再将其后的每一轮权值进行平均后,得到最终的权值,用公式表示就是,普通的SGD方法权值更新过程为

math?formula=w_%7Bk%2B1%7D%20%3D%20w_k%20-%20%5Cgamma_k%20%5Cnabla%20f(w_k)uploading.4e448015.gif转存失败重新上传取消math?formula=w_%7Bk%2B1%7D%20%3D%20w_k%20-%20%5Cgamma_k%20%5Cnabla%20f(w_k)uploading.4e448015.gif转存失败重新上传取消math?formula=w_%7Bk%2B1%7D%20%3D%20w_k%20-%20%5Cgamma_k%20%5Cnabla%20f(w_k)uploading.4e448015.gif转存失败重新上传取消w_{k+1} = w_k - \gamma_k \nabla f(w_k)

其中k代表迭代次数,而f则是loss function,这就是普通的一个SGD权值更新迭代式子,那么ASGD则把它变成了

math?formula=w%20%3D%20%5Cfrac%7B1%7D%7BK-T%2B1%7D%5Csum_%7Bi%3DT%7D%5EK%20w_iuploading.4e448015.gif转存失败重新上传取消math?formula=w%20%3D%20%5Cfrac%7B1%7D%7BK-T%2B1%7D%5Csum_%7Bi%3DT%7D%5EK%20w_iuploading.4e448015.gif转存失败重新上传取消math?formula=w%20%3D%20%5Cfrac%7B1%7D%7BK-T%2B1%7D%5Csum_%7Bi%3DT%7D%5EK%20w_iuploading.4e448015.gif转存失败重新上传取消w = \frac{1}{K-T+1}\sum_{i=T}^K w_i

其中T是一个阈值,而K则是总共的迭代次数,这个式子的意思就是把迭代到第T次之后,对该参数在其后的第T轮到最后一轮之间的所有值求平均,从而得到最后模型的该参数值,而相应的,普通的SGD则是直接取math?formula=w%20%3D%20w_Kuploading.4e448015.gif转存失败重新上传取消math?formula=w%20%3D%20w_Kuploading.4e448015.gif转存失败重新上传取消math?formula=w%20%3D%20w_Kuploading.4e448015.gif转存失败重新上传取消w = w_K作为最后模型的参数值。

除了使用ASGD的方法训练模型之外,在普通的LSTM上一个时刻和下一个时刻之间的隐藏层之间是有连接的,并且这个连接通过一个全连接的矩阵相连,而这个模型则用了DropConnect的方法随机drop掉一些连接,从而减少了一些过拟合的风险,当然在其他的诸如输入层到隐藏层之间也有正常的dropout操作。

第三,微调的方法设计的非常精妙。作者提出了几种微调的技巧,它们是:discriminative fine-tuning, slanted triangular learning rates, 以及gradual unfreezing,分别来看一下。

discriminative fine-tune的基本思想是针对不同的层在训练更新参数的时候,赋予不同的学习率。这里的出发点是,一般来说,对于NLP的深度学习模型来说,不同层的表征有不同的物理含义,比如浅层偏句法信息,高层偏语义信息,因此对于不同层的学习率不同,自然就是比较合理的了。具体来说,公式是这样的

math?formula=%5Ctheta_%7Bt%7D%5El%20%3D%20%5Ctheta_%7Bt-1%7D%5El%20%2B%20%5Ceta%5El%20%5Cnabla_%7B%5Ctheta%5El%7D%20J(%5Ctheta)uploading.4e448015.gif转存失败重新上传取消math?formula=%5Ctheta_%7Bt%7D%5El%20%3D%20%5Ctheta_%7Bt-1%7D%5El%20%2B%20%5Ceta%5El%20%5Cnabla_%7B%5Ctheta%5El%7D%20J(%5Ctheta)uploading.4e448015.gif转存失败重新上传取消math?formula=%5Ctheta_%7Bt%7D%5El%20%3D%20%5Ctheta_%7Bt-1%7D%5El%20%2B%20%5Ceta%5El%20%5Cnabla_%7B%5Ctheta%5El%7D%20J(%5Ctheta)uploading.4e448015.gif转存失败重新上传取消\theta_{t}^l = \theta_{t-1}^l + \eta^l \nabla_{\theta^l} J(\theta)

这里的math?formula=%5Ceta%5Eluploading.4e448015.gif转存失败重新上传取消math?formula=%5Ceta%5Eluploading.4e448015.gif转存失败重新上传取消math?formula=%5Ceta%5Eluploading.4e448015.gif转存失败重新上传取消\eta^l便是不同的层math?formula=luploading.4e448015.gif转存失败重新上传取消math?formula=luploading.4e448015.gif转存失败重新上传取消math?formula=luploading.4e448015.gif转存失败重新上传取消l有不同的学习率,原文也给出了具体的选择:先指定最后一层的学习率,然后根据下式得到前面层的学习率,基本思想是让浅层的学习率要更小一些。

math?formula=%5Ceta%5E%7Bl-1%7D%20%3D%20%5Cfrac%7B%5Ceta%5El%7D%7B2.6%7Duploading.4e448015.gif转存失败重新上传取消math?formula=%5Ceta%5E%7Bl-1%7D%20%3D%20%5Cfrac%7B%5Ceta%5El%7D%7B2.6%7Duploading.4e448015.gif转存失败重新上传取消math?formula=%5Ceta%5E%7Bl-1%7D%20%3D%20%5Cfrac%7B%5Ceta%5El%7D%7B2.6%7Duploading.4e448015.gif转存失败重新上传取消\eta^{l-1} = \frac{\eta^l}{2.6}

而对于slanted triangular learning rates来说,主要思想便是在finetune的第一阶段,希望能够先稳定住原来已经在大规模语料集上已经预训练好的参数,所以选择一个比较小的finetune学习率;尔后希望能够逐步加大学习率,使得学习过程能够尽量快速;最后,当训练接近尾声时,逐步减小学习率,这样让模型逐渐平稳收敛(这个思想,个人觉得大概借鉴了2017年谷歌提出Transformer时用到的warm up的学习率调节方法,这个方法也是在训练的时候先将学习率逐步增大,尔后再逐步减小)。因此,这样一个三段论式的学习过程,用图表示如下

triangular_learning_rate.PNG

另外一个finetune的技巧是gradual unfreezing,主要思想是把预训练的模型在新任务上finetune时,逐层解冻模型,也就是先finetune最后一层,然后再解冻倒数第二层,把倒数第二层和最后一层一起finetune,然后再解冻第三层,以此类推,逐层往浅层推进,最终finetune整个模型或者终止到某个中间层。这样做的目的也是为了finetune的过程能够更平稳。

当然,值得提出的是,因为ULMFiT中包含了两次finetune,即在新任务上用语言模型finetune和在新任务上finetune训练一个最终的task-specifi-model(比如分类器),而论文中主要把discriminative fine-tuning, slanted triangular learning rates这两个技巧用在了语言模型的finetune阶段,把最后一个gradual unfreezing的技巧应用在最终task-specifi-model的finetune阶段。

通过上面的这些方法,ULMFiT最终在分类任务上表现惊艳,尤其是只需要100个标记数据,就足够能够学习到一个表现非常comparable的分类器,不得不说,这个过程中预训练的语言模型,对最终的表现起到了至关重要的作用。

4.5 GPT

大规模语料集上的预训练语言模型这把火被点燃后,整个业界都在惊呼,原来预训练的语言模型远不止十年前Bengio和五年前Mikolov只为了得到一个词向量的威力。然而,当大家还在惊呼,没过几个月,很快在2018年6月的时候,不再属于“钢铁侠”马斯克的OpenAI,发了一个大新闻(相关论文是《Improving Language Understanding by Generative Pre-Training》),往这把火势正猛的烈焰上加了一剂猛料,从而将这把火推向了一个新的高潮。

OpenAI的猛料配方里,第一剂主料便是谷歌于2017年年中的时候提出的Transformer框架(《Attention Is All You Need》),因此,让我们先来弄明白Transformer里面都有什么东西。私以为,Transformer里最为核心的机制是Self-attention,正是因为Self-attention的存在,才使得Transformer在做类似翻译问题的时候,可以让其Encoder不用做序列输入,而是将整个序列一次全输入,并且超长序列的输入也变得可能。而具体到Self-attention中,可以用下图表示

transformer_multi-headed_self-attention-recap.png

简单说来,输入为句子的矩阵,先分别通过三个全连接矩阵将输入矩阵变化为三个矩阵,分别为Q, K和V,然后通过Q和K的计算得到一些权值,将这些权值加权求和到V矩阵上,便可以得到一个新的矩阵表示。而Self-attention机制中的多头机制便是将这样的操作分别进行多次,这样能让句子的表征充分学习到不同的侧重点,最终将这些多头学习出来的表征concat到一起,然后再同一个全连接网络,便可以得到这个句子的最终Self-attention下新的表示。将其中的每一个头的操作过程用公式表示如下,需要注意的是softmax是针对矩阵的row方向上进行操作得到的。所以,说白了,这个公式表示的意思就是针对V进行加权求和,加权的权值通过Q和K的点乘得到。

self-attention-matrix-calculation-2.png

不过其实Self-attention和普通的attention机制在形式上几乎完全等价。主要区别在于,对于普通的attention机制,输入可能有多个,并且下式在求得math?formula=e_%7Bij%7Duploading.4e448015.gif转存失败重新上传取消math?formula=e_%7Bij%7Duploading.4e448015.gif转存失败重新上传取消math?formula=e_%7Bij%7Duploading.4e448015.gif转存失败重新上传取消e_{ij}中的math?formula=v_a%5ETuploading.4e448015.gif转存失败重新上传取消math?formula=v_a%5ETuploading.4e448015.gif转存失败重新上传取消math?formula=v_a%5ETuploading.4e448015.gif转存失败重新上传取消v_a^T实际上是一个全连接网络(当然这只是其中一种计算方法,也可以用dot attention的方式得到),将式子右边的部分(也就是attention的输入)映射为一个值,从而可以根据这个值计算attention的权值大小。除此之外,普通的attention和self-attention并没有本质不同,最大的区别还是在于在自我输入上计算attention权值大小。

attention-mechanisms.jpg

在Transformer的Encoder中,还有一些其他的设计,比如加入position embedding(因为Transformer的Encoder中不是时序输入词序列,因此position embedding也是主要的位置信息);Residual结构,使得模型的训练过程更为平稳,此外还有normalization层,接着便是feed forward层(本质上是一个两层的全连接网络,中间加一个ReLu的激活函数)。Decoder的结构与此类似,只不过在进行decode的时候,会将Encoder这边的输出作为Decoder中Self-attention时候的K和V。

transformer_resideual_layer_norm_3.png

对于decode的过程,具体来看,大致过程如下。

transformer_decoding_2.gif

(对具体实现细节不关心的可以略过此段)但是Decoder实际上还有很多细节,一般来说,训练的时候Decoder中的输入可以用矩阵的形式一次完成当前整个序列的decode过程,因为ground truth已经提前知道,只需要做好每个词的mask就好(为了避免待预测的词影响到当前的输入),然而在做inference的时候,Decoder必须按照序列输入,因为在生成每一个词的时候,必须先生成它的前一个词,无法一次将整个序列全部生成(当然理论上也可以,但是效果并不好)。在矩阵运算过程中,Decoder中有许多的mask操作,参与运算的三个矩阵Q,K和V都要做许多的mask操作,主要有两方面的作用:一方面是消除输入句子本身长度之外的padding的影响,另一方面是decoder必须要求不能提前看到待生成的词。除了mask操作,另外,值得注意的是,和Encoder中只有一种类型的Self-attention不同的,Decoder的attention实际上包含两部分,第一部分是带有mask的Self-attention,通过mask的作用将decode阶段的attention限定只会attention到已经生成过的词上,因此叫做Mask Self-attention;第二部分是普通的Self-attention操作,不过这个时候的K和V矩阵已经替换为Encoder的输出结果,所以本质上并不是一个Self-attention了。

下面的动图很好的表现了decoding过程,生成每一个词的时候,既和Encoder的输出信息有关,也和已经生成过的词相关。

transformer_decoding_3.gif

大体介绍完Transformer后,再来看看GPT中的是怎么用Transformer的。按照论文中的说法,GPT中使用的Transformer是只用了Decoder,因为对于语言模型来讲,确实不需要Encoder的存在,而具体模型,他们参考了2018年早些时候谷歌发表的一篇论文《Generating Wikipedia by Summarizing Long Sequences》(而GPT名称中的Generative该词便是来自于这篇文章,因为这二者都有用到生成式的方法来训练模性,也就是生成式的Decoder),关于这篇论文中提到的T-DMCA(Transformer Decoder with Memory-Compressed Attention),实际上就是一个Decoder,只不过这篇文章中要做超长的序列输入(可以长达11000个词),为了能够高效节省时间和内存的处理如此长的序列,做了一些Memory-Compressed的工作,主要是两方面:一方面是把一个batch内部的序列按长度进行分组,然后分别在每个组内部进行self-attention操作,这样可以避免将一些很短的句子也padding到整个语料的最大长度;另一方面,通过CNN的操作,把K和V压缩到序列长度更小的一个矩阵,同时保持Q不变,这样也能相当程度上减少计算量。

transformer_decoder_memory_compressed_attention.PNG

除了这些具体的模型细节外,GPT本质上就是用了语言模型的目标函数来优化和训练Transformer-Decoder,这个和上文提到过的语言模型保持一致。利用语言模型的目标函数预训练完成后,进阶这便可以在具体的任务上进行finetune,和ULMFiT中的finetune分为两个阶段的方法不一样的是,GPT直接把这两个过程糅合到一个目标函数中,如

math?formula=L_3(C)%20%3D%20L_2(C)%20%2B%20%5Clambda%20L_1(C)uploading.4e448015.gif转存失败重新上传取消math?formula=L_3(C)%20%3D%20L_2(C)%20%2B%20%5Clambda%20L_1(C)uploading.4e448015.gif转存失败重新上传取消math?formula=L_3(C)%20%3D%20L_2(C)%20%2B%20%5Clambda%20L_1(C)uploading.4e448015.gif转存失败重新上传取消L_3(C) = L_2(C) + \lambda L_1(C)

其中math?formula=L_2uploading.4e448015.gif转存失败重新上传取消math?formula=L_2uploading.4e448015.gif转存失败重新上传取消math?formula=L_2uploading.4e448015.gif转存失败重新上传取消L_2是task-specific的目标函数,math?formula=L_1uploading.4e448015.gif转存失败重新上传取消math?formula=L_1uploading.4e448015.gif转存失败重新上传取消math?formula=L_1uploading.4e448015.gif转存失败重新上传取消L_1则是语言模型的目标函数。论文中说这种联合学习的方式能够让训练效果更好。而在具体如何做迁移学习的方面,GPT大概也同样借鉴了上面提到的《Generating Wikipedia by Summarizing Long Sequences》论文中的做法,非常巧妙的将整个迁移学习的框架做到非常的精简和通用。分类问题中,直接在原序列的开始和末尾添加表示开始和末尾的符号,在Text Entailment问题中(比如Natural Language Inference),将Premise和Hypothesis通过一个中间分隔符“$”连接起来成为一个序列,尔后同样在开头和末尾添加标记符号,文本相似问题中,因为序列1和序列2没有先后关系,因此将先后关系相反的两个序列作为输入,在Question Aswering中,将query和每一个候选的answer都分别连接成一个序列作为输入,最后按各自的打分进行排序。因此,这套输入的表示方法,基本可以使用同一个输入框架来表征许多文本问题(以至于后来的BERT直接借用了这套做法)。除此之外,在输出层,只需要接入一个很简单的全连接层或者MLP便可以,根本不需要非常复杂的模型设计。而整个finetune阶段,新加入的参数极少,只有输出层以及输入层中添加的一些特殊标记(比如分隔符)。

正是因为有了输入层和输出层的这种通用化设计考虑,一旦中间的Transformer(当然,正如前文所说,这里的Transformer在使用语言模型进行预训练的时候只有Decoder部分,然而在将其当做文本特征提取器的时候,相应的也可以很便利的将其变成Encoder)表征能力足够强大,迁移学习在NLP任务中的威力也会变得更为强大。

果不其然,GPT在其公布的结果中,一举刷新了12项NLP任务中的9项榜单,效果不可谓不惊艳。然而对于OpenAI来讲,GPT底层模型使用的是谷歌提出的Tranformer,正是依靠了Transformer的强大表征能力,使得最终的效果有了一个坚实的基础,然而仅仅过了四个月之后的BERT横空出世,同样也是用了Transformer,同样是谷歌,甚至很多思想也是直接借鉴GPT,GPT作为与BERT气质最为接近的工作,同时也是BERT的前辈,得到的待遇差别如此之大,不知道GPT是否有些可惜和遗憾,相比BERT,GPT并没有带来特别巨大的反响,他的惊艳亮相,迅速变为水里的一声闷响,掀起了一阵涟漪后迅速消散,将整个舞台让位于正值青春光艳照人的BERT,颇有点“成也萧何败也萧何”的味道。

GPT.PNG



作者:weizier
链接:https://www.jianshu.com/p/81dddec296fa
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值