从语言模型到Seq2Seq:Transformer如戏,全靠Mask
By 苏剑林 | 2019-09-18 | 331117位读者 |相信近一年来(尤其是近半年来),大家都能很频繁地看到各种Transformer相关工作(比如Bert、GPT、XLNet等等)的报导,连同各种基础评测任务的评测指标不断被刷新。同时,也有很多相关的博客、专栏等对这些模型做科普和解读。
俗话说,“外行看热闹,内行看门道”,我们不仅要在“是什么”这个层面去理解这些工作,我们还需要思考“为什么”。这个“为什么”不仅仅是“为什么要这样做”,还包括“为什么可以这样做”。比如,在谈到XLNet的乱序语言模型时,我们或许已经从诸多介绍中明白了乱序语言模型的好处,那不妨更进一步思考一下:
为什么Transformer可以实现乱序语言模型?是怎么实现的?RNN可以实现吗?
本文从对Attention矩阵进行Mask的角度,来分析为什么众多Transformer模型可以玩得如此“出彩”的基本原因,正如标题所述“Transformer如戏,全靠Mask”,这是各种花式Transformer模型的重要“门道”之一。
读完本文,你或许可以了解到:
1、Attention矩阵的Mask方式与各种预训练方案的关系;
2、直接利用预训练的Bert模型来做Seq2Seq任务。
背景 #
自《Attention is All You Need》以后,基于纯Attention的Transformer类模型逐渐变得流行起来,而Bert的出现则将这股潮流推向了一个新的高度。而后,各种基于大规模预训练的Transformer模型的工作不断出现,有基于现成的模型做应用的,有试图更好地去解释和可视化这些模型的,还有改进架构、改进预训练方式等以得到更好结果的。总的来说,这些以预训练为基础的工作层出不穷,有种琳琅满目的感觉。甚至一定程度上来说,如果你还没有微调过Bert,那已经算是落后于主流的NLP技术了。
花式预训练 #
众所周知,传统的模型预训练手段就是语言模型,比如ELMo模型就是以BiLSTM为基础架构、用两个方向的语言模型分别预训练两个方向的LSTM的,后面的OpenAI的GPT、GPT-2也是坚定不移地坚持着用祖传的(标准的、单向的)语言模型来预训练。
然而,还有更多花样的预训练玩法。比如Bert就用了称之为“掩码语言模型(Masked Language Model)”的方式来预训练,不过这只是普通语言模型的一种变体;还有XLNet则提出了更彻底的“Permutation Language Modeling”,我们可以称之为“乱序语言模型”;还有UNILM模型,直接用单个Bert的架构做Seq2Seq,你可以将它作为一种预训练手段,又或者干脆就用它来做Seq2Seq任务...
如此花样百出,让我们不禁疑问:为什么刚好在Transformer流行的时代,才出现这种各种大型预训练模型“百花齐放,百家争鸣”的现象?
Transformer专属 #
事实上,除了单向语言模型及其简单变体掩码语言模型之外,UNILM的Seq2Seq预训练、XLNet的乱序语言模型预训练,基本可以说是专为Transformer架构定制的。说白了,如果是RNN架构,根本就不能用乱序语言模型的方式来预训练,至于Seq2Seq的预训练方式,则必须同时引入两个模型(encoder和decoder),而无法像Transformer架构一样,可以一个模型搞定。
这其中的奥妙主要在Attention矩阵之上。Attention实际上相当于将输入两两地算相似度,这构成了一个$n^2$大小的相似度矩阵(即Attention矩阵,$n$是句子长度,本文的Attention均指Self Attention),这意味着它的空间占用量是$\mathcal{O}(n^2)$量级,相比之下,RNN模型、CNN模型只不过是$\mathcal{O}(n)$,所以实际上Attention通常更耗显存。然而,有弊也有利,更大的空间占用也意味着拥有了更多的可能性,我们可以通过往这个$\mathcal{O}(n^2)$级别的Attention矩阵加入各种先验约束,使得它可以做更灵活的任务。说白了,也就只有纯Attention的模型,才有那么大的“容量”去承载那么多的“花样”。
而加入先验约束的方式,就是对Attention矩阵进行不同形式的Mask,这便是本文要关注的焦点。
分析 #
在《〈Attention is All You Need〉浅读(简介+代码)》一文中笔者已经对Attention做了基本介绍,这里仅做简单回顾。Attention的数学形式为:
\begin{equation}Attention(\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}) = softmax\left(\frac{\boldsymbol{Q}\boldsymbol{K}^{\top}}{\sqrt{d_k}}\right)\boldsymbol{V}\end{equation}
这里的$\boldsymbol{Q}\in \mathbb{R}^{l_q\times d_q},\boldsymbol{K}\in\mathbb{R}^{l_k\times d_q},\boldsymbol{V}\in\mathbb{R}^{l_k\times d_v}$,分别代表query、key、value的向量序列,其中我们可以认为key和value是一一对应的,而$\boldsymbol{Q}\boldsymbol{K}^{\top}$则是将query、key的向量两两做内积,然后用$softmax$归一化,就得到一个$l_q\times l_k$的Attention矩阵,它描述的就是query和key之间任意两个元素的关联强度,后面我们要讲的故事,都是在这个Attention矩阵上下功夫。最后再与$\boldsymbol{V}$相乘,相当于按照这个关联强度将$\boldsymbol{V}$的各个向量加权求和,最终输出一个$l_q\times d_v$的向量序列。
目前最常用的Attention方式当数Self Attention,即$\boldsymbol{Q},\boldsymbol{K},\boldsymbol{V}$都是同一个向量序列经过线性变换而来的,而Transformer则是Self Attention跟Position-Wise全连接层(相当于kernel size为1的一维卷积)的组合。所以,Transformer就是基于Attention的向量序列到向量序列的变换。
在本节中,我们将会比较详细地分析Attention矩阵的Mask方式,这分别对应单向语言模型、乱序语言模型、Seq2Seq的实现原理。
单向语言模型 #
语言模型可以说是一个无条件的文本生成模型,如果读者还不了解文本生成模型,可以自行查阅相关资料并配合《玩转Keras之seq2seq自动生成标题》一文来理解。单向语言模型相当于把训练语料通过下述条件概率分布的方式“记住”了:
\begin{equation}p(x_1,x_2,x_3,\dots,x_n)=p(x_1) p(x_2|x_1) p(x_3|x_1,x_2) \dots p(x_n|x_1,\dots,x_{n-1})\end{equation}
我们一般说的“语言模型”,就是指单向的(更狭义的只是指正向的)语言模型。语言模型的关键点是要防止看到“未来信息”。如上式,预测$x_1$的时候,是没有任何外部输入的;而预测$x_2$的时候,只能输入$x_1$,预测$x_3$的时候,只能输入$x_1,x_2$;依此类推。
RNN模型是天然适合做语言模型的,因为它本身就是递归的运算;如果用CNN来做的话,则需要对卷积核进行Mask,即需要将卷积核对应右边的部分置零。如果是Transformer呢?那需要一个下三角矩阵形式的Attention矩阵:
如图所示,Attention矩阵的每一行事实上代表着输出,而每一列代表着输入,而Attention矩阵就表示输出和输入的关联。假定白色方格都代表0,那么第1行表示“北”只能跟起始标记<s>相关了,而第2行就表示“京”只能跟起始标记<s>和“北”相关了,依此类推。所以,只需要在Transformer的Attention矩阵中引入下三角形形式的Mask,并将输入输出错开一位训练,就可以实现单向语言模型了。(至于Mask的实现方式,可以参考《“让Keras更酷一些!”:层中层与mask》的Mask一节。)
乱序语言模型 #
乱序语言模型是XLNet提出来的概念,它主要用于XLNet的预训练上。说到XLNet,我觉得它的乱序语言模型这种预训练方式是很有意思的,但是我并不喜欢它将基本架构换成了Transformer-XL。我觉得谁有资源可以试试“Bert+乱序语言语言模型预训练”的组合,或许会有意外的发现。
乱序语言模型跟语言模型一样,都是做条件概率分解,但是乱序语言模型的分解顺序是随机的:
\begin{equation}\begin{aligned}p(x_1,x_2,x_3,\dots,x_n)=&p(x_1) p(x_2|x_1) p(x_3|x_1,x_2) \dots p(x_n|x_1,x_2,\dots,x_{n-1})\\
=&p(x_3) p(x_1|x_3) p(x_2|x_3,x_1) \dots p(x_n|x_3,x_1,\dots,x_{n-1})\\
=&\dots\\
=&p(x_{n-1})p(x_1|x_{n-1})p(x_n|x_{n-1}, x_1)\dots p(x_2|x_{n-1}, x_1,\dots,x_3)\end{aligned}\end{equation}
总之,$x_1,x_2,\dots,x_n$任意一种“出场顺序”都有可能。原则上来说,每一种顺序都对应着一个模型,所以原则上就有$n!$个语言模型。而基于Transformer的模型,则可以将这所有顺序都做到一个模型中去!
那怎么做到这一点呢?还是以“北京欢迎你”的生成为例,假设随机的一种生成顺序为“<s> → 迎 → 京 → 你 → 欢 → 北 → <e>”,那么我们只需要用下图中第二个子图的方式去Mask掉Attention矩阵,就可以达到目的了:
跟前面的单向语言模型类似,第4行只有一个蓝色格,表示“迎”只能跟起始标记<s>相关,而第2行有两个蓝色格,表示“京”只能跟起始标记<s>和“迎”相关,依此类推。直观来看,这就像是把单向语言模型的下三角形式的Mask“打乱”了。
也就是说,实现某种特定顺序的语言模型,就相当于将原来的下三角形式的Mask以某种方式打乱。正因为Attention提供了这样的一个$n\times n$的Attention矩阵,我们才有足够多的自由度去以不同的方式去Mask这个矩阵,从而实现多样化的效果。
说到这里,读者可能会有一个实现上的疑问:打乱后的Mask似乎没看出什么规律呀,难道每次都要随机生成一个这样的似乎没有什么明显概率的Mask矩阵?事实上有一种更简单的、数学上等效的训练方案。这个训练方案源于纯Attention的模型本质上是一个无序的模型,它里边的词序实际上是通过Position Embedding加上去的。也就是说,我们输入的不仅只有token本身,还包括token所在的位置id;再换言之,你觉得你是输入了序列“[北, 京, 欢, 迎, 你]”,实际上你输入的是集合“{(北, 1), (京, 2), (欢, 3), (迎, 4), (你, 5)}”。
既然只是一个集合,跟顺序无关,那么我们完全可以换一种顺序输入,比如刚才的“<s> → 迎 → 京 → 你 → 欢 → 北 → <e>”,我们可以按“(迎, 4), (京, 2), (你, 5), (欢, 3), (北, 1)”的顺序输入,也就是说将token打乱为“迎,京,你,欢,北”输入到Transformer中,但是第1个token的position就不是1了,而是4;依此类推。这样换过来之后,Mask矩阵可以恢复为下三角矩阵,所以只需要在输入层面打乱即可,这样操作起来就更简单了。
Seq2Seq #
现在到我们的“重头戏”了:将Bert等Transformer架构跟Seq2Seq结合起来。为什么说重头戏呢?因为原则上来说,任何NLP问题都可以转化为Seq2Seq来做,它是一个真正意义上的万能模型。所以如果能够做到Seq2Seq,理论上就可以实现任意任务了。
将Bert与Seq2Seq结合的比较知名的工作有两个:MASS和UNILM,两者都是微软的工作,两者还都在同一个月发的~其中MASS还是普通的Seq2Seq架构,分别用Bert类似的Transformer模型来做encoder和decoder,它的主要贡献就是提供了一种Seq2Seq思想的预训练方案;真正有意思的是UNILM,它提供了一种很优雅的方式,能够让我们直接用单个Bert模型就可以做Seq2Seq任务,而不用区分encoder和decoder。而实现这一点几乎不费吹灰之力——只需要一个特别的Mask。
(插曲:事实的顺序是笔者前两周自己独立地想到了用单个Bert模型做Seq2Seq的思路,然后去找资料发现这个思路已经被做了,正是UNILM。)
UNILM直接将Seq2Seq当成句子补全来做。假如输入是“你想吃啥”,目标句子是“白切鸡”,那UNILM将这两个句子拼成一个:[CLS] 你 想 吃 啥 [SEP] 白 切 鸡 [SEP]。经过这样转化之后,最简单的方案就是训练一个语言模型,然后输入“[CLS] 你 想 吃 啥 [SEP]”来逐字预测“白 切 鸡”,直到出现“[SEP]”为止,即如下面的左图:
不过左图只是最朴素的方案,它把“你想吃啥”也加入了预测范围了(导致它这部分的Attention是单向的,即对应部分的Mask矩阵是下三角),事实上这是不必要的,属于额外的约束。真正要预测的只是“白切鸡”这部分,所以我们可以把“你想吃啥”这部分的Mask去掉,得到上面的右图的Mask。
这样一来,输入部分的Attention是双向的,输出部分的Attention是单向,满足Seq2Seq的要求,而且没有额外约束。这便是UNILM里边提供的用单个Bert模型就可以完成Seq2Seq任务的思路,只要添加上述形状的Mask,而不需要修改模型架构,并且还可以直接沿用Bert的Masked Language Model预训练权重,收敛更快。这符合“一Bert在手,天下我有”的万用模型的初衷,个人认为这是非常优雅的方案。
实验 #
事实上,上述的这些Mask方案,基本上都已经被集成在笔者写的bert4keras,读者可以直接用bert4keras加载bert的预训练权重,并且调用上述Mask方案来做相应的任务。下面,我们给出一个利用UNILM的思路做一个快速收敛的Seq2Seq模型的例子。
代码开源 #
这次代码的测试任务依然是之前的标题生成,代码调整自《玩转Keras之seq2seq自动生成标题》里边的代码,并且得益于bert4keras的封装,模型部分的代码实现非常简单清爽。这一次直接使用了THUCNews的原始数据集,读者可以自行下载数据集和源码测试复现。
详细请看:task_seq2seq_autotitle.py
这个效果能有多好呢?经过实验,在标题生成的任务上,从第一个epoch(1000个iteration)开始,就已经能生成基本可读的标题了。相应地,以前用LSTM做的时候,大概需要多几十倍的iteration才有同样的效果。
简单说明 #
下面对代码的关键部分做简要说明。
首先,输入格式还是以token_id
和segment_id
输入,比如:
tokens = ['[ClS]', u'你', u'想', u'吃', u'啥', '[SEP]', u'白', u'切', u'鸡', '[SEP]']
token_ids = [token_dict[t] for t in tokens]
segment_ids = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1]
segment_ids
用来区分输入句子和目标句子,0对应的为输入句子,1对应的为目标句子,只需要自带的tokenizer.encode
就可以生成这种token_id
和segment_id
了。
至于搭建模型,就只有寥寥几行:
model = build_transformer_model(
config_path,
checkpoint_path,
application='unilm',
keep_tokens=keep_tokens
)
model.summary()
y_in = model.input[0][:, 1:] # 目标tokens
y_mask = model.input[1][:, 1:]
y = model.output[:, :-1] # 预测tokens,预测与目标错开一位
# 交叉熵作为loss,并mask掉输入部分的预测
cross_entropy = K.sparse_categorical_crossentropy(y_in, y)
cross_entropy = K.sum(cross_entropy * y_mask) / K.sum(y_mask)
注意build_transformer_model
中只要设置application='unilm'
,就会自动加载Bert的MLM部分,并且传入对应的Mask,剩下就只需要把loss写好就行了。另外还有一个keep_tokens
,这个是用来精简Embedding层用的,对于中文Bert来说,总的tokens大概有2万个,这意味着最后预测生成的token时是一个2万分类问题。但事实上有接近一半的tokens都不会分出来(理论上都不会),因此这2万分类浪费了一些计算量。于是这里提供了一个选项,我们可以自行维护一个token表,然后传入对应的id,只保留这部分token,这样就可以降低计算量了(精简后一般只是原来的一半,甚至更少)。
剩下的就是通过beam search来解码等步骤了,这与一般的Seq2Seq无异,不再赘述,大家看《玩转Keras之seq2seq自动生成标题》和代码即可。
总结 #
本文相对系统地总结了Transformer中Attention矩阵的Mask技巧,并且给出了用UNILM方案来做Seq2Seq的实现。对于同语言的Seq2Seq的文本生成任务来说,采用UNILM的思路加载Bert的MLM预训练权重,能够有效、快速地实现并提升生成效果,值得一试。
转载到请包括本文地址:https://kexue.fm/archives/6933
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Sep. 18, 2019). 《从语言模型到Seq2Seq:Transformer如戏,全靠Mask 》[Blog post]. Retrieved from https://kexue.fm/archives/6933
@online{kexuefm-6933,
title={从语言模型到Seq2Seq:Transformer如戏,全靠Mask},
author={苏剑林},
year={2019},
month={Sep},
url={\url{https://kexue.fm/archives/6933}},
}
September 18th, 2019
膜拜
September 19th, 2019
大佬好,self-attention中,Q, K, V的维度,行数不是一样的吗?即在《分析》section,lq=lk=lk=len(input sequences), 是我理解错了嘛还是?
但我写$(1)$的时候没说是Self Attention呀,我后面才说的“目前最常用的Attention方式当数Self Attention”。
哈哈,是的大佬,我问完之后又读了一遍,已经发现了~_~
September 23rd, 2019
非常感谢,看完以后醍醐灌顶。
September 25th, 2019
苏神写得非常好,通俗易懂!
September 25th, 2019
苏神好,我想问下代码中的114行,beam_search那一段
for j, (ids, sco) in enumerate(zip(target_ids, target_scores)):
if i == 0 and j > 1:
continue
for k in _topk_arg[j]:
_candidate_ids.append(ids + [k + 3])
_candidate_scores.append(sco + _log_probas[j][k])
这里j > 1是否应该是j >= 1?,当预测第一个字的时候,因为这时候用于预测的两个编码序列是一样的,如果这里j > 1的话会经过0和1两次遍历,_candidate_ids中会出现重复的预测字符,当后续进行argsort选topk的时候将选择出topk个相同的字符,没有达到beam_search的效果,不知这样有没有理解正确?谢谢大佬的分享
感谢你的仔细阅读,确实是这样的,已经修改。
September 26th, 2019
苏神,读了你的文章,收获满满,非常感谢。发现task_seq2seq.py代码的一个笔误,118行代码应该是重复了。
感谢指出,已经修正。
September 26th, 2019
大佬啊
September 29th, 2019
HI,很高兴认识你,都是同届校友。看了你博客更新的挺频繁,技术涉及面也挺广,那么有一些疑问想跟你了解一下。
1. 听说你是在追一工作,想知道你在那边的定位是纯技术研究么?如果不是,怎么平衡业务和技术研究?毕竟你博客更新比较频繁,涉猎面也广。当然,牛人做什么都高效我就没话说了
2. 涉猎面广是好事,你怎么看待技术研究的深度和广度呢?目前是怎么做到兼顾,还是说凭兴趣使然?例如专攻问答或者阅读理解。
3. 既然你那么多精力花在技术上,那么运动健康管理、理财、游玩上会花时间么?从你日常生活状态上看
4. 你对tackle一些NLP核心问题有兴趣么?例如一些跨领域的idea融合来创新模型,或者如何让一个模型系统半监督或者无监督地,学习概念和语义,做到更高的泛化性。毕竟现在的预训练模型是要finetune,但是finetune可能会效果好,也可能受数据质量问题影响。真正的泛化性应该是像人那样,学习完就可以用了。
5. RL可以处理离散空间上面的决策问题,为了maximize一个长远的reward,跟文本的理解或者生成共通性感觉很大,不知道你有什么见解
1. 目前的岗位基本算是纯技术;
2. 我不是刻意涉猎广,我只是挑我感兴趣的内容琢磨下去,所以在我这里不存在广度和深度这样的问题;
3. “运动健康管理、理财、游玩”这几个没什么兴趣,但有其他兴趣,比如做菜、下棋;
4. 有;
5. 强化学习瓶颈还是很大,目前能生效的强化学习无非有两种:一种是靠强大的算力搜索足够多的时间,这种情况下强化学习算是一种相对智能一些的枚举技巧罢了;另一种是先用普通目标预训练,再用强化学习训练,预训练能有效缩小搜索空间,这种情况下,强化学习的作用就是finetune。
October 3rd, 2019
谢谢您的分享,真的很有启发
冒昧的问下,请问平时你在公司做深度学习的研究也是用python吗?
有其他Java或者C++的要求吗?
我是纯研究居多,只用python
October 5th, 2019
> "说白了,如果是RNN架构,根本就不能用乱序语言模型的方式来预训练,至于Seq2Seq的预训练方式,则必须同时引入两个模型(encoder和decoder),而无法像Transformer架构一样,可以一个模型搞定。"
杠一下:
1、RNN 也可以用 PLM 的目标来训,把输入打乱带上 position embedding 就行,就像你后面说的 “(迎, 4), (京, 2), (你, 5), (欢, 3), (北, 1)” 这样。
2、RNN 也可以用一个模型来做,把源语言和目标语言句子拼在一起,然后训一个语言模型;这种方式本质上就是 encoder-decoder 共享参数的、不带 attention 的 seq2seq 模型,在 NMT 最早期有过这样的模型。如果带上 attention,但是 encoder-decoder 依然共享参数,效果也很好,在某些系统中有应用,例如以下 GitHub issue:https://github.com/pochih/RL-Chatbot/issues/4
,或是论文 Tied Transformers: Neural Machine Translation with Shared Encoder and Decoder 中的 4.4 节(虽然标题是 Tied Transformer,但这一节也做了 LSTM 的实验,效果也是正面的,BLEU 比 baseline 高了一个点)。还有另一种只用单个 RNN 做机器翻译的方案,详见论文 You May Not Need Attention,大意是把源语言和目标语言的句子同时输入一个 RNN,但是目标语言的句子稍等几个时间步错位一下,然后每一步预测目标语言的下一个词。当然这个方案 Transformer 也能用。
总的来说,RNN 确实不如 Transformer 效果好,毕竟 Transformer 可以直接提取任意两个单词的关联关系,可以堆叠的很深,而 RNN 不行。
你说的那两点,其实我都考虑过,原则上没有问题,但实际上会有问题的。
1、RNN的递归性本身会建立位置信息,如果用PLM训练RNN的话,反而会强迫RNN放弃自己学习位置信息,仅保留position embedding的位置信息,可谓得不偿失;
2、”把源语言和目标语言句子拼在一起,然后训一个语言模型“缺点在于无法在源句子中实现双向推理。
总的来说用RNN去实现这类思路,并没有Transformer来得自然优雅,所以我才否定了RNN对这类思路的价值