
1. 为什么Transformer能通吃NLP和CV先搞清它解决的是个什么问题我一直觉得很多人对Transformer模型的最大误解不是看不懂那张架构图而是不知道它到底在解决什么问题。你问一个刚入门的人Transformer是什么他能告诉你有Q、K、V有自注意力有编码器和解码器。但你再问他一句这玩意儿凭什么能把循环神经网络和卷积神经网络挤下牌桌他多半答不上来。这篇文章我想把所有关键点重新捋一遍重点不是堆公式而是把结构图和注意力机制一张一张“画”出来用文字图解的方式讲明白。1.1 所有序列模型都逃不过“长距离依赖”这个坎在Transformer出现之前NLP里处理序列的主流工具是RNN、LSTM这类循环神经网络。它们的逻辑很直白一个token接一个token地读用一个隐状态把前面的信息往后传。问题是这条路是单行线信息传得越远丢得越多。你让LSTM去读一篇1000字的文章想让它对齐第50个词和第800个词之间的关系它往往做不到。CNN在图像上很强但它在处理自然语言时同样别扭。卷积核只在一个局部窗口里看东西想看到全局就得把网络堆得很深层数一多优化难度和计算量都上去了。就算到了后来把卷积核做得很大本质上还是在用“逐步扩大感受野”的笨办法。所以序列建模真正难的点是如何让模型在每一步都能直接看到整条序列里的任意一个位置。Transformer的答案非常简单粗暴——彻底不要循环也不要卷积只靠注意力机制。注意力机制可以计算任意两个token之间的关联权重一步到位建立全局依赖。1.2 2017年那篇论文的真正贡献给序列建模换了个思路很多人以为《Attention Is All You Need》发明了注意力机制其实不是。注意力机制在2015年就被用在机器翻译的Seq2Seq模型里了当时它只是作为RNN的辅助组件存在。Transformer真正的贡献是把注意力从“辅助”变成了“全部”用纯注意力搭建了整个序列模型。原始论文做的是机器翻译任务。输入一句英文输出一句中文中间没有RNN没有CNN只靠多头注意力和前馈网络堆叠。这听起来简单但效果非常惊艳训练速度远快于循环模型并且翻译质量更好。此后不到两年这个架构衍生出了BERT、GPT、ViT等一大票模型席卷了自然语言处理和计算机视觉。所以你在理解Transformer时最好带着一个核心视角它本质是一个“让序列里的任意两个位置直接通信”的框架。后面所有概念包括位置编码、多头注意力、残差连接都在为这个目标服务。2. 编码器-解码器整体架构用一张文字图看懂数据怎么流Transformer原始结构是标准的Encoder-Decoder架构。如果你只接触过BERT或GPT可能会觉得Transformer模型就是一堆Transformer块堆在一起。没错但原始版里分为“读入端”和“写出端”两端职责不同理解清楚这点后面看变体就容易多了。2.1 数据从输入到上下文表示的完整路径先看图这是我把架构简化后的文字版输入句子: [CLS] 我 爱 自然 语言 处理 [SEP] │ ▼ ┌─────────────────────────┐ │ Token Embedding │ 每个token得到一个d_model维向量 └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ 位置编码Position │ 叠加绝对位置或相对位置信息 └─────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Encoder Layer × 6/12/24 │ 每一层内部 │ │ 1. 多头自注意力 │ Multi-Head Self-Attention │ 2. 残差连接 LayerNorm │ ↓ │ 3. 前馈网络FFN │ Add LayerNorm │ 4. 残差连接 LayerNorm │ ↓ │ │ Feed-Forward Network │ │ ↓ │ │ Add LayerNorm │ └─────────────────────────────────┘ │ ▼ 上下文表示序列 H编码器里的每一层都是“自注意力 前馈网络”的组合。自注意力负责让不同位置的token互相交换信息前馈网络负责对每个token的语义做一次非线性变换。这里有一个经常被忽略的点前馈网络是“逐位置”的也就是说它对序列里的每个token独立执行同样的一组参数不会跨token做混合。真正让token之间产生交互的地方只有注意力模块。所以你可以把编码器理解成两件事的交替先开会讨论注意力再各自写会议纪要FFN。开会时大家互相看写纪要时关起门来自己算。2.2 解码器里为什么多了一个“交叉注意力”解码器负责生成目标序列。它的结构和编码器很像但有三个明显区别。第一解码器里的自注意力带了一个mask只能看到当前位置及之前的位置不能看未来的token。这叫做自回归限制。如果让解码器在预测第3个词时直接看到第5个词那就等于开卷作弊生成任务就没有意义了。第二解码器在自注意力和前馈网络之间多了一个交叉注意力模块。这个模块的Q来自解码器K和V来自编码器的输出。它的作用是让解码器在生成每个目标词时都去源语言里寻找需要参考的信息。比如中英翻译输出中文“我”的时候要不要看英文句子里“I”这个位置交叉注意力就是干这个的。第三解码器最后接了一个线性层和Softmax用来把隐藏状态映射成词表上的概率分布。2.3 编码器和解码器怎么配合训练机器翻译模型时输入是英文句子输出是中文句子。英文句子过编码器得到一组上下文表示中文句子过解码器但每次预测某个位置时它只能看到中文句子里该位置左侧的内容同时通过交叉注意力去查英文句子里的内容。你可以把交叉注意力理解成“带着问题去查资料”解码器当前生成的词是“问题”Query编码器输出的所有位置就是“资料库”Key/Value。这个模块在原始Transformer里出现但在后来的纯解码器模型比如GPT里反而消失了。原因很简单GPT不做翻译不需要读入一段额外的源文本它只需根据自己已经生成的内容继续往后写。3. 注意力机制的分步可视化Q、K、V一个都不能少注意力是整个Transformer的心脏。很多人卡在这其实是因为没想清楚Q、K、V在现实里对应什么。我换一个生活化的说法你在一家大型图书馆里查书。3.1 用查图书馆理解Q、K、VQuery查询是你心里想找的那类书的关键词比如“机器学习”。Key键是每本书书脊上贴的检索标签比如“深度学习”“概率论”“食谱”。Value值是书本身的正文内容。你的查询会和每个Key做相似度匹配匹配度越高说明这本书越可能对你有用。然后你不是把整本书原样拿过来而是按匹配度把不同书里的相关章节拼起来得到最终的信息。这个过程对应注意力的三步计算Query与所有Key的相似度用Softmax把相似度变成和为1的权重按权重对所有Value做加权求和放在句子里也一样。假设一句话是“小明喜欢小狗因为它很可爱”。模型在处理“它”这个token时需要搞清“它”指代谁。此时“它”对应的Query会与句子中所有位置的Key比较。高概率它会发现“小狗”那个位置的Key更匹配所以从代表“小狗”的Value里取信息。这个过程也不一定是非此即彼可能是一个分布40%关注“小狗”30%关注“小明”剩下分散到其他词。3.2 缩放点积注意力的公式和那个除法公式只有一行Attention(Q, K, V) softmax( Q × K^T / sqrt(d_k) ) × V其中Q、K、V都是矩阵d_k是每个注意力头里Key向量的维度。为什么需要除以sqrt(d_k)这得从点积的分布说起。如果Q和K中的元素来自标准正态分布也就是均值0、方差1那么两个d_k维向量的点积结果的均值是0方差却是d_k。d_k越大点积的数值范围就越大。想象一下点积结果很大比如20、30那么Softmax以后几乎所有的概率都会压在最大值那一个位置上其他位置的梯度会变得非常小模型就学不动了。除以sqrt(d_k)是为了把方差重新拉回到1附近让Softmax的输入保持在一个梯度友好的范围内。这个操作虽然简单但直接影响训练稳定性。3.3 注意力矩阵长什么样注意力权重矩阵是形状为“序列长度 × 序列长度”的方阵行代表Query位置列代表Key位置。下面是一个四词句子的注意力权重示意我 爱 NATURE 语言 我 0.42 0.28 0.17 0.13 爱 0.20 0.45 0.25 0.10 NATURE 0.11 0.16 0.55 0.18 语言 0.08 0.21 0.35 0.36每一行经过Softmax后和为1。第i行第j列的值表示第i个token在更新自己的表示时从第j个token那里吸收了多少信息。对角线通常比较大因为token天然和自己是相似的这也是为什么残差连接在不同层之间存在某种“身份保护”。你还可以把多头注意力的多个矩阵叠加起来看有的头更喜欢看对角线表示它在做局部特征有的头非对角线也存在高权重说明它在做长距离依赖。这是后面多头注意力存在的意义。3.4 多头注意力到底拆出了什么多头注意力做的事情不复杂不直接对d_model维的Q、K、V做一次注意力而是把Q、K、V切成h份每份d_k维分别独立做注意力最后把h份结果拼接起来再线性变换。这样做的好处是一个注意力头只能学习一种“关系模式”多头则可以在不同子空间并行学习多种关系。例如注意力头可能学到的关系头1关注相邻词类似局部语法结构头2关注指代关系比如代词对应名词头3关注句法依赖比如动词搭配主语宾语头4关注远距离的语义转折实际训练中头的作用不一定会这么清晰划分但思路是成立的。多头把“关注谁”这件事从单数变成了复数让模型有更强的表达能力。4. 位置编码给每个token发一个座位号注意力机制本身对顺序完全无感。你把句子里词的位置乱换只要每个token还是那些token注意力矩阵里的权重计算结果是相同的。这显然是错的。比如“猫追狗”和“狗追猫”词相同但语义完全相反。所以必须把位置信息硬塞进模型。4.1 为什么需要位置信息你把“我打你”和“你打我”丢进不带位置编码的Transformer里两个句子在模型眼里是同一串token的不同排列注意力机制却会给出几乎一样的结果。比如“打”同时和“我”“你”都有关系但谁是施事者、谁是受事者模型不知道。早期NLP任务常用词向量直接相加但词向量本身没有顺序。位置编码就是为了给每个token标一个“第几个位置”的编号让模型能区分顺序。4.2 原始Transformer的正弦余弦位置编码原始论文使用了一组正弦函数和余弦函数PE(pos, 2i) sin(pos / 10000^(2i/d_model)) PE(pos, 2i1) cos(pos / 10000^(2i/d_model))这里pos表示token在序列里的绝对位置从0开始i表示向量的维度下标。你可以把这一串数想象成一个波长从2π一直变化到10000×2π的“坐标系统”。这么设计的玄机在于不同维度对应不同频率频率低的维度变化慢频率高的维度变化快。于是位置0和位置1之间的差异在若干维度上都很明显而位置500和位置501之间的差异在高频维度上可能还明显但在低频维度上已经几乎一样。这样模型既能区分相邻位置又能感知远处的相对距离。还有一点值得注意三角函数有一个天然优势。通过线性变换可以用位置p的编码去表示位置pk的编码也就是说模型有机会学到“相对位置”的概念而不是死记硬背绝对编号。实际使用中位置编码是加在Token Embedding上的而不是拼接这样不会增加额外维度。4.3 现代位置编码的演进绝对、相对与旋转原始正弦位置编码是“绝对位置编码”因为它编码的是token的绝对位置号。后来人们发现有时候模型更该关注两个token“间隔多远”。相对位置编码会在注意力打分时加上一个距离偏差比如“相邻词权重0.2隔了10个词权重-0.5”。近年来大模型里最流行的做法是旋转位置编码RoPE。它的核心思想是把每个向量看作复数空间里的一个向量然后按绝对位置旋转一个角度。两个向量做内积时结果只与它们的相对角度差有关也就是相对位置。RoPE在LLaMA系列里大量使用既能建模相对距离又保持了绝对位置的一定信息。还有一种更简单的ALiBi方法它在注意力分数上直接加一个和相对距离成正比的负偏置距离越远惩罚越大。这个方法外推能力很强训练时序列长度短也能在长文本上凑合着用。这里要说一个工程上常见的外推问题如果模型只在2048长度上训练过直接让它推理8192长度的文本效果通常会崩。原因很简单位置编码没见过那么大的数值。解决办法要么是位置插值把新位置压缩到训练范围内要么是改RoPE的底座参数。这已经是长文本大模型优化里的一个重要方向了。5. 残差连接、层归一化和FFN深层Transformer不崩的三大支柱注意力确实是主角但真正支撑模型堆到几十层不崩的是三个看起来不起眼的配角前馈网络、残差连接、层归一化。5.1 FFN每个token都会经过的“加工车间”前馈网络的结构是FFN(x) W2 × GELU(W1 × x b1) b2第一层把d_model维的向量升到4倍维度激活后第二层再降回d_model。这个过程在每个token上是独立执行的也就是说同一个FFN被序列里的每个token共享参数。它不做token之间的通信只对单个token的语义表示做非线性变换。为什么要这一步注意力模块本质上是在做“加权求和”这是一种线性操作。即便叠加很多层如果不引入非线性整个模型的表达能力非常有限。FFN提供的非线性让模型能在每个token身上做更复杂的特征组合比如把一个token的语法角色、词义、上下文信息揉在一起。实际中FFN占据了Transformer大部分参数。一个d_model为768FFN维度为3072的BERT-base两个FFN矩阵的参数量就能接近总参数的2/3。所以不少轻量化尝试都在压缩FFN而不是动注意力头。5.2 残差连接给梯度修一条高速公路“残差连接”就是下面这行output x Sublayer(x)注意力或FFN计算出的结果先加上原来的输入x再做归一化。这样做最直接的好处是反向传播时梯度可以沿着“x”这条短路回传不容易在几十层里衰减。没有残差连接Transformer深了以后会有明显的优化困难loss下降慢甚至不降。有了残差连接哪怕堆到几十层每一层的输出和输入差别也不会太大训练更稳。这里必须提一下Pre-LN和Post-LN的区别。原始Transformer用的顺序是“注意力 → 残差 → LayerNorm”这种叫Post-LN。后来很多模型把LayerNorm放到了子层之前变成“LayerNorm → 注意力 → 残差”这种叫Pre-LN。Pre-LN在深层网络上训练更稳定很多开源大模型都改成Pre-LN了。代价是最终准确率有时略低于调好的Post-LN但工程上稳定性优先。5.3 LayerNorm为什么不是BatchNormLayerNorm层归一化是针对每个样本、每个token的所有特征维度做归一化计算出均值和方差然后归一化到均值0方差1。从形式上看它和BatchNorm很像但归一化的方向完全不同。BatchNorm在NLP里不好用原因有几个第一文本序列长度往往不一样一个batch里的短句会被paddingBatchNorm统计均值方差时会被这些无效位污染。第二训练和推理时的统计口径不一致。BatchNorm推理时通常用滑动平均小batch下效果波动大。LayerNorm在训练和推理时都是直接算当前样本本身的统计量行为一致。第三Transformer里每个token的表示理论上都是独立可解释的LayerNorm更符合“每个token自我归一”的语义而BatchNorm强行拉平不同样本的全局统计反而会丢失个体差异。LayerNorm之后通常会加两个可学习参数γ和β用来缩放和平移归一化后的结果。千万别小看这两个参数它们是LayerNorm恢复表达能力的关键。6. 训练一个Transformer前要定好的工程参数很多人看Transformer理论的时候很顺一到自己训练就废。原因在于Transformer对超参数非常敏感尤其学习率和初始化稍有偏差就会loss起飞。6.1 常用超参数组合不同模型规模的Transformer超参数一般遵循一些约定配置d_model层数注意力头数FFN维度总参数量约Tiny12824512几千万Base768121230721.1亿左右Large1024241640963.4亿左右大模型常用4096323216384百亿级这里注意一个关系多头注意力的d_model要被头数整除。每个头的维度是d_model / h比如d_model768h12则每个头维度d_k64。头数不是越多越好头数太多每个头的维度过低表现力反而下降。6.2 优化器、warmup和学习率衰减是稳定训练的关键Transformer原论文使用的优化器是Adam超参数为β10.9β20.98epsilon1e-9。同时学习率不是一上来就用大值而是先线性warmup到一个峰值然后再按步数倒数衰减。原文里warmup_steps设置为4000。为什么需要warmup训练初期参数是随机的注意力矩阵的输出分布非常不稳定。如果第一步就把学习率拉满很容易把参数冲到一个坏区域后面再也救不回来。warmup相当于先让模型用小步快走的方式探路等注意力分布稍微稳定了再放开步子。如果你自己训练一个小Transformer可以从“max_lr1e-4warmup_ratio0.1cosine衰减”开始调。我发现很多loss暴增的问题第一排查项基本都是学习率太大其次才是数据里有脏标签。6.3 显存估算和batch选择注意力矩阵是主要敌人训练Transformer时最耗显存的往往不是模型参数而是中间激活和注意力分数矩阵。单层单个注意力分数矩阵的大小是batch_size × 头数 × 序列长度 × 序列长度 × 4字节举个例子batch_size8头数16序列长度1024。那么单个注意力矩阵的显存是8 × 16 × 1024 × 1024 × 4 536,870,912字节 ≈ 512MB这还只是一层里的一个中间张量反向传播时还需要保存梯度实际占用还会翻几倍。如果你还开着梯度检查点还需考虑额外开销。所以长序列场景下学术界和工业界都会做几件事使用FlashAttention通过kernel融合和分块计算减少对完整注意力矩阵的显存占用使用梯度检查点牺牲一点计算速度换取激活值不常驻显存使用梯度累积用小batch多次前向后再更新参数等效大batch手动截断或滑窗处理超长序列这些手段在搞长文本任务时几乎缺一不可。7. Transformer家族的三条主要路线BERT、GPT和ViT原始Transformer是编码器-解码器结构但它后来演化成了三个分支只保留编码器的BERT只保留解码器的GPT以及把图像当序列的ViT。7.1 BERT所有注意力都双向适合理解任务BERT只用编码器部分。它预训练时会随机遮住句子里15%的token让模型根据上下文双向预测被遮住的词。因为双向可以看到左右两侧的信息BERT非常适合文本分类、命名实体识别、语义相似度这类理解任务。BERT的输入除了token embedding和位置编码还加了segment embedding来区分句子A和句子B。它的位置编码是可学习的而不是原始的正弦余弦。可学习位置编码的上限好处是灵活坏处是一旦超过训练长度外推性更差。7.2 GPT只留解码器适合生成任务GPT只保留了解码器部分并把交叉注意力去掉只做带mask的自注意力。训练目标是给定前文预测下一个token。这个范式叫自回归语言建模。日常用到的对话大模型、写作助手基本都是这种结构。它和你对话时其实是不断循环“读入已有文本预测下一个token把结果拼回去再预测下一个”。这也是为什么它生成慢因为每一步都只能蹦一个token。为什么只留解码器也能这么强因为语言生成本身就是一个自回归过程。预训练数据量足够大时这种简单的“预测下一个词”任务能让模型学到非常丰富的语义和世界知识。后来大家把这类模型统称为“仅解码器大语言模型”。7.3 ViT把图片切成patch的注意力模型ViTVision Transformer把视觉问题转成序列问题。做法不复杂把一张图片切成固定大小的patch比如16×16像素一个patch每个patch拉平成向量再通过线性映射变成token embedding然后加上位置编码。如果一个token代表CNN里的一个像素区域那么ViT里的token就代表一个patch。模型通过自注意力让每个patch都能看到整张图的其他patch这和CNN通过卷积逐步扩大感受野的思路完全不同。ViT的第一个坑是它很“挑食”。CNN天生带很强的局部先验所以数据少的时候也勉强能train。ViT全靠数据喂所以在ImageNet这种千万级数据集上才能超过CNN小数据集上表现反而不如简单CNN。后来的Swin Transformer引入窗口限制把局部注意力变成重点才让视觉Transformer在小数据上更实用。8. 我从实际项目里踩过的几个坑排查链路和最终解决最后分享几个我在真实项目里踩过的坑都是那种“网上文档不会写、但你不注意就废一天”的问题。8.1 Loss不收敛先查位置编码和embedding的scale有一个文本分类任务我从零训练一个只有4层的Transformer。前500步loss降得挺顺但到1500步后loss开始上下震荡后面直接不降了。我第一步检查的是数据没问题。第二步看梯度发现某些层的梯度norm小到几乎为0。后来我定位到问题Token Embedding初始化方差设大了导致进入注意力之前各个token的向量差异极大Softmax变得非常尖锐反向传播时大部分位置的梯度都被压死。解决办法是一改两件事把Embedding的初始化标准差调小在输入进编码器前先过一个LayerNorm。更稳妥的做法是参考原始论文把Embedding乘以sqrt(d_model)保证输入到模型前向计算时的数值范围合理。改完之后loss重新正常下降。这个坑给我的教训是Transformer对数值范围非常敏感看到loss不降先查embedding和初始化的scale再调学习率。8.2 长序列直接把显存撑爆改FlashAttention和梯度累积做一个长文档摘要任务序列长度在2048左右batch_size设成了8模型是BERT-large。结果一跑起来直接OOM。我当时列了个账BERT-large有24层每层12头单层单个注意力矩阵就是8 × 12 × 2048 × 2048 × 4算出来约1.6GB。24层累积下来光注意力矩阵就几十GB还不算FFN激活值。这个账一算谁来了都扛不住。后来我做了三件事用FlashAttention替代标准注意力显存从O(len²)变成近线性batch_size降到2配合梯度累积8步等效还是batch_size16把序列切成两段做滑窗只在需要跨段时用一个额外的全局token传递信息这三板斧下来单卡V100居然能跑了。说实话如果你不做长文本任务可能永远感受不到注意力矩阵多占显存一但做了你就再也不会忽略O(n²)这个复杂度。8.3 生成推理慢KV Cache是必选项用GPT类模型做生成时我最开始没有做KV Cache结果每个token的生成都要重新算一遍前面所有token的Key和Value速度慢到怀疑人生。做了KV Cache以后生成第100个token时前99个token的K、V直接复用只需要计算新token的K、V和注意力速度提升非常明显。KV Cache就是字面意思把历史Key和Value缓存在显存里。代价是显存占用随生成长度线性增长。所以现在很多推理框架都做了量化或者PagedAttention来管理这块缓存。普通用户在推理时如果显存紧张可以降低序列长度或者限制最大生成长度否则缓存也会OOM。我个人的体会是Transformer的坑大部分都集中在“你没真正跑一遍就不知道自己会踩什么”。所以这篇文章虽然讲得全但我还是建议你找一个下午从零实现一个极简Transformer在玩具数据集上跑通一次。跑通了那些架构图和公式才会真正长在你脑子里。