· 77 min read
交互式理解 Transformer:从一个点积到一整个模型
Transformer 是现在几乎所有大语言模型、视觉基础模型的骨架,但它的论文《Attention Is All You Need》只有 11 页,核心也只有一个公式。这篇文章不堆公式,而是把每个零件做成可以动手的小部件:拖一拖向量看点积怎么变,点一个词看它在「看」谁,拧一下温度看采样分布怎么变。
文章分四部分:先看整体,用一张可以点的全景图搞清楚数据怎么从一句话流到下一个词;再拆零件,每一节只引入一个新概念,由浅入深;然后从文本到图像,看同一套结构怎么一字不改地用到图片上(ViT);最后回到整体做总结。所有部件都在页面里实时计算,不是预先画好的图,所以你可以随意调参,看看什么时候直觉会被打破。
第一部分:先看整体
先不管细节,把 Transformer 当成一个黑盒:输入一串 token,输出同样长度的一串向量,每个输出向量都融合了整句话的信息。拿它来做语言模型时,再在每个位置的输出上接一个分类器,预测「下一个词是什么」。
拿本文贯穿使用的玩具例句走一遍,看数据的形状怎么变( 是模型维度, 是词表大小, 是层数):
| 阶段 | 数据 | 形状 |
|---|---|---|
| 原文 | 小猫 没有 过 马路 因为 它 太 累 了 | 一个字符串 |
| 分词 | [小猫, 没有, 过, 马路, 因为, 它, 太, 累, 了] | 9 个 token id |
| 词嵌入 + 位置编码 | 每个 id 查出一个向量,再加上位置向量 | |
| 第 1 层 … 第 层 | 每层:多头注意力 → 残差 + 归一化 → 前馈网络 → 残差 + 归一化 | 始终是 |
| 输出投影 | 每个位置投影到词表,softmax | |
| 预测 | 第 9 个位置的分布给出「了」后面最可能的词 | 个概率 |
注意中间那一行:无论多少层,形状永远不变。 这是 Transformer 能随便加深的原因,也是本文后面各节的组织方式:每一节讲一个零件,零件的输入和输出都是「一串 维向量」。
下面是整个前向过程的全景图,这是 GPT 那一类纯解码器模型的结构。点任意一个方块,会显示它在做什么,以及对应本文的哪一节。
点一个方块。
两个常见变体只是在这张图上做减法或加法:BERT 一类的编码器模型去掉因果掩码和采样,把整句话双向地编码成向量,用于分类、检索;原始论文的编码器-解码器结构则多一个编码器栈,解码器每层多一个交叉注意力去看编码器的输出,用于翻译。第三部分要讲的 ViT 就是一个编码器,只是输入从词换成了图片块。
第二部分:拆开每个零件
下面按依赖顺序逐个拆开。前两节是数学工具,第 3 到 6 节合起来就是注意力层,第 7、8 节把层摞成模型,第 9、10 节讲怎么用它生成以及它有多大。
1. 一切从点积开始
Transformer 里的「注意力」说到底就是在问:两个向量有多像? 衡量相似度最便宜的办法是点积:
方向一致时点积最大,垂直时为零,方向相反时为负;向量越长,点积的绝对值也越大。拖动下面两个箭头的端点感受一下。
| a | |
| b | |
| a·b | |
| cos θ |
试试把两个箭头拉成直角(点积归零),再拉成反方向(点积为负)。注意拉长箭头时点积也变大,这正是后面要「缩放」的原因之一。
2. 把相似度变成权重:softmax
有了一组相似度分数(叫它们 logits),我们想把它们变成加起来等于 1 的权重,这样就能用它们做加权平均。softmax 就是干这个的:
这里的 是温度。 越小,最大的分数越「赢家通吃」; 越大,分布越平。注意力里的 缩放,本质上就是在选一个合适的温度。
把 T 拉到 0.1,看概率几乎全压在最大的 logit 上;拉到 5,四个概率趋于相等。softmax 只看分数之间的差,所以把四个 logit 同时加上一个常数不会改变结果。
3. 词怎么变成向量
模型看不到「小猫」两个字,它看到的是一个编号(token id),然后在一张大表(embedding 矩阵)里查出这个编号对应的向量。这张表是训练出来的,训练结束后语义相近的词会落在相近的方向上。
回到第一部分的玩具例句。每个词我手工给了一个 6 维向量,维度分别对应「实体、有生命、动作、功能词、状态、代词」。真实模型的维度是几百到上万,而且每一维没有这么直白的含义,但道理一样:向量的每一维是一个特征,点积在比较特征是否匹配。
点「它」:它与「小猫」的点积远高于与「马路」,因为两者都有「有生命」这一维。点「因为」或「了」:它们只在「功能词」维度上有值,所以彼此很像,和实词都不像。
4. 注意力:带着问题去查表
现在把前三节拼起来。对句子里的每个词,我们想生成一个新的表示,它不只包含这个词自己的信息,还融合了上下文里相关词的信息。比如「它」这个词,单看是没有意义的,只有知道它指「小猫」才有意义。
注意力的做法是给每个词三个身份:
| 身份 | 符号 | 作用 | 类比 |
|---|---|---|---|
| 查询 | 我在找什么 | 搜索框里打的词 | |
| 键 | 我能被怎么找到 | 网页的标题和关键词 | |
| 值 | 找到我后你能拿走什么 | 网页的正文 |
三者都由同一个词向量 经过三个不同的线性变换得到:,,。这三个矩阵就是注意力层要学的东西。然后:
- 用第 个词的查询 和所有词的键 做点积,得到分数 。
- 除以 做缩放,再 softmax,得到权重 ,每一行加起来是 1。
- 用权重对所有值 做加权平均,就是第 个词的输出:。
写成矩阵就是那个著名的公式:
下面的部件对玩具例句真的执行了这三步。这里的 是我手写的:它把「代词」特征转成对「实体 + 有生命」的查询,其他特征原样保留; 和 取单位阵。点任意一个词,看它在看谁。
点「它」:权重主要落在「小猫」上,输出向量于是带上了「实体、有生命」的信息,相当于完成了一次指代消解。把缩放系数拉大,权重越来越尖锐;拉小则趋于平均,相当于什么都没学到。勾上因果掩码再点「小猫」,它只能看到自己,这就是解码器的处境。
为什么要除以
如果 和 的每一维都是均值 0、方差 1 的随机数,那么点积 的方差是 。真实模型里 或更大,分数的标准差会到 8 甚至更高,softmax 会被推到饱和区:权重几乎变成 one-hot,梯度几乎为零,训练不动。除以 把方差拉回 1,相当于给 softmax 设了一个合理的温度。上面部件里把缩放系数拉到 6,就是模拟不缩放时的饱和效果。
这和 RNN 有什么不同
RNN 把句子从左到右串行地压进一个隐状态,第 9 个词想用第 1 个词的信息,得穿过 8 次非线性变换。注意力让任意两个词之间只隔一步,且所有词的计算可以并行。代价是计算量和序列长度的平方成正比,这就是长上下文昂贵的根源。
5. 多头注意力:几个角度同时看
上一节的注意力头学会了「代词找先行词」,但语言里还有很多别的关系:动词找宾语、形容词找名词、每个词看前一个词。一个头只能学一种加权模式,所以 Transformer 并排放 个头,每个头有自己的 ,各自算注意力,最后把输出拼起来再过一个线性层 :
为了不让计算量随头数线性增长,每个头的维度取 。例如 ,,每个头在 64 维空间里工作。
下面并排放了两个头。头 A 就是上一节的语义头。头 B 是我手写的「位置头」:它的查询和键只用位置信息,查询被构造成「我前一个位置的位置编码」,所以每个词都在看自己的前一个词。真实模型里确实常观察到这类头,它们只关心位置,不关心内容。
同一个词在两个头里的权重分布完全不同。拼接之后,输出既带有「它 = 小猫」的信息,也带有「它的前一个词是因为」的信息。头数越多,能并行捕捉的关系越多,但每个头的维度越小。
6. 位置编码:注意力天生不知道顺序
回看第 4 节的公式:如果把句子里的词打乱顺序,每个词的 都不变,注意力权重的集合也不变,输出只是跟着换了位置。换句话说,注意力是置换等变的,它完全不知道「小猫」在「马路」前面。 「小猫过马路」和「马路过小猫」对它来说一样。
解决办法是在词向量上加一个只和位置有关的向量 。原论文用正弦函数:
每一对维度是一个不同频率的正弦波:低维变化快,像秒针;高维变化慢,像时针。任意位置都有唯一的「指针组合」。更妙的是, 可以由 线性变换得到,所以模型容易学会「看前 k 个词」这种相对位置模式,第 5 节的头 B 就是这么构造的。
点积在 k = 0 处最大,随 |k| 增大而衰减,并且只和 k 有关,与 p 无关。这就是「相对位置」能被点积感知的原因。
现代模型大多不再用固定正弦编码。GPT-2 用可学习的位置向量;LLaMA 等用 RoPE(旋转位置编码),它把位置信息以旋转的方式直接乘进 和 ,让点积天然只依赖相对位置;还有 ALiBi 直接在注意力分数上加一个随距离线性衰减的偏置。思路都一样:让点积能感知到位置差。
7. 一个完整的编码器层
注意力只是层里的一个子模块。一个标准的编码器层由两个子层组成,每个子层外面都包着残差连接和 LayerNorm:
点「下一步」按钮,跟着数据走一遍。
点「下一步」开始。
LayerNorm 在做什么
LayerNorm 对单个 token 的向量做标准化:减去这个向量各维的均值,除以标准差,再乘以可学习的缩放 、加上偏移 。它不跨 token、不跨 batch,所以对序列长度和 batch 大小都不敏感。作用是把每层的激活值拉回一个稳定的范围,防止深层网络里数值越来越大或越来越小。
原论文把 LayerNorm 放在残差相加之后(Post-LN),现在的大模型几乎都放在子层之前(Pre-LN),即 。Pre-LN 让残差通路上完全没有归一化操作,梯度能无损地流回底层,训练深层模型时稳定得多。
8. 解码器:不许偷看未来
编码器可以看整句话,因为它的任务是「理解」。解码器的任务是「生成」:预测第 个词时,只能看前 个词,否则训练时它直接抄答案,学不到任何东西。
实现上只需在 softmax 之前把分数矩阵的上三角( 的位置)设成 ,softmax 后这些位置的权重恰好为 0。回到第 4 节的部件勾上「因果掩码」就能看到效果:注意力矩阵变成了下三角。
训练时整句话一次性喂进去,靠掩码保证第 i 行只用到前 i 列,因此 n 个位置的预测可以并行计算,这叫 teacher forcing。推理时每生成一个词都要重新跑一遍,但前面各词的 K 和 V 不会变,可以缓存下来,这就是 KV cache。
原始的 Transformer 是编码器-解码器结构,用于翻译。解码器每层比编码器多一个交叉注意力子层:查询来自解码器自己,键和值来自编码器的输出,这是译文「看」原文的地方。三种注意力的区别只在于 从哪里来:
| 类型 | Q 来自 | K, V 来自 | 掩码 | 出现在 |
|---|---|---|---|---|
| 自注意力 | 本序列 | 本序列 | 无 | 编码器 |
| 掩码自注意力 | 本序列 | 本序列 | 因果 | 解码器 |
| 交叉注意力 | 解码器 | 编码器输出 | 无 | 解码器 |
BERT 只用编码器,适合理解类任务;GPT 系列只用解码器,所有任务都转成「续写」;T5 保留完整的编码器-解码器。今天的大语言模型几乎清一色是纯解码器,因为它结构最简单、最容易扩展,而且「续写」这一种任务形式就能覆盖问答、翻译、摘要等所有事情。
9. 自回归生成与采样
解码器最后一层的输出经过一个线性层投影到词表大小,再 softmax,就得到下一个词的概率分布。然后从这个分布里选一个词,拼到输入末尾,再跑一次,循环直到生成结束符。每一步怎么「选」,就是采样策略。
下面是模型读完「小猫 没有 过 马路 因为 它 太 ___」之后给出的一个假想分布。拧动三个旋钮,再点「采样」多试几次。
小猫 没有 过 马路 因为 它 太 ___
温度 → 0 等价于贪心解码,永远选「累」,输出单调但稳定。温度 > 1 会让「马路」「了」这类不通顺的词也有机会被选中。top-k 只保留概率最高的 k 个词,top-p 只保留累计概率达到 p 的最小词集,两者都是在截掉分布的长尾,避免采到离谱的词。
10. 参数都在哪里
把前面的零件数一数,就能估算任何 Transformer 的参数量。记 为 d_model, 为前馈中间维度, 为词表大小, 为层数:
- 每层注意力: 各 ,共 。
- 每层前馈:,取 时为 。
- 词嵌入:,输出投影通常和它共享权重。
所以主体参数约为 。拖动滑块,看看这个公式和几个熟悉的模型对不对得上。
注意力分数矩阵的大小是 n² 乘以层数和头数,和参数量无关,这就是长上下文的内存瓶颈。LLaMA 等模型的前馈层用 SwiGLU,有三个矩阵而不是两个,参数会比公式略多;GPT-3 的实际参数量 175B 和这里的估算非常接近。
第三部分:从文本到图像
11. ViT:把图片切成「词」
前面十节没有一处用到「这是文本」这个假设:注意力、前馈、残差、归一化,处理的都只是「一串 维向量」。所以只要能把别的东西变成一串向量,整套结构就能原样搬过去。Vision Transformer(ViT,Dosovitskiy et al., 2020)做的就是这件事,而且做得极其朴素。
第一个问题:什么是图像的 token? 最直接的想法是一个像素一个 token。一张 的图有 50176 个像素,注意力的计算量是序列长度的平方,约 25 亿个分数,一层都算不起。ViT 的答案是把图切成 的小块(patch), 时只有 个 token,和一段短文差不多长。
第二个问题:patch 怎么变成向量? 把一个 的块展平成 768 维的像素向量,乘一个可学习的矩阵 。这和「对图像做一个卷积核为 、步长为 16 的卷积」完全等价,所以代码里通常就写成一个 Conv2d。之后加上可学习的位置嵌入(一维的,模型并不知道 196 个 patch 排成 ,但训练后位置嵌入之间的相似度会自己呈现出二维网格结构),再在序列最前面放一个额外的 [CLS] token,它不对应任何 patch,最后一层它的输出就作为整张图的表示,接一个分类头。
剩下的就是第二部分讲过的 层编码器,一个字都不用改。
下面的部件对一张真实照片执行了「切块 → 序列化」,并且用一个手工构造的注意力头演示 patch 之间的关注关系。真实 ViT 的头是学出来的,这里的头用每个 patch 的平均颜色和纹理(标准化后)做 ,分数取负的欧氏距离,意思是「去看和我长得像的块」。点图上任意一块试试。
还没有选中 patch。
点一块地板,所有地板块都亮起来;点一块毛,亮的是毛。这就是自注意力和卷积的根本区别:卷积只看邻居,注意力第一层就能把整张图里相似的区域连起来。把 patch 调成 56,整张图只剩 16 个 token,每个 token 里混了很多东西,注意力就模糊了,这是 token 粒度和计算量之间的取舍。照片是作者家的猫,也可以上传自己的图。
ViT 丢掉了什么,又用什么补回来
卷积网络自带两条关于图像的先验:局部性(相邻像素相关)和平移等变(猫移到右边还是猫)。ViT 一条都没有,它得从数据里自己学。结果是在中等规模数据集(ImageNet-1k)上 ViT 不如同等大小的 CNN,但在 3 亿张图的 JFT-300M 上预训练后反超,而且模型越大越好。这和语言模型的规律一致:先验少的结构更能吃下数据。
之后几年的工作基本都在「怎么用更少的数据训好 ViT」上:
- DeiT 用一个 CNN 当老师做蒸馏,只用 ImageNet-1k 就能训出好的 ViT。
- Swin Transformer 把注意力限制在局部窗口内,层与层之间错开窗口,并逐层合并 patch 做下采样,等于把局部性和层级结构加回来,成了检测、分割任务的主力骨干。
- MAE 随机遮住 75% 的 patch,让模型从剩下 25% 重建像素,这种自监督预训练不需要任何标签。它能成立的原因正是注意力能从任意远处取信息,遮掉大半也能推断。
- DINO 用自蒸馏做自监督,训练完后
[CLS]对各 patch 的注意力图直接就是一张物体分割图,没有用过任何分割标注。
论文里有一个很有意思的分析:计算每个头的「平均注意力距离」,底层的头有的只看邻近 patch(像卷积),有的一上来就看全图;越往高层,所有头都趋向全局。也就是说 ViT 在需要时自己学出了局部性,而不是被结构规定。
12. 从图像分类到匹配与多模态
既然 token 可以是词也可以是图像块,自然也可以是别的东西。对本站的读者来说最贴近的例子是特征匹配:
- SuperGlue 把两张图各自检测出的关键点(位置 + 描述子)当作 token,在同一张图内做自注意力、在两张图之间做交叉注意力,交替若干层,最后用最优传输解出匹配。它和第 8 节的编码器-解码器几乎是同一个东西:自注意力整合图内上下文,交叉注意力让一张图的点去「查」另一张图。
- LoFTR 更进一步,不检测关键点,直接在 CNN 输出的 分辨率特征图上把每个位置当 token,用线性注意力做 dense 匹配,所以在弱纹理区域也能匹配上。
- DUSt3R、MASt3R 之类的工作干脆把两张图的 patch 一起扔进 ViT,直接回归出三维点图,传统的「检测、描述、匹配、三角化」流水线被一个网络吞掉了。
再往外走一步就是多模态。CLIP 用一个文本 Transformer 和一个 ViT 分别编码文字和图片,用对比学习把匹配的图文对拉到同一个向量空间,于是「一张猫的照片」和「a photo of a cat」有了可比的向量。今天的多模态大模型则更直接:ViT 把图片编码成一串 patch 向量,经过一个小的投影层,直接当作普通 token 拼进语言模型的输入序列。语言模型对此毫无感知,它只看到「一串 维向量」,这正是第一部分那张表里不变的形状带来的红利。
第四部分:回到整体
13. 训练:一个损失函数走天下
纯解码器模型的训练目标简单得不像话:给定前文,预测下一个 token,损失是交叉熵。一段长度为 的文本提供 个训练信号,靠第 8 节的因果掩码一次前向全部算完。没有标注,互联网上的任何文本都是训练数据,这是 Transformer 能被「喂」到今天这个规模的前提。
几个工程上绕不开的细节:
- 学习率预热与衰减。 Transformer 对学习率很敏感,开头几千步从 0 线性升到峰值,再按余弦或平方根倒数衰减。
- Dropout 与权重衰减。 原论文在注意力权重、残差路径和嵌入上都加了 dropout;大模型因为数据远多于参数,往往不用 dropout,只用权重衰减。
- 混合精度与梯度裁剪。 前向用 bf16 或 fp16,主权重用 fp32;梯度范数裁到 1.0 防止突然爆炸。
- 缩放定律。 在算力固定时,参数量和数据量应当按大致相同的比例增长(Chinchilla 定律给出的是每个参数约 20 个 token)。这个结论直接决定了今天模型的大小。
14. 回头看
把这篇文章的部件串起来,Transformer 就是:
- 把 token 变成向量,加上位置信息(第 3、6 节)。
- 每一层里,先让每个词带着查询去看所有词,用点积算相似度、softmax 算权重、加权平均取信息(第 1、2、4 节),并且同时用多个头看多种关系(第 5 节)。
- 再对每个词独立做一次前馈变换;两个子层都包上残差和 LayerNorm(第 7 节)。
- 摞 N 层。生成时加因果掩码,最后投影到词表、采样(第 8、9 节)。
- 换一个「分词器」,把图片切成 patch 线性投影成向量,同一个编码器就成了 ViT;再把关键点、图文对当 token,就是匹配网络和多模态模型(第 11、12 节)。
它之所以赢,不是因为注意力多么精巧,而是因为这个结构里每一步都是矩阵乘法,没有串行依赖,可以在 GPU 上把算力用满,并且随着规模增长性能持续提升;再加上它对输入是什么毫不关心,文本、图像、关键点、音频都能变成「一串向量」喂进去。理解了这十来个部件,再去读任何一篇 Transformer 变体的论文,都只是在问:它改了哪个零件,为什么。
延伸阅读
- Vaswani et al., Attention Is All You Need,原论文。
- Jay Alammar, The Illustrated Transformer,经典的图解教程。
- Andrej Karpathy, Let’s build GPT: from scratch, in code,两小时从零写一个 GPT。
- Harvard NLP, The Annotated Transformer,逐行注释的 PyTorch 实现。
- Su et al., RoFormer,RoPE 位置编码。
- Dosovitskiy et al., An Image is Worth 16x16 Words,ViT 原论文。
- He et al., Masked Autoencoders Are Scalable Vision Learners,MAE。
- Sarlin et al., SuperGlue,用注意力做特征匹配,本站有详细笔记。
- Hoffmann et al., Training Compute-Optimal Large Language Models,Chinchilla 缩放定律。