Attention机制
回顾RNN结构
- 讲attention之前先回顾一下RNN的各种结构
N to N
- 如:语音处理,时间序列处理
N to 1
- 如:情感分析,输入一段视频判断类型
1 to N
或
- 如:从图像生成文字,从类别生成语音或音乐
N to M
- 这种就够又叫
encoder-decoder
模型,或Seq2Seq
模型
或
- 如:机器翻译,文本摘要,阅读理解,语音识别…
回归正题Attention
-
在
encoder-decoder
结构中,显然当要处理的信息长度很长的时候,一个c存储不了那么多信息,导致处理精度下降 -
所以我们打算计算很多个c
- 这样翻译不同的y的时候,对于x就有不同的权重
-
计算结构如下
-
每一个c会去选取和当前所要输出的y最合适的上下文信息。
-
具体的,我们用 a i j a_{ij} aij来衡量encoder中第j阶段的 h j h_j hj(hidden state),和当前decoder中第i阶段的相关性
-
以机器翻译为例
-
上图标有红色的地方就是和decoder当前阶段最相关的地方,对应的值较大;其他的地方对应的值较小。这里就是attention的精髓所在了——每个decoder的状态对于每个encoder的状态分配注意力(当然, ∑ j = 1 L x a i j = 1 \sum_{j=1}^{L_x}a_{ij}=1 ∑j=1Lxaij=1)
h t = R N N e n c ( x t , h t − 1 ) h t ′ = R N N d e c ( c t , h t − 1 ′ ) c i = ∑ j = 1 L x a i j h j ( T x 是 x 总 长 ) h_t=RNN_{enc}(x_t,h_{t-1})\\ h'_t=RNN_{dec}{(c_t,h'_{t-1})}\\ c_i=\sum_{j=1}^{L_x} a_{ij}h_j\qquad (T_x是x总长) ht=RNNenc(xt,ht−1)ht′=RNNdec(ct,ht−1′)ci=j=1∑Lxaijhj(Tx是x总长)
-
-
-
接下来就是求 a i j a_{ij} aij
- b i j = s c o r e ( h i − 1 ′ , h j ) ( 我 们 的 h ′ 是 从 0 开 始 的 ) a i j = e b i j ∑ e b i k b_{ij}=score(h'_{i-1},h_j)\qquad (我们的h'是从0开始的)\\ a_{ij}=\frac{e^{b_{ij}}}{\sum e^{b_{ik}}} bij=score(hi−1′,hj)(我们的h′是从0开始的)aij=∑ebikebij
-
那么我们的score是怎么计算的呢
- 最简单的方方法就是直接计算点乘,点积类似计算相似度。
-
而,attention极值就是来解决这个问题的,定义如下
- 给定一组向量集合
value
,以及一个向量集合query
,attention机制就是根据query
计算value
的加权求和机制
- 给定一组向量集合
attention的本质思想
Query(Q),Key(K),Value(V),Source是有<Key,Value>的数据对构成
- 1、给定Target中的某个元素Query,通过计算Query和各个Key的相似性
- 2、得到每个Key对应Value的权重系数
- 3、然后对Value进行加权求和,即得到了最终的Attention数值
本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数,可写为如下公式
A
t
t
e
n
t
i
o
n
(
Q
,
<
K
,
V
>
)
=
∑
i
=
1
L
x
S
i
m
i
l
a
r
i
t
y
(
Q
,
K
i
)
∗
V
i
=
s
o
f
t
m
a
x
(
s
i
m
(
Q
,
K
)
)
V
Attention(Q,<K,V>)=\sum_{i=1}^{L_x}Similarity(Q,K_i)*V_i\\ =softmax(sim(Q,K))V
Attention(Q,<K,V>)=i=1∑LxSimilarity(Q,Ki)∗Vi=softmax(sim(Q,K))V
具体计算可分为两个过程:
- 1、根据Query和Key计算权重系数(可分为如下两个阶段)
- 一(阶段一)、根据Query和Key计算两者的相似性
- 二(阶段二)、对第一阶段的原始分值进行归一化处理
- 2、根据权重系数对Value进行加权求和
attention 的效果
- 上图为英语-德语翻译的注意力概率分布(可视化地展示了在英语-德语翻译系统中加入Attention机制后,Source和Target两个句子每个单词对应的注意力分配概率分布。)
主要也就一块代码
class AttDecoder_RNN(nn.Module):
def __init__(self, word_num, hidden_dim, dropp=config.drop_p, max_length=config.max_length):
super(AttDecoder_RNN, self).__init__()
self.word_num = word_num
self.hidden_dim = hidden_dim
self.embed = nn.Embedding(word_num, hidden_dim)
self.gru = nn.GRU(hidden_dim, hidden_dim)
self.dropout = nn.Dropout(dropp)
self.attn = nn.Linear(2 * hidden_dim, max_length)
self.attn_C = nn.Linear(2 * hidden_dim, self.hidden_dim)
self.out = nn.Linear(self.hidden_dim, self.word_num)
def forward(self, encoder_state, input, hidden=None):
batch_size = input.size(0)
if hidden is None:
hidden = t.rand(1, batch_size, self.hidden_dim)
emb = self.embed(input)
emb = self.dropout(emb)
att_w = F.softmax(self.attn(t.cat((emb, hidden[0]), 1)), dim=1) #先用上一层hidden与input拼接,然后通过网络映射到max_length得到权重a,得到权重再softmax
att_c = t.bmm(att_w.unsqueeze(0).permute(1, 0, 2), encoder_state.permute(1, 0, 2)) #用权重a与encoder的hidden相乘得到c,(permute是pytorch调整维度)
output = t.cat((emb, att_c.permute(1, 0, 2)[0]), 1) #再把input与c拼接
output = self.attn_C(output).unsqueeze(0) #将长度映射还原,拼接之后hidden长度会加倍
output, hidden = self.gru(output, hidden)
output = F.log_softmax(self.out(output[0]), dim=1)
return output, hidden, att_w
self-attention模块——扔掉RNN
-
一般任务的Encoder-Decoder框架中,输入Source和输出Target内容是不一样的,比如一边英文一边中文,Attention机制发生在Target的元素Query和Source中的所有元素之间
-
而self-attention机制,Attention机制发生在Source内部元素之间或者Target内部元素之间,也可以理解为Target=Source这种特殊情况下的注意力计算机制(就是计算对象发生了变化,Q=K=V)
- 具体的,比如翻译“I arrived at the bank after crossing the river”,要翻译“bank”是银行还是河岸,需要结合river这个词,所以我们需要在翻译“bank”的时候,river的Attention Score就有较高的值;
- 而普通的RNN在两词相距较远的时候效果较差,而且顺序处理效果较低
- 步骤效果如下,思路很简单,就这样就把RNN扔掉了
-
transformer模型中的相似性使用的是缩放点积模型(scaled dot-product attention):
s i m ( K i , Q ) = K i T Q d k sim(K_i,Q)=\frac{K_i^TQ}{\sqrt{d_k}} sim(Ki,Q)=dkKiTQ -
流程图如下
-
不过没有了RNN,encoder过程就没有了hidden state,那怎么办?
- 我们对每一个word做embedding,然后用embedding代替hidden state来self-attention
- 所以Q矩阵装的都是word embedding(K,V也一样)
- v i v_i vi是第i个word的embedding,对 v i v_i vi的attention为下面第一个
-
注意,在decoder中的self-attention的流程和encoder中的差不多,不过encoder中的word是一次性全部输入进去的,decoder中的word是从一遍生成的(如从左到右),那么对于 v i v_i vi是没有机会和 v j ( j ≥ i ) v_j(j\geq i) vj(j≥i)做attention的,这个时候我们需要使 s c o r e ( v i , v j ) = − ∞ score(v_i,v_j)=-∞ score(vi,vj)=−∞,有 a i j = s o f t m a x ( s c o r e ( v i , v j ) ) = s o f t m a x ( − ∞ ) = 0 a_{ij}=softmax(score(v_i,v_j))=softmax(-∞)=0 aij=softmax(score(vi,vj))=softmax(−∞)=0
- 这个其实相当于一个masked的操作,就是transformer模型中的masked muti-head attention中在decoder中的操作
优点
- 优点
- 引入Self Attention后会更容易捕获句子中长距离的相互依赖的特征
- Self Attention对于增加计算的并行性也有直接帮助作用
Multi-head Attention模块
-
muti-head attention就是多个self-attention结构的结合,每个head学到不同表示空间中的特征。
- 将模型分为多个头,形成多个子空间,可以让模型关注不同方面的信息
-
如下图,两个head学到的特征可能会不同
-
其实就是把self-attention进行stacking,把每个word的embedding拆分成几块分别作self-attention最后拼接起来
-
步骤大致如下
Q i = Q W i Q , K i = K W i K , V i = V W i V i = 1 , . . . , h s h e a d i = A t t e n t i o n ( Q i , K i , V i ) i = 1 , . . . , h s M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h s ) W O ( h i d d e n d i m = h s ∗ h e a d d i m ) Q_i=QW_i^Q,K_i=KW_i^K,V_i=VW_i^V\quad i=1,...,h_s\\ head_i=Attention(Q_i,K_i,V_i)\quad i=1,...,h_s\\ MultiHead(Q,K,V)=Concat(head_1,...,head_{h_s})W^O\\ (hidden_{dim}=h_s*head_{dim}) Qi=QWiQ,Ki=KWiK,Vi=VWiVi=1,...,hsheadi=Attention(Qi,Ki,Vi)i=1,...,hsMultiHead(Q,K,V)=Concat(head1,...,headhs)WO(hiddendim=hs∗headdim)
- 上面那几个 W W W映射矩阵可以用 h i d d e n d i m ∗ h i d d e n d i m hidden_{dim}*hidden_{dim} hiddendim∗hiddendim的linear layer实现
Layer Normalization与残差连接
可以发现在“Multi-Head Attention”旁还有一条直连的边,这里用的即是ResNet中的残差(F(X)+X)。
- 原文写的公式是:LayerNorm(X + SubLayer(X)),然后attention的部分就是我们现在要学习的部分SubLayer(X)(即F(x))
LN放的位置探究
- all attention is all your need原论文中LN放的位置叫:Post-LN,即LN放在残差(residual)之后
- 还有另一种方式叫Pre-LN:即先LN,再放残差。Transformers without Tears: Improving the Normalization of Self-Attention
就是这么简单的改变,可以大大提升模型的调参难度与学习效率。
- 在Post-LN中,transfomer对参数的变化十分敏感,需要仔细的调参以及使用warm-up的学习策略,非常的慢。主要的问题再LN的位置,导致layer的梯度范数级增长
简要代码如下
if self.pre_lnorm:
pre = self.self_attn_norm(src)
src = src + self.self_attn(pre, pre, pre, src_mask) # residual connection
pre = self.pff_norm(src)
src = src + self.pff(pre) # residual connection
else:
src = self.self_attn_norm(src + self.self_attn(src, src, src, src_mask)) # residual connection + layerNorm
src = self.pff_norm(src + self.pff(src)) # residual connection + layerNorm
简要实现如下
class MultiHeadAttentionLayer(nn.Module):
def __init__(self, hid_dim, n_heads, dropout, device):
super().__init__()
assert hid_dim % n_heads == 0
self.hid_dim = hid_dim # in paper, 512
self.n_heads = n_heads # in paper, 8
self.head_dim = hid_dim // n_heads # in paper, 512 // 8 = 64
self.fc_q = nn.Linear(hid_dim, hid_dim)
self.fc_k = nn.Linear(hid_dim, hid_dim)
self.fc_v = nn.Linear(hid_dim, hid_dim)
self.fc_o = nn.Linear(hid_dim, hid_dim)
self.dropout = nn.Dropout(dropout)
self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).to(device) # sqrt(64)
def forward(self, query, key, value, mask = None):
batch_size = query.shape[0]
#query = [batch size, query len, hid dim]
#key = [batch size, key len, hid dim]
#value = [batch size, value len, hid dim]
Q = self.fc_q(query)
K = self.fc_k(key)
V = self.fc_v(value)
#Q = [batch size, query len, hid dim]
#K = [batch size, key len, hid dim]
#V = [batch size, value len, hid dim]
Q = Q.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
K = K.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
V = V.view(batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
#Q = [batch size, n heads, query len, head dim]
#K = [batch size, n heads, key len, head dim]
#V = [batch size, n heads, value len, head dim]
energy = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale
#energy = [batch size, n heads, query len, key len]
if mask is not None:
energy = energy.masked_fill(mask == 0, -1e10)
attention = torch.softmax(energy, dim = -1)
#attention = [batch size, n heads, query len, key len]
x = torch.matmul(self.dropout(attention), V) #x = [batch size, n heads, query len, head dim]
# 将x还原成linear layer可以process的size
x = x.permute(0, 2, 1, 3).contiguous()
# contiguous 返回一个内存连续的有相同数据的tensor,如果原tensor内存连续,则返回原tensor. 一般与transpose,permute, view搭配使用
# transpose、permute等维度变换操作后,tensor在内存中不再是连续存储的,而view操作要求tensor的内存连续存储,所以需要contiguous来返回一个contiguous copy
x = x.view(batch_size, -1, self.hid_dim) #x = [batch size, query len, n heads, head dim]
x = self.fc_o(x) #x = [batch size, query len, hid dim]
return x, attention