一、Attention 机制
概述
实现Attention的方式有很多种,这里展示比较常用的一种。在Encoder的过程中保留每一步RNN单元的隐藏状态h1……hn,组成编码的状态矩阵Encoder_outputs;在解码过程中,原本是通过上一步的输出yt-1和前一个隐藏层h作为输入,现又加入了利用Encoder_outputs计算注意力权重attention_weight的步骤。
相比于原始的Encoder-Decoder模型,加入Attention机制后最大的区别就是它不在要求编码器将所有输入信息都编码进一个固定长度的向量之中。而是,编码器需要将输入编码成一个向量的序列,在解码的时候,每一步都会选择性的从向量序列中挑选一个子集进行进一步处理。这样,在产生每一个输出的时候,都能够做到充分利用输入序列携带的信息。而且这种方法在翻译任务中取得了非常不错的成果。
过程梳理
这是一个基本的attention模型,encoder保存每次的hidden。在decoder端把input传入和隐藏单元拼接起来传入线性层,将其映射到seqlen维,每一维描述的是输入encoder中各位置元素对当前decoder输出单词的重要性占比。然后利用矩阵相乘获取到加权求和之后的注意力向量,用于描述“划了重点”之后的输入序列对当前预测这个单词的影响。然后将注意力向量和input拼接在一起,再利用一个线性层将其映射到RNN的输入维度,最后送入RNN,得到新的hidden和output。output作为下一个的输入。
Attention的本质
attention其实是一种软寻址,将Source中的构成元素想象成是由一系列的<Key,Value>数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。所以本质上对source中的各个value进行加权求和,而query和key用来计算对应value的权重系数。
总结成一个公式:
如果细分可以分为三个步骤,第一步是计算query与各个key的相关性;第二步是通过softmax对第一阶段的原始分值归一化处理;第三个步骤是根据权重系数对value进行加权求和。
二、Self-Attention模型
self-attention模型指的不是Target和Source之间的Attention机制,而是Source内部元素之间或者Target内部元素之间发生的Attention机制。
1、宏观角度分析
编码组件部分由一堆编码器(encoder)构成(论文中是将6个编码器叠在一起——数字6没有什么神奇之处,你也可以尝试其他数字)。解码组件部分也是由相同数量(与编码器对应)的解码器(decoder)组成的。
所有的编码器在结构上都是相同的,但它们没有共享参数。每个解码器都可以分解成两个子层。
从编码器输入的句子首先会经过一个自注意力层,这层帮助编码器在对每个单词编码时关注输入句子的其他单词。(意思是:对于某个单词,关注该词与该句子中其他单词之间的attention),自注意力层的输出会传递到前馈网络中。(每个位置的单词进入的前馈网络是完全一样的)
2、微观角度分析
计算自注意力的第一步就是从每个编码器的输入向量(每个单词的词向量)中生成三个向量。也就是说对于每个单词,我们创造一个查询向量query、一个键向量key和一个值向量value。这三个向量是通过词嵌入与三个权重矩阵后相乘创建的。
第二步是计算打分。假设我们在为这个例子中的第一个词“Thinking”计算自注意力向量,我们需要拿输入句子中的每个单词对“Thinking”打分。这些分数决定了在编码单词“Thinking”的过程中有多重视句子的其它部分。这些分数是通过打分单词(所有输入句子的单词)的键向量与“Thinking”的查询向量相点积来计算的。
第三步和第四步是将分数除以8(8是论文中使用的键向量的维数64的平方根,这会让梯度更稳定。这里也可以使用其它值,8只是默认值),然后通过softmax传递结果。softmax的作用是使所有单词的分数归一化,得到的分数都是正值且和为1。这个softmax分数决定了每个单词对编码当下位置(“Thinking”)的贡献。显然,已经在这个位置上的单词将获得最高的softmax分数,但有时关注另一个与当前单词相关的单词也会有帮助。
最后是将每个值向量乘以softmax分数,然后对加权后的值向量求和,得到的向量就可以传给前馈神经网络。
3、通过矩阵运算实现自注意力机制
第1步是计算查询矩阵、键矩阵和值矩阵。为此,我们将输入句子的词嵌入装进矩阵X中(X矩阵中的每一行对应于输入句子中的一个单词),将X乘以我们训练的权重矩阵(WQ,WK,WV)。
我们再次看到词嵌入向量 (512,或图中的4个格子)和q/k/v向量(64,或图中的3个格子)的大小差异。
我们可以看到:原先的词嵌入向量4个格子(512维),转为成对应的QKV后是3个格子(64维),他们的维度是不同的。
最后,由于我们处理的是矩阵,我们可以将步骤2到步骤6合并为一个公式来计算自注意力层的输出。
放一个自己的剖析:Q*K
矩阵表示对于每个查询向量Q(i),都要与所有的键向量K做内积,得到Q(i)对于所有的键向量K的相关性的打分,通过除根号d再softmax归一化得到Q(i)对于所有键值K的权重。
最后用这个权重矩阵乘以V,得到加权和向量Z(i)。每个查询向量Q(i)都这样的操作,最终得到Z矩阵。
再捋一遍Q / K / V:
Q:查询向量
K:表示被查询信息与其他信息的相关性的向量
V:表示被查询信息的向量
输入向量为:x, 维度:(1,m)的向量
Q = x * Wq
K = x * Wk
V = x * Wv
x对应信息V的注意力权重 与 Q*K.tranpose
成正比
等于说:x的注意力权重,由x自己来决定,所以叫自注意力。Wq,Wk,Wv
会根据任务目标更新变化,保证了自注意力机制的效果。
4、多头注意力机制
它给出了注意力层的多个“表示子空间”,对于“多头”注意机制,我们有多个查询/键/值权重矩阵集(Transformer使用八个注意力头,因此我们对于每个编码器/解码器有八个矩阵集合)。这些集合中的每一个都是随机初始化的,在训练之后,每个集合都被用来将输入词嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
在“多头”注意机制下,我们为每个头保持独立的查询/键/值权重矩阵,从而产生不同的查询/键/值矩阵。和之前一样,我们拿X乘以WQ/WK/WV矩阵来产生查询/键/值矩阵。
如果我们做与上述相同的自注意力计算,只需八次不同的权重矩阵运算,我们就会得到八个不同的Z矩阵。
我们现在可以得到八个注意力头,但是前馈层只需要一个,所以可以直接把这些矩阵拼接在一起,然后用一个附加的权重矩阵WO与它们相乘,最终将这八个矩阵压缩压缩成一个矩阵。
多头注意力总结:
5、Q K V究竟是什么?
其实,其来源是X与矩阵的乘积,本质上都是X的线性变换。
为什么不直接使用 X 而要对其进行线性变换?
——当然是为了提升模型的拟合能力,矩阵 W都是可以训练的,起到一个缓冲的效果。
位置编码
最后再补充一点,对self-attention来说,它跟每一个input vector都做attention,所以没有考虑到input sequence的顺序。更通俗来讲,大家可以发现我们前文的计算每一个词向量都与其他词向量计算内积,得到的结果丢失了我们原来文本的顺序信息。对比来说,LSTM是对于文本顺序信息的解释是输出词向量的先后顺序,而我们上文的计算对sequence的顺序这一部分则完全没有提及,你打乱词向量的顺序,得到的结果仍然是相同的。
这就牵扯到Transformer的位置编码了。在论稳中,作者提出了两个公式对每个单词做了位置编码,其意义在于捕捉一句话中的单词间相对位置关系,将位置编码+embedded作为模型的input。
实现一个简单的Self_attention:
import torch
import torch.nn as nn
from math import sqrt
class Self_attention(nn.Module):
# input : batch_size * seq_len * input_dim
# q : batch_size * input_dim * dim_k
# k : batch_size * input_dim * dim_k
# v : batch_size * input_dim * dim_v
def __init__(self,input_dim,dim_k,dim_v):
super(Self_attention, self).__init__()
self.q = nn.Linear(input_dim,dim_k)
self.k = nn.Linear(input_dim,dim_k)
self.v = nn.Linear(input_dim,dim_v)
self._norm_fact = 1 / sqrt(dim_k)
def forward(self,x):
Q = self.q(x) # Q: batch_size * seq_len * dim_k
K = self.k(x) # K: batch_size * seq_len * dim_k
V = self.v(x) # V: batch_size * seq_len * dim_v
atten = nn.Softmax(dim=-1)(
torch.bmm(Q, K.permute(0, 2, 1))) * self._norm_fact
# Q * K.T() # batch_size * seq_len * seq_len
output = torch.bmm(atten, V) # Q * K.T() * V # batch_size * seq_len * dim_v
return output
x = torch.randn(4,3,2)
print(x.size())
self_attention = Self_attention(2,4,5) # putput: [batch,seqlen,v_dim]
res = self_attention(x)
print(res.size())
output:
torch.Size([4, 3, 2])
torch.Size([4, 3, 5])
6、残差网络
resnet残差网络,可以防止梯度消失,因为它是F(x)+x
,所以某一层或者某几层可以看做是做了一个F(x),如果这些层的梯度接近消失,整体对于x的梯度还是保持在1+df/dx
。此时就算原来的导数df/dx很小,这时候误差仍然能够有效的反向传播,这就是核心思想。
在transformer中, 每一层的output都要加上input做归一化,如下如所示。
7、Batch-Normalization与Layer-Normalization
Normalization归一化的作用:快速收敛,最优解的寻优过程明显会变得平缓,更容易正确的收敛到最优解。
(1)Batch-Normalization
BN是对每一个特征在一个nimi-batch维度分别操作。在NLP中,是将每句话的第i位置的词(0<=i<seqlen)
做均值和方差。是针对每句话的每个位置进行缩放,这不符合NLP的规律。
而且如果BN用到nlp中,由于一个mini batch中的每个句子长度不一致,存在paddding,对列缩放的话会造成误差。
(2)Layer-Normalization
BN 感觉是对样本内部特征的缩放,LN 是样本直接之间所有特征的缩放。为啥BN不适合NLP 是因为NLP模型训练里的每次输入的句子都是多个句子,并且长度不一,那么 针对每一句的缩放才更加合理,才能表达每个句子之间代表不同的语义表示,这样让模型更加能捕捉句子之间的上下语义关系。如果要用BN,它首先要面临的长度不一的问题。有时候batch size 越小的BN 效果更不好。
8、Decoder讲解
首先需要强调的是,encoder部分的做并行的,而decoder是采用RNN那种序列概念一个一个产生的。第一步是给一个Masked Multi-Head Attention
一个输入,第一个输入是【SOS】
(即start of sentence,然后做self-attenttion,(decoder部分的mask不能像encoder一样对全部上下稳信息做自注意力,而是要把这个step之后的遮盖住,只专注与与前面的单词的自注意力)那么这里的mask如何理解,就是生产第一步只有一个词,自己做self-attention,第二步生成两个词的时候,就做两个词的self-attention。假设这一层的输出为Q,然后继续把Q送到中间的Multi-Head Attention
层,和encoder最顶层的的K,V做attention。其实这里和传统的用RNN做的seq2seq+attention
几乎一样,只不过那个是用decoder的上一步产生的hiddenLayer的值(作为Q)和encoder的hiddenLayer的值(作为K)做attention,然后把encoder的input作为V,加权给到decoder的输入。所以二者其实换汤不换药。
References
- Attention is All You Need:https://arxiv.org/abs/1706.03762
- https://zhuanlan.zhihu.com/p/410776234