Skip to content

Seq2Seq(序列到序列)

Seq2Seq 是处理"变长输入 → 变长输出"问题的经典架构,是机器翻译、文本摘要、对话系统的基础,也是现代 Transformer 的前身。

1. 什么是 Seq2Seq?

Seq2Seq(Sequence to Sequence)的本质:将一个输入序列映射为另一个输出序列,两者长度可以不同。

由 Sutskever 等人在 2014 年 Google 论文 "Sequence to Sequence Learning with Neural Networks" 中提出。

典型应用

任务输入输出
机器翻译"I love you""我爱你"
文本摘要500 字新闻50 字摘要
对话系统用户提问AI 回答
语音识别音频帧序列文字序列
代码生成自然语言描述Python 代码

2. 架构组成

Seq2Seq 由三个核心组件构成:

输入序列 [I, love, you]

┌─────────────────┐
│    Encoder      │  → 压缩为 Context Vector (固定维度)
│  (LSTM/GRU)     │
└─────────────────┘
    ↓ Context Vector c
┌─────────────────┐
│    Decoder      │  → 逐步生成输出序列
│  (LSTM/GRU)     │
└─────────────────┘

输出序列 [我, 爱, 你]

Encoder(编码器)

  • 通常是 LSTM 或 GRU
  • 逐步读入输入序列,将全部信息压缩为一个固定维度的隐藏状态向量(Context Vector)
  • c=hT(编码器最后一个时间步的隐藏状态)

Context Vector(上下文向量)

  • 信息瓶颈:整个输入序列的语义被压缩进一个向量
  • 维度通常是 [1,batch,hidden_dim]

Decoder(解码器)

  • 以 Context Vector 作为初始隐藏状态
  • 第一个输入是特殊标记 <BOS>(句子开始)
  • 每步预测一个词,将上一步预测结果作为下一步输入
  • 直到生成 <EOS>(句子结束)标记

3. 翻译示例(逐步展开)

输入:I love you → 输出:我爱你

编码阶段:
  t=1: 读入 "I"    → h₁
  t=2: 读入 "love" → h₂
  t=3: 读入 "you"  → h₃ = Context Vector c

解码阶段(初始状态 = c):
  t=1: 输入 <BOS>  → 预测 "我"
  t=2: 输入 "我"   → 预测 "爱"
  t=3: 输入 "爱"   → 预测 "你"
  t=4: 输入 "你"   → 预测 <EOS>  → 停止

4. PyTorch 实现

编码器

python
import torch
import torch.nn as nn

class Encoder(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, n_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, n_layers,
                           batch_first=True, dropout=dropout)
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, src):
        # src: [batch, src_len]
        embedded = self.dropout(self.embedding(src))   # [batch, src_len, embed]
        outputs, (hidden, cell) = self.lstm(embedded)
        # hidden: [n_layers, batch, hidden_dim] → 作为 Decoder 的初始状态
        return outputs, hidden, cell

解码器

python
class Decoder(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, n_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, n_layers,
                           batch_first=True, dropout=dropout)
        self.fc = nn.Linear(hidden_dim, vocab_size)
        self.dropout = nn.Dropout(dropout)
    
    def forward(self, tgt_token, hidden, cell):
        # tgt_token: [batch](单个时间步的词索引)
        tgt_token = tgt_token.unsqueeze(1)             # [batch, 1]
        embedded = self.dropout(self.embedding(tgt_token))  # [batch, 1, embed]
        
        output, (hidden, cell) = self.lstm(embedded, (hidden, cell))
        # output: [batch, 1, hidden]
        
        pred = self.fc(output.squeeze(1))  # [batch, vocab_size]
        return pred, hidden, cell

完整 Seq2Seq 模型

python
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, tgt, teacher_forcing_ratio=0.5):
        """
        src: [batch, src_len]
        tgt: [batch, tgt_len]
        teacher_forcing_ratio: 训练时使用真实标签的概率(防止误差累积)
        """
        batch_size = src.shape[0]
        tgt_len = tgt.shape[1]
        tgt_vocab_size = self.decoder.fc.out_features
        
        # 存储解码器输出
        outputs = torch.zeros(batch_size, tgt_len, tgt_vocab_size).to(self.device)
        
        # 编码
        enc_outputs, hidden, cell = self.encoder(src)
        
        # 解码(逐步生成)
        # 第一个输入是 <BOS>
        dec_input = tgt[:, 0]  # [batch]
        
        for t in range(1, tgt_len):
            pred, hidden, cell = self.decoder(dec_input, hidden, cell)
            outputs[:, t] = pred
            
            # Teacher Forcing:随机决定下一步用真实词还是预测词
            use_teacher_force = torch.rand(1).item() < teacher_forcing_ratio
            if use_teacher_force:
                dec_input = tgt[:, t]           # 真实词(训练时加速收敛)
            else:
                dec_input = pred.argmax(dim=1)  # 模型预测词
        
        return outputs

