资讯动态

Seq2Seq与Attention机制:从RNN到Transformer的序列生成核心架构

发布时间:2026/10/13 19:11:55 来源:尧图企业网站定制
老读者都知道这个系列前面两篇把词向量和循环神经网络的基本功打了一遍今天这篇终于要讲到真正的重头戏Seq2Seq模型也就是Encoder-Decoder架构外加一个绕不开的Attention机制。这三样东西是神经机器翻译、文本摘要、对话系统、语音识别等一堆场景的共同底座甚至可以说现在大模型里天天讲的Transformer它的核心思路也是从这条线演化出来的。这篇面向的是已经熟悉RNN/LSTM基本用法、知道词嵌入是什么、但还没系统搭过序列生成模型的同学。文章不会堆公式吓人重点放在直觉理解、结构拆解、训练和推断的实操区别以及我这些年踩过的坑。看完你会发现Seq2Seq并没有想象中那么神秘它不过是在解决一个很朴素的问题把一句话变成另一句话而这句话的两端长度还经常不一样。1. 先搞清楚Seq2Seq到底在解决什么问题1.1 为什么普通神经网络搞不定序列映射假设你手头有一个文本翻译任务输入是“我爱自然语言处理”输出是“I love natural language processing”。稍微观察一下就能发现输入有7个词输出有5个词词与词之间不是一一对应关系。用普通的全连接网络或者CNN做这种映射第一关就卡死了——网络结构要求输入的维度是固定的你没法让它处理任意长度的句子。前面系列里讲的RNN确实能处理变长输入但RNN擅长的是“看完一整句话之后输出一个结果”比如情感分类、文本分类这类任务本质上是一个句子映射到一个标签。可翻译、摘要、对话这种任务输入是一句话输出是另一句话而且输出序列的长度还得自己决定这就不是普通RNN能干的了。Seq2Seq这个名字本身就说明了它的定位它是一个序列到序列sequence to sequence的映射框架。输入是序列输出也是序列两者长度不必相同内部也不需要对齐。1.2 生活化类比帮你建立直觉我经常跟刚入门的朋友打这个比方Seq2Seq就像你请了一个同声传译。第一个阶段是“听”你把整段话听完在脑子里形成一个完整的意思这个过程就是Encoder编码器。第二个阶段是“说”你根据脑子里形成的那个完整意思一个字一个字地往外蹦译文每次蹦出来的词又会影响接下来怎么说这个过程就是Decoder解码器。但这里有个关键细节同声传译听完一整段话之后脑子里不是记住原文的每个词而是记住核心语义这个“核心语义”在模型里就是一个向量我们管它叫上下文向量context vector。Encoder的工作就是把输入序列的信息压缩成这个向量Decoder再从这个向量出发逐个生成输出词。1.3 Encoder-Decoder架构的标准结构标准Seq2Seq由两个RNN组成。Encoder是一个RNN按顺序读入输入序列的每个词每一步输出一个隐状态最后把最后一个隐状态作为整个句子的语义表示。Decoder是另一个RNN它的初始隐状态来自Encoder的最后一个隐状态然后它每一步预测输出序列的下一个词。这里有个特别容易误解的地方Encoder的最后一个隐状态和Decoder的初始隐状态到底是不是同一个东西从工程实现上看常见做法是直接把Encoder最后一个隐状态赋值给Decoder作为初始隐状态但在注意力机制出现之后这个赋值就不是唯一的信息传递方式了后面会详细讲。用公式看就三个步骤Encoder逐词读入更新隐状态把最终隐状态作为上下文向量传给DecoderDecoder自回归地生成输出词每一步把上一步生成的词作为当前输入。2. Encoder的细节比你想象的要多2.1 输入表示从词到向量的第一步Encoder的输入不是直接喂词而是把每个词转成词向量。这个环节的做法在系列第一篇已经讲过但这里要强调一个工程细节在Seq2Seq场景里词嵌入矩阵通常需要特殊处理。比如你的词表里要预留几个特殊token最常见的是BOSbegin of sentence句子开头、EOSend of sentence句子结束、PAD填充、UNK未登录词。EOS这个token极其重要它是Decoder判断“何时停止生成”的信号。没有它Decoder就不会知道该在哪个词停下来。PAD是为了让一个batch里的句子长度对齐因为GPU并行计算要求一个batch内所有序列长度一致短的句子就要用PAD填充。UNK则是兜底用的输入里出现词表外的词统一映射成它。2.2 为什么选择LSTM/GRU而不是朴素RNN理论上Encoder可以用任何RNN单元但实践中几乎不用朴素RNN。原因在于梯度消失问题朴素RNN在长序列上反向传播时梯度会连乘地衰减导致前面的词对最终隐状态的贡献趋近于零。LSTM通过门控机制解决了这个问题GRU是LSTM的简化版效果接近但参数更少。我做项目时倾向于用GRU作默认选择理由很实际参数少意味着训练更快在相同数据量下更不容易过拟合而且效果跟LSTM基本持平。如果你的任务是大规模机器翻译数据量充足可以试试LSTM甚至多层LSTM小规模任务用GRU就够了。2.3 两层还是单向聊聊多层和双向编码在标准Seq2Seq基础上有两个常见的增强手段。第一个是堆多层两层LSTM/GRU作为Encoder底层捕捉局部的词法和短语信息高层捕捉更抽象的语义信息效果通常比单层好但参数量翻倍训练难度也会变大。第二个是双向即一个正向RNN从前往后读一个反向RNN从后往前读每个位置的最终隐状态由两个方向的隐状态拼接而成。双向Encoder的意义在于让每个词在编码时能同时看到它左边和右边的上下文。单向RNN在某个位置的隐状态只能看到已经读过的词后面的词还没读到信息是不完整的。我见过不少人在情感分类任务中体会到双向的巨大提升但在Seq2Seq里双向编码对低资源语种的效果提升尤其明显因为模型能获得更充分的上下文信息。需要注意一个坑如果Encoder是双向的Decoder的初始隐状态维度要跟Encoder输出的维度匹配。最简单的方式是只取其中一个方向的最终隐状态作为Decoder初始状态或者把两个方向的隐状态拼接起来后再经过一个线性层压缩到Decoder的维度。2.4 上下文向量的“信息瓶颈”问题早期的Seq2Seq把Encoder最后一个隐状态当作整个句子的唯一语义表示这里有个致命缺陷。句子越长最后一个隐状态需要承载的信息就越多而这个向量的维度是固定的不可能无限增加。信息瓶颈information bottleneck说的就是这个现象长句子的翻译质量明显下降因为前面的词经过多步计算后信息已经被冲淡了。我实测过一个中英翻译的对比句子长度在15个词以内标准Seq2Seq表现还算正常一旦超过20个词翻译质量断崖式下跌很多关键信息被漏掉或错译。这不是你调参能解决的问题是架构本身的结构性缺陷所以业界才需要引入Attention机制后面会重点展开。3. Decoder训练和推断是两种完全不同的玩法3.1 Decoder的结构和工作原理Decoder本身也是一个RNN它在每一个时间步做的事情是接收上一个时间步生成词的词向量结合当前的隐状态计算一个输出概率分布然后从分布中采样或取最大值作为当前步的预测词。展开说第t步的输入是第t-1步的预测词训练时是真实词Decoder的隐状态进行更新然后通过一个全连接层加softmax把隐状态映射到词表大小的概率分布上。这个分布中概率最大的词就是模型认为最合适的输出词。Decoder的初始隐状态来自Encoder的最终状态初始输入通常是BOS告诉模型“该开始生成了”。这里有个细节Decoder每一步输出的词不仅仅依赖当前隐状态从直觉上它应该“想起”之前已经输出的内容但这个记忆完全靠隐状态内部维护模型内部会逐步历经整个输出历史RNN的隐状态承担的就是这个职责。3.2 Teacher Forcing训练时的重要手段训练Decoder时有个经典问题如果某一步预测错了那这个错误词会作为下一步的输入错误就会像滚雪球一样越滚越大。Teacher forcing教师强制策略则把这个隐患直接绕开了训练时不管模型上一步预测的是什么我们都强制把真实的词喂给模型作为下一步输入。Teacher forcing的好处是训练稳定、收敛快模型能学到正确的条件分布。但它带来一个训练和推断的不一致推断时没有真实词可用只能把模型自己预测的词喂回去这被称为暴露偏差exposure bias。解决办法之一是计划采样scheduled sampling训练初期多用真实词训练后期逐渐增加预测词的占比让模型逐步适应推断时的环境。这个训练和推断的解码差异是初学Seq2Seq最容易懵的地方。Debug的时候训练loss看起来很漂亮一到测试就表现拉胯十有八九是这个差异没处理好。3.3 推断时的贪心解码与Beam Search推断时最朴素的做法是贪心解码每一步都取当前概率最大的词作为输出然后喂回Decoder直到生成EOS或者达到最大长度。贪心的问题很明显——容易陷入局部最优。比如翻译“I love NLP”第一步如果选了概率最高的“我”但可能“我爱”才是整体更优的开头贪心策略永远没机会修正这个选择。Beam Search集束搜索就是为了解决这个问题每一步不是只保留一个候选而是保留概率最高的K个候选序列最终选取总分最高的序列作为输出。我用beam search时常用的K值是3到5K越大搜索空间越大效果未必成正比但速度会明显下降。还要注意处理长序列的惩罚因为beam search倾向于给长序列更高的总分——每一步的概率都是小于1的词越多累乘起来越容易变小所以如果不加长度惩罚模型会倾向于输出过短的句子。标准做法是生成结束后按序列长度做归一化或者采用长度惩罚项。3.4 最大生成长度怎么设Decoder必须有个停止条件不然会一直生成下去。常见做法是设一个最大生成长度通常是输入序列长度的固定倍数比如1.5倍或2倍或者设一个绝对上限。我习惯的做法是max_len len(src) * 1.5 10翻译任务的效果还不错。但你要清楚这是个经验值不同任务差异很大摘要任务通常要长一些翻译任务短一些具体任务需要跑实验确定。4. Attention机制给Decoder一双会看的眼睛4.1 从信息瓶颈到注意力模型是怎么“想明白”的前文提到标准Seq2Seq的唯一信息通道就是Encoder的最后一个隐状态这个瓶颈让模型在长句子上表现很差。Attention机制的出发点非常朴素Decoder在生成每一个词的时候不应当只依赖一个被压缩过的向量而应当有权“回看”输入序列的每一个位置重点关注与当前生成词最相关的部分。用人话解释就是当你在翻译“I love NLP”中的“NLP”时你的注意力应该集中在源句“自然语言处理”的这几个词上而不是试图回忆整句话的所有细节。Attention机制做的事就是让Decoder每走一步都去计算一下源句各个位置的关联程度然后加权汇总一份“注意力向量”来辅助当前词的预测。4.2 注意力权重的数学直觉注意力的核心是计算一个权重分布。给定Decoder当前的隐状态以及Encoder所有位置的隐状态我们需要计算它们之间的相关性分数。最常用的打分函数有两种。加性注意力Bahdanau风格用一个小型全连接网络计算相关性分数 可学习参数矩阵作用在拼接向量上。乘性注意力Luong风格直接用点积计算相关性分数 decoder隐状态和encoder隐状态的点积。点积的直觉很好理解两个向量方向越一致点积越大就代表它们越相关。加性注意力表达能力更强一点但计算稍复杂。得票率最高的实际是点积方案因为Transformer里用的是缩放点积注意力而且矩阵运算在GPU上非常高效。拿到分数之后用softmax把它们归一化成权重所有权重加起来等于1。然后用这些权重对Encoder的隐状态做加权求和得到注意力上下文向量。这个上下文向量会跟Decoder当前隐状态拼接起来共同决定当前步的输出词概率分布。4.3 全局和局部Attention两种视角各有各的用武之地Luong等人把Attention分成两种视角。全局Attentionglobal attention让Decoder每一步都能看到源句的所有位置这种方案实现简单、效果稳定也是我日常使用的默认选项。局部Attentionlocal attention则先预测一个源句的定位点只关注这个定位点附近窗口内的词。局部Attention的动机是计算效率——源句很长时每一步都要对所有源词做加权求和成本很高。但实践中我发现对大多数中小规模任务全局Attention的复杂度完全可接受直接用它就好。只有面对超长文本比如篇章级的摘要生成时局部Attention才有明显的性能收益但它的实现和调参复杂性也上去了。4.4 MaskAttention里最容易漏掉的细节Attention计算有个伴随问题如果某个输入位置是PAD填充出来的那它不应该参与注意力加权否则模型会把注意力浪费在无意义的填充符号上。解决方法是加注意力mask在softmax之前把填充位置的分数设置为一个很大的负数比如-1e9这样softmax之后这些位置的权重就趋近于零。这一步必须做否则就是你训练时loss不下降、或推理时生成乱七八糟字符的很大原因。4.5 Attention可视化证明模型学到了什么Attention权重有个特别直观的用法可视化。把源句和目标句的词对矩阵画出来格子颜色越深代表注意力越强。好的对齐结果会呈现明显的对角线分布比如英文词“cat”对应的注意力集中在源句中“猫”的位置。我第一次画出这种对角线可视化图的时候心里最直观的感受就是模型真的“听懂了”。它知道该去关注源句的哪些位置即使没有人教过它对齐关系。这也是排查模型问题的一个抓手——如果可视化图是一团毛线球没有对角线规律多半是模型没训练好或者数据有脏东西。5. 从理论到落地训练配置和工程经验5.1 词表和超参数选择的实操建议先定词表大小。中英翻译场景源语言和目标语言各建一个词表是标准做法。词表大小取决于你的数据量数据少就设置到1万到2万数据多可以到3万到5万。词表开得太大一方面占显存另一方面训练到生僻词的时间很长词表开得太小UNK出现率过高翻译质量会和完整性都会受影响。隐藏层维度方面我这个经验值作为参考小规模任务128或256中等规模任务512大规模任务可以到1024。隐状态维度不一定非要很大更大维度带来的收益会随着模型容量接近数据量上限而递减。我见过有人一上来就把隐层开到1024结果小数据量的时候表现反而不如256的版本原因就是过拟合。Batch size也一样常用的经验是32到64。Batch size过大会导致GPU显存爆掉过小会导致梯度噪声大、收敛不稳。如果显存紧张可以考虑梯度累积gradient accumulation用多个小batch的梯度叠加模拟大batch的效果。5.2 训练策略teacher forcing比例和学习率训练前期建议全程Teacher forcing让模型快速走上正轨。训练中后期可以尝试计划采样按一定比例把模型自己的预测词喂回去。但这里要提醒一句计划采样的比例要设置得很小心比例太高会导致训练不稳定模型反复横跳。我的建议是最多让模型预测词的比例占到20%或30%仍然以真实词为主。学习率方面SGD配合学习率衰减是经典做法Transformer时代开始普遍改用Adam。对Seq2Seq来说Adam比较省心初始学习率设在0.001左右基本不会出大问题。但Adam不代表你可以不关注学习率调度我通常会配合一个热身warmup加衰减的调度策略前几千步线性升到最大值之后按步数逐渐衰减。5.3 损失函数和评估指标的细节训练用的是交叉熵损失cross entropy loss这个没什么争议。但评估指标需要多唠叨两句机器翻译最常用的自动评估指标是BLEUBilingual Evaluation Understudy它通过比较模型输出和参考译文的n-gram重合度来打分范围0到100分数越高越好。BLEU的局限很明显它只看词或短语级的重合不看语义是否真正等价所以它跟人工评估的相关性并不是百分之百。我的经验是BLEU作为模型开发和调参阶段的辅助指标完全够用但真正上线前一定要做一定量的人工评估尤其是那些BLEU分数高但语义别扭的case只有人工能看出来。5.4 工程实现时的显存和速度优化训练Seq2Seq最烦的问题之一就是显存不够。除了减小batch size还有两个常用技巧。一是按长度动态batch把句子长度相近的样本放在一个batch里最短的句子的填充量最少能显著节省显存和计算量。二是梯度累积不更新参数地连续跑几个小batch把梯度加在一起再更新一次相当于变相扩大了batch size。推理阶段的速度优化则相反如果你的核心诉求是快可以考虑动态规划缓存来复用重复计算或者考虑用更轻量的Decoder结构。注意提升速度不要损失太多质量具体取舍要靠业务场景去权衡。6. 常见问题与排查技巧实录6.1 问题速查表这里整理几个我在实战中反复遇到的典型问题和排查路径希望帮你省去一些弯路。问题表现可能原因排查方向训练loss不下降学习率过大或过小检查学习率给模型一个lr-scheduler做预热再看loss曲线训练loss下降但测试效果差Teacher forcing导致暴露偏差尝试计划采样检查训练/测试数据分布是否一致生成结果全是UNK词表太小或未做子词切分扩大词表尝试BPE/WordPiece子词方案长句子效果断崖式下跌序列太长导致信息瓶颈引入/加强Attention机制检查是否遗漏mask生成的句子重复冗长推断未设置长度惩罚使用beam search并带长度惩罚项注意力可视化一团糟训练不充分或数据噪声过大继续训练检查平行语料的对齐质量GPU显存爆掉batch size过大或填充过多动态长度batch 梯度累积解码时在EOS后还继续输出停止条件没设置好检查推断代码是否在输出EOS后立即终止6.2 中文分词和子词切分的坑中英机器翻译跟英法、英德这种印欧语系之间的翻译有个本质差异中文没有天然空格分词而英文有。所以中文端你得先分词而分词器选得好不好直接影响翻译质量的下限。现在更主流的做法是直接在字符级或子词级建模绕过分词这层。BPEByte Pair Encoding是常见选择通过迭代地合并出现频率最高的相邻符号组合来获得子词好处是词表大小可控还能处理未登录词因为生僻词可以被拆成几个高频子词拼起来。我在自己项目里做过对比同样一份中英平行语料用BPE子词方案比用固定词典加jieba分词的方案BLEU高了差不多3到5个点而且生僻词的翻译成功率高了很多。强烈建议你在新建项目时直接采用子词方案。6.3 Tensor shape不对应该怎么debug写代码时最常遇到的非技术Bug是Tensor维度不匹配。这个问题的Debugg技巧其实很朴素不要用直觉猜直接在关键节点打印shape。我自己的debug顺序是看Input batch的shape - 看Encoder输出的隐状态shape - 看Attention分数矩阵的shape - 看Decoder输入的shape - 看最终logits的shape。哪一步跟预期不一致就查对应的代码。大部分维度问题出在attention分数的维度上Encoder隐状态是[batch, seq_len, hid_dim]Decoder当前隐状态是[batch, hid_dim]点积先要调整维度再算分数然后mask再softmax最后加权求和。6.4 实战经验如何调试一个不工作的Seq2Seq如果你照着文章搭了一个模型但结果很烂我建议你的排查不要沉迷于看训练曲线而是先抓出实际的生成结果。挑几条有代表性的短句子打印出模型的输出、参考翻译和注意力可视化图对比看看差在哪。如果输出完全没有语义关联检查是不是向量维度出问题或者目标端词表索引错位了。如果输出和参考翻译很像但局部不对大概率是某个词的对齐有问题可视化注意力能帮你定位。如果输出不断重复大概率是训练不充分或者beam search的惩罚项没加对。总而言之调试Seq2Seq的核心不是盯着代码死磕而是把模型“吐”出来的东西作为最重要的观察窗口。模型不会说话但你给它一句话它吐出来的那串词就是最有信息量的反馈。7. 从Seq2Seq到Transformer这条脉络值得你理清学到这里你已经掌握了序列到序列的经典架构。回头再看Transformer你会发现它并没有凭空长出来而是在解决Seq2Seq遗留问题的过程中自然演化出来的。Positional Encoding承担了原来RNN按时间步展开的位置信息Self-Attention让每个位置都能直接看到所有位置的信息不再依赖隐状态一步步传递Multi-Head Attention则让模型能在不同子空间里关注不同侧面的相关性。可以说Seq2SeqAttention的这套思维框架就是理解Transformer的一把钥匙。你今天学到模块化的Encoder/Decoder思路、注意力权重的计算方式、Mask的必要性放到Transformer里全都用得上。这也是为什么我强烈不建议初学者绕过Seq2Seq直接猛啃Transformer——Transformer是一套更精密的注意力机器但你要先理解注意力到底在解决什么问题才能真正理解它为什么这么设计。我个人在这些年做过的翻译、摘要、对话项目里反复验证了一件事很多被当成灵丹妙药的复杂改进本质都是在修补当前架构的一个具体缺陷。只有当你亲手搭过一个最朴素的Seq2Seq亲手调过Attention的权重亲手看过那些对角线可视化图之后你才真正有了判断各种新模型“为什么有效”“适合什么场景”的直觉。这种直觉是读多少篇论文都换不来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