序列到序列(Sequence-to-Sequence,简称 Seq2Seq)是一种将变长输入序列映射为变长输出序列的端到端学习框架。它由 Google 的 Sutskever 等人于 2014 年首次提出,最初用于机器翻译——将一段英文(变长)翻译成一段中文(变长),中间无需任何手工规则。这一范式迅速成为自然语言处理领域的基石技术,并演化为今天 Transformer 的核心思想。

核心架构:编码-解码范式

Seq2Seq 的本质是”先压缩、再展开”:编码器把输入序列压缩为一个语义表示,解码器从这个表示出发逐步生成输出序列。整个架构由三个核心组件构成:编码器、解码器、上下文向量。

编码器 (Encoder)

  • 定义:编码器是一个循环神经网络(RNN / LSTM / GRU),逐时刻读取输入序列 $x = (x_1, x_2, \ldots, x_n)$,在每个时刻 $t$ 更新隐状态 $h_t = f(h_{t-1}, x_t)$,最终输出一组隐状态序列 $(h_1, h_2, \ldots, h_n)$ 和最后一个隐状态 $h_n$。
  • 特点:编码器不直接输出”结果”,它的产物是隐状态序列——可以理解为输入序列的”记忆”。LSTM 和 GRU 因门控机制能缓解梯度消失,实践中几乎完全取代了普通 RNN。双向编码器(BiLSTM / BiGRU)同时从左到右和从右到左扫描,能捕获更完整的上下文信息。
  • 注意:编码器的隐状态维度是一个关键超参数。维度太小,语义信息表达不足;维度太大,计算开销和过拟合风险增加。常见选择为 256、512 或 1024。

解码器 (Decoder)

  • 定义:解码器同样是一个循环神经网络,但它的工作方式是自回归的——在每个时刻 $t$,它以前一时刻的输出 $y_{t-1}$ 和当前隐状态 $s_t$ 为输入,预测当前时刻的输出 $y_t$,直到生成结束标记<EOS> 为止。
  • 特点:解码器的第一步输入通常是特殊的起始标记<BOS>(Beginning of Sequence)。在训练阶段,解码器每一步接收的是真实标签(Teacher Forcing),而非模型自己的预测;在推理阶段,则只能用上一步的实际输出作为输入。这种训练-推理的差异被称为”暴露偏差”(exposure bias),是 Seq2Seq 的已知痛点之一。
  • 注意:解码器和编码器可以使用不同类型的 RNN、不同数量的层数、不同的词表。例如,英译中模型中编码器使用英文词表,解码器使用中文词表,两者完全独立。

上下文向量 (Context Vector)

  • 定义:上下文向量 $C$ 是编码器传递给解码器的”桥梁”。在最基础的 Seq2Seq 中,$C$ 就是编码器的最终隐状态 $h_n$,直接作为解码器的初始隐状态 $s_0 = C$。
  • 特点:上下文向量的维度是固定的(等于编码器隐状态维度),与输入序列长度无关。这意味着无论输入是 5 个词还是 50 个词,信息都被压缩到同一个维度的向量中。
  • 注意:这正是 Seq2Seq 的核心瓶颈——固定维度的向量难以承载长序列的全部信息。当一个句子有 40 个词时,要求 $h_n$”记住”所有细节是不现实的,编码器会不可避免地丢失早期信息。这一问题直接催生了注意力机制。

注意力机制:突破固定向量瓶颈

注意力机制让解码器在每一步都能”回看”编码器的所有隐状态,动态决定该关注输入的哪些位置,而不是被迫只依赖一个固定向量。这一机制由 Bahdanau 等人于 2015 年提出,是 Seq2Seq 架构最重要的改进。

注意力的核心思想

在基础 Seq2Seq 中,解码器每一步只能通过初始隐状态 $s_0 = h_n$ 获取输入信息。注意力机制的思路是:解码器在生成第 $t$ 个输出词时,不再只依赖 $s_t$,而是计算一个”动态上下文向量” $c_t$——它是编码器所有隐状态的加权平均,权重由当前解码器状态 $s_t$ 与各编码器隐状态 $h_i$ 的相似度决定。

直观地说,翻译 “I love deep learning” 时,生成 “深” 这个字,注意力应集中在 “deep” 上;生成 “学” 时,注意力应集中在 “learning” 上。注意力权重正好捕捉了这种对齐关系。

注意力计算流程

注意力的计算分三步:对齐打分 → 归一化 → 加权求和。

