我是在2018年第一次认真把《Attention Is All You Need》读完的。那会儿团队里的序列建模主力还是LSTM,机器翻译、文本分类、摘要生成几乎全是它的天下。后来我在一个翻译任务上做了一轮对比实验,把编码器换成 Transformer,训练速度直接上了一个台阶,效果也稳了不少。从那时起,Transformer架构的每一次核心算法调整,我都会认真跟一遍。这篇内容我打算以“演进”为主线,把自注意力、多头机制、位置编码这些底层算法拆开讲清楚,再聊聊稀疏注意力、线性注意力、Swin 这些变体到底在优化什么,最后分享一些手写和调参阶段的实测经验,希望能给正在入门或者想深入理解 Transformer 的同学一点参考。
1. 从循环到自注意力:Transformer 到底改了什么东西
1.1 循环神经网络的三个坎
在 Transformer 出现之前,处理序列数据的主流方案是 RNN 和它的两个改进版:LSTM、GRU。它们的核心思想是“逐步消化信息”——把序列里的元素一个一个喂进网络,靠隐藏状态把之前的信息带到后面。这种方式在处理短序列时表现不错,但一遇到长文本就暴露问题。
第一个坎是长距离依赖。假如让 LSTM 记住一段话的开头信息,然后跨越几百个 token 再使用它,经过多轮门控计算后,信息被反复取舍,能留下的已经很少了。梯度在反向传播时也存在类似问题,连乘效应会让较早位置的梯度变得极小,训练自然就难上去。就算 LSTM 和 GRU 通过门控机制缓解了梯度消失,本质上仍然没有跳出“串联”的限制。
第二个坎是并行性差。循环神经网络的每个时间步都依赖前一个时间步的输出,所以它没法像卷积网络那样一个 batch 直接并行算完。训练 Transformer 之后你会发现,同样规模的语料,用 LSTM 跑要几天,用 Transformer 不用那么久,原因倒不全在模型结构本身,而是 GPU 的利用率差太多了。
第三个坎是建模方式“局部化”。在 LSTM 眼里,当前时刻主要参考的是最近几个时刻的隐藏状态,这给它带来了很强的顺序偏好。可很多 NLP 任务需要的是全局信息——比如判断一个句子里“苹果”是指水果还是公司,必须去看句子后面出现的词。循环架构天然不擅长这类全局互动。
1.2 自注意力机制:一次全局建模的尝试
Transformer 给人的第一印象是“彻底抛弃循环”,只用注意力机制来捕捉序列内部的关系。这就是核心算法演进最关键的一步:把串行计算改成并行计算,把局部视野改成全局视野。
怎么理解自注意力?我常用的一个类比是:你把手里的问题清单递给所有候选答案,让每个答案自己说明跟问题有多少关联,再按关联程度把内容汇总回来。这个“关联程度”是模型自己学出来的,不用人工指定。对一段文本来说,每个词都会跟序列里所有词算一遍关联度,然后按关联度加权融合所有词的信息,所以一次计算就能覆盖整个序列。
这样做带来的变化是本质性的。长度 500 的序列,RNN 要按顺序走 500 步才能把所有信息串起来,Transformer 里每一个 token 一步就能“看到”其余 499 个 token,信息交互效率完全不同。代价也非常明显:传统自注意力的计算复杂度是 O(n²),序列越长,花销越大。这也是为什么后来演进方向里,大量工作都在解决注意力机制在大长度下的效率问题,这部分后文会细聊。
从工程角度看,自注意力还有一个容易忽视的优点:它是“排列可变”的,也就是打乱输入顺序后,如果不加额外信息,结果不会变。这既是好事也是坏事。好事是它天然适合捕捉非顺序依赖;坏事是它完全丢失了位置信息,所以 Transformer 必须额外注入位置编码来做“补救”。这个细节是很多人手写实现时踩坑最重的地方,后面我会专门展开。
2. 核心算法拆解:那些被反复引用的公式到底怎么设计出来的
2.1 Scaled Dot-Product Attention:为什么非要缩放 √d_k
绝大多数 Transformer 变体,核心公式都是这一个:
Attention(Q, K, V) = softmax(QKᵀ / √d_k) V
Q 是查询,K 是键,V 是值。通俗讲,Q 相当于“我在找什么类型的信息”,K 相当于“每个位置自带的信息标签”,V 相当于“每个位置真正的内容”。QKᵀ 算出来的点积越大,说明查询与标签匹配度越高,softmax 之后就会给这个位置更高的注意力权重,最后用这些权重对 V 做加权求和。
为什么要除以 √d_k?这是很多初学者第一个问的问题。假设 Q 和 K 每个分量的期望都是 0、方差都是 1,那么两者做内积后,结果的期望是 0,方差会变成 d_k。也就是说,当 d_k 变大时,点积结果的尺度也会变大。
点积数值一大,softmax 就会进入饱和区,这会产生两个问题:一是梯度会变得非常小,训练效率极低;二是有没有加缩放,注意力分布会非常陡峭,几乎变成 one-hot 形式,模型就失去了“软”注意力那种平滑加权的表达能力。除以 √d_k 之后,点积的方差被拉回 1 左右,softmax 不会过度饱和,模型能学到更细致的注意力分布。这个设计看着不起眼,但在我调过的不少模型里,去掉缩放因子训练几乎都不稳定。
2.2 Multi-Head Attention:多头到底在做什么
单一注意力有个局限:它只能计算一种“匹配关系”。可真实文本里的关系是多种多样的——可能是句法关系,可能是指代关系,也可能是语义相关性。为了捕捉这些不同维度的关系,论文提出把 Q、K、V 分别投影到多个子空间里,各自做注意力,再把结果拼起来,这就是 Multi-Head Attention。
我用一个比较生活化的方式来理解多头机制。就像你开一个侦探会,一个侦探盯着时间线,一个侦探盯着人物关系,一个侦探盯着地点切换,最后大家把发现汇总到一起,就能还原出全貌。每个注意力头其实就是在学一种“匹配规则”,有的头可能对局部 n-gram 敏感,有的头可能对远距离核心词敏感,它们的分工不是人为设定的,是训练中自己分化出来的。
从数学实现上看,多头注意力的计算方式是把头的数量 h 作为一个维度,把 d_model 维的输入拆成 h 份,每份 d_k = d_model / h,各自做 Scaled Dot-Product Attention,然后拼接并通过一个线性层融合。这种操作放在 GPU 上可以完全并行,几乎不增加额外的计算负担,这也是多头发明的精妙之处。
实际使用中,头的数量并不是越多越好。我见过不少项目把 h 从 8 改成 16、32,结果训练变慢不说,效果有时候反而会下降,因为每个头分到的维度太少,表达能力不足。一般还是按论文默认来,d_model=512、h=8 是一个比较稳的组合。
2.3 位置编码与网络主体:少了哪块都转不起来
自注意力是“不认位置”的,所以 Transformer 必须显式告诉模型每个 token 在序列里的位置。原始论文用的是 sin 和 cos 函数生成一组确定性的位置向量:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
这种设计有两个好处:一是不用学习,省参数;二是理论上能把模式推广到比训练序列更长的位置。实际跑下来你会发现,位置编码的数值在长序列里会变得非常小,对位置变化的区分度下降,所以在一些长文本任务里,很多人会改用可学习的位置编码,也就是直接把位置索引映射成可训练的向量。
Transformer 主体除了多头注意力,还有两个关键组件:前馈网络和层归一化。每个 Transformer Block 里,经过注意力后会把结果喂进一个两层 MLP(通常是 4 倍 d_model 的宽度),中间用 ReLU 激活,然后再做一次残差连接和 LayerNorm。前馈网络的作用是对注意力输出做非线性变换和特征增强,它占据了模型大部分参数,某种意义上承担了“存储记忆”的功能。
关于 LayerNorm 的位置也值得一提。原始论文用的是 Post-LN,也就是“注意力/FFN -> 残差 -> LayerNorm”的顺序。但很多人在训练深层 Transformer 时发现 Post-LN 容易出现训练不稳定的问题,于是后续 GPT、BERT 的很多实现改成了 Pre-LN,也就是“LayerNorm -> 注意力/FFN -> 残差”。Pre-LN 训练更稳,收敛更快,但表达能力理论上略弱一些。我自己的实践是:层数浅的话两者差别不大,层数深了还是 Pre-LN 省心。
3. 算法演进的主线:从原始论文到三大变体方向
3.1 两条主流血脉:Encoder 双向与 Decoder 自回归
Transformer 原始结构是 Encoder-Decoder,但后续演进很快分化出两条独立的血脉。一条是 Encoder-only 的双向理解模型,代表是 BERT;另一条是 Decoder-only 的自回归生成模型,代表是 GPT 系列。今天绝大多数大语言模型走的是后者的技术路线。
BERT 的做法很取巧:去掉 Decoder,只保留 Encoder,用“完形填空”的方式做预训练。输入句子时随机遮挡一些 token,让模型预测被遮住的词,由于 Attention 能同时看到左右两侧的上下文,所以它是双向的。这种设计特别适合语义理解任务,比如文本分类、命名实体识别、句对匹配。我早期做分类任务时,用 BERT 直接替换 LSTM 和词向量组合方案,效果提升非常明显,而且稳定。
GPT 则只保留 Decoder,并且用“预测下一个词”做预训练。它在自回归解码时引入了因果掩码,也就是当前位置只能看到它之前的位置,不能偷看未来。很多人觉得这种单向设计在理解上吃亏,但当模型规模足够大时,自回归目标反而能倒逼模型学到更强的语言模式。从 GPT-3 开始,这条路线一路崛起,最终成为大模型时代的主流底座。
这两条血脉也说明了 Transformer 是名副其实的“万能框架”——同一个核心算法,既能做理解,也能做生成,关键在于任务形式和注意力掩码怎么设计。理解这个之后,你就会发现很多变体其实只是在掩码结构、输入输出头上有差异,底子还是那套自注意力体系。
3.2 高效注意力:稀疏化、线性化与算子级优化
原始自注意力的 O(n²) 复杂度,在长文本场景是个大问题。Sequence 长度从 512 涨到 8192、16384 时,显存和计算量会爆炸式增长,所以高效注意力成了一个庞大的演进分支,大致有三条路。
第一条路是稀疏注意力。代表工作是 Longformer 和 BigBird。它们的想法很简单:不是所有位置都两两计算注意力,而是让每个 token 只跟邻近窗口内的 token、以及少量全局 token 计算注意力。这样复杂度从 O(n²) 降到了 O(n)。代价是实现复杂,稀疏 mask 的访问模式会影响 GPU 的计算效率,实际加速往往达不到理论预期。
第二条路是线性注意力。代表工作是 Performer 和 Linear Transformer。它们把 softmax 核函数近似成可用矩阵乘法顺序换换展开的形式,也就是用核方法把 QKᵀ 先算,变成先算 KᵀV 再与 Q 相乘,把复杂度降到 O(n)。这类方法理论上优雅,但实际实现时的近似误差会影响模型精度,在精度敏感的任务上需要多花功夫调。
第三条路是算子级优化。代表是 FlashAttention。它没有改变注意力计算数学本质,而是在 IO 层面做了文章——通过分块计算,避免把完整的注意力矩阵写回显存,让计算在 GPU 的 SRAM 上完成合并,显著减少显存占用和 IO 开销,训练速度也随之提升。这条路线是我现在最推荐的:算法不变、模型不改,只改工程实现就能拿到明显收益,而且效果无损。
3.3 跨领域改编:ViT、Swin 与多模态融合
Transformer 的另一条演进方向是跨出 NLP 边界。ViT 把图片切成固定大小的 patch,把每个 patch 当成一个“token”送进 Encoder,成为计算机视觉领域第一个大规模成功的纯 Transformer 模型。Swin Transformer 在此基础上引入了层次化金字塔结构和窗口注意力,先把特征图分成不重叠的窗口,在每个窗口内做注意力,再在相邻层之间做窗口偏移,让信息能跨窗口流动。
Swin 的高明之处在于,它解决了 ViT 的两个痛点:一是计算复杂度不再随图像尺寸平方增长,而是线性增长;二是特征从局部到全局的层次结构与 CNN 体系更兼容,适合做检测、分割这类需要多尺度特征的任务。我在一次语义分割项目里试过把 ResNet 骨干换成了 Swin-T,配合简单的 FPN 头,mIoU 提升了好几个点,验证了它在稠密预测任务上的价值。
同样值得关注的还有多模态融合 Transformer。多模态融合 Transformer 会把文本 token 和图像/视频特征统一投影到同一个特征空间,再做跨模态注意力。比如遥感图像语义分割任务里,模型同时接收图像特征和地理位置/时间等辅助信息,用多层级融合模块让两种模态的特征相互修正。这类方法的演进方向本质上是让 Transformer 的注意力机制充分发挥跨模态关联能力。
4. 实操记录:手写小型 Transformer 与调试心得
4.1 手写时最容易翻车的三个细节
“纸上得来终觉浅”,如果你一直只调现成库,很难理解 Transformer 内部细节。我强烈建议手写一个极简版本,哪怕只在玩具任务上跑通。写的时候有三个细节最容易出问题。
第一个是掩码处理。Transformer 里有两类掩码:padding mask 和 casual mask。Padding mask 是把填充位置的注意力权重设为“无效”,标准做法是在 softmax 之前把掩码位置的 logit 加上一个非常大的负数,比如 -1e9,而不能直接设为 0,否则 softmax 会把权重分配到填充位置。Causal mask 用于自回归生成,做法是构造一个上三角矩阵,把对角线以上的位置全部 mask 掉。很多人把这两种 mask 搞混或者叠加出错,导致模型训练时“作弊”或者注意力泄漏。
第二个是维度对齐。多头注意力在实现时常需要做 reshape 和 transpose,如果你把 batch、head、seq_len 的维度顺序搞乱,代码不报错但结果是错的。我一般的习惯是:先把 [B, L, D] reshape 成 [B, L, H, d_k],再 transpose 成 [B, H, L, d_k],最后用 einsum 或 matmul 计算,每一步都把形状打印出来核对一遍。
第三个是参数初始化。Transformer 对初始化比较敏感,残差网络的输出分布会叠加。实践里我在残差分支的最后一层线性层上做小标准差初始化,或者干脆从零开始先用小模型把整体跑通,再慢慢放大 d_model 和层数。直接用默认初始化跑深层模型,非常容易出现训练震荡。
4.2 用 Transformer 预测正弦数据的完整踩坑
热词里有一条是“transformer预测正弦数据”,这是个很好的入门实操题,但也真的是一个容易让人怀疑人生的任务。正弦函数是周期函数,局部信息不够,必须让模型看到至少一个完整的周期,否则预测会稳定地错半拍。
我当初的做法是:把数据集按固定窗口切分,每个样本用过去 100 个点预测未来 50 个点。先对数据做归一化,因为正弦信号范围是 [-1, 1],如果叠加了噪声或者偏移,不归一化会让注意力机制把精力浪费在数值大小上。训练时我用的是简单的 MSE 损失,优化器选 Adam,学习率 1e-3 搭配 warmup,batch size 64,模型只用了 2 层、d_model=64、4 个头。
看起来配置很低吧?实际试的时候照样踩坑。第一次跑,loss 很快就掉到很低,但预测出来的曲线几乎没有波动,基本是一条直线。后来排查发现,是因为输出窗口太长,模型学到的是“输出接近均值”这种保守解。解决办法是把输出窗口从 50 逐步减到 10,同时把预测方式改成递归式,也就是每次只预测下一个点,再把预测结果拼回输入继续往后推。这样效果立刻好了,曲线形状能大致跟上,但在波峰和波谷附近会有明显的相位偏移。
这个例子让我体会很深:Transformer 对序列数据的建模能力强,但它不是“算命先生”,数据窗口和信息充分性必须由实验者自己保证。很多人一上来抱怨模型不行,其实往往是把任务设计得不合理。
4.3 训练不收敛与显存爆炸的排查速查
这里整理一份我在实战中反复用到的排查清单,按经验排序,命中率从高到低:
| 症状 | 最常见原因 | 排查与解决方向 |
|---|---|---|
| loss 不降或震荡 | 学习率过高、warmup 缺失 | 减小 lr,加入 warmup 再试 |
| loss 下降后发散 | 梯度爆炸 | 加梯度裁剪,clip_norm 设 1.0 |
| 预测结果全是均值 | 输出目标太长 | 缩短输出窗口,改为递归预测 |
| 显存 OOM | batch 过大、序列过长 | 减小 batch,梯度累积,或使用 FlashAttention |
| 训练很慢但 loss 正常 | 自注意力 O(n²) 瓶颈 | 换线性/稀疏注意力,或减少头数 |
| 位置信息没生效 | 忘了加位置编码 | 检查输入是否拼接/叠加了 PE |
梯度裁剪是我强烈推荐的默认操作。Transformers 在深层堆叠后,梯度的范数经常会突然变得很大,Adam 虽然能缓解,但不如 clip 直接。我几乎每个 Transformer 训练脚本里都会加一行clip_grad_norm_(model.parameters(), 1.0),省了很多不必要的调试。
混合精度训练也值得开启。用 FP16 或 BF16 之后,显存占用能下降近一半,在 RTX 系列上训练速度也能明显提高。只是要注意,如果 loss 出现奇怪地跳到 NaN,先关掉 AMP 试一试,很多时候是部分算子在 fp16 下的数值溢出导致的。我的经验是,LayerNorm、softmax 这些组件尽量保持 fp32 计算,能避开大部分数值问题。
5. 演进仍在继续:近期值得关注的方向
5.1 状态空间模型与超长序列
Transformer 的 O(n²) 复杂度始终是向更长序列推进时的天花板,所以近年的演进方向之一是结合状态空间模型。这类模型把序列建模转化为一个“线性时不变系统”的递归计算,复杂度是 O(n),同时保留了类似注意力的“选择性信息过滤”能力。这类架构在很多基准上逼近甚至超过同规模的 Transformer,但它也面临一个大问题:序列顺序固定,无法像 Transformer 那样轻松处理多模态和跳步依赖。
从算法演进角度来看,我不认为状态空间模型完全取代 Transformer,更多是激发了一轮“混合架构”探索——比如在某些层保留自注意力,在另一些层用高效递归模块替代,在不牺牲效果的前提下拉长序列上限。这类讨论在社区里很活跃,你也能在不少新发布的模型中看到它们的影子。
5.2 工程架构层面的协同演进
Transformer 的算法演进不只发生在网络结构内部,也深刻影响了一层部署与工程架构。大模型时代,模型服务化之后,推理系统普遍采用类似“前置代理 + 动态批处理 + 流式返回”的服务架构,把在线请求按 batch 聚合起来喂给 GPU,最大化利用注意力计算的高并行特性。
顺着这个角度看,你会发现在“LLM + API 架构”“AI Agent 主流架构”这些讨论背后,底座仍然是 Transformer 及其注意力机制。Agent 系统在用户请求和工具调用之间来回切换,每次交互都要经过 token 化、上下文拼接、注意力计算、增量推理这一套流程,本质上是让同一个 Transformer 内核高效地处理多轮、多角色的对话序列。
我自己做过的不少项目里,真正让效果跃升的关键往往不是某个花哨新模块,而是把现有 Transformer 的输入组织方式、掩码设计、位置编码策略打磨到位。这也印证了一个朴素的判断:核心算法演进,从来不是单独某一层的翻新,而是结构、优化和工程三方共同推进的结果。
回头再看,从 LSTM 到 Transformer 是一轮根本性的范式转向,从原始 Transformer 到高效注意力、视觉与多模态变体、再到工程架构适配,每一步都是在解决前一步遗留的具体问题。如果你正在读代码、改模型、调参数,我的建议很简单:别急着追新架构,先把自注意力里 Q、K、V、mask、位置编码这几个核心环节亲手过一遍,哪怕只是在正弦预测这样的小任务上跑通,你后续读任何一篇论文都会轻松很多。