1.1.3 注意力机制
Seq2Seq结构的模型将输入序列的信息都压缩到上下文向量中,而上下文向量的长度是固定的,因此,当输入序列的长度较长时,一个上下文向量可能存不下那么长的信息,也就是说,此时模型对较长的序列处理能力不足。对此,Bahdanau等人在2014年和Luong等人在2015年提出了一个解决方案——注意力机制。这种机制可以让模型根据需要来关注输入序列中的相关部分,从而放大重点位置的信号,因此添加了注意力机制的模型会比没有添加注意力机制的模型产生更好的结果。
注意力机制在直观上非常好理解,如图1-4所示,当我们拿到一张新的图片时,我们的注意力会自动聚焦到一些关键信息上,而不需要扫描全图。人类的注意力机制能够减少资源损耗,提高信息处理效率。使用注意力机制的深度学习模型也是如此,能够更有效地找到图中的关键信息,并给予较高的权重。
图1-4 注意力机制的图片演示
带有注意力机制的Seq2Seq结构模型相比于经典的Seq2Seq结构模型有以下两点不同。
1)如果有多个编码器,带有注意力机制的Seq2Seq结构模型会将更多的中间数据传递给解码器。经典Seq2Seq结构模型只会将编码阶段的最后一个隐藏状态向量传递给解码器,而带有注意力机制的Seq2Seq结构模型会将编码阶段的所有隐藏状态向量传递给解码器,如图1-5所示。
图1-5 注意力机制下从多编码器到解码器的隐藏状态传递示意
2)解码器在生成输出时会执行额外的计算:首先,接收编码器传递的隐藏状态向量;然后,为每个隐藏状态向量进行打分;最后,将每个隐藏状态向量乘以其softmax分数,从而放大高分的隐藏状态向量。如图1-6所示。
图1-6 解码器利用注意力机制进行计算输出的示意
为了计算注意力权重,我们可以添加一个前馈层。这个前馈层的输入是解码器的输入和隐藏状态。由于训练数据中的句子长度各不相同,我们需要选择一个最大句子长度作为参考,用于创建和训练这个前馈层。对于较短的句子,将只使用前几个注意力权重进行计算,而对于最长的句子,将使用所有的注意力权重。这样可以确保模型能够处理不同长度的句子,并且在训练中学习到如何完成合适的权重分配。
具体的注意力机制的演算过程我们将在1.2节介绍Transformer模型架构时详细说明,此处我们先给出一个简单的代码实现。
python
class Attention(nn.Module):
def __init__(self, hidden_size):
super(Attention, self).__init__()
self.Wa=nn.Linear(hidden_size, hidden_size)
self.Ua=nn.Linear(hidden_size, hidden_size)
self.Va=nn.Linear(hidden_size, 1)
def forward(self, query, keys):
scores=self.Va(th.tanh(self.Wa(query)+self.Ua(keys)))
scores=scores.squeeze(2).unsqueeze(1)
weights=F.softmax(scores, dim=-1)
context=th.bmm(weights, keys)
return context, weights
这段代码定义了一个Attention类,用于实现注意力机制的功能。在模块的初始化方法中,通过如下三个线性层(nn.Linear)来定义模块的参数。
❑self.Wa的输入大小为hidden_size,输出大小也为hidden_size。
❑self.Ua的输入大小为hidden_size,输出大小也为hidden_size。
❑self.Va的输入大小为hidden_size,输出大小为1。
在前向传播过程中,模块会使用两个输入参数:query和keys。query是一个用于查询的向量,而keys是一个包含多个用于比较的向量的集合。模块首先将输入的query和keys经过线性变换,然后经过tanh激活函数,再相加以产生分数(scores)。分数用来衡量query和每个key之间的相关性。接着,对scores进行squeeze和unsqueeze操作,将其维度从(1, N, 1)变为(1, 1, N),其中N为key的数量。然后,通过softmax函数对scores进行标准化操作,得到每个key的权重(weights)。最后,使用权重对keys进行加权求和(torch.bmm),得到一个加权的上下文向量(context),并将其与权重一起返回。
python
class AttentionDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, dropout_p=0.1):
super(AttentionDecoderRNN, self).__init__()
self.embedding=nn.Embedding(output_size, hidden_size)
self.attention=Attention(hidden_size)
self.rnn=nn.RNN(2 * hidden_size, hidden_size, batch_first=True)
self.out=nn.Linear(hidden_size, output_size)
self.dropout=nn.Dropout(dropout_p)
def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):
batch_size=encoder_outputs.size(0)
decoder_input=th.empty(batch_size, 1, dtype=th.long).fill_(SOS_token)
decoder_hidden=encoder_hidden
decoder_outputs=[]
attentions=[]
for i in range(MAX_LENGTH):
decoder_output, decoder_hidden, attn_weights=self.forward_step(decoder_input, decoder_hidden, encoder_outputs)
decoder_outputs.append(decoder_output)
attentions.append(attn_weights)
if target_tensor is not None: # 强制学习
decoder_input=target_tensor[:, i].unsqueeze(1)
else:
_, topi=decoder_output.topk(1)
decoder_input=topi.squeeze(-1).detach()
decoder_outputs=th.cat(decoder_outputs, dim=1)
decoder_outputs=F.log_softmax(decoder_outputs, dim=-1)
attentions=th.cat(attentions, dim=1)
return decoder_outputs, decoder_hidden, attentions
def forward_step(self, input, hidden, encoder_outputs):
embedded= self.dropout(self.embedding(input))
query=hidden.permute(1, 0, 2)
context, attn_weights=self.attention(query, encoder_outputs)
input_rnn=th.cat((embedded, context), dim=2)
output, hidden=self.rnn(input_rnn, hidden)
output=self.out(output)
return output, hidden, attn_weights
这段代码定义了一个名为AttentionDecoderRNN的类,用于实现带有注意力机制的解码器。在模块的初始化方法中,定义了几个子模块。
❑self.embedding是一个嵌入层,用于将输入序列转化为嵌入向量。它的输入大小为output_size(目标语言的词表大小),输出大小为hidden_size(隐藏状态的维度)。
❑self.attention是一个注意力模块,用于实现注意力机制。它的输入维度为hidden_size(隐藏状态维度),后面会将编码器的输出和隐藏状态拼接在一起作为解码器RNN的输入,因此输入维度翻倍,为2 * hidden_size。
❑self.rnn是一个卷积神经网络,用于处理输入序列。它的输入维度为2 * hidden_size(嵌入向量和上下文向量的拼接),输出维度为hidden_size。
❑self.out是一个线性层,用于将解码器的输出映射到最终目标语言的词表大小。它的输入维度为hidden_size,输出维度为output_size。
❑self.dropout是一个dropout层,用于防止过拟合,可以在训练过程中随机将一些节点置为0。
在前向传播过程中,输入参数为encoder_outputs(编码器输出)、encoder_hidden(编码器隐藏状态)和target_tensor(目标序列)。首先,通过encoder_outputs的形状获取batch_size。然后,初始化解码器的输入为一个大小为(batch_size, 1)的LongTensor,并用起始标记填充。隐藏状态起始为编码器的隐藏状态。
循环进行解码的过程,迭代次数为MAX_LENGTH(最大解码长度)。在每个时间步中,调用forward_step方法完成一步解码操作。将解码器的输出、注意力权重和上一时间步的解码器输入存储到decoder_outputs和attentions列表中。
将decoder_outputs和attentions分别连接(cat)在一起,并对decoder_outputs进行log_softmax操作。该模块的输出包括decoder_outputs(解码器输出)、decoder_hidden(解码器隐藏状态)和attentions(每个时间步的注意力权重)。
在forward_step方法中,输入参数为input(解码器输入)、hidden(隐藏状态)和encoder_outputs(编码器输出)。首先,通过嵌入层对input进行嵌入和dropout操作。然后,对隐藏状态进行维度转换,以适应注意力模块的输入要求。通过调用attention模块,获取上下文向量(context)和注意力权重(attn_weights)。将嵌入向量和上下文向量沿第三个维度(维度索引为2)进行拼接,作为RNN层的输入。使用RNN层对输入进行处理从而得到输出(output)和隐藏状态(hidden)。最后,通过线性层将输出映射到目标语言的词表大小,得到最终的解码器输出。
下面我们实例化一个带注意力机制的解码器对象,然后使用玩具数据看一下最终输出向量和注意力权重的维度。
python
decoder=AttentionDecoderRNN(hidden_size=5, output_size=10)
target_vector=th.tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
encoder_outputs, encoder_hidden=encoder(input_vector)
output, hidden, attentions=decoder(encoder_outputs, encoder_hidden, input_vector)
print("输出向量的维度:", output.size()) # 输出向量的维度: torch.Size([1, 10, 10])
print("注意力权重的维度:", attentions.size()) # 注意力权重的维度: torch.Size([1, 10, 10])