1. 为什么翻译任务绕不开Transformer
1.1 从编码器-解码器到自注意力:翻译任务的本质
先说结论:想用深度学习做文本翻译,Transformer模型是目前最稳、效果最好、资料也最全的起点。我前后带过几个团队做过机器翻译相关的项目,从早期的RNN/LSTM系列到后来的Transformer,可以说凡是涉及翻译任务的落地应用,最后几乎都收敛到了这个架构上。
翻译的本质是什么?一句话:给定源语言句子的条件下,最大化目标语言句子出现的概率。这是典型的序列到序列(Seq2Seq)问题。早期的RNN/LSTM方案一个词一个词地处理输入,把整句话压缩成一个固定维度的向量,这会导致两个问题——长距离依赖容易丢失,比如句子一长,开头的主语信息到后半段就模糊了;同时只能按顺序计算,无法并行,训练速度受到很大限制。
Transformer模型的思路是彻底抛弃循环结构,改用自注意力机制(Self-Attention)来直接建模句子中任意两个词之间的依赖关系。这意味着不管两个词相隔多远,模型都能直接计算它们之间的关联权重,再通过多头机制从不同子空间捕获语义特征。就翻译任务而言,这种全局建模能力天然契合“理解整个句子后再生成译文”的需求。
1.2 Transformer带来的四个关键改变
第一个改变是并行计算。RNN必须按时间步依次处理,Transformer可以同时处理整个序列,训练速度提升了一个数量级。第二个改变是长距离依赖建模能力,注意力机制让任意距离的词都能直接交互,翻译长句时不再容易出现信息丢失。
第三个改变是多头注意力的特征提取能力。多个头可以分别关注不同的语义关系,比如一个头关注语法依赖,另一个头关注指代关系,这让模型比单一的注意力更丰富。第四个改变是位置编码的引入。因为没有循环结构,模型本身不知道词的顺序,所以需要用位置编码赋予每个词位置信息。这三个字“位置编码”看起来简单,实际上整个Transformer对语序的敏感度全靠它支撑。
这四个改变叠加起来,使得Transformer模型成为翻译任务的默认选择。再加上开源社区里有大量预训练权重和成熟实现,做深度学习实战项目时不需要从零造轮子,可以把精力集中在数据、调参和工程优化上。
2. 开工前的数据准备:语料与分词
2.1 平行语料的选择与预处理
模型再强,没有数据也是空谈。文本翻译项目需要的是平行语料——同一句话的双语对照版本。以中英翻译为例,常见的公开数据集有公开的WMT任务数据、开源影视字幕、实验室整理的多语言平行语料等。第一次做项目不建议一上来就追求超大规模数据,几十万到一两百万句对的规模足够把流程跑通,而且训练时间可控。
拿到原始语料后,第一步是清洗。这个环节很多人会跳过,但我建议一定要认真做。我踩过的坑包括:全角半角符号混用、HTML残留标签、重复句子、严重长度不匹配的句对、语言标签错误(比如中文句子里混着大段英文)。这些噪声如果不处理,训练出来的模型会在推理阶段产生莫名其妙的输出。
我的清洗脚本通常会做这么几件事:统一标点符号、去除空白字符和不可见字符、过滤掉包含URL或特殊标记的句子、过滤长度超过阈值(如80个token)的句子、删除明显没有对应语义的重复句对。做完清洗后,再随机抽一两百对样本人工检查,心里有个底。
这里有个容易被忽视的问题是语料切分。不要只随机切,因为翻译任务要求源语言和目标语言逐句对齐。建议先把两边的文件按行对齐,确认总行数一致,再按比例切分训练集、验证集、测试集,比如8:1:1。切分前先做一次行级别的shuffle,但要保证两个文件用同一个随机种子,否则对齐就乱了——这个错误我犯过一次,后来每次写脚本都刻意校验行数。
2.2 BPE分词:模型词汇量和未登录词的处理
分词是翻译任务数据准备中最关键的一步。直接用空格分词会导致词汇量爆炸,因为英文有各种词形变化,而中文根本没有空格分词这个概念。更麻烦的是未登录词问题——遇到训练时没见过的词,模型完全无法处理。我的解决方案是BPE(Byte Pair Encoding,字节对编码),现在大家更熟悉的是它的变体SentencePiece和WordPiece。
BPE的核心逻辑很简单:从字符级别开始,反复统计频次最高的相邻符号对,把它们合并成一个新符号。这个迭代过程会生成一张合并表,合并到预定的词表大小(比如32000或8000)就停止。训练完的BPE词表能覆盖绝大多数常见的子词单元,翻译时遇到新词也能拆成已知子词来处理,基本解决了未登录词问题。
用BPE分词时建议在源语言和目标语言上分别训练两个分词器,而不是共用一个。原因很简单,中文和英文的子词合并规律完全不同,共用词表会浪费词表容量。词表大小的选择上,经验值是源语言和目标语言各8000到16000比较均衡,太小会导致子词拆得过碎,模型学起来吃力;太大会让词表矩阵变大,显存和训练时间都上升。
分词后的数据还需要做两个处理:加上特殊标记,比如句子开头标记和结束标记;统一按最大长度截断。在实现上可以给每个Batch动态 padding 到这一批的最长长度,而不是整个训练集的最大长度,这样可以大幅减少无效计算。这一招在显存受限时尤其好用,能让每一步训练的数据量增加不少。
3. 模型搭建:Encoder-Decoder核心结构拆解
3.1 多头注意力与位置编码:让模型理解上下文和词序
Transformer模型的整体结构是编码器-解码器架构,编码器负责理解源语言句子,解码器负责生成目标语言译文。编码器和解码器都由若干层堆叠而成,每一层内部主要由多头自注意力机制和前馈全连接网络组成。
先解释自注意力机制。给定一个句子,每个词会生成三个向量——查询向量(Query)、键向量(Key)、值向量(Value),这三个向量通过线性变换从输入嵌入中得到。注意力的分数就是查询向量和所有键向量的点积,再用词向量的维度开根号进行缩放,然后接Softmax变成权重,最后和值向量加权求和。这个缩放不是摆设,因为点积的结果会和维度一起变大,如果维度过高,Softmax的梯度会变得非常小,加入缩放因子可以抵消这种影响。
多头注意力就是把上述过程并行做多组,每组称为一个“头”。比如模型的隐藏维度是512,设置8个头,每个头就处理64维的子空间。8个头可以从不同角度学习到不同的语义关系,有的头可能侧重邻近词间的语法结构,有的头可能捕捉到更远的指代关系。把8个头的输出拼接起来再经过一个线性层,就得到最终的注意力输出。
位置编码的原理是给每个位置的词向量加一个正弦和余弦信号。这个信号用不同频率的正弦函数就可以构造出来,偶数维度用正弦、奇数维度用余弦。选择三角函数而不是直接学一个位置向量,好处是模型可以泛化到训练时没见过的序列长度。实际使用中也可以直接用可学习的位置嵌入,效果差异不大,但在处理超长序列时三角函数的方法更稳。
3.2 掩码机制:Decoder的三个注意力模块
解码器比编码器多了一层注意力模块,每个解码器层内部有三个核心部分。第一层是带掩码的自注意力层,这个掩码叫未来掩码,作用是把当前位置右边尚未生成的词全部屏蔽掉。因为推理过程中解码器生成第 i 个词时,只能看到前 i-1 个词,训练阶段如果不加掩码,模型就等于偷看了答案,推理时必然出问题。
第二层是编码器-解码器注意力层,查询向量来自解码器的上一层输出,而键和值向量都来自编码器的输出。这一层的目的是让解码器在生成每个词时,都能从源语言句子中“检索”到最相关的信息。也就是说,当模型要翻译“苹果”这个词时,它可以通过注意力查找到源语言句子中对应的那个词,实现真正的语义对齐。
第三层是前馈全连接网络,有两层线性变换加一个非线性激活函数在中间。这个网络的作用是对注意力输出做逐位置的变换,增强模型的表达能力。前馈网络的中间层维度一般比模型维度大好几倍,比如512维的模型,中间层通常用2048或4096。
我在实际搭建模型时踩过一个坑,就是忘记在解码器的自注意力层加未来掩码,导致训练损失很快降到很低,但推理时生成的内容完全不通顺。这种问题训练指标上很难看出来,一定要在搭建阶段就检查注意力矩阵的形状和掩码逻辑。
多头注意力实现的另一个细节是残差连接和层归一化。每一层子层的输出都需要和输入做残差相加,再做层归一化。残差连接帮助梯度顺畅地反传,层归一化保证训练稳定性。这两个机制看起来简单,但缺失任何一个都会导致训练不收敛或收敛极慢。
4. 训练配置:从损失函数到学习率
4.1 损失函数、标签平滑与优化器选择
训练翻译模型使用的是交叉熵损失函数,就是让模型对每个目标位置正确单词的预测概率尽量高。不过直接使用标准的交叉熵会带来一个问题——模型会变得过度自信,对训练数据里的细微错误也照单全收,导致泛化能力变差。
实际项目中我几乎都会加上标签平滑(Label Smoothing)技术。具体做法是把原本唯一的正确词概率稍微降低一些,比如从1降到0.1或0.9,把多出来的概率平均分摊到词表里的所有词上。这个改动看起来微不足道,但能有效防止模型过拟合,同时还能让模型的输出概率更平滑,推理时的置信度也更合理。常用的平滑系数在0.1左右,系数过大会让模型学不到足够清晰的概率区分度。
优化器选择上,Transformer模型论文推荐的是Adam优化器,但需要注意两个超参数和标准默认值不同。第一个是beta1,建议设为0.9;第二个是beta2,建议设为0.98而不是默认的0.999,因为Transformer训练中梯度变化比较剧烈,需要更敏感的动量调整;epsilon建议设为1e-9,防止数值不稳定。
此外,梯度裁剪对于翻译模型来说是必须的。RNN时代用梯度裁剪比较多,很多人到了Transformer就觉得不需要了,实际上训练初期偶尔还是会出现梯度爆炸导致损失突然变成NaN的情况。我的做法是设置最大梯度范数为1.0或5.0,在不影响收敛速度的前提下提供安全兜底,实测下来训练过程稳定很多。
4.2 学习率调度与正则化的实战配方
Transformer模型还有一个标志性的学习率调度策略——先预热后衰减。论文里给出的公式是学习率等于模型维度的负二分之一次方乘以(步数的负二分之一次方和步数乘以预热步数的负二分之三次方两者之间的最小值)。翻译成大白话就是:训练最开始用很小的学习率,在预热步数内线性增长到峰值,之后按照步数的倒数平方根逐步衰减。
这个设计的合理性在于:训练刚开始时模型参数是随机初始化的,直接用大的学习率容易让更新走偏;经过一段时间的预热后,模型找到了一个大体正确的方向,这时可以加大学习率快速推进;随着训练深入,再用衰减的学习率收敛到更精细的极小值。一般预热步数设为4000步,你可以根据自己数据集大小做调整,数据集越小预热步数可以适当减少。
如果不想要这么复杂的调度器,也可以用带warmup的固定学习率替代,比如前4000步从1e-5线性升到5e-4,之后保持不变或手动阶段衰减。虽然效果略差,但调试起来更直观。
Dropout是另一个重要的正则化手段。Transformer模型中一般会在每个子层的输出上加Dropout,还会在词嵌入层和位置编码相加之后加一个Dropout。翻译任务中Dropout比例设为0.1比较合适,比例太高会导致模型欠拟合,尤其是数据量本来就不大的时候,过高的Dropout会让模型学不到足够的特征。
另外一个性价比很高的训练技巧是混合精度训练。如果你的显卡支持,用半精度浮点数代替全精度能显著降低显存占用、加快训练速度。我实测下来,在同样配置下训练速度能提升40%以上,而模型精度几乎没有损失。只需注意在损失缩放和梯度裁剪上做好配合,尤其是反向传播时要做梯度缩放和反缩放,否则数值精度会出问题。
还有一个比较隐蔽的坑是随机种子。翻译模型训练过程中如果每个实验用不同的随机种子,训练结果会有明显的浮动,这会给调参带来很大干扰。建议在数据shuffle、模型参数初始化、优化器初始化这几个环节都固定随机种子,保证实验结果可复现。
5. 推理与评估:从翻译到打分
5.1 Beam Search与温度系数的取舍
训练完模型后,推理阶段生成译文的方式也有讲究。如果每次只选概率最高的那个词,生成结果往往会比较短、比较无趣,这叫贪心搜索。更好的方案是束搜索,每次保存前K个概率最高的候选序列,生成完毕后在K个候选中挑选一个整体得分最高的。这个K就是束宽,一般翻译任务取4到8比较合适,K太大会让推理速度变慢,而且对质量的提升非常有限。
束搜索实现时需要注意长度归一化。模型在计算候选序列得分时,惩罚长句的代价也是实际工程中的技术重点——如果不对长度做处理,束搜索会倾向于生成较短的句子,因为每个词的概率乘起来,句子越长乘积越小。解决方法是给序列得分除以一个长度惩罚因子,通常设定在1.0到1.5之间,这个因子越大越倾向于长句。比如设为1.2,就是用一个长度指数对总的对数概率做归一化后比较。
温度采样是另一个可选方案。温度系数用来调整模型输出概率分布的“锐度”。温度越低,分布越尖锐,采样结果越接近贪心搜索;温度越高,分布越平滑,生成结果更多样性但错误率也会上升。翻译任务中要控制好温度系数的切分,过低的温度导致翻译机械,而过高的温度会让句子结构崩坏。如果做的是基于翻译模型的文本复述或改写作扩散类应用,可以考虑提高温度;如果做的是严格意义的机器翻译,贪心或束搜索依然是更好的选择。
还有一个推理阶段常见的工程问题是重复生成。束搜索容易陷入重复循环,同一句话反复生成几遍。解决方案有针对性优化惩罚策略,已经生成过的词组在得分上施加额外的惩罚,比如在计算候选得分时对已出现在输出序列中的重复词乘上惩罚系数。这个技巧在长句翻译时非常有用,我强烈建议加上。
5.2 BLEU评估指标的局限与替代方案
评估翻译质量最常用的指标是BLEU。它的思想很简单:比较机器译文和人工参考译文之间的n-gram重合程度。n-gram尺度取1到4,再加上一个简短惩罚系数——译文太短时做降权惩罚。BLEU的优点是与人工评估的相关性较好、计算简单,缺点是它只看表面词汇重合,不关注语义等价。
举例来说,参考译文是“这是一个很棒的项目”,机器输出“项目很棒”,从语义上完全能理解,但BLEU得分会很低。反之,两个句子用词相同但语序错乱,BLEU得分反而可能很高。这就是为什么我在项目里不会只依赖BLEU,还会配合使用chrF指标。
chrF基于字符n-gram而不是单词n-gram,它对词序变化和形态变化更敏感,在处理中日韩等语言时表现比BLEU更好。此外,随着近年来看得更多,基于向量的语义相似度也被越来越多地引入到评测中,直接计算译文和参考句向量之间的余弦相似度,能在语义层面衡量翻译质量。
实际评估时还有一个重要原则:验证集不能太小。最少要有1000句以上的参考译文,否则BLEU得分的波动会非常大,无法有效判断模型改进的好坏。某次实验中我在一个只有200句的验证集上测试,BLEU在两次训练间的波动从低点29到高点39都有,根本没法判断哪个配置更优秀。所以数据准备阶段就要把测试集留够。
6. 实测填坑:那些训练中一定会遇到的问题
6.1 显存不够怎么办
Transformer模型对显存的要求很高,尤其是词表大、序列长的时候。我最早跑一个中英翻译任务,用的词表是32000,序列长度60,batch size 16就已经爆掉了8GB显存。这里有几个实用的降显存手段。
第一个是梯度累积。把一个大Batch拆成若干小Batch,分别计算梯度,累积到一定步数后再统一更新参数。这样在效果上和直接使用大Batch差别不大,但显存占用只有原来的几分之一。需要注意累积步数不要设太极端,8步以内比较合理,超过之后BN和训练稳定性会受影响。
第二个是动态填充。前面提到过,每个Batch只填充到当前Batch最大长度,而不是训练集最大长度。这个优化通常能节省20%到40%的显存和计算量。第三个是混合精度训练,开启混合精度一般能把显存占用减少一半左右。
第四个方案是合理限制序列长度和词表大小。做实验阶段没必要追求和论文相同规模,序列长度从60截断到40到50,词表从32000砍到8000,训练速度和显存压力都能大幅下降,而翻译质量的损失可能只在几个点以内,完全在可接受范围。
6.2 过拟合与收敛速度的平衡
小数据集上训练Transformer最大的风险就是过拟合——训练损失持续下降,验证集BLEU却不升反降。一个判断过拟合的方法是把训练集和验证集的损失曲线画在一起看它们的差距。差距始终很小说明模型还没充分拟合训练集,差距快速拉大然后验证损失开始回升就是过拟合的典型信号。
针对过拟合,我常用的缓解手段有增大Dropout比例到0.2或0.3,增加标签平滑系数从0.1到0.2,使用更小的模型维度,比如把512降到256、层数从6降到4。加数据增强也是一个思路,比如对源语言句子做轻微的词序打乱或对目标语言做反向翻译回译增强,但要控制增强强度,否则会引入噪声。
反过来,如果发现训练集上的损失迟迟降不下来,而验证集表现也跟着拉垮,这不是过拟合而是欠拟合。这时应该优先检查学习率是否太小、预热步数是否太长、模型容量是否不足。我曾经试过用一个层数2、维度128的“迷你Transformer”去跑一个中等难度的翻译任务,结果训练了两天BLEU始终在20以下打转,换成标准大小后很快就突破25。模型容量和任务复杂度要匹配,不能一味追求小模型。
一个值得关注的工程细节是训练过程中要定期保存checkpoint并评估验证集BLEU。不要只在训练结束时保存一个最终模型,因为训练过程中验证集BLEU通常会出现先升后降的曲线,最佳状态往往出现在提前停止的地方。我的习惯是每1000到2000步保存一次checkpoint,同时记录验证损失和BLEU,训练结束后再选一个最好的checkpoint用于推理,而不是用最后一个。
6.3 曝光偏差的缓解
曝光偏差是训练生成模型时一个隐蔽但常见的问题,翻译任务中尤为明显。训练阶段,解码器的每一步输入都是来自真实译文的词,模型是在“正确答案”的条件下学习的;但推理阶段,模型的每一步输入是自己的“错误预测”,这会造成训练和推理之间的分布不一致。
缓解曝光偏差的方法中,计划抽样是最直接的。它的做法是在训练过程中以一定概率用模型自己的预测词替换真实词作为解码器输入。这个概率可以设置成随训练步数从1逐渐衰减到0.2或0.1。计划抽样做得好能明显提升推理时的鲁棒性,缺点是训练收敛变慢,一开始模型会看到很多错误样本,学习效率下降。
另一种做法是强化学习后训练,先正常训练一个模型到收敛,再用强化学习的目标进一步微调,比如用BLEU或句子级奖励直接优化最终的翻译质量。这个方案的实现复杂度较高,但在实际项目中可以获得几个点数的稳定性收益。如果你做的是课程设计或实战练手,还是先用计划抽样和长度惩罚就能解决大部分问题,不用一上来就上强化学习。
综合起来,如果你正打算从零手写一个翻译模型练手,我的建议是:先用一个小的公开数据集把完整流程跑通,从数据处理、模型搭建、训练到推理评估全流程走一遍,再逐渐换更大规模的语料和更精细的调参。第一版模型不需要追求最强的翻译质量,重要的是理解每个环节的作用——明白为什么Transformer能并行处理序列、为什么需要位置编码、为什么解码器要加掩码、为什么束搜索比贪心好、为什么评估不能只看BLEU。
这些“为什么”搞清楚了,再去看任何基于Transformer的翻译系统或预训练语言模型,你都能快速抓住核心脉络。文本翻译只是Transformer应用的起点,掌握了这个项目,后续无论是做文本摘要、命名实体识别还是对话生成,都只是数据形态和任务目标的变化,底层的机制是相通的。