
很多人第一次接触Transformer是被网上那张经典的Encoder-Decoder架构大图劝退的满屏方块箭头、各种Add Norm、QKV三个字母看得人头皮发麻。我自己当年学的时候也一样对着图发了一上午呆最后发现真正难的不是结构本身而是没人用大白话讲清楚它到底在干什么。所以这篇文章我把Transformer拆成一层一层来讲保证你看完不只是认识这张图而是发自内心觉得“哦原来就这么回事”照着文章里的代码也能自己跑通一个最小实现。Transformer的核心是什么一句话一种基于自注意力机制的序列建模架构2017年由Google在《Attention Is All You Need》中提出。它让序列中任意两个位置可以直接交互解决了老一代RNN模型无法并行、长距离依赖容易丢失的痛点。如今大模型背后的基石——BERT、GPT、T5、ViT、Swin Transformer——全都是从这个架构演化出来的。这篇内容适合三类人想入门NLP或视觉Transformer的同学、面试前需要啃原理的求职者、以及准备手写源码却不知从哪下手的工程师。1. 核心机制用大白话先讲懂自注意力就是全场都听得到的一次投票很多人一上来就啃数学公式结果被矩阵运算劝退。其实自注意力的思想用现实的场景就很好理解我先把它讲成人话再回头看公式你会发现公式不过是在描述这个日常直觉。1.1 开会投票和检索档案两个最直观的类比想象一个会议室里坐满了人也就是一个句子里的所有词。每个人发言之前都要先听一遍全场其他人的发言然后判断“谁说的话对我更有价值谁的嗓门大我就多信谁一点”。这里的关键在于这个判断不是由别人替我做的而是每个词自己独立决定要参考谁。这就是“自”注意力——序列里的每一个元素都在和序列里的所有其他元素做信息交互。另一个我更喜欢用的类比是档案检索。你要写一篇关于“苹果公司发布新iPhone”的新闻稿你现在最想知道的是“苹果”在这句话里到底是水果还是公司。这个需求就是一个Query查询而句子里的每个词身上都挂着一个标签标签上写着“我是关于什么的”这是Key标签背后真正的内容比如“公司”这个词的语义向量就是Value。你拿手里的Query去挨个比对标签和“公司”这个词的标签相似度最高于是你重点参考它的内容最终决定把“苹果”理解成一家企业。公式学久了容易忘但这三个字母不会忘Query是我要找什么Key是我有什么标签Value是我能提供的内容。有了这个基底后面所有代码都是围绕这个机制在转。1.2 Q、K、V不是凭空来的从词向量到可学习的三个变换矩阵这里有个很容易被忽略的问题为什么原始的词向量不能直接拿来算相似度非得先通过三个矩阵变换成Q、K、V你品一下这个逻辑——如果把原始词向量直接点积那模型只做了一次固定的线性比较没有任何学出来的“角度”。比如“苹果”和“公司”在原始向量空间里谁来决定它们的相关度谁也没决定这完全由预训练词向量决定模型改不了。引入三个可学习的权重矩阵Wq、Wk、Wv之后模型在训练时可以逐步调整“到底从哪个角度提问、从哪个角度展示标签、用哪个角度输出内容”。同一个词在不同上下文里经过矩阵变换后得到的Q、K、V都是不同的这给了模型巨大的表达能力。这也是为什么Transformer很少从头训随机词向量它吃的是大规模语料预训练出来的底座QKV变换只是在这个底座上继续微调交互策略。1.3 为什么点积还要除以根号d_k一个容易忽略的数学细节自注意力的公式里有一个让新手最不解的操作softmax里为什么要除以√d_k。直接说结论怕softmax在输入很大的时候梯度消失。假设Q和K的每一维都是独立随机变量均值0、方差1。两个d_k维向量的点积方差约等于d_k。也就是说维度越高点积的绝对值越大softmax在输入绝对值很大的区域已经进入了饱和区梯度几乎为零模型根本学不动。除以√d_k之后方差被拉回到1附近softmax运行在一个梯度健康的区间。这个细节当年看第一遍的时候完全没在意直到自己手写模型发现早停不收敛回头排查才发现问题出在这。2. 为什么从RNN切到Transformer三个设计决策解决真实痛点既然Transformer这么牛得知道它到底解决了什么问题。不是Google突发奇想要换个结构而是RNN那一套在2017年已经真实卡住了AI发展的脖子。这个背景搞明白你才算真正读懂了Transformer。2.1 RNN/LSTM的致命短板串行处理与越来越模糊的长期记忆RNN这两个问题学习NLP的人应该都有体感。第一处理第t个词时必须等t-1个词的输出一步一步串行往下走。这天然没法并行因为计算图中存在一条无法绕开的时间依赖链。第二信息在一步步传递中会被不断“消化”开头的信息传到句尾时已经衰减得差不多了。LSTM用门控机制缓解了部分问题但本质还是靠一条状态通道往前带序列一长照样丢信息。Transformer的做法可以说是釜底抽薪直接把“顺序”从模型结构里拿掉自注意力让任意两个位置之间的交互距离永远是1跳不存在“第1个词传到第100个词要经历99次衰减”的问题。你想让“它”和“小明”建立关联注意力一脚就跨到那里去了这比LSTM硬要跑完整条时间线高效太多了。2.2 并行训练的意义GPU时代提升效率是关键筹码既然是做深度学习训练效率直接决定一个团队敢不敢做大规模模型。RNN那种串行计算GPU的空闲浪费多到吓人。而Transformer里每个位置的注意力计算在数学上都是独立的可以一次性构造成一个矩阵乘法Q乘以K的转置。这一步落地到GPU上就是一个大规模矩阵乘整句话并行算完。训练吞吐量提升带来的效果是革命性的——同样的时间你能在多几倍的语料上训练这才是后来大模型能铺开的技术前提。2.3 位置编码模型失去顺序感之后如何找回语序自注意力是完全无视顺序的你把手里的句子随机打乱每个词和其他词的相似度算出来一模一样。可语言几乎是纯顺序的信息结构“小明把球传给了小红”和“小红把球传给了小明”就是两个完全相反的世界。原版Transformer的做法是用一组正弦和余弦函数生成每个位置的位置编码。# 位置编码维度为 [max_seq_len, d_model] for pos in range(max_seq_len): for i in range(d_model): if i % 2 0: angle pos / (10000 ** (2 * i / d_model)) pe[pos, i] sin(angle) else: angle pos / (10000 ** (2 * (i - 1) / d_model)) pe[pos, i] cos(angle)为什么要用三角函数而不是直接学一组位置参数原论文的理由是这样做可以让模型泛化到比训练时更长的序列。后来的BERT和GPT直接改成可学习位置编码效果也差不多说明泛化性更多是理论上的优势不过正弦编码在工程上确实更省参数。位置编码和词向量是相加而不是拼接原因也很实际——拼接会让输入维度翻倍参数和计算量都大幅上升而相加在几乎不增加成本的情况下把位置信息嵌入了每个词的表示里。3. 架构逐层拆解编码器、解码器和三种注意力的分工核心机制搞清楚了我们来看Transformer的完整架构。很多教程一上来给你堆一整张图这就是劝退的根源。我建议把它拆开成几个模块单独理解最后再拼起来。3.1 多头注意力同时开几个“视角”做独立投票一个自注意力层如果只有一个头它只能学一种关注模式。但我们想让模型同时兼顾多种关系——语法结构、指代消解、相邻词共现、核心实体识别。一个头根本不够用。多头注意力就是把这个注意力过程拆成h份比如8头每一份有自己的Wq、Wk、Wv在各自子空间里做注意力计算最后拼起来过一层线性层。你可以这么理解一排在同一个场景下看一幅画有人关注画面的色彩有人关注构图线条有人关注画面中的物体关系最后一个“总结者”把所有人的观察结果汇总成一份完整报告。通过多头机制Transformer能够在同一层同时获得多种关系特征这比单头注意力在表达能力上有数量级的提升。下面是一个例子在“苹果公司发布新款iPhone”这句话里词“苹果”在某个头中会重点看“公司”确认自己是个公司名在另一些头里它可能去看“iPhone”稳固产品背景的上下文。不同头各司其职这就是多头的威力。3.2 前馈网络和Add Norm注意力的输出为什么还要再加工自注意力输出的是对“每个词应该从上下文收集哪些信息”的重表示。但这个收集动作本质上是线性变换加Softmax加权表达能力有限。Transformer在这里接了一个两层全连接网络FFN(x) ReLU(x W1 b1) W2 b2一般中间维度会放大4倍比如512维变成2048维激活函数用ReLU。这个FFN的作用是对注意力收集来的信息做非线性加工你可以把它理解成“先把信息打碎再用更丰富的维度重新组合”。注意力负责通信FFN负责计算两个组件各有明确分工。每个子层外面包一层残差连接和层归一化这就是“Add Norm”的含义。残差连接解决的是深层网络梯度退化的问题如果某层学不到有用的变换它至少可以直接把输入原样传出去保证梯度能顺畅回传。层归一化则是在每个样本的特征维度上做归一化让训练更稳定对batch size的依赖也更小。3.3 Masked自注意力解码器生成词时为什么必须“只看左边”编码器可以看完整句话但解码器做生成任务时不能在未来还没发生的时候就偷看答案。比如生成“我 爱 北京”处理到“北京”这个位置时它只能看到“我”“爱”和它自己绝不能看到后面的词。实现办法是在softmax之前把未来位置的分数替换成负无穷大。# 下三角矩阵保留当前和之前的位置掩盖未来位置 mask torch.triu(torch.ones(seq_len, seq_len), diagonal1).bool() scores scores.masked_fill(mask, float(-inf))负无穷经过Softmax之后权重会变成0相当于未来位置根本没有参与注意力计算。这一步是解码器和编码器最核心的区别之一也是很多新手写生成模型时最容易忘的一个操作——忘了加mask模型直接开卷考试性能还虚高一部署就露馅。3.4 一图看懂三种注意力在架构中打什么工Transformer里其实有三种注意力机制很多人学了半天没搞清谁负责什么。我直接列个表注意力类型所在位置Q、K、V来自哪里能看哪些位置编码器自注意力Encoder层Q、K、V都来自编码器输入输入序列所有位置解码器掩码自注意力Decoder层都来自解码器输入当前位置及之前的位置交叉注意力Decoder层Q来自解码器K、V来自编码器输出输入序列所有位置和已生成位置比较交叉注意力是解码器连接编码器信息的桥梁。解码器每生成一个词都要去“询问”编码器侧对整个输入的理解本质上就是解码器的Query去检索编码器提供的Key和Value从而知道自己目前生成的内容和目标输入的整体语义是否匹配。4. 手写最小多头注意力代码把形状和mask彻底盘明白理论说再多都不如自己跑一遍代码印象深刻。我当时真正觉得“醍醐灌顶”的时刻就是对着代码把张量形状一步步画出来之后。下面我带你从头实现一个最小可用的多头注意力模块顺便把那些代码里容易踩的坑都讲清楚。4.1 为什么建议从写代码开始而不是先啃论文论文里公式、图表太紧凑信息的“压缩率”很高新手很难展开。代码则相反——每一步都是明确的张量运算、明确的形状变化跑通了就证明你理解对了。我的经验是先跑代码再回看论文最后自己动手写一遍。顺序反了很容易变成“背公式式学习”脑子会了手不会。4.2 完整的多头注意力实现PyTorch下面这段代码删掉了无关花活保留了核心流程可以直接在GPU或CPU上跑。建议你一边读一边把每一行的张量形状注释出来。import torch import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout0.1): super().__init__() assert d_model % n_heads 0, d_model必须能被n_heads整除 self.d_model d_model self.n_heads n_heads self.head_dim d_model // n_heads # 将Q、K、V的线性变换合并成一个大矩阵计算效率更高 self.wq nn.Linear(d_model, d_model) self.wk nn.Linear(d_model, d_model) self.wv nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) self.dropout nn.Dropout(dropout) def forward(self, query, key, value, maskNone): batch_size query.size(0) # 形状变化: [batch, seq_len, d_model] - [batch, seq_len, d_model] Q self.wq(query) K self.wk(key) V self.wv(value) # 拆分为多头: [batch, n_heads, seq_len, head_dim] Q Q.view(batch_size, -1, self.n_heads, self.head_dim).transpose(1, 2) K K.view(batch_size, -1, self.n_heads, self.head_dim).transpose(1, 2) V V.view(batch_size, -1, self.n_heads, self.head_dim).transpose(1, 2) # 缩放点积注意力Q K^T 再除以 sqrt(head_dim) scores Q K.transpose(-2, -1) / (self.head_dim ** 0.5) # scores形状: [batch, n_heads, seq_len_q, seq_len_k] if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn_weights F.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) # 加权求和: [batch, n_heads, seq_len_q, head_dim] output attn_weights V # 把多头结果合并回去 output output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) return self.out_proj(output)4.3 几个关键的形状变化请务必亲自画一遍先理解输入形状[batch, seq_len, d_model]。过Q/K/V各自的线性层维度不变。前面说过为什么要除$d_k$的平方根代码里体现为self.head_dim ** 0.5。然后view将其拆成[batch, seq_len, n_heads, head_dim]再用transpose把n_heads挪到第1维变成[batch, n_heads, seq_len, head_dim]。为什么要这样调因为Q K.transpose(-2, -1)做矩阵乘法时只有最后两维参与运算前面的维度全部当batch处理这样整个batch中的每句话、所有头的注意力计算全在一步矩阵乘里完成。注意masked_fill里用的是mask 0来控制默认传入的掩码矩阵有内容的位置是1要掩盖的位置是0。还有一种实现是传入布尔mask写法会是masked_fill(mask, float(-inf))两种都行但要搞清楚自己用的是哪种语义这是最容易出bug的细节之一。4.4 手写之后一定会踩的坑mask、初始化、数值稳定性我把自己复现Transformer和后续写生成模型踩过的坑整理成几个要点mask必须在softmax之前不是在加权求和之后才处理。很多人习惯在算完attention之后乘0这虽然把权重清零了但softmax的归一化分母仍然包含了被掩盖位置的贡献严格来说语义不对训练时会漏信息。fp16训练时float(-inf)很危险。低精度下负无穷可能导致NaN扩散尤其是当被掩盖的位置刚好是某个头里唯一的候选位置时。工程上我经常用一个很小的负数比如-1e9来代替。多头输出拼接后必须加一层out_proj。这一层让多头信息能在输出前做一次融合如果你省略它模型的表达能力会明显下降。初始化参数不要太大。虽然PyTorch默认初始化一般没问题但如果你自定义初始化记得保持线性层权重在较小范围否则早期attention分布过尖锐训练不稳定。5. Transformer家族进化与训练调参经验Transformer不是孤立出现的它的价值在于催生了一整个模型家族。了解这些变体的演进逻辑既能帮你理解为什么这个架构能通吃NLP和CV也能让你在选型时更有底气。5.1 从BERT到GPT预训练微调范式彻底改变NLPBERT用的是Transformer的Encoder部分通过“完形填空”式遮蔽语言模型做预训练——随机遮住一些词让模型预测被遮住的词。GPT则用Decoder部分走的是自回归路线——从左到右逐词预测下一个词。方向不同带来了各自擅长任务的分野BERT更适合理解类任务比如文本分类、NER、句对判断GPT更适合生成类任务比如对话、创作、摘要。它们共同的关键成功因素不是Transformer结构让预训练模型变大了而是Transformer天然支持并行处理长文本极大降低了在大规模语料上做预训练的门槛。后来大家意识到“大力出奇迹”的规律后预训练模型的参数规模开始起飞这也为多模态大模型铺了路。5.2 ViT和Swin Transformer把视觉任务也变成“词序列”ViTVision Transformer的思路非常直接把一张图片切成固定大小的patch比如16x16每个patch拉平成向量当作“词”再像处理文本一样过Transformer。这个方法在数据量足够大时效果可以超过CNN。但它有个明显短板自注意力是全局的一张高分辨率图片切成几百上千个patch计算复杂度是O(n^2)动辄就爆显存。Swin Transformer的解决办法是引入窗口注意力先只在局部窗口内做注意力再用移位窗口让信息跨窗口流动这样既能保留想象中的全局建模能力又把复杂度降到了和图像分辨率近似线性的水平。此外通过patch merging不断合并并缩小特征图空间尺寸形成类似CNN特征金字塔的层级结构这让视觉Transformer在高分辨率任务上有实际落地的可能。说到这类变体顺带提一下前面热搜里出现的hgformer这类工作。它们的方向就是在Vision Transformer的基础上继续增强拓扑建模能力——比如用超图结构Hypergraph去刻画patch之间更复杂的高阶关联而不是只依赖两两配对。这类研究本质上都是在回答同一个问题在自注意力之外还能怎么更高效地建模空间结构关系。对刚入门的读者来说不用深究这属于某个细分方向的进阶话题理解基础Transformer后自然能顺着看下去。5.3 标准注意力的复杂度问题与实用调参经验自注意力虽然强大但标准实现的复杂度是O(n^2)n是序列长度。假设序列长度从512翻倍到1024注意力部分计算量变成4倍序列再翻倍就变16倍。这在实际项目中是非常真实的限制。后续一系列优化方向也由此而来稀疏注意力只让局部窗口或固定间隔的位置互相注意线性注意力用近似方法把复杂度降到O(n)FlashAttention则换个角度从GPU显存I/O瓶颈下手不改变算法本身却大幅加速训练。了解这些方向之前一定要把标准注意力的形状和计算流程吃透否则很容易被这些新名词“唬住”。实际训练中还有几个频率很高的经验学习率必须配合warming up。Transformer对学习率敏感直接用大学习率启动非常容易训练震荡甚至不收敛。推荐先用较小的学习率如5e-5量级热身几千步再逐渐过渡到目标学习率。深层的Transformer需要多加dropout但不是每层都要强加。如果你发现验证集损失不降反升先怀疑dropout率和过拟合问题不要急着改模型结构。长序列训练先跑通短序列。把序列截断到128或256确认loss在下降之后再逐步拉长序列长度排查bug的效率要高很多。这次从头到尾把Transformer从思想到架构再到代码走了一遍。我个人最深的一个体会是学这类复杂模型一定不要被论文劝退它本质上就是一个“让序列内部彼此沟通然后各取所需地更新自己”的机制所有的技巧都是在解决沟通中的效率和稳定问题。如果你看完这篇觉得有收获建议立刻照着第4节的代码自己敲一遍再跑一个玩具任务比如字符级文本生成很多纸上谈兵的问题会在实际运行中自己暴露出来。等你能把代码里的每个形状讲给别人听再回头翻原版论文——那时候你会发现所有段落都变得顺眼了。