机器翻译 Seq2Seq:编码器-解码器与注意
什么是 Seq2Seq?
序列到序列(Sequence-to-Sequence,简称 Seq2Seq)是一种将任意长度输入序列转换为任意长度输出序列的深度学习模型结构。它最初由 Google 在 2014 年提出,并迅速成为机器翻译、文本摘要、对话生成等任务的基础框架。
Seq2Seq 的核心思想由两个递归神经网络(RNN)组成:
- 编码器:读取输入序列(如源语言句子),并将其压缩为一个固定长度的上下文向量(context vector)。
- 解码器:接受该上下文向量,并逐步生成输出序列(如目标语言句子)。
这种结构天然地解决了输入输出长度不对齐的问题,因此被称为“序列到序列”。
编码器-解码器架构(Encoder-Decoder)
编码器如何工作?
编码器通常是一个 RNN(如 LSTM 或 GRU),按顺序处理输入序列的每个元素(单词或字符)。对于输入序列 x = (x₁, x₂, ..., x_T),编码器在每个时间步 t 更新隐藏状态:
h_t = RNN_enc(x_t, h_{t-1})
最后一个时间步的隐藏状态 h_T 通常被用作上下文向量 c,它捕捉了整个输入序列的语义信息。有时也使用所有隐藏状态的某种函数(如平均或最后一个状态 + 反向状态)。
解码器如何工作?
解码器是另一个 RNN,它根据上下文向量 c 和已经生成的部分序列,逐个预测目标序列 y = (y₁, y₂, ..., y_T')。
训练时,解码器在时间步 t 接收上一个真实目标词 y_{t-1}(教师强制),结合自身隐藏状态 s_{t-1} 和上下文 c,计算当前隐藏状态:
s_t = RNN_dec(y_{t-1}, s_{t-1}, c)
然后通过一个全连接层和 softmax 输出词表中每个词的概率:
p(y_t | y_{<t}, c) = softmax(W * s_t + b)
推理时,解码器用自己上一步预测的词作为下一步输入,直到生成结束符 <eos>。
信息瓶颈问题
编码器-解码器虽然简洁,但有一个致命缺陷:所有输入信息必须压缩在一个固定长度的向量 c 中。对于长句子,这个向量很难保留全部细节,导致翻译质量随句子长度增加而急剧下降。
注意力机制(Attention Mechanism)
注意力机制正是为解决信息瓶颈而生。它让解码器在生成每个目标词时,能够动态“回顾”编码器的所有隐藏状态,而不是仅仅依赖最后一个上下文向量。
核心思想
在解码的每个时间步 t,模型计算一个注意力权重分布 α_t,表示当前生成对编码器不同位置信息的关注程度。然后,用该分布对编码器的隐藏状态进行加权求和,得到一个动态上下文向量 c_t。
计算过程(以 Bahdanau 注意力为例)
-
计算对齐分数:
对解码器当前隐藏状态s_{t-1}(或s_t,取决于设计)和每个编码器隐藏状态h_i,计算分数e_{t,i}:e_{t,i} = v^T * tanh(W_a * s_{t-1} + U_a * h_i)这通常是一个前馈网络,参数
v, W_a, U_a可学习。 -
归一化得到注意力权重:
α_{t,i} = softmax(e_{t,i}) = exp(e_{t,i}) / Σ_j exp(e_{t,j})α_{t,i} 表示生成第 t 个目标词时,对源序列位置 i 的注意力强度。
-
生成上下文向量:
c_t = Σ_i α_{t,i} * h_ic_t是编码器隐藏状态的加权和,能够聚焦于当前翻译最相关的源语言部分。 -
更新解码器状态并预测: 将
c_t与解码器输入连接(或加法等),计算当前状态:s_t = RNN_dec(y_{t-1}, s_{t-1}, c_t) p(y_t | y_{<t}, x) = softmax(W * [s_t; c_t] + b) // 有时也拼接 c_t
注意力可视化
注意力权重矩阵可以解释模型预测的“对齐关系”。例如在英德翻译中,当生成德语词“Hund”时,模型对英语单词“dog”分配了很高的注意力权重。这种可视化不仅帮助调试,也增强了模型的可解释性。
常见的注意力变体
- Bahdanau 注意力(加性注意力):使用前馈网络计算分数,公式如上。
- Luong 注意力(乘性注意力):直接用解码器状态与编码器状态的内积计算分数:
score(s_t, h_i) = s_t^T * W_a * h_i (或 s_t^T * h_i) - 自注意力(Self-Attention):Transformer 中使用的注意力,源序列内部或目标序列内部元素相互关注,完全摒弃了 RNN。
完整的机器翻译流程
以一个简化的英译中任务为例:
输入(英文):"the cat sat on the mat"
输出(中文):"猫坐在垫子上"
- 预处理:切词、建立词表、添加
<sos>(开始符)和<eos>(结束符),将序列转为索引。 - 编码:英文单词索引序列通过嵌入层,送入编码器 LSTM,得到隐藏状态序列
[h₁, h₂, h₃, h₄, h₅, h₆](每个词一个状态)。 - 解码初始化:解码器初始状态设为编码器最后一个隐藏状态(或学习到的变换)。
- 逐词生成:
- 时间步 1:输入
<sos>,结合注意力机制计算c₁,预测第一个中文词“猫”。 - 时间步 2:输入“猫”,注意力可能聚焦在“sat”,预测“坐”。
- 时间步 3:输入“坐”,注意力转到“on”,预测“在”。
- 时间步 4:输入“在”,注意力转到“mat”,预测“垫子”。
- 时间步 5:输入“垫子”,注意力再次覆盖“mat”和句末,预测“上”。
- 时间步 6:输入“上”,预测
<eos>,生成结束。
- 时间步 1:输入
- 后处理:合并子词、去标记等,得到最终译文。
训练与损失函数
Seq2Seq 模型使用交叉熵损失进行训练。对于目标序列中每个位置,损失是模型预测概率分布与真实词(one-hot)的交叉熵之和:
$$ L = -\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T'} \log p(y_t^{(n)} | y_{<t}^{(n)}, x^{(n)}) $$
其中 N 是 batch 的大小,T' 是目标序列长度。
通常采用教师强制(teacher forcing)加速收敛:训练时解码器的输入始终使用真实的上一词,而不是模型自己预测的词。但这样可能造成训练和推理不一致,缓解方法包括计划采样(scheduled sampling)。
代码示例结构(PyTorch 风格伪代码)
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)
def forward(self, src):
# src: [seq_len, batch]
embedded = self.embedding(src)
outputs, (hidden, cell) = self.rnn(embedded)
# outputs: [seq_len, batch, hid_dim]
return outputs, hidden, cell
class Attention(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()
self.attn = nn.Linear(enc_hid_dim + dec_hid_dim, dec_hid_dim)
self.v = nn.Linear(dec_hid_dim, 1, bias=False)
def forward(self, hidden, encoder_outputs):
# hidden: [batch, dec_hid_dim]
# encoder_outputs: [src_len, batch, enc_hid_dim]
src_len = encoder_outputs.shape[0]
hidden = hidden.unsqueeze(0).repeat(src_len, 1, 1)
energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim=2)))
attention = self.v(energy).squeeze(2) # [src_len, batch]
return F.softmax(attention, dim=0)
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, attention):
super().__init__()
self.attention = attention
self.embedding = nn.Embedding(output_dim, emb_dim)
self.rnn = nn.LSTM(emb_dim + enc_hid_dim, dec_hid_dim)
self.fc = nn.Linear(dec_hid_dim + enc_hid_dim + emb_dim, output_dim)
def forward(self, input, hidden, cell, encoder_outputs):
# input: [batch]
# hidden, cell: [1, batch, dec_hid_dim]
input = input.unsqueeze(0) # [1, batch]
embedded = self.embedding(input)
a = self.attention(hidden[-1], encoder_outputs)
a = a.unsqueeze(0).permute(1, 0, 2) # [1, batch, src_len]
weighted = torch.bmm(a, encoder_outputs.permute(1, 0, 2)).permute(1, 0, 2)
rnn_input = torch.cat((embedded, weighted), dim=2)
output, (hidden, cell) = self.rnn(rnn_input, (hidden, cell))
embedded = embedded.squeeze(0)
output = output.squeeze(0)
weighted = weighted.squeeze(0)
prediction = self.fc(torch.cat((output, weighted, embedded), dim=1))
return prediction, hidden, cell
常见问题与改进方向
- 曝光偏差(Exposure Bias):训练使用教师强制,推理却依赖自身预测,微小错误会累积。计划采样能逐步让模型使用自己的预测。
- OOV 问题:未登录词难以处理,可采用 BPE 子词切分或复制机制。
- 长句子遗忘:LSTM 本身对长依赖仍然吃力,注意力机制显著改善,但 Transformer 的自注意力彻底解决并行和长距依赖问题。
- 解码策略:贪心解码可能陷入非最优,束搜索(Beam Search)可保留多个候选路径提高译文质量。
总结
Seq2Seq 模型通过编码器-解码器结构优雅地解决了序列转换问题,而注意力机制的引入让模型能够动态聚焦源信息,显著提升了长句翻译效果。理解这两个基础组件是掌握现代 NLP 模型(如 Transformer)的关键一步。建议从实现一个简单的基于注意力机制的英法翻译器开始,逐步加深理解。