
2017年那篇《Attention Is All You Need》我前后完整读过四遍第一遍是2019年刚接触NLP时囫囵吞枣只记住了“Transformer”这个名词第二遍是动手复现时逐公式抠细节第三遍是给别人做分享被迫把每个“为什么”都讲清楚第四遍是最近重读发现当年跳过的翻译措辞和实验设置里藏着不少被忽略的判断依据。这篇博文就是这四遍留下的笔记整理属于Transformer系列的第3篇重点放在论文本身的翻译与精读上。它适合三类人刚入门想搞懂Transformer原理的新手、准备手写Transformer的工程同学、以及读过论文但卡在某几个公式上的人。我会把摘要、模型结构、注意力公式、训练配置、实验结果按原文章节顺序拆开讲该给数值的地方给数值该补代码的地方补代码也会把我自己踩过的坑标出来。1. 读这篇论文前需要先打好的底子1.1 为什么这篇论文值得逐句读而不是只背结论很多人对这篇论文的印象停留在两个标签Transformer的起点、抛弃RNN。但如果只记住这两条后面看BERT、GPT、ViT、Swin Transformer时会一直处于“知道结构但不知道为什么要这么设计”的状态。这篇论文真正的价值在于它把“序列建模到底受限于什么”这个问题讲透了并且给出的解法是整套配套的注意力替代循环、多头并行、位置编码补序、残差加归一化保训练、学习率预热保收敛。每一块都不是孤立的任何一个环节删掉模型都跑不出论文里的成绩。我自己的体会是论文里有几处地方如果你只是浏览摘要会完全错过。比如缩放点积注意力里那个除以根号d_k的操作作者在脚注里给了解释很多人第一次读根本不会注意到脚注再比如多头注意力为什么要降维到64维论文3.2.2节明确说了是“由于减少了每个头的维度总计算成本与全维度的单头注意力相似”这句话是理解多头设计的关键。还有学习率公式它写在3.2节正文里但很多人写代码时随手用了个固定学习率训练不收敛还以为是模型问题。逐句读还有一层好处论文的英文表述本身非常克制几乎没有废话每一句话都对应一个设计决定。翻译的时候如果只按字面来很容易丢掉这层信息。比如“scaled”译成“缩放”是对的但如果不停下来想一下“缩放谁、缩放多少、为什么要缩放”这个词就只是一个名词。所以我在下面每一节里都会先给一段我自己的翻译再讲这句话背后的判断。另外要提醒的是不要一上来就啃模型图。正确的顺序是先读3.1节编码器解码器堆叠结构再读3.2节注意力然后回头看图1最后读3.3到3.5的位置编码、前馈网络和嵌入层。图1是结论的浓缩不是入口。我第一次读时死盯图1结果卡了整整一个下午因为它把六层堆叠、三个子层、两种注意力全画在一起信息密度太高。1.2 必须提前补上的三个概念底子在正式进论文之前有三个概念如果不清楚后面会处处卡壳。第一个是序列到序列任务的典型结构。在这篇论文之前主流的机器翻译模型是编码器-解码器架构编码器把源语言句子压成一个或多个向量解码器再逐词生成目标语言。编码器和解码器内部通常是RNN或LSTM按时间步展开。理解这个背景才能明白论文说的“recurrent”具体指什么也才能明白为什么作者要强调“编码器和解码器都堆叠了6层相同的层”。第二个是注意力机制的来龙去脉。注意力不是这篇论文发明的它最早是作为RNN编码器解码器之间的对齐工具出现的后来有了Bahdanau注意力和Luong注意力。论文的贡献不是提出注意力而是提出只用注意力、不用循环和卷积。这个区别很关键因为很多人误以为论文发明了注意力机制。读2节背景和相关工作时作者自己写得很清楚注意力机制已经是各类序列模型里的标配他们的工作是第一个完全不依赖循环结构的序列转换模型。第三个是自注意力的含义。自注意力是指查询、键、值三组向量都来自同一个序列序列内部每个位置去和所有位置算相似度。这跟传统的编码器解码器注意力不同后者的查询来自解码器、键值来自编码器。论文里两种都用了编码器用自注意力解码器先掩码自注意力再交叉注意力。分不清这三种用法看代码时会一直被Q、K、V是三份还是两份困扰。补完这三个底子再进论文会顺很多。我自己第二次读的时候就是因为先手动实现了一个小号RNN编码器解码器才真正理解“顺序计算”这个瓶颈到底卡在哪里。2. 论文整体架构把一句话拆成三块2.1 摘要与引言作者到底主张什么摘要原文很短核心就一句话的意思当前主流的序列转换模型都基于复杂的循环或卷积网络且包含编码器和解码器而性能最好的模型还通过注意力机制把编码器和解码器连接起来。我们提出一种新的简单网络架构Transformer完全基于注意力机制彻底摒弃了循环和卷积。在两项机器翻译任务上这些模型在质量上更优同时更可并行化训练时间显著减少。这里有几个翻译细节值得停留。原文用的是“dispensing with recurrence and convolutions entirely”dispensing with译成“摒弃”比“舍弃”更贴因为它带有“主动不要”的意味而不是“被动放弃”。另一个是“more parallelizable”我译作“更可并行化”因为这里的比较对象是循环网络循环网络沿时间步串行注意力则是整句一次性算完这是并行度的质变而不是量变。引言部分作者先交代背景循环网络在序列建模中长期占据主导LSTM和GRU在语言建模和机器翻译上取得了大量成果。接着指出循环网络的根本限制隐藏状态h_t由h_{t-1}和当前输入决定这个依赖关系天然排除了并行化序列一长显存和时间的瓶颈就出来了。虽然有不少工作尝试用分解技巧和条件计算来缓解但串行的本质没变。然后作者转向注意力注意力机制已经成为序列建模和转换模型的重要组成部分但几乎总是和循环网络搭配使用。于是他们提出Transformer第一个完全依靠注意力来计算输入输出表示的转换模型不依赖循环也不依赖卷积。引言最后一段给了两个具体成绩和一条自我评价在WMT 2014英德翻译上取得28.4 BLEU超过当时最好结果2个BLEU以上在WMT 2014英法翻译上在八块GPU上训练3.5天后取得41.8 BLEU是当时该任务的最好成绩而且训练成本只是文献中最好模型的一小部分。这段话要结合后面的表2一起看才有直观感受。2.2 模型总览图怎么读从下往上分四层图1是整个论文最容易被低估的部分。它画的不是细节而是数据流。我的建议是把它拆成四层来读从下往上依次是输入嵌入与位置编码层、编码器堆叠层、解码器堆叠层、输出线性与softmax层。最底层左侧是输入嵌入把源语言词元映射成d_model维向量论文里d_model取512。注意论文3.4节提到嵌入层权重乘以了根号d_model这个操作在图上没有画出来但代码里必须实现否则位置编码的数值尺度会把嵌入向量淹没。右侧同理是目标语言的输出嵌入。第二层是位置编码。因为模型里没有任何循环和卷积如果不加位置信息打乱词序得到的注意力结果是一样的。“我打你”和“你打我”在纯注意力下无法区分。所以作者把位置编码直接加到嵌入向量上用正弦和余弦函数生成波长从2π到10000·2π构成等比数列。这种设计的好处是可以外推到比训练时更长的序列。第三层是编码器和解码器的堆叠。论文里编码器和解码器各堆叠6层用N表示。编码器每层包含两个子层多头自注意力和前馈网络。解码器每层包含三个子层掩码多头自注意力、对编码器输出的多头注意力、以及前馈网络。每个子层都套了残差连接和层归一化论文写作LayerNorm(x Sublayer(x))。这个写法很关键是后归一化而不是现在常见的预归一化训练时对学习率预热比较敏感。第四层是最上面的线性层加softmax。解码器输出经过一个线性变换映射到词表维度再softmax得到下一个词的概率分布。论文3.4节提了一句这个线性层和两个嵌入层共享同一份权重矩阵这是减少参数量的经典做法在GPT系列里也一直沿用。读图时还有一个容易忽略的细节所有子层以及嵌入层输出的维度都是d_model等于512这是为了让残差相加时维度对齐。前馈网络内层是2048维属于先升维再降维但输入输出仍是512。这一点如果没注意到写代码时残差连接会直接报维度错误。2.3 砍掉循环和卷积之后代价和收益各是什么论文3.1节最后用一段和表1给出了定量比较这是整篇论文最有说服力的部分之一但很多人读的时候直接跳过了。表1比较了四种层类型自注意力、循环层、卷积层、受限自注意力比较维度是每层计算复杂度、最少顺序操作数、以及任意两个位置之间的最大路径长度。自注意力的每层复杂度是O(n²·d)n是序列长度d是表示维度。循环层是O(n·d²)。卷积层是O(k·n·d²)k是卷积核大小。受限自注意力是O(r·n·d)r是邻域大小。顺序操作数方面自注意力和卷积层都是O(1)循环层是O(n)。最大路径长度方面自注意力是O(1)循环层是O(n)卷积层是O(log_k n)。这三个指标合起来说明了三件事。第一自注意力的并行度最高因为整句一次性算完不存在时间步依赖这是它能用8块P100在12小时内训完base模型的直接原因。第二自注意力的路径长度是常数级意味着任意两个词之间的信息传递只需要一步长距离依赖不会像RNN那样被稀释这对翻译这种经常需要跨句子成分对齐的任务非常关键。第三自注意力的代价是O(n²)复杂度序列变长时显存吃紧这也是后来各种稀疏注意力、线性注意力、滑动窗口注意力出现的原因。卷积层的顺序操作数也是O(1)但最大路径长度是O(log_k n)需要堆叠多层才能覆盖全序列。这解释了为什么后来很多工作又回过头把卷积加回Transformer比如在视觉任务里Swin Transformer用窗口注意力加移位来换取局部性和效率本质上是在O(n²)和O(n)之间找平衡点。我在实际项目里的感受是短序列场景下自注意力的O(n²)完全不是问题512长度下显存和速度都很舒服一旦序列上到几千就必须做取舍了。所以理解表1不是为了考试而是为了在选型时有判断依据。3. 注意力机制逐层精读3.1 缩放点积注意力里那个根号d_k不是随便加的论文3.2.1节把注意力描述成一个映射输入是查询Q、键K、值V输出是值的加权和权重由查询和键的相似度决定。公式写作Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V。先看QK^T这一步。Q的每一行是一个查询向量K的每一行是一个键向量两者做点积得到一个n×n的分数矩阵第i行第j列表示第i个查询对第j个键的匹配程度。这一步是整篇论文的核心也是“注意力”三个字的落点用一个向量去找和它最相关的其他向量。再看除以根号d_k。论文在脚注4里解释了原因假设q和k是独立随机变量均值为0、方差为1那么它们的点积q·k的均值为0、方差为d_k。d_k越大点积结果的方差越大数值分布越分散。softmax在输入数值很大或很小的时候梯度会趋近于0也就是常说的饱和区。除以根号d_k正是把方差拉回1让softmax处在梯度健康的工作区间。我实测过一个对照把d_k设为64不加缩放时softmax输出的最大概率经常在0.99以上梯度几乎为零训练前几千步基本不动加上缩放后概率分布明显更平滑loss下降稳定。这就是为什么这个看似不起眼的除法必须写进代码。最后是softmax的结果乘以V。这一步是加权求和权重之和为1所以输出向量的尺度不会被序列长度放大。要注意掩码的介入位置论文里解码器的自注意力在softmax之前把未来位置的分数设为负无穷这样softmax之后那些位置的权重就是0。如果用代码实现通常是把分数加上一个形状相同的掩码矩阵允许位置加0禁止位置加负1e9。论文还提了一句两种注意力的对比加性注意力和点积注意力在复杂度上接近但点积注意力可以用高度优化的矩阵乘法实现速度快、显存效率高这也是作者选择点积的原因。加性注意力用单层隐层网络计算分数理论上在d_k很大时表现更好但实测中缩放点积已经够用。3.2 多头注意力一次判断不如八次不同角度的判断多头注意力的公式在3.2.2节MultiHead(Q, K, V) Concat(head_1, ..., head_h) W^O其中head_i Attention(Q W_i^Q, K W_i^K, V W_i^V)。先解释为什么不是单头。单头注意力每个位置只能产生一组权重分布注意力被“平均”掉了多个语义关系会被压到同一个分布里容易顾此失彼。多头相当于让模型在不同的表示子空间里各自做一次注意力每个头可以关注不同的模式有的头可能盯着主谓一致有的头盯着指代有的头盯着局部相邻词。最后把结果拼接再线性变换让模型自己决定怎么整合。论文里base模型用h8个头每个头的维度d_kd_vd_model/h512/864。这个取值不是随意的。论文原话是“由于每个头的维度减少总计算成本与全维度的单头注意力相似”。算一下就明白单头情况下d_k512QK^T的计算量是n²·512八头情况下每个头d_k64八次计算的总量是8·n²·64正好等于n²·512。所以多头不是靠堆算力换性能而是在同样算力预算下换取更丰富的表示。大模型big配置则是h16d_model1024每个头64维逻辑一致。实现上有两个必须注意的点。第一W_i^Q、W_i^K、W_i^V这三组投影矩阵在代码里通常合并成三个大矩阵一次性做完投影再按头切分这样比循环8次做小矩阵乘法快得多。第二切分维度时要搞清楚是按哪个轴切形状从(batch, seq, d_model)变成(batch, h, seq, d_k)这里涉及一次reshape加一次transpose轴顺序写错是最常见的bug来源输出不报错但结果全错。我在第一次写的时候就因为把seq和h两个维度搞反训练loss能降但BLEU只有个位数排查了很久才发现。3.3 三种用法别混在一起自注意力、交叉注意力、掩码注意力论文里注意力出现了三次用途完全不同混起来看会很乱。编码器自注意力Q、K、V全部来自编码器上一层的输出所有位置都能看到所有位置没有任何掩码。作用是把源语言句子内部的依赖关系抽出来。解码器掩码自注意力Q、K、V来自解码器上一层的输出但在softmax之前加了掩码位置i只能看到位置i及之前的位置。这是自回归生成的硬性要求生成第t个词时不能偷看第t1个词。论文3.2.3节专门讲了为什么必须掩码并且提了一句他们更倾向于用掩码而不是把整个序列拆成多个子序列分别算因为后者在训练时会带来额外的开销。解码器交叉注意力Q来自解码器上一层的输出K和V来自编码器的输出。这一步是把目标语言当前生成的位置和源语言句子对齐也是传统编码器解码器注意力的位置。注意这里没有掩码因为源语言整句都是可见的。这三种写法在代码里区别只在传入的Q、K、V来源以及是否加掩码模块本身是同一个类。我建议在实现时给这个类加一个mask参数默认None解码器自注意力时传入因果掩码另外两处不传。这样结构清晰也方便复用。补充一个实践中容易忽略的点论文的掩码是上三角为负无穷对角线保留。如果写成上三角包含对角线那当前位置自己都看不到输出会整体崩塌且不易察觉因为loss只是偏高而不是明显异常。4. 从论文到代码关键模块手写实现4.1 先把张量形状约定死能省掉一半的调试时间写Transformer之前第一件事是定好命名和形状约定不然做到一半一定会乱。我习惯用这几个记号B是批大小S是序列长度D是模型维度即d_modelH是头数Dk是每个头的维度即D/H。输入嵌入层输出形状(B, S, D)。位置编码形状(1, S, D)加在嵌入上广播。编码器每层输出保持(B, S, D)。解码器掩码自注意力输出(B, S, D)。交叉注意力的Q是(B, S_tgt, D)K和V是(B, S_src, D)输出是(B, S_tgt, D)。最后线性层把(B, S, D)映射到(B, S, V)V是词表大小。把这张形状表贴在屏幕上写代码时对照着看基本不会出维度错误。有个细节是批大小和序列长度这两个维度名字都容易混尤其在交叉注意力里B相同但S不同广播的时候要特别小心。注意论文里没有出现batch这个维度所有公式都是单样本描述的。落到代码上必须自己补上批量维这是读论文和写代码之间最容易掉链子的地方。4.2 多头注意力的PyTorch实现与逐行说明下面这份实现我用了很多次逻辑和论文完全对齐import math import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): def __init__(self, d_model512, num_heads8, dropout0.1): super().__init__() assert d_model % num_heads 0, d_model必须能被num_heads整除 self.d_model d_model self.h num_heads self.d_k d_model // num_heads # 三组投影合并成大矩阵一次算完再切头 self.W_q nn.Linear(d_model, d_model) self.W_k nn.Linear(d_model, d_model) self.W_v nn.Linear(d_model, d_model) self.W_o nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def split_heads(self, x, b, s): # (B, S, D) - (B, H, S, Dk) x x.view(b, s, self.h, self.d_k) return x.transpose(1, 2) def forward(self, query, key, value, maskNone): b, s_q, _ query.shape s_k key.shape[1] q self.split_heads(self.W_q(query), b, s_q) # (B,H,Sq,Dk) k self.split_heads(self.W_k(key), b, s_k) # (B,H,Sk,Dk) v self.split_heads(self.W_v(value), b, s_k) # (B,H,Sk,Dk) # 缩放点积注意力 scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_k) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn F.softmax(scores, dim-1) attn self.dropout(attn) out torch.matmul(attn, v) # (B,H,Sq,Dk) out out.transpose(1, 2).contiguous().view(b, s_q, self.d_model) return self.W_o(out)几个关键点展开说。第一W_q、W_k、W_v都是nn.Linear(d_model, d_model)论文里的W_i^Q是(D, Dk)的小矩阵八个拼起来正好是(D, D)所以这样实现是等价的而且矩阵乘法效率更高。第二split_heads里的view和transpose顺序不能反。先view把D拆成H和Dk再transpose把H提到前面。如果先transpose再view切出来的头就是错的模型仍然能跑但每个头看到的是乱七八糟的混合维度。第三缩放用的是math.sqrt(self.d_k)而不是sqrt(d_model)因为论文的d_k指的是单个头的维度。这一点在多头场景下极容易写错写错后训练也能收敛只是效果差一截属于隐蔽性很强的坑。第四masked_fill用的值我习惯用-1e9而不是负无穷。用float(-inf)在某些混合精度设置下会产生NaN-1e9在fp16里也能安全表示softmax后基本就是0。第五transpose之后必须加contiguous()再view否则view会因为内存不连续报错。这个错误信息很直白遇到了直接加就行。4.3 位置编码、前馈网络和残差归一化的落地写法位置编码直接用论文公式实现偶数维用sin奇数维用cosclass PositionalEncoding(nn.Module): def __init__(self, d_model512, max_len5000, dropout0.1): super().__init__() pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) self.register_buffer(pe, pe.unsqueeze(0)) # (1, max_len, D) self.dropout nn.Dropout(dropout) def forward(self, x): return self.dropout(x self.pe[:, :x.size(1)])这里有两个工程细节。一是div_term用exp和log计算等价于10000的负指数但数值上更稳定也比直接写pow快。二是pe注册成buffer这样它会跟着模型一起搬到GPU且不会被优化器更新比写成普通张量安全得多。前馈网络在3.3节结构是全连接、ReLU、全连接内层维度d_ff2048外层回到512FFN(x) max(0, xW_1 b_1)W_2 b_2。论文提了一句这可以看作两个核大小为1的卷积。这个视角在理解了卷积网络的人看来会更直观。实现就是两个Linear夹一个ReLU中间加dropout。残差加层归一化按论文写是LayerNorm(x Sublayer(x))也就是后归一化。有个坑必须提前说后归一化在训练初期不稳定必须配合学习率预热否则loss会震荡甚至发散。现在很多开源实现改成了预归一化LayerNorm(x Sublayer(LayerNorm(x)))训练更稳、对预热不敏感但和论文原版有差异。做复现实验时要明确自己用哪种别一边用后归一化一边用恒定学习率。编码器层和解码器层就是把上面几块拼起来。编码器层两个子层解码器层三个子层每个子层外面包残差和归一化。堆叠6层就是N6。这里有一个小细节论文说解码器三个子层里第一个是掩码自注意力第二个是交叉注意力第三个是前馈网络顺序不能换换成交叉注意力在前后模型仍然能训但和论文不是同一个东西对比结果就没意义了。5. 训练配置与复现细节5.1 优化器、学习率预热和批量大小的取值依据论文5.3节的训练配置写得很具体这部分照着抄就行但我想把每个数值的理由说清楚。优化器用Adambeta10.9beta20.98epsilon1e-9。注意beta2是0.98不是默认的0.999。这个改动的原因是训练步数多、梯度噪声大0.98让二阶矩估计跟得更快实测在长训练里更稳。epsilon取1e-9比默认的1e-8更小配合缩放后的注意力分数效果更好。学习率不是固定的按公式调整lrate d_model^(-0.5) · min(step_num^(-0.5), step_num · warmup_steps^(-1.5))warmup_steps取4000。这个公式分两段前4000步线性增长4000步之后按步数的平方根倒数衰减。前一段叫预热作用是在训练初期参数还很随机时避免大学习率把模型带偏尤其是后归一化结构对初始阶段很敏感。后一段衰减是为了后期精细收敛。按base模型d_model512算一下峰值学习率d_model^(-0.5)约等于0.0442在step_num等于4000时step_num^(-0.5)约等于0.0158两者相乘约等于0.0007。这就是峰值学习率训练过程中不会超过这个值。这个数字看起来很小但配合25000个token的批大小正好合适。批量大小按token数算每批约25000个源token和25000个目标token而不是按句子数。这样做的原因是句子长度差异大按句数会导致每批实际token量波动剧烈。base模型在8块P100上训练约12小时共10万步big模型训练30万步约3.5天。这两个数字在表2里也能对上训练成本那一列。还有一个训练技巧是checkpoint平均把最后20个检查点的权重平均起来用。这个方法在论文里一笔带过但收益不小通常能稳定提升零点几个BLEU而且零成本。我在自己的项目里也保留了这个习惯。5.2 正则化三件套dropout、标签平滑和注意力dropout论文5.4节列了三种正则化手段值得单独拎出来讲。残差dropout每个子层的输出在加到残差之前做一次dropoutbase模型dropout率0.1。注意位置是在Sublayer(x)之后、相加之前不是加到之后再drop。注意力权重dropoutsoftmax之后的注意力权重也做dropout同样是0.1。这个操作比很多人想的更有用它逼着模型不要把全部权重压在一个位置上间接鼓励多头分工。标签平滑标签平滑值取0.1。这个技术的做法是把正确类别的目标概率从1降到0.9剩下0.1均匀分给其他类别。它会让模型输出的概率分布不再那么极端虽然会降低困惑度指标的好看程度但能显著提升BLEU和准确率。论文特意提了一句他们用了标签平滑之后模型学到的分布更不确定但翻译质量更高。这个反直觉的现象值得记住困惑度和生成质量不是一回事。三种正则化叠加使用时要注意总强度。我试过把dropout全部提到0.3结果训练前期loss下降很慢最后效果反而变差。0.1这个值是作者调过的base模型够用小数据集上可以适当提高大数据集上可以适当降低。5.3 数据预处理与批处理策略的实际考虑论文5.1节讲数据。英德用了WMT 2014约450万句对英法用了更大的WMT 20143600万句对句子被切成约32000个词元的子词单元。这里“子词单元”就是后来的BPE那一类做法目的是平衡词表大小和未登录词。批处理按近似序列长度分组论文说他们组内句子长度相近一个批大约包含25000个源token和25000个目标token。这个策略的作用是减少padding浪费如果一批里既有5个词的句子又有100个词的句子短句要补到100算力大量浪费在填充位上。按长度分桶之后padding比例大幅下降训练速度提升明显。我自己的经验是分桶的粒度不要太细太细会导致每个批的样本量很小、梯度噪声大也不要太粗粗了padding又浪费。一般按长度分10到20个桶、桶内打乱是个比较稳的折中方案。另一个容易被忽略的是词表共享。论文3.4节说编码器嵌入、解码器嵌入和输出线性层的权重共享。这样做参数量直接减少两倍多而且让输入输出在同一语义空间里对齐。但在源语言和目标语言不同的时候共享嵌入需要先把它们映射到同一个子词词表否则维度对不上。实现时如果用两套独立词表就不要强行共享改成只共享解码器嵌入和输出层。6. 实验结果怎么读以及常见疑问排查6.1 表2和表3里的数字到底在比什么论文表2是主结果表在英德和英法两个方向上和当时的最好模型对比。看这张表要抓三个维度BLEU分数、训练成本、模型参数量。Transformer big在英德上28.4 BLEU比此前最好结果高2个BLEU以上英法上41.8 BLEU训练成本只有对比模型的四分之一左右。这个对比最有力的地方不是分数更高而是“用更少的算力拿到更高的分数”说明瓶颈确实被解开了。表3是模型变体分析作者在英德开发集上做了几组消融。有几个结论值得记住。第一头数不是越多越好单头比8头低约0.9 BLEU但16头并没有明显超过8头说明多头收益有上限。第二key的维度d_k减小会掉分这印证了相似度计算对表达能力的要求。第三模型越大效果越好big比base高1.4 BLEU左右但仍然不如多头带来的收益稳定。第四位置编码换成学习式嵌入和正弦式差距很小说明正弦编码的价值主要在于可外推而不在于性能本身。第五dropout去掉后掉分明显正则化不能省。这几组消融是论文里信息密度最高的部分我强烈建议做复现的人把这几个变体都跑一遍比读十遍论文管用。6.2 常见问题速查表从loss不降到BLEU异常低第一次复现Transformer的人遇到的问题高度相似。下面这张表是我自己踩过和帮别人排查过的典型情况。现象可能原因排查与解决loss完全不降学习率固定、没有预热换成论文的预热加衰减公式warmup设4000loss震荡发散用了后归一化但学习率过高降低峰值学习率或改用预归一化loss降但BLEU个位数多头切分维度顺序写错检查view与transpose的先后顺序生成结果重复同一句缺少因果掩码或掩码位置错确认上三角为负无穷、对角线保留训练几百步出现NaN掩码用负无穷加混合精度改成-1e9并在softmax前统一数据类型位置编码后loss变差嵌入未乘根号d_model嵌入输出乘以sqrt(d_model)或让编码缩放到相近量级推理速度远慢于训练逐词解码未缓存实现增量解码或先用beam search小批量验证显存爆掉序列过长导致注意力矩阵过大缩短序列长度、使用梯度累积替代大batch这张表里最后一行值得多说两句。注意力矩阵是n×n的序列长度翻倍注意力相关显存翻四倍。显存不够时正确做法是减小批大小同时用梯度累积凑出等效批大小而不是简单砍序列长度因为砍长度会直接损失长距离依赖建模能力。论文里的批大小25000 token也是在一批P100上折腾出来的结果硬件不同不要照搬。另外补充一个隐蔽问题训练时如果用的是教师强制推理时却是自回归解码两者之间的分布差异会导致推理阶段表现差一截这就是曝光偏差。缓解手段有调度采样、加噪声、以及标签平滑。论文用标签平滑的一部分动机也在这里。6.3 从这篇论文延伸出去的分支以及我建议的阅读顺序把论文读完之后往下走有几条主线。第一条是预训练语言模型线BERT用编码器堆叠加双向注意力GPT用解码器堆叠加因果注意力两者都是Transformer的直接后代。理解这篇论文里编码器和解码器的分工差异就能明白为什么BERT适合理解类任务、GPT适合生成类任务。第二条是效率线O(n²)的复杂度推动了稀疏注意力、线性注意力、滑动窗口注意力、内存压缩注意力等一系列工作。这些方法的不同点在于它们怎么近似注意力矩阵有的按稀疏模式裁剪有的用核函数重写有的做低秩分解。想搞清楚这些前提是对表1里的复杂度有概念。第三条是跨模态线Vision Transformer把图像切成patch当tokenSwin Transformer引入窗口注意力和移位机制恢复局部归纳偏置语音和视频领域也有各自的自注意力变体。共同点是保留了Transformer的核心骨架只在输入表示和注意力模式上做适配。第四条是工程线混合精度、激活重计算、FlashAttention、张量并行和流水线并行。这些不是论文内容但决定了你能不能把模型真正训起来。我个人建议的阅读顺序是先啃完这篇论文的3.1到3.4节然后动手写一版小模型在玩具数据上跑通再回头读5.3和5.4的训练配置最后去看后续工作的改进点。顺序颠倒的话很容易在还没理解注意力是怎么算的时候就先去研究FlashAttention的显存布局效率很低。最后再分享一个小技巧。如果你想把这篇论文彻底吃透最好的办法不是反复读而是找一个现成的开源实现把里面的每一行和论文逐句对照凡是注释里没写清楚的自己补上并推导一遍。我当时就是这么做的一个六百行的实现对照了两天之后再读任何Transformer变体基本都能在半小时内看懂结构。