seq2seq 与机器翻译
本教程共 60 篇 · 第 43 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:理解编码器-解码器(Encoder-Decoder)结构如何解决变长到变长的翻译问题,明白注意力(Attention)为什么出现,并看懂带注意力的 seq2seq 训练与解码流程。
上一章的字符级 RNN 生成名字,输入输出都是字符。机器翻译更复杂:输入是法语,输出是英语,两边长度不一样,词的顺序还可能相反。比如法语「je ne suis pas le chat noir」直译是「我 不 是 那 猫 黑」,英语却要说「我不是黑猫」。
逐个词对齐翻译行不通。序列到序列(Sequence to Sequence,seq2seq)的思路是:先整句读进去,理解了再整句说出来。
编码器:把句子压成一个向量
seq2seq 由两个 RNN 组成。第一个叫编码器(Encoder):一个词一个词读输入句子,每读一步更新隐状态。读完后,最后的隐状态浓缩了整句的信息,叫上下文向量(Context Vector)。
编码器的输入是词索引序列,先过第 41 章的词嵌入层变成向量,再喂 RNN。batch 为 1 时,每个时间步的形状是 (1, 1, hidden_size)。
解码器:从向量里展开句子
第二个叫解码器(Decoder):从上下文向量出发,一个词一个词生成输出。开头先给一个特殊标记 SOS(Start of Sentence,句子开始),每步预测下一个词,直到吐出 EOS 为止。
打个比方:编码器是读者,把法语句子读成一段「摘要」;解码器是作家,凭摘要写英语句子。
简单解码器的短板:信息瓶颈
摘要只能记大概。句子一长,一个固定大小的向量装不下所有细节,翻译质量明显下滑。这叫信息瓶颈,是朴素 seq2seq 的硬伤。
注意力:翻到哪,看哪
注意力机制(Attention)给解码器加了一个能力:生成每个词时,回头扫一遍编码器所有时间步的输出,按相关度加权,取回最需要的信息。
具体分三步:
- 用解码器当前状态和每个编码器输出算相关度分数。
- softmax 归一化,得到一组注意力权重(Attention Weights)。
- 权重和编码器输出加权求和,得到这一步的「上下文」,喂给解码器。
关键区别在这:信息来自编码器每一步的输出,而不是最后那一个固定向量,信息瓶颈就此解开。
翻译「black cat」时,生成 cat 那一步的权重会集中在法语的 noir 上。注意力矩阵画出来,能看到一条漂亮的斜对角线——两种语言的对齐关系一目了然。
数据准备:先建两个词表
翻译要建两个词表:法语一个、英语一个。教程用 Lang 类维护双向映射,并预置两个特殊标记:SOS(句子开始,索引 0)和 EOS(句子结束,索引 1)。
SOS_token = 0
EOS_token = 1
class Lang:
def __init__(self, name):
self.name = name
self.word2index = {}
self.index2word = {0: "SOS", 1: "EOS"}
self.n_words = 2 # 两个特殊标记占前两个位置
def add_sentence(self, sentence):
for word in sentence.split(" "):
if word not in self.word2index:
self.word2index[word] = self.n_words
self.index2word[self.n_words] = word
self.n_words += 1
lang = Lang("fra")
lang.add_sentence("je suis la")
print(lang.n_words) # 5
句子转张量时,末尾要追加 EOS 索引,解码器才学得会「什么时候停」。
PyTorch 里的实现
编码器很简洁:词嵌入 + GRU,返回每个时间步的输出和最终隐状态。
import torch
import torch.nn as nn
class EncoderRNN(nn.Module):
def __init__(self, input_size, hidden_size):
super().__init__()
self.hidden_size = hidden_size
self.embedding = nn.Embedding(input_size, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
def forward(self, x, hidden):
embedded = self.embedding(x).view(1, 1, -1)
output, hidden = self.gru(embedded, hidden)
return output, hidden
def init_hidden(self):
return torch.zeros(1, 1, self.hidden_size)
带注意力的解码器也不难:注意力权重由一个线性层算出来,用 torch.bmm 做加权求和。
class AttnDecoderRNN(nn.Module):
def __init__(self, hidden_size, output_size, max_length=10):
super().__init__()
self.embedding = nn.Embedding(output_size, hidden_size)
self.attn = nn.Linear(hidden_size * 2, max_length)
self.attn_combine = nn.Linear(hidden_size * 2, hidden_size)
self.gru = nn.GRU(hidden_size, hidden_size)
self.out = nn.Linear(hidden_size, output_size)
def forward(self, x, hidden, encoder_outputs):
embedded = self.embedding(x).view(1, 1, -1)
attn_weights = torch.softmax(
self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
attn_applied = torch.bmm(attn_weights.unsqueeze(0),
encoder_outputs.unsqueeze(0))
output = torch.cat((embedded[0], attn_applied[0]), 1)
output = self.attn_combine(output).unsqueeze(0)
output, hidden = self.gru(torch.relu(output), hidden)
return torch.log_softmax(self.out(output[0]), dim=1), hidden, attn_weights
Note
max_length是训练前设定的最长句子长度,编码器输出张量按它预留,短句只占前面几行。这是早期教程的简化做法,后来 Transformer 的 SDPA 注意力不再需要这个限制。
Tip训练时解码器每步都要拿编码器的全部输出,所以流程是:编码器先把整句跑完、把每个时间步的输出都存起来,再开始解码。batch 为 1 时形状是 (max_length, hidden_size)。
Teacher Forcing:训练时的「参考答案」
训练解码器有个选择:下一步的输入,用真实答案还是模型自己的预测?
用真实答案叫教师强制(Teacher Forcing):收敛快,像有人扶着学骑车。但扶习惯了,模型没机会纠正自己的错误,推理时可能越错越离谱。标准做法是设一个比例,比如 50% 概率用真实答案,其余时间用自己的预测。
teacher_forcing_ratio = 0.5
if random.random() < teacher_forcing_ratio:
decoder_input = target[di] # 教师强制:喂真实答案
else:
decoder_input = topi.detach() # 自己的预测,detach 断开梯度
数据与评估
翻译数据是成千上万句「法语-英语」对照对,官方教程用的数据集由 Tatoeba 项目整理、可从 manythings.org 下载。预处理包括:统一转 ASCII、小写、去标点,再过滤掉超过 10 个词的长句,只留「i am / he is / she is」开头的高频句式。约 13.6 万句筛完剩 1 万句左右,词表各几千词,CPU 上几十分钟就能训完。
评估时没有标准答案,解码器只能用自己的预测一路滚下去:每步取概率最大的词,碰到 EOS 停。这就是贪心解码(Greedy Decoding)。想要更好的结果,可以用束搜索(Beam Search):每步多留几个候选,最后挑整体概率最高的句子。beam 宽度越大效果越好,代价是计算量,工程上通常取 4~8。
训练 7.5 万次迭代后,随便抽几句评估:
> je pars en vacances pour quelques jours .
= i m taking a couple of days off .
< i m taking a couple of days off .
> je suis fou de vous .
= i m crazy about you .
< i m crazy about you .
短句翻得相当准,长句开始露馅——这正是信息瓶颈的直观体现,也是注意力要解决的痛点。
Tip评估过程用
torch.no_grad()包住。注意力权重记得存下来,plt.matshow(attentions)画出来,是验证模型有没有学会对齐的直观证据。
从 RNN 到 Transformer
今天的主流翻译模型早已换成 Transformer(第 23 章):编码器-解码器结构没变,但「读」从逐步接力变成并行自注意力。seq2seq 里的编码器输出、注意力加权这些概念,在 Transformer 里原样保留。现在的翻译工具界面也还是一样:一句进去,一句出来,中间隔着注意力。学懂这一章,大模型翻译的原理就通了一大半。
本章一句话:编码器读进去压成向量,解码器吐出来展开成句,注意力让解码器每步都能回看原文——变长到变长的翻译问题,就这样被拆解掉了。