赞
踩
Transformer是Google2017年在《Attention is all you need》这篇论文中提到的模型。论文发布以后也是受到了大家广泛关注和应用。这篇论文主要亮点在于采用了attention(Multi-Headed Attention)机制。
关于Transrofmer模型的理解特别推荐这两篇文章《The illustrated Transformer》、《Transformer原理详解》。
Transformer模型和Attention模型一样,也采用了 encoer-decoder 架构。但其结构相比于Attention更加复杂,论文中encoder层由6个encoder堆叠在一起,decoder层也一样。
每一个encoder和decoder的内部简版结构如下图:
对于encoder,包含两层,一个self-attention层和一个前馈神经网络(ffnn),self-attention能帮助当前节点不仅仅只关注当前的词,从而能获取到上下文的语义。decoder也包含encoder提到的两层网络,但是在这两层中间还有一层attention层,帮助当前节点获取到当前需要关注的重点内容。
现在我们知道了模型的主要组件,接下来我们看下模型的内部细节。首先,模型需要对输入的数据进行一个embedding操作,(也可以理解为类似word2vector的操作),embedding结束之后,输入到encoder层,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,得到的输出会输入到下一个encoder。
接下来我们详细看一下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联系起来。如下图单头注意力机制的例子:我们看it这个词最后得到的R矩阵里面,就会表示出这个it到底是指的什么, 可以看到R1和R2和it最相关,就可以认为it表示的是The animal。(颜色越深说明attention值越大,相关性越高)
接下来我们看下详细的处理过程:
1、首先,self-attention会计算出三个新的向量,在论文中,向量的维度是512维,我们把这三个向量分别称为Query、Key、Value,这三个向量是用embedding向量与三个矩阵(WQ、WK、WV)相乘得到的结果,这三个矩阵是随机初始化的,维度为(64,512)注意第一个维度可以随意取值,第二个维度需要和embedding的维度一样,其值在BP的过程中会一直进行更新,相乘后得到的这三个向量的维度是64。
2、计算self-attention的分数值(也可以称为第一个字的注意力权重),该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2。
3、接下来,把点成的结果除以一个常数,这里我们除以8(论文中提到的一个trick,防止值过大),这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大。
4、下一步就是把Value和softmax得到的值进行相乘,如图(得到的Z1,Z2)并相加,得到的结果即是self-attetion在当前节点的值。
在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵,得到了结果Z矩阵。
这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention。其实scaled dot-Product attention就是我们常用的使用点积进行相似度计算的attention,只是多除了一个(为K的维度,这是论文中提到的一个 trick)起到调节作用,使得内积不至于太大。
这篇论文更厉害的地方是给self-attention加入了另外一个机制,被称为"multi-headed attention",该机制理解起来很简单,就是说不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组,tranformer是使用了8组,所以最后得到的结果是8个矩阵。
这样会有一个问题,前馈神经网络没法输入8个矩阵。所以我们需要一种方式,把8个矩阵降为1个,首先,我们把8个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个矩阵和这个组合好的矩阵相乘,最后得到一个最终的矩阵。
这就是multi-headed attention的全部流程了,这里其实已经有很多矩阵了,我们把所有的矩阵放到一张图内看一下总体的流程。
多头attention(Multi-head attention)整个过程可以简述为:Query,Key,Value首先经过一个线性变换,然后输入到放缩点积attention(注意这里要做h次,其实也就是所谓的多头,每一次算一个头,而且每次Q,K,V进行线性变换的参数W是不一样的),然后将h次的放缩点积attention结果进行拼接,再进行一次线性变换得到的值作为多头attention的结果。可以看到,google提出来的多头attention的不同之处在于进行了h次计算而不仅仅算一次,论文中说到这样的好处是可以允许模型在不同的表示子空间里学习到相关的信息,后面还会根据attention可视化来验证。
那么在整个模型中,是如何使用attention的呢?如下图,首先在编码器到解码器的地方使用了多头attention进行连接,K,V,Q分别是编码器的层输出(这里K=V)和解码器中多头attention的输入。其实就和主流的机器翻译模型中的attention一样,利用解码器和编码器attention来进行翻译对齐。然后在编码器和解码器中都使用了多头自注意力self-attention来学习文本的表示。Self-attention即K=V=Q,例如输入一个句子,那么里面的每个词都要和该句子中的所有词进行attention计算。目的是学习句子内部的词依赖关系,捕获句子的内部结构。
对于使用自注意力机制的原因,论文中提到主要从三个方面考虑(每一层的复杂度,是否可以并行,长距离依赖学习),并给出了和RNN,CNN计算复杂度的比较。可以看到,如果输入序列n小于表示维度d的话,每一层的时间复杂度self-attention是比较有优势的。当n比较大时,作者也给出了一种解决方案self-attention(restricted)即每个词不是和所有词计算attention,而是只与限制的r个词去计算attention。在并行方面,多头attention和CNN一样不依赖于前一时刻的计算,可以很好的并行,优于RNN。在长距离依赖上,由于self-attention是每个词和所有词都要计算attention,所以不管他们中间有多长距离,最大的路径长度也都只是1。可以捕获长距离依赖关系。
现在我们已经接触了attention的header,让我们重新审视我们之前的例子,看看例句中的“it”这个单词在两头的注意力机制情况下(这里不同颜色代表attention不同头的结果,颜色越深attention值越大)。
上图这个是两头的注意力机制,上面说到这个橙色的这个注意力反映了it这个词指代的信息。 而这个绿色的这个注意力,反应了it这个词的状态信息,可以看到it经过这个绿色的注意力机制后,tired这个词与it关联最大,就是说it,映射过去,会更关注tired这个词,因为这个正好是它的一个状态。 它累了。这个角度就是用多个头的注意力矩阵体现的。这就是每个字多重语义的含义。
但是,如果我们将所有注意力添加到图片中,可能有点难理解:
到目前为止,transformer模型中还缺少一种解释输入序列中单词顺序的方法。为了处理这个问题,transformer给encoder层和decoder层的输入添加了一个额外的向量Positional Encoding,也就是位置嵌入,位置嵌入的维度为[max_sequence_length, embedding_dimension],和embedding的维度一样,然后跟输入embedding相加得到multi-head attention 的输入。这个向量采用了一种很独特的方法来让模型学习到这个值,这个向量能决定当前词的位置,或者说在一个句子中不同的词之间的距离。这个位置向量的具体计算方法有很多种,论文中的计算方法如下:
其中,PE为二维矩阵,大小跟输入embedding的维度一样,行表示词语,列表示词向量;pos 表示词语在句子中的位置,取值范围是[0,max_sequence_length);dmodel表示词向量的维度embedding_dimension;i表示字向量的维度序号,取值范围是 [0, embedding_dimension/2)。因此,上述公式表示在每个词语的词向量的偶数位置添加sin变量,奇数位置添加cos变量,以此来填满整个PE矩阵,然后加到input embedding中去,这样便完成位置编码的引入了。
在transformer中,每一个子层(self-attetion,ffnn)之后都会接一个残差模块,并且有一个Layer normalization的步骤:
在进一步探索其内部计算方式,我们可以将上面图层可视化为下图:
残差模块:输入x1,x2 经 self-attention 层之后变成z1,z2 ,然后和输入X1embedding_pos,X2embedding_pos (Xiembedding_pos = Positional Embedding+Xi) 进行残差连接,经过 LayerNorm 后输出给全连接层。全连接层也有一个残差连接和一个 LayerNorm,最后再输出给下一个 Encoder(每个 Encoder Block 中的 FeedForward 层权重都是共享的)。残差模块的主要好处有两点:一是解决梯度消失的问题,二是解决权重矩阵的退化问题。(具体可以看一下最下边的总流程图)。
Layer normalization:Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。
Layer normalization,它是归一化数据的一种方式,不过 LN 是在每一个样本上计算均值和方差,而不是BN那种在批方向上计算均值和方差:
下面看一下 LN 的公式:
1、以矩阵的列为单位取均值
2、以矩阵的列(column)为单位求方差
3、然后用每一列的每一个元素减去这列的均值,再除以这列的标准差,从而得到归一化后的数值,加ε是为了防止分母为 0。
说到 normalization,那就肯定得提到 Batch Normalization。BN的主要思想就是:在每一层的每一批数据上进行归一化。我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。
BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:
可以看到,右半边求均值是沿着数据 batch_size的方向进行的,其计算公式如下:
到这里为止就是全部encoders的内容了,如果把两个encoders叠加在一起就是这样的结构,在self-attention需要强调的最后一点是其采用了残差网络中的short-cut结构,目的是解决深度学习中的退化问题。
上图是transformer的一个详细结构,相比本文一开始结束的结构图会更详细些,接下来,我们会按照这个结构图讲解下decoder部分。
可以看到decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion,这里的mask也是transformer一个很关键的技术,我们一起来看一下。
mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。
其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。
什么是 padding mask 呢?在Self Attention 的计算过程中,每个batch批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。 但这样的话softmax 就会产生问题。回顾 softmax 函数,e的0次方是1,
是有值的,这样的话 softmax 中被 padding 的部分就参与了运算,相当于让无效的部分参与了运算,这可能会产生很大的隐患,因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法组要进行一个mask,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!
而我们的 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。
文章前面也提到,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。
具体做法:产生一个上三角矩阵,上三角的值全为1,下三角的值全为0,对角线也是0。把这个矩阵作用在每一个序列上,就能达到我们的目的。
编码器通过处理输入序列启动。然后将顶部编码器的输出转换为一组注意向量k和v。每个解码器将在其“encoder-decoder attention”层中使用这些注意向量,这有助于解码器将注意力集中在输入序列中的适当位置:
当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,很简单,只需要在结尾再添加一个全连接层和softmax层,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。
字向量与位置编码
自注意力机制
self-attention 残差连接与 Layer Normalization
下面进行 Encoder block 结构图中的第 4 部分,也就是 FeedForward,其实就是两层线性映射并用激活函数激活,比如说 RELU
FeedForward 残差连接与 Layer Normalization
其中
我们先观察一下 Decoder 结构,从下到上依次是:
Masked Self-Attention
具体来说,传统 Seq2Seq 中 Decoder 使用的是 RNN 模型,因此在训练过程中输入 t 时刻的词,模型无论如何也看不到未来时刻的词,因为循环神经网络是时间驱动的,只有当 t 时刻运算结束了,才能看到t+1 时刻的词。而 Transformer Decoder 抛弃了 RNN,改为 Self-Attention,由此就产生了一个问题,在训练过程中,整个 ground truth 都暴露在 Decoder 中,这显然是不对的,我们需要对 Decoder 的输入进行一些处理,该处理被称为 Mask.
举个例子,Decoder 的 ground truth 为 “< start > I am fine”,我们将这个句子输入到 Decoder 中,经过 WordEmbedding 和 Positional Encoding 之后,将得到的矩阵做三次线性变换(WQ,Wk,Wv)。然后进行 self-attention 操作,首先通过
得到 Scaled Scores,接下来非常关键,我们要对 Scaled Scores 进行 Mask,举个例子,当我们输入 “I” 时,模型目前仅知道包括 “I” 在内之前所有字的信息,即 “< start >” 和 “I” 的信息,不应该让其知道 “I” 之后词的信息。道理很简单,我们做预测的时候是按照顺序一个字一个字的预测,怎么能这个字都没预测完,就已经知道后面字的信息了呢?Mask 非常简单,首先生成一个下三角全 0,上三角全为负无穷的矩阵,然后将其与 Scaled Scores 相加即可.
之后再做 softmax,就能将 - inf 变为 0,得到的这个矩阵即为每个字之间的权重
Masked Encoder-Decoder Attention
其实这一部分的计算流程和前面 Masked Self-Attention 很相似,结构也一摸一样,唯一不同的是这里的 K,V为 Encoder 的输出, Q为 Decoder 中 Masked Self-Attention 的输出.(注:只用 Encoder 最后一层K,V对应每一层 Decoder的Q,而不是每一层的 Encoder Bolck K,V 对应每一层的 Decode Bolck 中的 Q)。
所以多头注意力机制细节总结起来就是下面这个图了:
参考:
https://wmathor.com/index.php/archives/1438/
https://jalammar.github.io/illustrated-transformer/
https://blog.csdn.net/qq_41485273/article/details/115695809
https://blog.csdn.net/zhulinniao/article/details/104462228/
https://blog.csdn.net/jiaowoshouzi/article/details/89073944
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。