尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解Transformer Decoder中的Exposure Bias:成因、影响与缓解策略

深入理解Transformer Decoder中的Exposure Bias:成因、影响与缓解策略 1. 先搞明白一件事Decoder是在“逐字造句”不是在“填词”很多朋友一上来就盯着Transformer的架构图看注意力全放在那个多头注意力模块上结果看完了还是说不清Decoder和Encoder到底差在哪。其实Decoder的工作原理用一个场景就能讲透它本质上是一个“逐字造句”的过程。想象一下你在做英语翻译拿到一句中文“今天天气很好”你嘴上说英文的时候是一口气把整句说完的吗不是。你会先说出“The weather”然后根据已经说出来的这几个词决定下一个词是“is”还是“was”还是别的什么。Decoder的工作方式就是这样的自回归链条每一步的输出都会作为下一步的输入一个token接着一个token地往下续。这个机制对比Encoder是决定性的差异。Encoder是“读全文”它拿到整个输入序列之后每个位置都能看到上下文的所有信息所以Self-Attention不需要掩盖未来位置。而Decoder是“写文章”它在写第5个词的时候理论上就不应该看到第6个词是什么——因为在真实推理场景里第6个词还不存在它是由第5个词生成之后才可能出现的。如果训练时让Decoder偷看了未来那学出来的模型在推理时就会“失灵”因为它推理时根本拿不到未来的信息。这就是为什么Decoder的Self-Attention要做Mask也就是所谓的Masked Self-Attention。它把注意力矩阵的上三角部分全部置为负无穷经过Softmax之后这些位置的注意力权重就趋近于零。换句话说第i个位置只能 attend 到第1到第i个位置不能往后看。这个Mask不是可学习的参数而是硬编码的、固定的操作每个batch、每条样本都一样。我在看代码的时候发现很多初学者会忽略一个细节Masked Self-Attention做Mask的时候到底Mask的是“key的位置”还是“query的位置”答案是要Mask key的位置。具体来说Attention的计算是Q乘以K的转置得到一个形状为[batch_size, num_heads, seq_len, seq_len]的分数矩阵然后在这个矩阵上用torch.triu生成一个上三角为1的掩码矩阵把对应位置替换成负无穷。这里的行索引是query的位置列索引是key的位置所以masked_fill操作实际上是在阻止“当前query看到未来的key”。这个细节如果你自己手写一个Transformer一定会踩到我后面会专门提。推理阶段的Decoder是一个纯粹的循环你输入一个起始符比如bos模型输出第一个词的分布你从分布里采样或者取argmax得到第一个词然后把这个词拼接回输入继续跑第二次前向得到第二个词……一直到模型输出eos结束符或者达到最大长度阈值。这个过程无法并行因为每一步都依赖上一步的输出。这也是为什么Transformer推理速度比训练慢得多的根本原因——训练时你可以并行推理时必须串行。理解了这一点接下来的问题就顺理成章了既然推理是串行的那训练难道也要一个词一个词地慢慢跑答案当然不是。训练时我们需要用到一种叫Teacher Forcing的技术让它能够并行计算而这恰恰是后面exposure bias问题的源头。2. Transformer训练的本质让模型学会“条件概率分布”2.1 训练目标的数学直觉极大似然估计在做什么Transformer的训练目标不是让模型“记住”某个正确答案而是让模型学习一个条件概率分布——给定输入序列和已经生成的前缀下一个token的概率分布是什么。用数学语言说就是最大化整个输出序列的联合概率p(y1, y2, ..., yT | x) p(y1 | x) × p(y2 | x, y1) × p(y3 | x, y1, y2) × ... × p(yT | x, y1, ..., yT-1)这个公式看起来有点吓人但展开之后就很好理解整个句子的概率等于“一个词一个词地预测”的概率乘积。训练的目标是让这个联合概率最大化也就是极大似然估计。这个目标函数在训练时怎么做答案是通过Teacher Forcing。2.2 Teacher Forcing为什么训练能“一口气”并行生成整句话Teacher Forcing的核心思想非常直接训练的时候不管模型上一步预测得对不对我们都把真实的token喂给它作为下一步的输入。举个例子目标句子是“The weather is good today”训练时无论模型在“The”之后预测的是不是“weather”我们在预测下一个词的时候都强行把真实的“weather”喂进去。这样做有两个巨大的好处。第一训练可以并行因为所有的输入token都是已知的真值不需要等待上一步的输出所以我们可以把整个目标序列从左到右一次性塞进Decoder配合Masked Self-Attention一个batch内同时计算出所有位置的预测结果。第二训练收敛快模型始终在“标准的语境”下学习不会被自己早期的随机错误带偏梯度的信噪比更高。具体到代码层面训练时的做法是把源语言句子经过Encoder编码后得到memory然后把目标语言句子整体前面拼一个bos后面去掉最后一个token作为Decoder的输入。Decoder的输出和真实目标序列计算交叉熵损失同时用Padding Mask把pad位置的损失屏蔽掉。用伪代码表示就是# 训练阶段 encoder_out encoder(src_tokens, src_mask) # [batch, src_len, d_model] decoder_input target_tokens[:, :-1] # 去掉最后一个token前面拼bos decoder_out decoder(decoder_input, encoder_out, tgt_mask, src_mask) # decoder_out 的形状 [batch, tgt_len-1, vocab_size] loss cross_entropy(decoder_out.reshape(-1, vocab_size), target_tokens[:, 1:].reshape(-1))看到没这里decoder_input用的是目标序列的真实token而不是模型自己上一步预测出来的token。这就是Teacher Forcing的“教师”二字所在每一步都有老师告诉你正确答案你只需要在这个正确答案的基础上继续练习下一步。2.3 Cross-Entropy Loss的细节并不是只算最后一个token的loss很多入门教程会把损失函数轻描淡写地带过实际实现的时候细节还挺多的。Transformer的损失是在每个位置都计算交叉熵然后对非Padding位置求平均。也就是说解码器在每个位置都输出一个词表大小的logits向量和真实的下一个token计算交叉熵。这里有个容易忽略的点Decoder的输入和输出之间有一个“错位”。我们通常把目标句子整体作为输入然后把目标句子右移一位或者等价地输出端对齐来作为预测目标。比如目标句是“The weather is good today”Decoder输入是“bosThe weather is good”输出端的期望是“The weather is good today”这样每个位置的预测目标恰好就是该位置的“下一个词”。还有一个常见的问题是Padding位置的loss处理。由于batch内句子长度不同短句子的尾部会补pad而这些pad位置不应该计算loss否则模型会学到“预测pad”这种无意义的模式。通常做法是生成一个tgt_mask把所有pad位置标记为False然后对loss做masked average。另一个细节是label smoothing。如果词表很大、数据量有限模型很容易对训练集中的某些正确token给出过高的置信度导致过拟合和泛化能力下降。label smoothing的做法是把one-hot标签变成软标签比如词表大小为32000时把正确token的概率从1.0减小到0.9剩下的0.1均分给其他所有token。这个技巧虽然会让训练时的loss略高但实测能明显提升BLEU分数我自己的实验里通常能涨0.5到1个点。2.4 为什么训练要用Masked Self-AttentionEncoder那边却不用这里再澄清一个容易混淆的点。很多人问Decoder内部的第一层Self-Attention要做Mask但Decoder和Encoder交互的那层Cross-Attention为什么不用Mask原因很简单Cross-Attention的Query来自Decoder而Key和Value来自Encoder的完整输出。Encoder已经读完了整个源句子Decoder在生成第i个词的时候本来就应该看到源句子的全部信息——源句子对Decoder来说是“已经存在的事实”不存在“偷看未来”的问题。所以Cross-Attention不需要Mask只有Decoder内部的Self-Attention需要Mask因为那是“约束模型只能依据已生成的前缀做预测”。这个区分非常重要我在面试候选人的时候经常问这个问题能答清楚的人基本说明他真的动手写过Transformer而不是只看过架构图。3. Exposure Bias到底是什么训练时“开卷考试”推理时“闭卷考试”3.1 问题根源输入分布偏移现在我们来到这篇文章的核心问题exposure bias。我见过很多技术文章把exposure bias解释得云山雾罩其实它一句话就能说清楚训练时模型吃的输入是真实token推理时模型吃的输入是自己的预测token这中间存在分布偏移就叫exposure bias。你可以把它理解成一个学生的日常训练和真实考试。平时训练的时候每一道题做完老师都会立刻把标准答案告诉他让他基于标准答案做下一道题。这位学生每次都拿到正确答案做起题来顺风顺水。但到了考试那天没有人告诉他正确答案了他只能把自己上一道题的答案作为下一步推理的依据。如果第一道题就做错了后面的推理全都会建立在这个错误答案之上越错越离谱。这就是exposure bias的本质——训练环境和推理环境的不一致。为什么这个看似简单的错位影响这么大因为Decoder是一个递归结构每一步的输入都依赖前一步的输出。如果某一步预测错了这个错误的token会成为后续所有步骤的输入导致模型进入一个“训练时从未见过的状态”。模型在训练时从没见过“错误的上下文”但推理时恰恰要处理大量这样的错误上下文于是模型的预测质量会迅速恶化。3.2 累积误差为什么是指数级的要真正理解exposure bias为什么可怕光知道“误差会累积”还不够你得理解累积的方式。假设模型每一步的预测准确率都是p比如95%那么在长度为T的序列中整句完全生成正确的概率是p的T次方。当T10时0.95的10次方约等于0.6当T50时0.95的50次方约等于0.077。也就是说即使每一步的准确率高达95%生成长度为50的句子时整句完全正确的概率还不到8%。这个数学事实解释了三个现象。第一句子越长生成质量越差这是所有自回归生成模型共有的“长句诅咒”。第二exposure bias的影响在长句任务如文本摘要、长对话生成中格外致命但在短句任务如短文本翻译中不那么明显。第三单步准确率的小幅提升经过序列长度的指数放大最终效果可能非常显著——这也是为什么很多人在优化模型时死磕下一个token预测的准确率。3.3 一个具体的例子错误是如何滚雪球的为了让你直观地感受这个过程我们拿一个极简场景举例。假设模型在生成“I love this movie”这句话目标输出是这5个词。训练时的轨迹是这样的输入bos预测“I”对了输入真值“I”预测“love”对了输入真值“love”预测“this”对了输入真值“this”预测“movie”对了。推理时的轨迹输入bos预测分布中“I”概率最高取到了“I”输入“I”预测“love”但这一次分布没那么集中“like”和“love”的概率差不多模型选了“like”下一步输入变成了“I like”模型需要在“I like”这个从来没见过的上下文条件下预测下一个词而训练时它只见过“I love”开头的句子于是预测质量开始下降……这就是错误滚雪球的完整链路。训练时的每一步都“接得住”推理时一旦某一步偏了模型就跌进了一个分布外的区域在这个区域里它没有经过充分训练预测质量雪崩。需要特别指出的是exposure bias并不仅仅是贪心解码Greedy Decoding的问题。很多人以为用Beam Search就能解决实际上Beam Search只是缓解了“走错一步就没法回头”的问题——它保留了多个候选路径但整个搜索过程仍然是基于模型自己的预测来展开的如果模型在某个前缀条件下本身的分布就不好Beam Search只是在“矮子里面拔将军”。另外还有一个常见的误解exposure bias和“推理速度慢”是两回事。推理速度慢是因为自回归的串行特性即使没有任何预测错误它也会慢exposure bias是预测错误导致的分布偏移问题哪怕推理速度再快它也依然存在。这两个问题经常被放在一起讨论但它们的解决思路完全不同。3.4 为什么这个问题在机器翻译里特别突出机器翻译是Decoder自回归生成最典型的应用场景之一所以exposure bias在这个领域被研究得最多。一个有趣的现象是短句翻译比如句子长度在10个token以内很少能明显观察到exposure bias因为错误还没怎么累积就已经结束了但长句翻译比如30个token以上的复杂句就会非常明显——前半句可能还像模像样后半句常常开始重复、漏词甚至语法崩坏。这也是为什么很多机器翻译系统的BLEU分数在短句上虚高在长句上掉得很厉害。你看论文的时候如果一篇论文只在短句上报告BLEU那它的方法很可能没有真正解决长句生成的问题。反过来如果你在做长文本生成类任务摘要、故事生成、对话exposure bias就是你必须正面面对的问题。4. 缓解exposure bias的几条路线从Scheduled Sampling到序列级优化既然exposure bias的根源是训练和推理的输入分布不一致那最自然的思路就是训练的时候也让模型吃点自己预测出来的token让它逐渐适应推理时的状态。沿着这个思路学术界和工业界发展出了几类方案我按实用程度从高到低来讲。4.1 Scheduled Sampling最直接也最“便宜”的修补方案Scheduled Sampling是Bengio团队在2015年提出的方法思路非常简单训练过程中每一步以概率ε选择使用模型自己预测的token作为下一步输入以概率1-ε使用真实token。训练初期ε设得比较大比如0.1让模型大部分时间还是在真值环境下学习随着训练推进ε逐渐增大比如线性增加到0.3、0.5让模型逐步暴露在自己产生的错误中。这个方法的实现难度极低在原有训练代码上改动很少。关键点是即使使用模型预测的token后续位置的目标标签仍然是真实token也就是说我们只是改变了模型看到的输入分布并没有改变学习目标。这样一来反向传播链路依然能稳定地提供梯度。我在实际项目中用过Scheduled Sampling效果要分场景说。在文本摘要任务上它确实能带来ROUGE分数的稳定提升大概1到2个点但在机器翻译上提升就比较有限甚至有时候会掉BLEU。原因在于Scheduled Sampling引入的噪声在训练后期会干扰模型对“标准语境”的学习——模型既要学会在真值语境下预测准确又要学会在错误语境下纠偏这两者是有拉锯的。如果任务本身的单步预测难度高比如翻译这种拉锯会更明显。所以我对Scheduled Sampling的建议是可以试但要小心调整ε的调度曲线和上限。不要贪心把最大值控制在0.2到0.3比较安全别让它淹没了真值语境下的学习。4.2 在推理侧兜底Beam Search和Length Penalty虽然Beam Search不能根治exposure bias但它仍然是推理阶段最实用的“保险丝”。它的核心思想是每一步不只保留一个最佳候选而是保留top-k个候选k通常取4到8最后在所有候选中选择整体概率最高的序列。这样一来即使某一步的最佳预测错了第二、第三好的候选可能还走在正确的方向上不至于一错到底。在实际工程里我强烈建议你同时做两件事第一用长度惩罚Length Penalty调整候选得分防止模型倾向于生成短句——因为在概率连乘的框架下句子越长每个token的概率都要乘上去导致总分自然偏低模型会偏向提前终止。第二对eos的得分单独处理不要让模型太早输出结束符。还有一个比较反直觉的经验Beam Search的宽度并不是越大越好。宽度从1增到4效果提升很明显从4增到8提升就很小了再往大增加反而可能出现“质量下降”——因为宽度太大会让模型在早期保留一些明显不对劲的候选这些候选在后期即使概率不高也会占据beam位置挤掉真正有潜力的候选。我自己常用的配置是beam4或5配上长度惩罚系数α0.6到1.0。4.3 更彻底的方案大规模预训练、序列级损失和RLHFScheduled Sampling和Beam Search说到底都是“修补”不是“根治”。想要彻底解决exposure bias核心在于改变训练目标让模型在训练时就以“整个序列的质量”为优化目标而不是单纯以“单步分类准确率”为目标。大规模预训练是目前最有效的手段之一。GPT系列模型在海量文本上做自回归语言模型训练本质上就是在极大规模上不断让模型“看自己预测的结果”——因为预训练数据里就是真实的文本模型在预训练阶段接触到的上下文多样性极高对各种可能的前缀状态都有充分的覆盖这天然缓解了分布偏移问题。这也是为什么GPT这类模型在生成时显得比较“稳”不是它的exposure bias消失了而是它的训练数据覆盖面太大了模型见过的“错误”上下文足够多泛化能力足够强。序列级损失Sequence-level Loss的思路则更直接地优化整句质量。比如Minimum Risk TrainingMRT把BLEU、ROUGE等序列级评价指标直接作为损失函数来优化不再只是逐token的交叉熵。它的训练过程是这样的给定一个输入模型先用beam search或采样生成N个候选句子然后根据候选句子的真实得分比如BLEU和模型概率构造损失。这样模型学到的是“生成一个整句得分高”的参数而不是“每个token都猜对”的参数。这个思路在机器翻译和文本摘要上都有很多成功案例缺点是训练不稳定需要仔细调参。最后是RLHFReinforcement Learning from Human Feedback。它在语言模型的基础上再用强化学习来优化一个由人类反馈训练出的奖励模型。从exposure bias的角度看RLHF本质上就是让模型在训练时直接面对自己生成的整个序列并根据序列级反馈奖励学习如何生成更好、更符合人类偏好的输出。这是目前在长文本生成和对话系统中效果最强的方法尤其适合开放式生成任务如AI对话、故事创作但对算力和数据标注的要求也最高。4.4 不同方案的效果对比与适用场景方案核心思路实现成本效果强度适用场景Teacher Forcing训练完全用真值零成本默认方案基线所有任务起步Scheduled Sampling训练中混入模型预测低中等视任务而定摘要、生成任务Beam Search Length Penalty推理时保留多条候选路径低中等翻译、摘要等硬约束任务大规模预训练海量数据覆盖各种上下文高强所有生成任务Minimum Risk Training序列级损失直接优化评价指标中强但不稳定翻译、摘要RLHF序列级奖励模型优化很高最强开放式对话、创作4.5 一个容易被忽略的工程细节训练时采样和推理时解码策略要保持一致这一点是我在实际项目中踩过坑之后才悟出来的。如果你训练时用了label smoothing、Scheduled Sampling之类的技术推理时就别用纯贪心解码反过来如果你推理时确定了要用beam search训练时最好也在验证集上同步用beam search来评估。因为推理策略的变化会直接影响模型最优的参数状态训练和推理的不一致越少最终效果越稳定。具体来说我通常在训练过程中定期跑一次“模拟推理”用当前模型在验证集上跑beam search看生成结果的质量。这样做的好处是能提前发现exposure bias的迹象而不是等训练全部结束之后才发现模型在推理时崩了。5. 踩坑记录与实用建议手写Transformer时最容易翻车的几个细节前面讲了这么多理论最后分享几个我在实际写代码和调模型过程中反复踩过的坑希望能帮你省点时间。第一个坑Mask矩阵的方向搞反。这是写Masked Self-Attention时最经典的问题。上三角Mask生成之后你到底把哪些位置置为负无穷正确的是score_mask torch.triu(torch.ones(seq_len, seq_len), diagonal1).bool()然后score score.masked_fill(score_mask, -1e9)。注意diagonal1这个参数如果写成diagonal0对角线也会被Mask掉模型就看不到当前位置自己的信息了。另一个细节是masked_fill里填充的是-1e9不要用float(-inf)因为Softmax里遇到-inf在某些低精度计算FP16下可能出现NaN。第二个坑训练和推理时Decoder的输入格式不一致。训练时我们输入的是带bos前缀的完整目标序列推理时输入从只有bos开始逐步拼接。这个不一致会导致训练好的模型在推理第一步就表现不佳尤其是如果你忘了在目标序列前面加bos。很多开源代码的预处理里目标句子和源句子都加了起始符但Decoder的输入侧和目标标签侧的错位关系很容易搞错——记住上面那段伪代码里的target[:, :-1]和target[:, 1:]错位处理对不上loss就会一直不降。第三个坑不要忽略Padding Mask在Cross-Attention里的作用。很多人只给Decoder的Self-Attention加了Mask却忘了在Cross-Attention里把Encoder输出的pad位置也Mask掉。如果不MaskDecoder在attend源句子的时候会看到一堆pad位置的信息这些位置对应的向量基本是无意义的会把注意力权重带偏。我见过有模型的训练loss怎么都降不到底最后排查发现就是Cross-Attention漏了Padding Mask。第四个坑label smoothing和Scheduled Sampling同时用时要谨慎。这两个技巧一个让目标标签变软一个让输入分布变脏叠加起来模型会很难学。我自己的经验是如果数据量充足优先用label smoothing如果数据量少且长度长优先用Scheduled Sampling两者都想要的话把Scheduled Sampling的ε上限压到0.1以下。第五个坑监控训练时的“教师强制困惑度”和“推理困惑度”差距。这是一个很好的早期预警指标。如果训练集上的困惑度已经很低但在验证集上做推理时困惑度明显高出一截多半就是exposure bias在起作用。这时候不要盲目加数据先考虑调整解码策略或者引入Scheduled Sampling。最后再分享一个我个人的工作习惯无论做什么生成任务我都会在训练早期就固定一个验证集的小样本用当前模型跑一次热启动推理把生成结果打印出来看一看。这一步看起来简单但比任何指标都直观——模型在刚学会说话的时候是什么样子训练中期是什么样子训练后期是什么样子肉眼观察这个演变过程能帮你快速建立对模型状态的直觉而不是等训练完了才发现问题。exposure bias这个问题说到底是一个“模型是否见过自己的错误”的问题你对这个问题的敏感度越高越能在项目早期就做出正确的策略选择。
返回列表