第一步:对齐打分。给定解码器当前隐状态 $s_t$ 和编码器第 $i$ 个隐状态 $h_i$,计算一个标量分数 $e_{t,i} = \text{score}(s_t, h_i)$,衡量 $s_t$ 与 $h_i$ 的相关程度。常见的打分函数有三种:

  • Bahdanau (Additive):$e_{t,i} = v^\top \tanh(W_s s_t + W_h h_i)$,通过一个小的前馈网络计算分数。
  • Luong Dot:$e_{t,i} = s_t^\top h_i$,直接做内积,要求 $s_t$ 和 $h_i$ 维度相同。
  • Luong General:$e_{t,i} = s_t^\top W h_i$,在内积基础上加一个可学习矩阵 $W$。

第二步:归一化。对所有分数做 Softmax,得到注意力权重:

$$\alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{n} \exp(e_{t,j})}$$

第三步:加权求和。用注意力权重对编码器隐状态加权求和,得到当前时刻的上下文向量:

$$ c_t = \sum_{i=1}^{n} \alpha_{t,i} \cdot h_i$$

最终,解码器将 $s_t$ 和 $c_t$ 拼接后送入输出层预测 $y_t$。注意力机制使每个输出词都能”看到”所有输入词,且权重 $\alpha_{t,i}$ 可视化后即为对齐矩阵——这为模型提供了天然的可解释性。

为什么需要 Seq2Seq

在 Seq2Seq 出现之前,处理”变长输入到变长输出”的任务主要依赖两类方法:统计机器翻译(SMT)和基于规则的系统。它们各有明显短板。

  • SMT 的困境:统计机器翻译(如基于短语的 Moses 系统)需要大量人工设计的特征——词对齐模型、短语提取、语言模型重排序等。整个流水线被切分为多个独立阶段,每个阶段只能局部优化,错误会逐级累积。开发者需要深厚的语言学知识和工程经验才能调出可用的系统。
  • 固定维度模型的局限:传统的分类模型(如 CNN 分类器)要求输入和输出维度固定。如果要翻译一个 40 词的句子,要么截断超出部分(丢失信息),要么填充到最大长度(浪费计算)。更关键的是,CNN 的卷积核感受野有限,难以建模词与词之间的长距离依赖。

Seq2Seq 带来了三个根本性改进:

  • 端到端学习:从输入到输出只需一个神经网络,无需手工特征工程,梯度可以从输出端直接回传到输入端,全局联合优化。
  • 变长映射:输入和输出长度可以不同,由<EOS> 标记自动决定终止时机,天然适配翻译、摘要、对话等任务。
  • 可解释性:注意力权重矩阵直接呈现源词与目标词的对齐关系,让模型行为不再是黑箱——这在调试和信任建立方面价值巨大。

模型演进与对比矩阵

Seq2Seq 架构经历了三代演进:基础 RNN 编码器-解码器、引入注意力机制、全面转向 Transformer。下表从六个维度对比这三代架构的核心差异。

维度 RNN Encoder-Decoder (2014) RNN + Attention (2015) Transformer (2017)
编码器 LSTM / GRU BiLSTM / BiGRU 多层自注意力
上下文传递 固定向量 $h_n$ 动态加权上下文 $c_t$ 全程隐状态序列(无压缩)
注意力 Bahdanau / Luong 自注意力(Self-Attention)
并行性 编码器可并行,解码器串行 编码器可并行,解码器串行 训练时编码器和解码器均可并行
长距离依赖 差(信息在 $h_n$ 处衰减) 较好(任意位置可直接访问) 优秀(任意两位置直连)
代表模型 seq2seq (Sutskever 2014) Bahdanau NMT, Luong NMT Transformer, BERT, GPT

从表中可以看出,演进的核心驱动力是消除信息瓶颈:从固定向量到动态加权,再到完全保留序列信息;同时从串行走向并行,大幅提升训练效率。

从零实现:PyTorch 代码与场景示例

本节用 PyTorch 实现一个完整的带 Bahdanau 注意力的 Seq2Seq 模型,涵盖编码器、注意力层、解码器和训练循环,并展示训练与推理两种工作模式的差异。

编码器实现

编码器将输入序列(已转为索引张量)通过 Embedding 层和 GRU 处理,输出隐状态序列和最终隐状态。

import torch
import torch.nn as nn
import torch.nn.functional as F

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1, dropout=0.1):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.rnn = nn.GRU(embed_dim, hidden_dim, num_layers,
                          batch_first=True, bidirectional=True)
        self.dropout = nn.Dropout(dropout)
        # 双向 GRU 输出 2*hidden_dim,需投影到 hidden_dim 供解码器使用
        self.fc = nn.Linear(hidden_dim * 2, hidden_dim)

    def forward(self, src):
        # src: [batch, seq_len]
        embedded = self.dropout(self.embedding(src))  # [batch, seq_len, embed_dim]
        outputs, hidden = self.rnn(embedded)  # outputs: [batch, seq_len, 2*hidden]
        # 拼接双向最后隐状态并投影
        hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)  # [batch, 2*hidden]
        hidden = torch.tanh(self.fc(hidden))  # [batch, hidden]
        return outputs, hidden

