一. Seq2Seq模型
序列到序列(Sequence-to-Sequence,简称Seq2Seq)模型是一种深度学习架构,主要用于处理输入和输出都是变长序列的任务,例如机器翻译、文本摘要、语音识别转文字、聊天机器人对话等自然语言处理任务。Seq2Seq模型的核心思想是使用两个主要部分组成的神经网络框架:编码器(Encoder)和解码器(Decoder)。
1. 编码器(Encoder)
- 职责是理解并捕获输入序列的语义信息。
- 它通常由一个或多层循环神经网络(RNN模型,如LSTM或GRU)构成,也可以是Transformer等结构。
- 输入序列按照时间步(或单词顺序)依次输入编码器,编码器在每次时间步都会更新其内部状态,最终输出一个固定长度的上下文向量(Context Vector)或一系列上下文向量(在使用双向RNN时会有前后向两个方向的信息),这个向量包含了输入序列的整体信息。
2. 解码器(Decoder)
- 职责是依据编码器产生的上下文向量生成对应的输出序列。
- 解码器也是一个RNN模型或使用更高级的Transformer模型,它的工作方式与编码器稍有不同。
- 解码器首先初始化其隐藏状态,通常会利用编码器输出的、最终的隐藏层状态(last_hidden_state)作为起点。
- 在生成序列的过程中,解码器每次输出一个元素后,会结合上一次的隐状态和当前时刻的输入(可能是先前生成的词或者特殊标记如起始符号)来更新自身的状态,并基于这个状态预测下一个输出符号的概率分布。
3. 模型训练过程
- Seq2Seq模型在训练阶段,会根据输入序列和对应的期望输出序列进行学习,优化模型参数使得输出序列尽可能接近真实的目标序列。
- 使用教师强制(Teacher Forcing)策略,即在训练时,即使模型预测错误,下一轮的输入也会使用正确的前一个预测值,而不是模型实际预测出的值。
二. 注意力机制在Seq2Seq模型中的应用
在Seq2Seq模型中,注意力机制通常被整合到解码器端的设计中,用来解决原始seq2seq模型在处理长输入序列时所遇到的问题:即解码器在生成每个输出元素时只能依赖于编码器输出的一个固定长度的上下文向量(通常是编码器最后一层的隐藏状态),这可能无法充分捕捉到输入序列的所有重要信息。
2.1 解码器端的注意力机制
在解码器端添加注意力机制后,解码器在生成每个输出词时能够动态地关注输入序列的不同部分。具体来说,解码器会在每一步生成过程中根据当前生成的状态和之前的上下文信息计算一个权重分布,这个分布代表了对输入序列各位置的关注程度。通过加权求和的方式,解码器可以根据这些权重实时重新组合编码器的所有隐藏状态,从而得到一个更灵活且针对当前生成步骤更有针对性的上下文向量。
2.2 编码器端的注意力机制
相比之下,将注意力机制放在编码器端并不常见,因为标准的seq2seq模型中编码器的主要职责是,尽可能完整地捕获输入序列的信息并将其压缩进一个或多个固定维度的向量中。如果要在编码器端引入注意力机制,可能是为了增强编码器内部不同时间步之间信息的交互或聚焦,但这并非注意力机制在seq2seq模型中最典型的应用场景。
然而,也有一些研究探索了在编码阶段使用注意力机制的方法,例如自注意力(self-attention,也称为内部注意力),这种机制允许输入序列内的各个元素相互比较和影响彼此的表示,这是Transformer模型等现代架构的核心组件。在这样的情况下,编码器端的注意力机制主要用于改进输入序列的编码过程,使其能够更加有效地提取全局依赖关系,而不是像解码器端那样用于动态参考输入序列。
总结来说,解码器端的注意力机制关注的是如何在生成输出时动态适应输入信息,而编码器端若采用注意力机制则更多是为了改进输入信息的编码质量或捕获输入序列内部复杂的依赖结构。
三. 代码示例
下面使用代码构建了一个由GRU编码器、含注意力的GRU解码器组成的Seq2Seq模型,用于完成机器翻译任务(N vs M类型):
3.1 Encoder部分
# 构建基于GRU的编码器
class GRU_Encoder(nn.Module):
def __init__(self, vocab_size,embed_dim,hidden_size):
super().__init__()
# 初始化属性
self.vocab_size = vocab_size # 输入词汇总数
self.embed_dim = embed_dim # 输入词汇的词嵌入维度
self.hidden_size = hidden_size # 隐藏层向量维度
# 实例化embed层
self.embed = nn.Embedding(vocab_size, embed_dim)
# 实例化GRU层
self.gru = nn.GRU(embed_dim, hidden_size, num_layers=1, batch_first=True)
def forward(self, input, hidden):
# 输入数据经过embed层
out_embed = self.embed(input)
# embed处理后的数据经过GRU层
output, hn = self.gru(out_embed, hidden)
return output, hn
def init_hidden(self):
# batch_size=1
hidden = torch.zeros(1, 1, self.hidden_size)
return hidden
3.2 含注意力的Decoder部分
# 构建加入attention的GRU解码器
class Attention_GRU_Decoder(nn.Module):
def __init__(self,vocab_size,embed_dim,encoder_hidden_size,gru_hidden_size,max_length=10):
super().__init__()
# 定义一些属性:模型参数
self.vocab_size = vocab_size # 输出词汇总数
self.embed_dim= embed_dim # 输出词汇的词嵌入维度
self.encoder_hidden_size = encoder_hidden_size # encoder隐藏层张量维度
self.gru_hidden_size = gru_hidden_size # GRU层解码时的隐藏层张量维度,这里使用encoder的最终隐藏状态作为初始化s0
self.max_length = max_length # 翻译语句的最大长度
# 模型结构
# 实例化embed层
self.embed = nn.Embedding(vocab_size ,embed_dim)
# droupout策略
self.droupout = nn.Dropout(p=0.1)
# 注意力计算第1步的线性层:Linear([Q,K])
self.attn = nn.Linear(embed_dim + encoder_hidden_size,max_length)
# 注意力计算第3步的线性层:对attn与Q拼接后的张量做线性变换,改变形状,得到对Q的注意力表示
self.attn_combine = nn.Linear(embed_dim + encoder_hidden_size, gru_hidden_size)
# 实例化GRU层
self.gru = nn.GRU(gru_hidden_size,gru_hidden_size,num_layers=1,batch_first=True)
# 实例化全连接层
self.out = nn.Linear(gru_hidden_size,vocab_size)
# 输出层out的softmax激活
self.softmax = nn.LogSoftmax(dim=-1)
# 前向传播:定义输入数据Q,K,V的计算过程
def forward(self,input,hidden,encoder_output_c):
# input=Q
# hidden=K
# encoder_ouput_c=V: 中间语义张量
# 输入数据进入embed层
input = self.embed(input)
# droupout处理:缓解过拟合
embedded = self.droupout(input)
# 注意力分配系数计算
# 1.与K拼接后,经过第一个线性层,再进行softmax激活
attn_weights = F.softmax(self.attn(torch.cat((embedded[0],hidden[0]),dim=-1)),dim=-1)
# 2.将attn_weights与V相乘
attn_applied = torch.bmm(attn_weights.unsqueeze(0),encoder_output_c.unsqueeze(0))
# 3.将attn_applied与Q拼接,经过线性层输出,准备送入GRU
gru_input = self.attn_combine(torch.cat((attn_applied,input),dim=-1))
# 进行relu激活
gru_input = F.relu(gru_input)
# gru模型
gru_output, hn = self.gru(gru_input,hidden)
# 对gru模型的输出进行分类: 经过out全连接层,并且对分类结果进行softmax激活
output = self.softmax(self.out(gru_output[0]))
return output, hn, attn_weights
# GRU模型测隐藏状态初始化
# 这里因为直接使用encoder最后输出的隐藏状态作为decoder的初始化隐藏状态,所以这里没有用到
def init_hidden(self):
hidden = torch.zeros(1, 1, self.hidden_size, device=device)
return hidden
3.3 补充说明
上面的代码中,解码器部分使用的注意力机制为 “加型Attention”,除此之外还有一种常用的注意力机制为 “乘型Attention"。“加型Attention”的注意力计算规则如下:
第一步:
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
S
o
f
t
m
a
x
(
L
i
n
e
a
r
(
[
Q
,
K
]
)
)
∗
V
Attention(Q, K, V) = Softmax(Linear([Q, K])) * V
Attention(Q,K,V)=Softmax(Linear([Q,K]))∗V
第二步:
将
Q
Q
Q 与第一步的计算结果进行拼接。