把"我爱你"翻成"I love you",对学过几天英语的人来说几乎不费脑子。但让计算机自己搞定这件事,人类折腾了几十年——从早年硬套语法规则的机器翻译,到统计机器翻译,再到如今大模型背后清一色的Transformer架构,这条路走得相当艰难。我自己第一次完整跑通一个翻译模型,看着终端里真真切切跳出"I love you"这几个词时,还是有那么一瞬间觉得挺神奇的。这篇文章就想把那瞬间的"魔法"拆开,用最直白的方式讲讲:Transformer的编码器和解码器,究竟是怎么把一个中文短句一步步变成英文的。
如果你对神经网络、注意力机制这些词只是听说过,但从来没真正搞懂过内部流程,这篇文章就是给你写的。我不会贴大段公式,尽量用"人话"把每个关键步骤讲透,顺带聊聊我自己在实操中踩过的坑和总结的经验。
1. "我爱你"凭什么难倒机器:翻译任务的核心难点拆解
翻译这件事,表面看是"查字典+调语序",但机器做起来远没那么简单。先说个最基础的观察:中文"我爱你"三个字,英文"I love you"也是三个词,好像一一对应,其实不然。
1.1 词序、省略与语境:机器翻译真正要解决的三大挑战
中文的"主语-谓语-宾语"结构相对固定,"我爱你"恰好是标准的SVO顺序,英文"I love you"也是SVO,看起来顺序都对得上。但如果把句子换成长一点的呢?比如"我昨天在公园里看见的那只猫,今天又跑来了"。中文里"那只猫"被一大串定语包裹,英文则习惯把修饰成分后置或者用从句处理,语序完全不同。
还有省略问题。中文是典型的"意合"语言,很多成分能省就省,比如"下雨了"就没有主语,翻译成英文必须补出"It is raining"。反过来,英文里的时态、单复数、冠词,在中文里基本没有对应物,"我爱你"看不出时态,但"I love you"和"I loved you"意思就差远了。机器必须从上下文里推断这些隐藏信息。
更麻烦的是语境。同一个词在不同句子里意思天差地别,"打酱油"的"打"和"打电话"的"打"不是一回事,"我爱你"里的"爱"和"爱停电不"(方言里"老是"的意思)也不是一回事。词典里查不到这种灵活性,必须看这个词周围的词才能判断。
1.2 为什么说"端到端学习"是真正的解法
早年的机器翻译系统尝试用语言学家手写的数千条语法规则来覆盖这些情况,结果规则之间互相冲突,越写越臃肿,遇到真实语料立刻崩溃。后来的统计机器翻译把翻译拆成"分词—调序—选择译词"等多个独立环节,每个环节单独训练,误差会不断累积,而且调序这种全局问题,局部优化根本搞不定。
Transformer走的是另一条路:端到端学习。输入一句中文,输出一句英文,中间不人为划分流水线,让模型自己从海量平行语料里学出"中文句子和英文句子之间存在什么样的映射关系"。编码器负责把中文句子的完整含义压缩成一组向量,解码器负责把这组向量重新展开成英文句子。整个过程中,模型自己学会了语序调整、上下文判断、信息取舍,不需要人类告诉它"这里该倒装"或"这里该补主语"。
我用一个粗浅的类比来帮你建立直觉:编码器像一个人读完一句中文后,在脑子里形成一个"完整的、不依赖任何语言的"意思表征;解码器像另一个人拿到这个意思表征后,用英文把它重新说出来。两个人都没看过对方的语言,但共用一个"意思空间"。
2. 编码器端:一句中文是怎么变成一组数字的
Transformer解码器的输入端是一串英文token,逐步生成;编码器的输入端则是整句中文一次喂入。两者都依赖同一个基础设施:词嵌入与位置编码。
2.1 词嵌入:把"我""爱""你"各自变成一串数字
神经网络不会读汉字,它只认数字。第一步是把每个词映射到一个高维向量。比如"爱"这个词,会被映射成一个例如512维的向量,这个向量里的每个数字表示"爱"在某个语义维度上的取值。这些数字不是人指定的,而是模型在训练中自动学出来的。我常用一个类比:词嵌入就像给每个词发了一张"语义身份证",身份证上的信息不是名字,而是一串"这台机器能读懂的特征码"。
有意思的是,训练好的词嵌入空间往往有语义规律:相近的词向量距离也近,"爱"和"喜欢"的向量会比"爱"和"桌子"的向量近得多;甚至能做向量加减法,比如"国王"向量减去"男人"向量加上"女人"向量,结果接近"女王"向量。这说明向量不是冷冰冰的编码,真的承载了语义内容。
2.2 位置编码:为什么"我爱你"和"你爱我"必须被区分
词嵌入只告诉你"这个词是什么",没告诉你"这个词在句子的哪个位置"。但Transformer里所有词是同时进入模型的,没有天然的先后顺序,"我爱你"和"你爱我"如果拆开看,词完全一样,向量也一样。必须额外注入位置信息。
Transformer用的是正弦余弦位置编码:每个位置i对应一个固定的向量,向量的第d维用sin或cos函数计算,频率从低到高排列。你可以把这种编码想象成给每个位置打了一个"独一无二的指纹":位置0的指纹是这样,位置1的指纹是那样,永远不会重复,而且相邻位置的指纹比较相似,距离远的位置指纹差异大。
有两点实际操作时要注意:一是输入的词向量和位置向量是直接相加的,不是拼接,这样既保留了词的语义特征,又叠加了位置特征,维度不变,后续网络处理起来更省事。二是Transformer里的位置编码有两种做法,一种是用固定公式生成,另一种是让模型自己学习一套位置向量。我在实验中观察,对于固定长度的句子两者差距不大,但如果句子可能特别长,学习式位置编码在训练时没见过的长度上表现会逐渐退化,正弦编码稍微能扛一点。
2.3 自注意力:让"爱"看看句子里谁和它关系最近
有了词向量和位置向量,编码器开始核心计算:自注意力。自注意力的目标,是让句子里的每个词,都能参考其他所有词的信息,来更新自己的表示。我给你拆解一下Self-Attention的具体过程。
每个词向量会被复制成三份,分别乘上三个不同的权重矩阵,得到查询向量(Query)、键向量(Key)、值向量(Value)。你可以这样理解:Query是"我想找什么",Key是"我拥有什么标签",Value是"我实际提供的内容"。"我"的Query会去和"爱"的Key、"你"的Key做匹配,得到一个相似度分数;分数经过softmax变成权重;最后按权重把所有Value加权求和,就是"我"在这个句子里的新表示。
具体到"我爱你"里,"爱"这个词的Query和"我"的Key、"你"的Key匹配度都很高,所以更新后的"爱"向量里,既包含了"爱"本身的含义,也混合了"我"和"你"的信息。这一下子就解决了前面说的"打酱油"问题:当"打"同时看到"酱油"时,它的向量会被"酱油"强烈影响,模型就能区分"打酱油"的"打"和"打电话"的"打"。
这个机制的好处是:不管两个词离得多远,只要语义上相关,注意力都能直接建立联系。它不需要像循环神经网络那样一步一步传递信息,信息就不会在长距离传递中衰减。我在实际跑模型时测试过,短句上自注意力和循环神经网络差距不大,但句子一旦超过三十个词,循环神经网络的优势就明显消失了,自注意力依然稳稳的。
2.4 多头注意力与残差连接:编码器的完整一轮计算
只看一遍注意力还不够。Transformer实际使用的是多头注意力:把注意力计算做成多组,每组使用不同的权重矩阵,相当于让模型从多个"视角"同时看这个句子。比如一个头可能关注语法结构,另一个头关注指代关系,还有一个头关注语义相似度。8个头并行计算,结果拼在一起,再通过一个线性层融合,最终每个词得到一个比单头注意力更丰富的表示。
每一层多头注意力后面,还跟着一个前馈神经网络,做一次非线性变换。整个流程外面套着残差连接和层归一化:
- 词向量加位置向量,进入多头注意力。
- 注意力输出加上原始输入(残差连接),再做层归一化。
- 结果进入前馈神经网络。
- 前馈输出加上上一步结果(又是一个残差连接),再做层归一化。
- 输出作为下一层编码器的输入。
刚才说的是一层编码器的流程。实际模型有6层(小模型)甚至更多,每一层的输出都是对句子语义更抽象的表示。第1层可能还在关注词与词的局部搭配,越到高层,每个位置的向量越能代表整个句子的某个侧面信息。所有层的最终输出,就是编码器交给解码器的"语义压缩包"。
实操中还有个细节,残差连接不是可选项。我去掉过某个模型的残差连接做对比实验,十几层网络根本训不动,梯度直接炸掉。层归一化的位置也很有讲究,现在很多模型用Pre-LN(先归一化再进子层),训练更稳,但原版论文用的是Post-LN。从我的经验看,想省事就上Pre-LN,想尽可能贴着原论文复现就保留Post-LN。
3. 解码器端:从"语义压缩包"里一句一句挤出英文
编码器把"我爱你"变成了一个语义压缩包,解码器的工作就是把它解压成"I love you"。这个解压过程和编码过程最大的不同在于:它是一次生成一个词的,不能一口气把所有英文词都算出来。为什么?因为英文句子的生成天然有顺序依赖:生成"I"之后,才知道下一个词可能是"love";如果先蹦出"you",整个句子就乱了。
3.1 掩码自注意力:解码器为什么不能"偷看"后面的词
解码器的第一个关键组件是掩码自注意力。从名字就能看出来,它和编码器的自注意力有区别:编码器里每个词可以看到整个句子的所有词,包括后面的词,因为编码器本来就是在处理一个已经完整的句子;但解码器在生成第t个词时,后面的词还没生成出来,自然不应该出现在计算里。
掩码的实现很粗暴:在计算注意力分数时,把当前位置之后的所有位置的分数设成负无穷。softmax里负无穷会变成零概率,等于那些位置完全不参与加权求和。这样一来,每个位置在生成时,只能看到自己和自己之前的词,不能偷看未来的词。就好比一个人写英文句子时,写"love"这个单词的当下,只知道前面已经写了"I",不知道后面会不会有"you"。
这个掩码是Transformer和传统自回归语言模型对齐的桥梁:新一代的大语言模型在预训练时都用这种掩码机制,这个细节我建议所有做NLP的人都记住——很多人一上来不理解为啥解码器还要分掩码自注意力和编码器自注意力,其实就是"能不能看到未来"的区别。
3.2 交叉注意力:解码器怎么"读取"编码器压缩的语义
掩码自注意力处理完当前已生成的英文词后,解码器进入第二个关键组件:交叉注意力。这里是编码器和解码器真正"对话"的地方。
交叉注意力的Query来自解码器当前层的输出,Key和Value都来自编码器的最终输出。也就是说,解码器在生成下一个词时,手里拿着两张牌:一张是已经生成的那串英文词(经过掩码自注意力处理),另一张是编码器给的中文句子语义压缩包(所有位置的向量)。它会让"已经生成的英文部分"去匹配"中文语义压缩包"里的每个位置,确认哪些位置的信息对生成当前这个词最有帮助。
拿"我爱你"举例:解码器生成"I love you"时,注意力的权重分配很有意思。它可能先在中文句子上重点对齐"我",生成"I";然后重点对齐"爱",生成"love";最后对齐"你",生成"you"。这一个简单的例子看不出精髓,但长句子里交叉注意力的价值就非常明显了:中文定语在名词前面,英文定语在名词后面,交叉注意力能让解码器在生成英文名词时,回头重新找到中文里那个被埋没的中心词。这个机制比任何人工设计的调序规则都灵活得多。
3.3 从LOGITS到词表概率:最后一个词是怎么选出来的
解码器每生成一个词,最后一步都是把这个词对应的隐藏状态通过一个线性层,映射到词表大小的一串数字上,这串数字叫logits。再经过softmax,变成词表里每个词的概率分布。比如当前词表有3万个词,"love"这个位置的概率可能是0.7,"adore"是0.1,"like"是0.05,剩下的词概率都很小,所有词的概率加起来正好是1。
这时候怎么选词就有讲究了。最简单的做法是贪心解码:直接选概率最高的那个词。看起来很合理,但实际效果往往一般。比如模型在生成英文时,前面的某个概率稍低的词,可能会让后面所有的词概率都大幅提升,贪心选择恰恰把这条路封死了。后面我会单独展开说生成策略这个话题,这里先知道"选词"不是无脑选最大就行。
解码器的整个生成过程,我画个时间线你会更清楚:
- 解码器收到一个特殊的开始标志(BOS,比如
<s>),准备生成第一个词。 - 经过掩码自注意力、交叉注意力、前馈网络,输出一个隐藏状态。
- 通过线性层和softmax,得到词表概率分布,选定第一个词"I"。
- 把"I"拼到已生成序列的末尾,作为下一步的输入。
- 重复步骤2到4,依次生成"love"、"you"。
- 当概率分布中出现结束标志(EOS,比如
</s>)时,停止生成。
整个过程就像一个人写文章:写下一个字,眼睛扫一眼已写的内容,再写下一个字,直到画上句号。
4. "I love you"是唯一答案吗:解码策略与波束搜索的实战视角
标题说"我爱你"变成"I love you",听起来像是有唯一标准答案。但如果你真的跑过生成模型,会发现同样一句中文,模型能给你好几个稍有出入的英文版本。I love you只是概率最高的那个。这一节就说清楚:一个句子的最终译文,到底是怎样被"敲定"的。
4.1 贪心解码的问题:为什么局部最优不等于全局最优
贪心解码每一步都选当前概率最大的词,但机器翻译的质量要看整句话的连贯程度。我用两天时间训练过一个玩具翻译模型,然后对比贪心解码和波束搜索的输出,发现贪心解码经常在长句子上翻车。你遇到的情况可以类比成下棋:每一步都走眼前最有利的一步,但十步之后可能走进死胡同;有时候第一步看似稍差,后面反而越来越顺。
在模型层面,这个现象的本质是:每个词的概率都是在给定前面已生成词的条件下计算的,条件里任何一个小变化都会像蝴蝶效应一样传导到后面。一个0.6概率的词和另一个0.4概率的词,它们的"后续发展空间"可能完全不同。贪心算法只看了当下,看不到后续发展的可能性。
4.2 波束搜索:同时保留多条候选路径
波束搜索是对贪心解码的改进。它不再每一步只留一个最优词,而是保留多个候选序列,假设波束宽度是4,那么每一步保留概率最高的4个完整候选序列,其他全部剪掉,直到序列结束或达到最大长度,再从保留的候选里选整体概率最高的那个。
波束宽度是个需要调的参数。波束太小,效果和贪心接近;波束太大,计算量成倍增加,而且翻来覆去生成相似的结果,边际收益越来越低。我在实际项目中常用4到8,配着长度惩罚一起用。长度惩罚的作用是避免模型生成过短的句子,因为概率是逐词相乘的,词数越多连乘的次数越多,数值越小,模型自然倾向选短句。长度惩罚给长句一个补偿分,让翻译结果不过分偏向短句。
这里还要提醒一个细节:分数计算时通常用对数概率相加,而不是原始概率相乘。原始概率乘起来会很快变成接近0的小数,数值上不稳定,对数能把乘法转成加法,所有候选的分数都在一个更舒服的数值范围内比较。
4.3 采样式解码:翻译任务之外,生成任务怎么用
如果做的是文本续写、对话生成这类开放式任务,波束搜索反而不是最佳选择:它太"保守"了,生成结果千篇一律。这时候一般换成采样式解码,比如温度采样。温度参数会改变softmax的分布形状:温度>1,分布变平缓,低概率词也有更多机会被选中,输出更多样;温度<1,分布变尖锐,输出更确定。文本生成里为了不出现完全无厘头的组合,还会加top-k或top-p过滤,只从概率最高的k个词或累计概率超过p的词里采样。
翻译任务通常不用采样,因为翻译追求的是准确和通顺,而不是花样百出。但有个特例:我见过有人用低温度采样做机器翻译的数据增强,给同一个源句生成多个略有差异的目标句,然后灌回训练集扩充数据多样性,效果还不错。这说明解码策略没有绝对的"好"与"坏",只有"适合这个任务"和"不适合这个任务"。
5. 训练一个翻译模型要经历什么:从数据到损失函数
前面讲的全是"推理"阶段,也就是模型训练好之后怎么用。但如果你自己动手训练一个翻译模型,会发现推理时的每一步都依赖训练阶段学到的参数。那训练阶段到底发生了什么?这一节把训练流程完整捋一遍。
5.1 平行语料与预处理:数据的质量决定模型的上限
翻译模型训练需要平行语料:同一个意思的中文句子和英文句子一一对应。公开数据集不少,但真实场景里自己攒数据才是常态。我在做某个领域翻译模型时,通用语料翻译专业术语一塌糊涂,后来费了很大力气从行业报告、论文摘要里扒了几万条平行句对,模型才勉强能用。数据预处理有几个容易踩的坑,我逐个说一下。
一是清洗。网页抓来的语料里满是HTML标签、乱码、重复句子,不做清洗直接训练,模型会学出一堆奇怪的东西。我的做法是写一套规则:去重、去噪、过滤掉过长过短的句子对。所谓"过长",我一般限制源句不超过100个token,超过就丢掉,因为训练时间会急剧增加,效果也不会更好。二是分词。中文要分词,英文要切分成子词(subword)。英文不直接按空格分,因为英文词形变化太多,每种形态都当成独立词的话,同一个词根的不同形态会被学成完全不同的向量,大幅增加词表规模,又浪费训练数据。子词切分把"loved"切成"love"加"ed",词根和时态后缀都能学到,词表马上小了一圈。中文分词现在常用的是直接用字或者用词,各有取舍:用字没有分词错误问题,但单个字携带的信息太少;用词信息更密集,但分词器本身会引入误差。我在自己项目里如果数据量不大,倾向用字或混合方式,稳妥一点。
三是加入特殊token。句首加<s>,句尾加</s>,未知词统一用<unk>代替。实操中很多人会忽略一个细节:训练时源句子和目标句子的特殊token要同样处理好,尤其是目标句子的EOS,必须出现在每个句子末尾,解码时模型才能学会在正常句子结束后输出EOS。
5.2 损失函数与Teacher Forcing:训练和推理的微妙差异
训练时,模型不会像推理那样一个词一个词地"猜",而是使用Teacher Forcing技巧:直接给定正确的前文,让模型预测下一个词。比如训练时已知目标句是"I love you",模型看到输入"<s> I love",应该预测"you"。这样做有几个明显的好处:训练收敛得快,因为每一步的输入都是正确的,不会被早期错误带偏。缺点是训练和推理时模型看到的输入分布不同——训练时看到的都是正确词,推理时前面只要错一个,整个输入就带污染了。这个不一致在学术界叫Exposure Bias,是很多研究者在尝试解决的经典问题。
损失函数用的则是交叉熵。每个位置上,模型输出一个词表大小的概率分布,我们要让真实词那个位置的概率尽量高。交叉熵衡量的是"模型预测分布"和"真实分布"之间的距离,距离越小越好。整个句子的损失就是每个位置交叉熵的平均。训练目标,就是找到一组模型参数,让所有训练句对的平均损失最小。
顺带说一个我踩过的坑:训练时loss降得很漂亮,但推理时效果崩了,十有八九是训练和推理的batch不匹配。我一开始在训练时设了batch size为64,但推理时为了省显存把batch改成1,结果发现部分算子在两种batch下行为有细微差异,生成的句子质量肉眼可见下降。后来固定成一样,问题消失。
5.3 训练中的隐藏细节:学习率、热身步数、梯度裁剪
Transformer对学习率特别敏感,这是我多次实验验证过的。直接用一个固定学习率,比如0.001,训练很容易震荡甚至发散。原论文用的是Noam学习率调度:先线性增加学习率到峰值,再按步数倒数平方根衰减。稳步上升的那段叫warmup步数,让模型在训练初期用较小的学习率先稳定起步,避免因为某些参数的梯度异常大而一下把参数推向不好的区域。
除此之外,梯度裁剪也几乎必备。我习惯把梯度范数裁剪到1.0,像给参数更新装了个"安全带"。transformer层数加深、batch加大后,偶发梯度爆炸是很正常的事,不裁剪的话训练可能直接变成NaN。你如果在训练过程中看到loss突然变成nan,第一件事就是检查有没有开梯度裁剪。
还有个容易被忽略的问题:显存管理。Transformer训练很吃显存,尤其 attention 计算矩阵很大。如果你的显存不够,优先考虑减少batch size或缩短序列长度,实在不行可以上梯度累积:把多个小batch的梯度加起来再更新参数,效果接近大batch训练。我个人的经验是,在大规模训练任务里,梯度累积步数设成2到4,效果通常是可以接受的,但对BatchNorm这类依赖全局统计量的模块要小心,Transformer里用LayerNorm所以还好。
6. 把"我爱你"背后那套机制迁移到今天的LLM:一点延伸观察
老实说,"我爱你"翻译成"I love you"只是Transformer的一个经典例子。今天你打开任何一个大语言模型,让它翻译、写摘要、回答问题时,底层仍然是这一套——只是规模变大了、数据变多了、训练方式进化了。很多人一开始搞不懂"编码器-解码器架构"和"大模型架构"的区别,这里我用最简短的方式帮你把认知对齐。
6.1 编码器-解码器架构与Decoder-only架构的区别
当年机器翻译用的Transformer是完整的编码器+解码器结构,名副其实的"编解码器"。后来语言模型发展出Decoder-only的架构——只有解码器,没有编码器。为什么能这样?因为语言模型的任务变成了"无条件续写",不需要一个单独的把源句变成语义向量的模块,而是直接根据已有的文本预测下一个词。GPT系列就属于这类。
两者的关系可以用一个比方:编解码器架构像专业的同声传译,先听一段完整的外语,再把它转换成母语;Decoder-only架构像小说家续写,读到哪写到哪。decoder-only模型也可以做翻译,比如直接把源语言和目标语言用一个分隔符隔开,扔给模型让它续写后面的目标语言部分,只是方式变了,但内核的注意力计算、位置编码、softmax选词,和前面讲的完全同源。
6.2 悟了Transformer之后,看大模型的论文会轻松很多
我自己的体会是:认真把"我爱你"变成一个英文句子的完整流程捋一遍之后,再去看大模型的论文、技术报告,心理门槛低了一大截。因为不管模型多大、训练数据多海量,核心的骨架你都已经见过了,剩下的无非是在这个骨架上做各种扩展:用更大的上下文、加更多的注意力头、引入强化学习做对齐、用人类反馈校准生成内容。
所以如果你想入门自然语言处理,与其被各种概念绕晕,不如先老老实实把这个最经典的翻译例子搞透。我曾经在带人的时候发现,很多人在理解注意力时,卡住的不是数学,而是没有实际观察过"注意力权重到底长什么样"。我的建议是:找一个小型翻译模型,跑一句中文,把交叉注意力的权重可视化出来,看看模型在生成每个英文词时,究竟把注意力放在了中文句子的哪些词上——你一定会觉得,"哦,原来它是真的在看中文"。
这种"把黑盒打开看一次"的体验,比读十篇综述都管用。
7. 自己动手跑一个小翻译模型:环境准备和我的避坑建议
讲了这么多原理,最后一定得落到动手实践。我强烈建议你亲手训练或者至少推理一个小型翻译模型,哪怕数据只有几万句对,都能对Transformer产生非常直观的认识。这里分享一下我常用的环境配置和踩过的坑。
7.1 推荐技术栈与最小环境配置
我的建议是:框架用PyTorch,不必一上来就碰分布式训练。硬件方面,训练一个几万句对的玩具翻译模型,一张8GB显存的显卡就够入门;推理阶段甚至可以纯CPU跑,就是速度慢一点。软件环境建议Python 3.10以上,PyTorch 2.x,再装上HuggingFace Transformers或自己用底层API搭一套。HuggingFace的好处是很多预训练模型直接可用,坏处是封装层级太多,初学者容易"只会调接口,不知其所以然"。我的做法是:先用HuggingFace跑通一个现成模型找感觉,再手写一个极简版Transformer加深理解。后一步看起来费时间,实际上对理解提升很大。
7.2 实战中影响翻译质量的关键调参项
参数不是越多越好,有几项对最终效果影响最大,我列在下面供你对照。
| 参数 | 推荐初始值 | 影响方向 | 备注 |
|---|---|---|---|
| 层数 | 6层编码器+6层解码器 | 层数多模型容量大,但更难训练 | 小数据集用更少的层,例如4+4,反而更好 |
| 嵌入维度 | 512 | 维度越高表达越丰富,但显存消耗上升 | 小数据可降到256或128 |
| 多头注意力头数 | 8 | 头数必须能整除嵌入维度 | 常用组合是512维配8头 |
| 波束宽度 | 4 | 越大越可能找到高分序列,但更慢 | 生产环境常用4到8 |
| 温度参数 | 1.0 | 大于1更随机,小于1更确定 | 翻译任务一般不调,生成任务再调 |
| 学习率峰值 | 0.0005(配合warmup) | 太大震荡甚至发散 | 用Noam调度,warmup约4000步 |
还有一个小验收技巧:训练完成后,别只看BLEU分数,一定要随机抽几十条样本,逐条看人工翻译和模型输出的差异。BLEU是词重叠度的度量,容易迷惑人——有些句子和标准翻译词都差得远,但语义完全对。反过来也有BLEU很高、实际不通顺的情况。做翻译任务的模型评估,我始终觉得人工抽检比单一指标靠谱得多。
7.3 几个容易踩的坑和我的解决方式
第一个坑:没有使用padding mask。句子有长有短,一个batch里要填充到相同长度,但如果注意力计算时连带填充位置一起参与,模型就会去"注意"那些全是填充符的位置,白白浪费注意力分数。解决方式是在计算注意力时,把这些位置的分数也设成负无穷,也就是和掩码自注意力类似的处理方式。我在第一次自己实现时漏掉这一步,结果训练曲线虽然正常,但翻译质量始终上不去,找了好几天才意识到问题。
第二个坑:标签平滑(label smoothing)没开。交叉熵损失如果对真实词位置给"1"的硬目标,模型学出来的概率会过于自信,泛化变差。给真实位置设成0.9,其余概率平摊到所有词上,看起来损失略微上升,但BLEU分数通常会提升一截。这个事情我在多个数据集上都验证过。
第三个坑:评估时忘了把特殊token过滤掉。输出里带着<s>和</s>去算指标,结果会被拉低不说,还会让排查问题变得很麻烦。写个简单脚本,比较前把特殊token全部剥掉,再看指标。
8. 写在最后:一个更省事的入口——直接用现成工具
如果真的没有精力从头训练,又想把Transformer翻译能力用在具体场景里,那就直接用现成的工具链。HuggingFace上有很多翻译模型,加载到本地做推理,十几行代码就能完成。对于追求稳定、快速上手的场景,这是性价比最高的方案。在动手前,至少把前面的原理再过一遍,因为你调参、排查问题、判断结果是否合理时,靠的就是那套底层认知。
我始终觉得,Transformer最迷人的地方不是它有多复杂,而是它用一个相对统一的机制同时解决了"理解"和"生成"两个问题。编码器理解,解码器生成,中间的注意力让理解和生成无缝对接。你弄懂了"我爱你"到"I love you"这条路上发生的每一件事,以后再遇到任何Transformer变体,拆开一看,大概率还是那几样东西在起作用。