Bahdanau 注意力层

注意力层接收解码器当前隐状态和编码器全部输出,计算对齐分数并返回上下文向量和注意力权重。

class BahdanauAttention(nn.Module):
    def __init__(self, hidden_dim):
        super().__init__()
        # 解码器隐状态投影
        self.W_s = nn.Linear(hidden_dim, hidden_dim, bias=False)
        # 编码器输出投影
        self.W_h = nn.Linear(hidden_dim * 2, hidden_dim, bias=False)
        # 对齐打分向量
        self.v = nn.Linear(hidden_dim, 1, bias=False)

    def forward(self, decoder_hidden, encoder_outputs):
        # decoder_hidden: [batch, hidden]
        # encoder_outputs: [batch, seq_len, 2*hidden]

        # 扩展解码器隐状态以便与编码器输出逐位置配对
        # [batch, 1, hidden] -> [batch, seq_len, hidden]
        dec_proj = self.W_s(decoder_hidden).unsqueeze(1)
        # [batch, seq_len, hidden]
        enc_proj = self.W_h(encoder_outputs)

        # 对齐打分: v^T * tanh(W_s * s_t + W_h * h_i)
        # score: [batch, seq_len, 1]
        score = self.v(torch.tanh(dec_proj + enc_proj))

        # 注意力权重 (Softmax 归一化)
        # alpha: [batch, seq_len]
        alpha = F.softmax(score.squeeze(-1), dim=1)

        # 加权求和得到上下文向量
        # context: [batch, 2*hidden]
        context = torch.bmm(alpha.unsqueeze(1), encoder_outputs).squeeze(1)

        return context, alpha

解码器实现

解码器在每个时刻接收当前输入词、前一时刻隐状态和编码器输出,通过注意力计算上下文向量后预测输出词。

class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, num_layers=1, dropout=0.1):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.attention = BahdanauAttention(hidden_dim)
        # GRU 输入 = embed_dim + 2*hidden_dim (上下文向量维度)
        self.rnn = nn.GRU(embed_dim + hidden_dim * 2, hidden_dim,
                          num_layers, batch_first=True)
        # 输出层输入 = hidden + context + embed
        self.fc_out = nn.Linear(hidden_dim + hidden_dim * 2 + embed_dim, vocab_size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, input_token, hidden, encoder_outputs):
        # input_token: [batch] (当前时刻输入词索引)
        # hidden: [batch, hidden]
        # encoder_outputs: [batch, seq_len, 2*hidden]

        embedded = self.dropout(self.embedding(input_token))  # [batch, embed]

        # 计算注意力上下文
        context, alpha = self.attention(hidden, encoder_outputs)  # [batch, 2*hidden]

        # 拼接 embedding 和 context 作为 GRU 输入
        rnn_input = torch.cat([embedded, context], dim=1).unsqueeze(1)  # [batch, 1, embed+2*hidden]
        output, hidden = self.rnn(rnn_input, hidden.unsqueeze(0))  # output: [batch, 1, hidden]

        # 拼接所有信息送入输出层
        output = output.squeeze(1)  # [batch, hidden]
        pred_input = torch.cat([output, context, embedded], dim=1)
        prediction = self.fc_out(pred_input)  # [batch, vocab_size]

        return prediction, hidden.squeeze(0), alpha

Seq2Seq 封装与训练

将编码器和解码器组合为完整模型,训练阶段使用 Teacher Forcing——每一步将真实标签而非模型预测喂给解码器。

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.device = device

    def forward(self, src, trg, teacher_forcing_ratio=0.8):
        # src: [batch, src_len]
        # trg: [batch, trg_len]
        batch_size = src.shape[0]
        trg_len = trg.shape[1]
        trg_vocab_size = self.decoder.fc_out.out_features

        # 存储每一步的预测
        outputs = torch.zeros(batch_size, trg_len, trg_vocab_size).to(self.device)

        # 编码
        encoder_outputs, hidden = self.encoder(src)

        # 第一步输入  标记
        input_token = trg[:, 0]  # [batch]

        for t in range(1, trg_len):
            # 解码一步
            prediction, hidden, alpha = self.decoder(input_token, hidden, encoder_outputs)
            outputs[:, t] = prediction

            # Teacher Forcing: 以概率 teacher_forcing_ratio 使用真实标签
            if random.random() < teacher_forcing_ratio:
                input_token = trg[:, t]
            else:
                input_token = prediction.argmax(dim=1)

        return outputs

    def inference(self, src, max_len=50, bos_idx=1, eos_idx=2):
        """推理阶段:自回归生成,无 Teacher Forcing"""
        self.eval()
        with torch.no_grad():
            encoder_outputs, hidden = self.encoder(src)
            batch_size = src.shape[0]
            input_token = torch.full((batch_size,), bos_idx, dtype=torch.long).to(self.device)

            output_tokens = []
            for t in range(max_len):
                prediction, hidden, alpha = self.decoder(input_token, hidden, encoder_outputs)
                input_token = prediction.argmax(dim=1)
                output_tokens.append(input_token)

                # 全部样本都生成了  则提前停止
                if (input_token == eos_idx).all():
                    break

            return torch.stack(output_tokens, dim=1)  # [batch, gen_len]

