2、缩放点积注意力Scaled Dot-Product Attention
在seq2seq中应用Attention(Bahdanau注意力)
注意力机制概述
心理学定义:在复杂的情况下关注值得注意的点
“是否包含自主性提示”将注意力机制与全连接层或汇聚层区别开来:
卷积、全连接、pooling都是只考虑非意志线索;
注意力机制则考虑意志线索:意志线索(自主性提示)称为查询(query)。 给定任何查询,注意力机制通过注意力汇聚(attention pooling) 将选择引导至感官输入(sensory inputs,例如中间特征表示)。 在注意力机制中,这些感官输入被称为值(value)。 更通俗的解释,每个值都与一个键(key)配对, 这可以想象为感官输入的非自主提示。 如 图10.1.3所示,我们可以设计注意力汇聚, 以便给定的查询(自主性提示)可以与键(非自主性提示)进行匹配, 这将引导得出最匹配的值(感官输入)。
是注意力权重
意志线索query就是f(x),式子中的x就是非意志线索key。简单来说,query就是对key做了一个加权算术,其中的权重就是query对key的偏向性选择。
注意力评分
注意力分数是query和key的相似度(举个栗子,去公司求职的时候,猜测自己的薪水,应该是看和自己专业相仿,工作内容和时间差不多的人的薪水,这些人的注意力分数就会高),没有Normalized的alpha就是注意力分数a。
查询q和键ki的注意力权重(标量) 是通过注意力评分函数a 将两个向量映射成标量, 再经过softmax运算得到的。。选择不同的注意力评分函数a会导致不同的注意力汇聚操作。
1、加性注意力Additive Attention
给定查询q∈Rq和 键k∈Rk, 加性注意力(additive attention)的评分函数为
将query和key连结起来后输入到一个多层感知机(MLP)中, 感知机包含一个隐藏层,其隐藏单元数是一个超参数h。 通过使用tanh作为激活函数,并且禁用偏置项。query和key可以是任意长度,通过矩阵乘法,最后得到的a是一个值,因为(1*h)*(h*1).
#@save
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
self.w_v = nn.Linear(num_hiddens, 1, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数,1,num_hidden)
# key的形状:(batch_size,1,“键-值”对的个数,num_hiddens)
# 使用广播方式进行求和
features = queries.unsqueeze(2) + keys.unsqueeze(1)
features = torch.tanh(features)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,“键-值”对的个数)
scores = self.w_v(features).squeeze(-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)
2、缩放点积注意力Scaled Dot-Product Attention
当n个query和m个Key的长度都是d时
#@save
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,“键-值”对的个数,d)
# values的形状:(batch_size,“键-值”对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)
在seq2seq中应用Attention(Bahdanau注意力)
注意在这个模型里什么是value\key\query
key——编码器中每一个词对应RNN的输出。
query——解码器对上一个词的预测输出(用来匹配相关的key)。
value——上下文的语义?
在seq2seq中使用attention的动机:
机器翻译中,每个生成的词可能相关于源句子中不同的词。但是seq2seq不能直接对此建模:decoder的输入是最后一个H, 虽然包含了前面的信息,但是我们要还原出位置信息。
加了attention后的区别
原来的输入永远都是encoder的RNN最后一个隐藏状态作为上下文context和embedding一起传入解码器的RNN。
在原来的基础上加上attention(用最后一个时刻的状态作为输入不好,应该根据翻译的词看前面几个时刻的隐藏状态)。相当于对前面的隐藏状态作了一个加权平均,取最相关的state.
具体的实现过程:
编译器encoder把每个词的输出作为Key和value(两者是一样的)放进attention里面,然后当decoder预测出hello的时候,把输出的预测作为query放入attention寻找hello周围的词(匹配match的Key)。
“用编译器创建索引,用解码器的预测来定位关注点”
最后的输入是attention的输出和embedding
Bahdanau注意力的代码实现
加了attention_weight函数
class Seq2SeqAttentionDecoder(AttentionDecoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
self.attention = d2l.AdditiveAttention(
num_hiddens, num_hiddens, num_hiddens, dropout)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.GRU(
embed_size + num_hiddens, num_hiddens, num_layers,
dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, enc_valid_lens, *args):
# outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,num_hiddens)
outputs, hidden_state = enc_outputs
return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)
def forward(self, X, state):
# enc_outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,
# num_hiddens)
enc_outputs, hidden_state, enc_valid_lens = state
# 输出X的形状为(num_steps,batch_size,embed_size)
X = self.embedding(X).permute(1, 0, 2)
outputs, self._attention_weights = [], []
for x in X:
# query的形状为(batch_size,1,num_hiddens)
query = torch.unsqueeze(hidden_state[-1], dim=1)
# context的形状为(batch_size,1,num_hiddens)
context = self.attention(
query, enc_outputs, enc_outputs, enc_valid_lens)
# 在特征维度上连结
x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
# 将x变形为(1,batch_size,embed_size+num_hiddens)
out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
outputs.append(out)
self._attention_weights.append(self.attention.attention_weights)
# 全连接层变换后,outputs的形状为
# (num_steps,batch_size,vocab_size)
outputs = self.dense(torch.cat(outputs, dim=0))
return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,
enc_valid_lens]
@property
def attention_weights(self):
return self._attention_weights