准备知识(往前面章节看):
- encode-decode模型
- attention模型
- self-attention模型
引言
自从Attention机制在提出之后,加入Attention的Seq2Seq模型在各个任务上都有了提升,所以现在的Seq2Seq模型指的都是结合RNN和Attention的模型。传统的基于RNN的Seq2Seq模型难以处理长序列的句子,无法实现并行,并且面临对齐的问题。
所以之后这类模型的发展大多数从三个方面入手:
(1)Input的方向性从单向到多向;
(2)深度从单层到多层;
(3)类型从RNN到LSTM GRU。
但是依旧收到一些潜在问题的制约,神经网络需要能够将源语句的所有必要信息压缩成固定长度的向量。这可能使得神经网络难以应付长时间的句子,特别是那些比训练语料库中的句子更长的句子;每个时间步的输出需要依赖于前面时间步的输出,这使得模型没有办法并行,效率低;仍然面临对齐问题。
再然后CNN由计算机视觉也被引入到Deep NLP中,CNN不能直接用于处理变长的序列样本但可以实现并行计算。完全基于CNN的Seq2Seq模型虽然可以并行实现,但非常占内存,很多的trick,大数据量上参数调整并不容易。
本篇文章创新点在于抛弃了之前传统的Encoder-Decoder模型必须结合CNN或者RNN的固有模式,只用Attention机制。文章的主要目的在于减少计算量和提高并行效率的同时不损害最终的实验结果。
attention的概念复习
传统的attention-based MT
基本公式
google是最先形式化地提出attention地概念的。它把attention的三个重要组件分别称为key,value和query。
直接解释这三个名词可能会很抽象难懂。我们从传统的attention-based MT模型入手。在图1中,query就是解码层的s,key和value都是编码层的输出o。字面意思上来说,query是查询,即:我与你们的相似度是多少?key是键,即query要比较的值,value是值,一个key对应一个value,即query与我的key相似度为a,则我这个值对query的贡献度就为a。那么每个位置解码的时候,query(s)会分别计算与编码层各个位置key的相似度,把相似度做为对应value的权重,最后此位置的attention的输出即为所有位置value的加权输出。即用公式表示为:
self-attention的概念复习
query来自于解码层,key和value来自于编码层时叫vanilla attention,即最基本的attention。
query,key和value都来自编码层的叫self attention。
于是,很自然的,把rnn换成self-attention。
在编码层,输入的word-embedding就是key,value和query,然后做self-attention得到编码层的输出。这一步就模拟了图1中的编码层,输出就可以看成图1中的h。
然后模拟图1中的解码层,解码层的关键是如何得到s,即用来和编码层做attention的query,我们发现,s与上个位置的真实label y,上个位置的s,和当前位置的attention输出c有关,换句话说,位置i的s利用了所有它之前的真实label y信息,和所有它之前位置的attention的输出c信息。label y信息我们全都是已知的,而之前位置的c信息虽然也可以利用,但是我们不能用,因为那样就又不能并行了(因为当前位置的c信息必须等它之前的c信息都计算出来)。于是我们只能用真实label y来模拟解码层的rnn。前面说过,当前位置s使用了它之前的所有真实label y信息。于是我们可以做一个masked attention,即对真实label y像编码层的x一样做self-attention,但每个位置的y只与它之前的y有关(mask),这样,self-attention之后每个位置的输出综合了当前位置和它之前位置的所有y信息,即可做为s(query)。
得到编码层的key和value以及解码层的query后,下面就是模仿vanilla attention,利用key和value以及query再做最后一个attention。得到每个位置的输出。
总结起来就是,x做self-attention得到key和value,y做masked self-attention得到query,然后key,value,query做vanilla-attention得到最终输出。
multihead attention
本质:将一个向量划分多份,每一份称为一个head。
multihead attention。这是这篇文章的一个创新,下面详细讲一讲。
原始的attention, 就是一个query (以下简称Q) 和一组key (以下简称K) 算相似度, 然后对一组value (以下简称V) 做加权和; 假如每个Q和K都是512维的向量, 那么这就相当于在512维的空间里比较了两个向量的相似度. 而multihead就相当于把这个512维的空间人为地拆成了多个子空间, 比如head number=8, 就是把一个高维空间分成了8个子空间, 相应地V也要分成8个head; 然后在这8个子空间里分别计算Q和K的相似度, 再分别组合V. 这样可以让attention能从多个不同的角度进行结合, 这对于NMT是很有帮助的, 因为我们在翻译的时候源语言和目标语言的词之间并不是一一对应的, 而是受很多词共同影响的. 每个子空间都会从自己在意的角度或者因素去组合源语言, 从而得到最终的翻译结果.
Attention is All You Need详细解读
https://www.wandouip.com/t5i267822/
https://zhuanlan.zhihu.com/p/48508221
https://www.cnblogs.com/baobaotql/p/11662720.html
1 整体框架
整体模型看上去看上去很复杂,其实这就是一个Seq2Seq模型,左边一个encoder把输入读进去,右边一个decoder得到输出:
我们看到了编码组件,解码组件以及它们之间的连接。
左边的encoders和右边的decoders都是由6层组成,内部左边encoder的输出是怎么和右边decoder结合的呢?再画张图直观的看就是这样:
2 展开
2.1 输入编码
如图所示,首先通过Word2Vec等词嵌入方法将输入语料转化成特征向量,论文中使用的词嵌入的维度为
在最底层的block中,
嵌入仅发生在最底部的编码器中。 所有编码器共有的抽象概念是,它们接收一个向量列表,每个向量的大小均为512。在底部编码器中是单词的嵌入,但在其他编码器中,它将是直接位于下面的编码器的输出 。 该列表的大小是我们可以设置的超参数–基本上,这就是训练数据集中最长句子的长度。
将单词嵌入到我们的输入序列中之后,它们中的每一个都会流经编码器的两层。
在这里,我们开始看到Transformer的一个关键属性,即每个位置的单词都流经编码器中自己的路径。 自我注意层中这些路径之间存在依赖性。 但是,前馈层不具有这些依赖性,因此可以在流过前馈层的同时并行执行各种路径。
接下来,我们将示例切换到较短的句子,然后看一下编码器每个子层中发生的情况。
Encoders的输出,会和每一层的Decoder进行结合。我们取其中一层进行详细的展示:
整体框架细节展示:
1)Encoders
Encoders有N=6层,每层包括两个sub-layers:
第一个sub-layer是multi-head self-attention mechanism,用来计算输入的self-attention
第二个sub-layer是简单的全连接网络。
在每个sub-layer我们都模拟了残差网络,每个sub-layer的输出都是:
其中Sublayer(x) 表示Sub-layer对输入 x 做的映射,为了确保连接,所有的sub-layers和embedding layer输出的维数都相同。
2)Decoders
Decoders也是N=6层,每层包括3个sub-layers:
第一个是Masked multi-head self-attention,也是计算输入的self-attention,但是因为是生成过程,因此在时刻 i 的时候,大于 i 的时刻都没有结果,只有小于 i 的时刻有结果,因此需要做Mask
第二个sub-layer是对encoder的输入进行attention计算。
第三个sub-layer是全连接网络,与Encoder相同.
同时Decoder中的self-attention层需要进行修改,因为只能获取到当前时刻之前的输入,因此只对时刻 t 之前的时刻输入进行attention计算,这也称为Mask操作。
图示参考上述总体框架细节图
3)The Final Linear and Softmax Layer
4) Position-wise Feed-forward Networks
在进行了Attention操作之后,encoder和decoder中的每一层都包含了一个全连接前向网络,对每个position的向量分别进行相同的操作,包括两个线性变换和一个ReLU激活输出:
这里再介绍下attention模型
3 Attention机制的本质思想
如果把Attention机制从上文讲述例子中的Encoder-Decoder框架中剥离,并进一步做抽象,可以更容易看懂Attention机制的本质思想。
我们可以这样来看待Attention机制(参考上图):将Source中的构成元素想象成是由一系列的
其中,Lx=||Source||代表Source的长度,公式含义即如上所述。上文所举的机器翻译的例子里,因为在计算Attention的过程中,Source中的Key和Value合二为一,指向的是同一个东西,也即输入句子中每个单词对应的语义编码,所以可能不容易看出这种能够体现本质思想的结构。
当然,从概念上理解,把Attention仍然理解为从大量信息中有选择地筛选出少量重要信息并聚焦到这些重要信息上,忽略大多不重要的信息,这种思路仍然成立。聚焦的过程体现在权重系数的计算上,权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息。
至于Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:第一个过程是根据Query和Key计算权重系数,第二个过程根据权重系数对Value进行加权求和。而第一个过程又可以细分为两个阶段:第一个阶段根据Query和Key计算两者的相似性或者相关性;第二个阶段对第一阶段的原始分值进行归一化处理;这样,可以将Attention的计算过程抽象为如图10展示的三个阶段。
在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个 Keyi ,计算两者的相似性或者相关性,最常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性或者通过再引入额外的神经网络来求值,即如下方式:
第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样,第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:
第二阶段的计算结果 ai 即为 Valuei 对应的权重系数,然后进行加权求和即可得到Attention数值:
通过如上三个阶段的计算,即可求出针对Query的Attention数值,目前绝大多数具体的注意力机制计算方法都符合上述的三阶段抽象计算过程。
4 Self-Attention
参考:transformer,Transformer模型详解
- 将输入单词转化成嵌入向量;
- 根据嵌入向量得到
- 为每个向量计算一个score:
- 为了梯度的稳定,Transformer使用了score归一化,即除以
- 对score施以softmax激活函数;
- softmax点乘Value值 得到加权的每个输入向量的评分 ;
- 相加之后得到最终的输出结果
接下来我们详细看一下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联系起来,接下来我们看下详细的处理过程。
1、首先,self-attention会计算出三个新的向量,在论文中,向量的维度是512维,我们把这三个向量分别称为Query、Key、Value,这三个向量是用embedding向量与一个矩阵相乘得到的结果,这个矩阵是随机初始化的,维度为(64,512)注意第二个维度需要和embedding的维度一样,其值在BP的过程中会一直进行更新,得到的这三个向量的维度是64低于embedding维度的。
那么Query、Key、Value这三个向量又是什么呢?这三个向量对于attention来说很重要,当你理解了下文后,你将会明白这三个向量扮演者什么的角色。
2、计算self-attention的分数值,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2
3、接下来,把点成的结果除以一个常数,这里我们除以8,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大
4、下一步就是把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值
在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵
其中 是我们模型训练过程学习到的合适的参数。上述操作即可简化为矩阵形式
这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention:
5 Multi-Head Attention
Multi-Head Attention相当于
就是说不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组【本质是通过初始化多个不同的】。tranformer是使用了8组,所以最后得到的结果是8个矩阵。
这给我们留下了一个小的挑战,前馈神经网络没法输入8个矩阵呀,这该怎么办呢?所以我们需要一种方式,把8个矩阵降为1个,首先,我们把8个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个矩阵W0(权值矩阵)和这个组合好的矩阵相乘,最后得到一个最终的矩阵。
这就是multi-headed attention的全部流程了,这里其实已经有很多矩阵了,我们把所有的矩阵放到一张图内看一下总体的流程。
下面动态演示下完整的流程: http://jalammar.github.io/illustrated-transformer/ 强烈推荐去看,能够完整清晰地了解训练过程细节。
Multi-head Attention 实例
这里面Multi-head Attention其实就是多个Self-Attention结构的结合,每个head学习到在不同表示空间中的特征,如下图所示,两个head学习到的Attention侧重点可能略有不同,这样给了模型更大的容量。
6 Positional Encoding
到目前为止,transformer模型中还缺少一种解释输入序列中单词顺序的方法。为了处理这个问题,transformer给encoder层和decoder层的输入添加了一个额外的向量Positional Encoding,维度和embedding的维度一样,这个向量采用了一种很独特的方法来让模型学习到这个值,这个向量能决定当前词的位置,或者说在一个句子中不同的词之间的距离。这个位置向量的具体计算方法有很多种,因为模型不包括Recurrence/Convolution,因此是无法捕捉到序列顺序信息的,例如将K、V按行进行打乱,那么Attention之后的结果是一样的。但是序列信息非常重要,代表着全局的结构,因此必须将序列的分词相对或者绝对position信息利用起来。
论文中的计算方法如下,
其中pos是指当前词在句子中的位置,i是指向量中每个值的index,可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码,最后把这个Positional Encoding与embedding的值相加,作为输入送到下一层
如果我们假设嵌入的维数为4,那么实际的位置编码将如下所示:
这种模式是什么样的?
在下图中,每行对应一个向量的位置编码。 因此,第一行将是我们要添加到输入序列中第一个单词的嵌入的向量。 每行包含512个值-每个值都在1到-1之间。 我们已经对它们进行了颜色编码,从而使图案可见。
嵌入大小为512(列)的20个单词(行)的位置编码的真实示例。 您会看到它看起来像是在中心向下分开的一半。 这是因为左半部分的值是由一个函数(使用正弦)生成的,而右半部分的值是由另一个函数(使用余弦)生成的。 然后将它们串联起来以形成每个位置编码向量。
论文中描述了位置编码的公式(第3.5节)。 您可以在get_timing_signal_1d()中看到用于生成位置编码的代码。 这不是位置编码的唯一可能方法。 但是,它具有能够缩放到看不见的序列长度的优点(例如,如果我们训练有素的模型要求翻译的句子比训练集中的任何句子更长的长度)。
7 The Residuals
Multi-Head Attention的输出分成3步:
- 将数据
- 将8个
- 特征矩阵经过一层全连接后得到输出
Layer normalization
在transformer中,每一个子层(self-attetion,ffnn)之后都会接一个残缺模块,并且有一个Layer normalization
残缺模块相信大家都很清楚了,这里不再讲解,主要讲解下Layer normalization。Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。
说到 normalization,那就肯定得提到 Batch Normalization。BN的主要思想就是:在每一层的每一批数据上进行归一化。我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:
可以看到,右半边求均值是沿着数据 batch_size的方向进行的,其计算公式如下:
那么什么是 Layer normalization 呢?它也是归一化数据的一种方式,不过 LN 是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差!
下面看一下 LN 的公式:
上图是transformer的一个详细结构,相比本文一开始结束的结构图会更详细些,接下来,我们会按照这个结构图讲解下decoder部分。
可以看到decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion,这里的mask也是transformer一个很关键的技术,我们一起来看一下。
8 Mask Multi-head self-attention
mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。
Padding Mask
什么是 padding mask 呢?因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!而我们的 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。
Sequence mask
文章前面也提到,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。
那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。
对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。
其他情况,attn_mask 一律等于 padding mask。
decoder中的第二层attention层就是一个正常的multi-head attention层。但是这里Q,K,V来源不同。Q来自于上一个decoder的输出,而K,V则来自于encoder的输出。剩下的计算就没有其他的不同了。
关于这两个attention层,可以理解为 mask-self-attention是计算当前翻译的内容和已经翻译的前文之间的关系,而encoder-decoder-attention 是计算当前翻译内容和编码的特征向量之间的关系。最后再经过一个全连接层,输出decoder的结果。
输出层
当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,很简单,只需要在结尾再添加一个全连接层和softmax层,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。