上述代码清晰地展示了两条执行路径:forward 方法用于训练,通过 Teacher Forcing 加速收敛;inference 方法用于推理,逐词自回归生成。

场景示例

  • 场景一:英中机器翻译。传统统计机器翻译(如 Moses)需要先训练词对齐模型,再提取短语表,最后用语言模型重排序——三阶段独立优化,调试周期长达数周。引入 Seq2Seq + Attention 后,只需准备平行语料(英文-中文句对),一个模型端到端训练即可。注意力矩阵还自动呈现英中词对齐关系,无需额外设计对齐算法。BLEU 分数通常提升 3~5 分,且模型可以持续用新数据微调迭代。
  • 场景二:文本摘要。传统抽取式摘要依赖 TextRank 等图排序算法,从原文中”挑选”关键句拼接——无法改写、无法概括跨句信息。Seq2Seq 生成式摘要则能”理解”全文语义后重新组织语言输出,可以浓缩、改写、合并跨段落信息。在 CNN/DailyMail 数据集上,带注意力的 Seq2Seq 模型 ROUGE-L 分数可达 28~30,显著优于纯抽取式方法的 24~26。

局限性与适用边界

Seq2Seq 并非万能方案,在以下场景中存在明确局限:

  • 暴露偏差 (Exposure Bias):训练时解码器每步看到的是正确答案,推理时只能依赖自身输出。一旦某步预测错误,后续输入就是”错误”的,误差会逐步放大。缓解方法包括 Scheduled Sampling(训练时逐步降低 Teacher Forcing 比例)和 RL 微调(直接优化 BLEU 等任务指标)。
  • 自回归生成速度慢:解码器必须逐词生成,无法并行化。翻译一个 50 词的句子需要 50 次前向传播。这一局限在 Transformer 中通过非自回归解码(Non-Autoregressive Decoding)部分缓解,但质量仍不如自回归方法。
  • RNN 长距离衰减未根除:即使有注意力机制,RNN 编码器本身仍按时刻串行处理,相隔很远的词之间的梯度仍然需要经过多步传递。Transformer 用自注意力彻底解决了这一问题——任意两个位置之间只经过一次矩阵乘法即可交互。
  • 生成重复与不连贯:Seq2Seq 在长文本生成中容易出现重复短语(如”我觉得我觉得”)或中途话题漂移。这通常需要 Beam Search、长度惩罚、重复惩罚等解码策略来缓解,但治标不治本。

结论

Seq2Seq 的核心贡献在于确立了”编码-解码”这一处理变长序列映射的通用范式,注意力机制则让这一范式真正实用化。从 2014 年的 RNN 编码器-解码器,到 2015 年的注意力机制,再到 2017 年 Transformer 全面采用自注意力,演进的主线是消除信息瓶颈、增强长距离建模能力、提升并行效率。

对于实践者而言:如果任务是翻译、摘要或对话且算力有限,带注意力的 RNN Seq2Seq 仍然是一个轻量可行的选择;如果追求最优效果且算力充足,直接使用基于 Transformer 的模型(如 T5、BART)是当前的最佳实践。理解 Seq2Seq 的编码-解码思想和注意力机制,是理解一切现代序列生成模型的基础。

参考文献 / 扩展阅读

  • Sutskever, I., Vinyals, O., & Le, Q. V. (2014). Sequence to Sequence Learning with Neural Networks.NeurIPS 2014.
  • Cho, K., et al. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation.EMNLP 2014.
  • Bahdanau, D., Cho, K., & Bengio, Y. (2015). Neural Machine Translation by Jointly Learning to Align and Translate.ICLR 2015.
  • Luong, M. T., Pham, H., & Manning, C. D. (2015). Effective Approaches to Attention-based Neural Machine Translation.EMNLP 2015.
  • Vaswani, A., et al. (2017). Attention Is All You Need.NeurIPS 2017.
0