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最合适的上下文信息。
- 具体的,我们用来衡量encoder中第j阶段的(hidden state),和当前decoder中第i阶段的相关性
- 以机器翻译为例
- 上图标有红色的地方就是和decoder当前阶段最相关的地方,对应的值较大;其他的地方对应的值较小。这里就是attention的精髓所在了——每个decoder的状态对于每个encoder的状态分配注意力(当然,)
- 接下来就是求
- 那么我们的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的权重系数,可写为如下公式
具体计算可分为两个过程:
- 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):
- 流程图如下
- 不过没有了RNN,encoder过程就没有了hidden state,那怎么办?
- 我们对每一个word做embedding,然后用embedding代替hidden state来self-attention
- 所以Q矩阵装的都是word embedding(K,V也一样)
- 是第i个word的embedding,对的attention为下面第一个
- 注意,在decoder中的self-attention的流程和encoder中的差不多,不过encoder中的word是一次性全部输入进去的,decoder中的word是从一遍生成的(如从左到右),那么对于是没有机会和做attention的,这个时候我们需要使,有
- 这个其实相当于一个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最后拼接起来
- 步骤大致如下
- 上面那几个映射矩阵可以用的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