# 初始化
SRC_VOCAB = 10000
TGT_VOCAB = 8000
HIDDEN_DIM = 256
EMBED_DIM = 128
N_LAYERS = 2

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

encoder = Encoder(SRC_VOCAB, EMBED_DIM, HIDDEN_DIM, N_LAYERS, dropout=0.3)
decoder = Decoder(TGT_VOCAB, EMBED_DIM, HIDDEN_DIM, N_LAYERS, dropout=0.3)
model = Seq2Seq(encoder, decoder, device).to(device)

print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")

5. Teacher Forcing(教师强制)

训练时,Decoder 应该用什么作为下一步的输入?

策略做法优缺点
纯自回归始终用上一步预测词推理一致,但错误会累积(曝光偏差)
纯教师强制始终用真实标签词收敛快,但训练/测试不一致
混合(推荐)按概率随机切换平衡收敛速度和泛化能力

6. Beam Search 解码

推理时,Decoder 不再贪心地选最高概率词,而是保留 k 个候选路径:

python
def beam_search(decoder, init_hidden, init_cell, bos_idx, eos_idx, 
                beam_width=5, max_len=50):
    """
    beam_width: 同时维护的候选序列数
    """
    # 初始化:beam 内容、对数概率、完成状态
    beams = [([bos_idx], 0.0, init_hidden, init_cell)]
    completed = []
    
    for _ in range(max_len):
        new_beams = []
        for sequence, score, hidden, cell in beams:
            last_token = torch.tensor([sequence[-1]])
            
            pred, new_hidden, new_cell = decoder(last_token, hidden, cell)
            log_probs = torch.log_softmax(pred, dim=-1).squeeze(0)
            
            # 取 top-k 候选
            topk_probs, topk_ids = log_probs.topk(beam_width)
            
            for prob, token_id in zip(topk_probs, topk_ids):
                new_seq = sequence + [token_id.item()]
                new_score = score + prob.item()
                
                if token_id.item() == eos_idx:
                    completed.append((new_seq, new_score))
                else:
                    new_beams.append((new_seq, new_score, new_hidden, new_cell))
        
        # 按分数排序,保留 top-k
        beams = sorted(new_beams, key=lambda x: x[1], reverse=True)[:beam_width]
        
        if not beams:
            break
    
    # 返回最高分序列
    all_candidates = completed + [(b[0], b[1]) for b in beams]
    return max(all_candidates, key=lambda x: x[1])[0]

7. Seq2Seq 的核心局限:信息瓶颈

长输入序列(100个词)
    ↓ Encoder
固定维度向量 c(256维)← 所有100个词的信息被压缩进来
    ↓ Decoder
生成输出序列

问题:句子越长,信息损失越严重!

输入长度性能
短句(<20词)翻译质量较好
长句(>50词)质量明显下降,信息丢失严重

解决方案:Attention 机制(2015年)

Decoder 在生成每个词时,不仅使用 Context Vector,还可以动态查询 Encoder 的所有隐藏状态,选择性地关注输入的不同部分。

8. 技术演进路线

2014  Seq2Seq(纯 LSTM 编码-解码)
  问题:信息瓶颈,长句丢失信息

2015  Seq2Seq + Attention(Bahdanau)
  解决:Decoder 能动态关注输入各位置

2017  Transformer(Attention Is All You Need)
  革命:完全用 Attention 替代 RNN,全并行

2018+ BERT / GPT / T5 等预训练大模型
  范式:大规模预训练 + 下游任务微调

总结

特性Seq2Seq
提出时间2014(Sutskever,Google)
核心架构Encoder(LSTM)+ Context Vector + Decoder(LSTM)
输入/输出变长序列 → 变长序列
核心局限信息瓶颈,长句性能差
后续改进Attention 机制 → Transformer
历史地位现代 NLP 编码-解码框架的鼻祖

AI 知识体系 — 从机器学习到大语言模型