手搭 Transformer:让 token 互相看见
这一步在整条主线的哪里
目标:亲手从 0 训练一个属于你自己的小 LLM。
M1 结束时,我们的模型输入是一串 token id,比如 [104, 588, 461]。每个 id 会被查成一个向量(embedding)。但此刻有个致命问题:这些向量彼此之间毫无关联——「它」这个词的向量,完全不知道前面提到的是「猫」还是「政策」。
Transformer 要解决的就是这个。它由一堆相同的**块(block)**堆叠而成,每个块只做两件事:
- 注意力(Attention)——让每个 token 去「看」序列里的其他 token,按相关度把信息汇集过来。这是本模块的绝对核心。
- 前馈网络(FFN)——每个 token 各自做一次非线性变换,消化刚汇集来的信息。
上游: M1 输出了 token id 序列,会被 embedding 层查成向量。
本节: 搭出 Transformer 块,核心是自注意力——让孤立的 token 向量学会互相看见。
下游: M3 会把这个搭好的模型架构真正训练起来(烧 GPU、看 loss 曲线)。
「它」指的是谁?——一个词的意思取决于上下文
以及:为什么之前的方案(RNN)不够好。
看这两句话里的「它」:
「猫追老鼠,因为它饿了。」——它 = 猫
「猫追老鼠,因为它跑得快。」——它 = 老鼠
同一个「它」,意思完全不同。要理解它,模型必须回头看前面的词,并判断跟谁最相关。这就是核心问题:如何让一个 token 的表示,融入它该关注的其他 token 的信息?
| 方案 | 怎么做 | 好处 | 致命伤 |
|---|---|---|---|
| RNN / LSTM(Transformer 之前的主流) | 从左到右逐个读词,把「记忆」一路传下去 | 天然有顺序 | ① 没法并行——必须读完第 5 个词才能读第 6 个,训练极慢;② 距离一远,早期信息就在传递中被稀释、忘掉了 |
| 自注意力(Transformer) | 让每个 token 一步到位直接和所有其他 token 算相关度 | ① 全部并行——一次矩阵运算算完所有 token 对;② 任意两个 token 的距离都是「1 步」,再远也不衰减。这两点让 Transformer 能吃下海量数据、扩到极大规模,是它取代 RNN 的根本原因。 |
「算相关度」——具体怎么算?两个词向量怎么得出一个「有多相关」的分数,又怎么用这个分数去汇集信息?这就是下一节的自注意力机制。
自注意力:Query 提问,Key 应答,Value 交付
Self-Attention。这是 Transformer 唯一真正新颖的部件,其余都是常规神经网络零件。
为什么需要三个矩阵(Q、K、V)
每个 token 的 embedding 向量,会分别乘上三个不同的权重矩阵 W_Q、W_K、W_V(这三个矩阵是训练出来的,是模型要学的参数),得到三个新向量:
Query(查询):这个 token 想找什么。相当于它主动发出的「提问」——比如「它」这个词的 Query 可能编码了”我在找一个前面出现过的名词”。
Key(键):这个 token 能提供什么。相当于每个词举起的「标签」,等着被别人的 Query 匹配。
Value(值):这个 token 真正要交付的信息内容。一旦被选中,交出去的就是它。
为什么不直接用 embedding 算,非要拆成三个?因为**「一个词想找什么」和「一个词能提供什么」是两回事**。同一个词,作为提问方和作为被查方,需要展示的信息不同。三个矩阵让模型能分别学习这三种角色。
整个注意力就三步,每个 token 都走一遍:
1. 打分:拿我的 Query,和序列里每个 token 的 Key 做点积——点积越大,说明这两个向量方向越一致、越「对得上」,相关度越高。
2. 归一化:把这些分数除以 √d_k(下面细讲为什么),再过一次 softmax,变成一组加起来等于 1 的权重。
3. 汇集:用这组权重,对所有 token 的 Value 做加权求和。谁的权重大,谁的信息就更多地进入我的新表示。
下面 6 个词各自有一个 4 维向量(简化演示,真实模型是几百维)。点一个词,把它设为 Query(提问者),看它如何对其余每个词打分、归一化、再汇集信息。
试三件事:① 点不同的词当 Query,看谁被关注;② 切到「不缩放」,看②里的权重怎么塌缩成一个接近 1、其余接近 0——这就是梯度消失的样子;③ 勾上 causal mask,看排在 Query 之后的词权重全变 0。
从一行到整个矩阵:真实模型里的那次矩阵运算
上面的计算器一次只算一个 Query 的一行。真实模型不这么干——它把所有 token 的 Query 摞成矩阵 Q(N 行 × d_k 列,一行一个词),Key 摞成 K,Value 摞成 V,然后一次矩阵乘法算完全部。这就是整个 Transformer 最核心的那行公式:
每行 = 一个词当 Query;每列 = 被看的词(Key)。点行头看整行;点任意格子把那一个数彻底拆开。
三件事:① 在「分数矩阵」视图开/关掩码,看右上三角整片变 −∞——这就是 causal mask 在矩阵里的样子(下三角);② 切到「softmax 后」,看每行独立归一化成 100%;③ 点「它」那一行,对照它和上面计算器算的是同一组数——矩阵只是把 6 个 Query 的行并行摞在一起,这就是”全部并行”的含义。
关键细节:为什么要除以 √d_k
这是 M2 最容易被跳过、但最该搞懂的一个点。d_k 是 Key 向量的维度。问题在于:向量维度越高,点积的数值天然就越大(更多项相加)。
如果不除以 √d_k,当维度很大时,点积会变得非常大,送进 softmax 后会被推向极端——一个权重接近 1,其余接近 0。这时候 softmax 的梯度几乎为 0,模型学不动了(梯度消失)。除以 √d_k 正好把点积的方差拉回到 1 附近,让 softmax 停留在梯度健康的区间。
上面的计算器里就有这个开关——切到「不缩放」,亲手看 softmax 怎么”塌缩”成一个赢家通吃的分布,这个现象一眼就懂了。
多头注意力:同时用几套眼睛看
上面讲的是一个 Q/K/V。实际上 Transformer 会并行搞好几套(比如 GPT-3 是 96 套),每套叫一个头(head)。为什么要多头?因为**「相关」有很多种**:一个头可能专门盯语法上的主谓关系,另一个头盯代词指代,还有的盯上下文的情感呼应。每个头有自己独立的 W_Q/W_K/W_V,各看各的,最后把所有头的输出拼接起来再融合。
🔍 为什么不能一个大头
一个大头(维度是所有头之和)在数学上表达能力更强,但注意力权重只有一组——它被迫用同一套”相关度”解释所有关系。拆成多头相当于强制模型分工:每个头在自己的低维子空间里学一种关系模式。
同一句话、同一批词,喂给 3 个各自独立的头。每个头有自己的 W_Q/W_K,于是每个词在每个头里的 Q/K 向量都不一样—— 这里直接把算完的 4 维 Q/K 给你(点开每个头下面的「看 Q/K 向量」可查)。分数 = Q·K/√4,逐行 softmax,全部实算。 看三张图有多不像。
为什么最后还要乘一个 WO:拼接只是把三段向量首尾接起来,各头之间还没「商量」过。
WO 是一个可学的方阵,负责把三段信息混合成一个整体表示,并且把维度收回成和输入一样(才能做残差相加)。
对上真实模型:GPT-2 small 是 dmodel = 768 = 12 头 × 64 维,WO 是 768×768;
本课程建议你搭的小模型是 384 = 6 头 × 64 维。注意 每头维度 = dmodel ÷ 头数——
所以多头不增加总计算量,只是把同样的宽度切成几份分工用。这也是本模块最后那张坑表里「头数除不尽维度就报错」的来源。
试三件事:① 对比三张热力图的亮点位置完全不在同一处——头 1 亮在「它→猫」,头 2 亮在「追→老鼠」,头 3 亮在「饿了→它」,同一句话被三种「相关」各读了一遍;② 点开任一个头下面的「看 Q/K 向量」,确认那些百分比真是从这几个数点乘出来的,不是画上去的;③ 在底部把词切到「它」,看三个头分别给它搬来了不同的词——这就是拼接要拼的东西,也是一个大头做不到的事。
④ 附加:勾上「加因果掩码」,看头 2 的最强连线从「追→老鼠」变成「追→猫」——因为老鼠在追的右边,被掩掉了。解说文字是从真实权重现算的,会跟着一起改口。
Causal mask:语言模型不能偷看未来
我们训练模型做的是预测下一个词。如果算「第 3 个词」的注意力时,它能看到第 4、5、6 个词——那等于在考试时看到了答案,训练就作弊了。causal mask(因果掩码)解决这个:算注意力分数时,把「当前位置之后」的所有分数设成负无穷,这样 softmax 之后它们的权重就变成 0。
注意一个常见误解:causal mask 不是「删掉」未来的词,而是「把分数设成 -∞ 再过 softmax」。为什么不直接删?因为要保持矩阵是规整的方阵,才能一次性并行算完所有位置——这正是 Transformer 快的前提。上面计算器里可以一键开关 mask,看 Query 之后的词怎么被「切掉」。
位置编码:注意力天生没有顺序感
这里有个反直觉的事实:自注意力本身完全不知道词的顺序。你把「猫追老鼠」打乱成「老鼠追猫」,如果不加额外信息,注意力算出来的结果一模一样——因为它只是在算两两之间的点积,跟谁在前谁在后无关。
所以必须额外把位置信息注入进去。现代 LLM(LLaMA、Qwen、DeepSeek)用的是 RoPE(旋转位置编码):不是给词向量加一个位置向量,而是按位置把 Q 和 K 向量旋转一个角度。妙处在于——旋转后,两个向量的点积会自动只跟它们的相对位置差有关。于是相对位置信息被优雅地编进了注意力计算本身。
「点积只跟相对位置差有关」听起来像是要推一堆公式才能信。下面这个实验让你直接看着数字确认:把 m、n 一起往后推,点积一个数都不变。
下面是两个普通的 2D 向量 q(提问者,在位置 m)和 k(被查者,在位置 n)。 RoPE 不给它们「加」任何东西,只是按各自的位置把它们转一个角度:位置 m 转 m×20°,位置 n 转 n×20°。 拖两个滑杆,盯住右下角那个「旋转后点积」。
关键是旋转矩阵是正交矩阵:R(φ)ᵀ = R(−φ),而且两次旋转可以相加:R(α)R(β) = R(α+β)。 于是 m 和 n 只能以 n − m 的形式活下来,绝对位置被约掉了。 这正是苏剑林推 RoPE 的方式——先立下「内积后只剩相对位置」这个方程,解出来就是旋转矩阵。
真实模型是怎么做的:q、k 不是 2 维而是几十上百维(比如每头 64 维)。RoPE 把它们按相邻两维一组,切成 d/2 个 2D 平面, 每组各自转,但频率不同:第 i 组转 m·θi,θi = 10000−2i/d。 高频那几组转得飞快(管邻近几个词的细节),低频那几组转得极慢(几千个 token 才转完一圈,管长距离)。 每一组都单独满足上面那个恒等式,所以把 d/2 组的点积加起来,整体仍然只依赖 m − n。 这也是为什么改一改这些频率(NTK / YaRN 等)就能把上下文窗口外推得更长。
试三件事:① 先只拖 m,看「旋转后点积」一路变;再把 m、n 拖到相等,会发现它精确地回到「旋转前点积」那个值——相对位置 0 = 什么都没发生;② 连点几次「整体平移 +1」,绝对位置从 3、1 一直推到 7、5,操作记录里「旋转后点积」那一列一个数字都不动——这就是「只依赖 m−n」的现场证据;③ 紧接着点「只把 m +1」做对照,位置差一变,点积立刻跟着变。
④ 顺手核对:中间那块「真实算术」是用 2×2 旋转矩阵逐个数字乘出来的,和解说里 cos 公式给的值完全一致——说明这不是巧合,是旋转矩阵正交性的必然结果。
残差连接 + LayerNorm:让深层网络训得动
Transformer 会堆很多层(GPT-2 有 12~48 层)。层一深,就会遇到两个老问题:梯度在反向传播时消失、每层输出的数值分布乱漂。两个零件解决它们:
- 残差连接(Residual):每个子层的输出不是直接往下传,而是
输出 = 子层(x) + x——把输入原封不动加回去。这样梯度总有一条「高速公路」直达底层,深层也训得动。 - LayerNorm(层归一化):把每个 token 的向量重新缩放到均值 0、方差 1,稳住数值分布。**为什么用 LayerNorm 而不是 CV 里常见的 BatchNorm?**因为语言是变长序列、每个 batch 里句子长短不一,BatchNorm 需要跨样本统计的前提在这里不成立——LayerNorm 只在单个 token 内部归一化,不受序列长度影响。
一个 Transformer 块 = 多头注意力 +(残差+LayerNorm)+ FFN +(残差+LayerNorm)。把这个块堆 N 层,加上开头的 embedding 和结尾的输出层,就是一个完整的 GPT。下面第三个实验里,你可以在 3D 可视化里看到这些零件一个个真实地排布在一起。
从示意到真实,三个实验层层加深
第一个(上面的计算器)是真算术;下面两个是跑真模型、看真结构。
上面是 4 维的简化演示。Transformer Explainer 在浏览器里跑一个真实的 GPT-2 small(124M 参数),你输入任意句子,能看到 Embedding → Q/K/V 三路分叉 → 真实的 attention 矩阵 → 输出概率的完整数据流,还能调 temperature 看输出分布怎么变。
看什么: ① Q/K/V 三条不同颜色的线从同一个 embedding 分叉——这一幕抵得上十分钟口头解释”为什么要三个矩阵”;② 逐个点开 12 个注意力头,看它们呈现出完全不同的注意力图案(这就是”多头”的意义);③ attention 矩阵是真实计算出来的,不是演示数据。
bbycroft.net/llm 把一个完整的 GPT 拆成 3D 立体结构,每个 token 变成向量后逐层穿过 Embedding → LayerNorm → 自注意力 → MLP,你能看到每一次加法和乘法落在哪个格子上。它是全网唯一能让”参数量”变成可以用眼睛数的物理体积的东西。
看什么: ① 左侧目录(Embedding / Layer Norm / Self Attention / MLP / Softmax)几乎就是本模块的概念清单,点一个就跳到那个零件;② 右上角切换 nano-gpt → GPT-2 → GPT-3,看模型体积如何暴涨——这个冲击力比任何一句”1750 亿参数”都有效,也是 M3 讲 scaling 的预告。
给我们的模型,搭出 Transformer 骨架
记住分工:代码 AI 来写,架构决策你来做。
本模块的项目任务单
| 步骤 | 你的决策 / 动作 | 说明 |
|---|---|---|
| 1. 定模型尺寸 | 层数、embedding 维度、头数(推荐起点:6 层 · 384 维 · 6 头) | 这三个数直接决定参数量。我们目标 ~50M 参数的小模型,不用贪大 |
| 2. 让 AI 写 block | 要求:手写多头自注意力(不许调 nn.MultiheadAttention)+ causal mask + RoPE + 残差 + LayerNorm | 亲手写一遍注意力,M2 才算真的懂。这也是 Karpathy 那个视频在做的事 |
| 3. 验算参数量 | 自己推一遍:为什么这个配置是约 X 百万参数 | 及格线:能说清 embedding 层、每个注意力头、FFN 各占多少参数 |
| 4. 冒烟测试 | 喂一小批 M1 切好的 token,跑一次前向,确认输出形状对(logits 维度 = 词表大小) | 还不训练,只验证架构搭对了、数据能流通 |
| 5. 提交 | 把模型定义存入 04-实践项目/model/,git commit | M3 会直接拿这个架构去训练 |
为什么坚持手写注意力: 直接调框架的 MultiheadAttention 三行就搞定,但你会永远停留在”知道有这么个东西”。手写一遍,Q/K/V 怎么来、mask 怎么加、多头怎么拼——这些”为什么”才真正长进脑子里。代码可以让 AI 生成,但你要能逐行说清它在干嘛。
预告下一步(M3 会遇到): 架构搭好后,真正开始训练时你会发现——loss 一开始飞快下降,然后卡在一个平台期不动;或者某一步突然发散(loss 变 NaN)。那是 M3 的主题:优化器、学习率、混合精度怎么调,以及怎么看懂 loss 曲线。
常见的坑
| 坑 | 症状 / 后果 | 怎么办 |
|---|---|---|
| 忘了加 causal mask | 训练 loss 低得离谱、但生成时胡言乱语 | 模型训练时”偷看”了答案,学到的是作弊。检查注意力矩阵是不是下三角 |
| 忘了除以 √d_k | 深层模型训不动、loss 卡住不降 | softmax 塌缩导致梯度消失(上面实验里见过)。缩放是标配 |
| 位置编码没加或加错 | 模型对词序不敏感,「猫追狗」和「狗追猫」输出接近 | 注意力本身没有位置概念,必须显式注入 RoPE |
| 残差连接漏了 | 层数一深就训不动 | 每个子层都要 out = sublayer(x) + x,给梯度留高速公路 |
| 头数除不尽维度 | 报错或维度对不上 | embedding 维度必须能被头数整除(384 / 6 = 64,每头 64 维) |
这一模块你应该带走的
- Transformer 的核心只有一件事:让每个 token 按相关度汇集其他 token 的信息(注意力);
- Q/K/V 是三种角色:Query 提问、Key 应答、Value 交付,都是从 embedding 用可学矩阵变出来的;
- 注意力三步:Q·K 点积打分 → ÷√d_k 再 softmax → 对 Value 加权求和;你亲手算过了;
- √d_k 防止 softmax 塌缩(梯度消失),causal mask 防止偷看未来,两个都是必需;
- 多头 = 同时用几套眼睛看不同的关系;
- 注意力天生没有顺序感,位置信息靠 RoPE 显式注入;
- 残差 + LayerNorm 让深层网络训得动;LayerNorm 而非 BatchNorm 是因为序列变长。
有不明白的? 随时在对话里问我(比如「为什么 Value 也要单独一个矩阵」「RoPE 具体旋转多少度」)。我会把问答记录到 05-问答记录/M2-Transformer-问答.md,沉淀成你自己的 FAQ 库。
概念索引(本模块登场): embedding · self-attention · Query/Key/Value · 点积打分 · √d_k 缩放 · softmax · 多头注意力 · causal mask · 位置编码 / RoPE · 残差连接 · LayerNorm(vs BatchNorm)· FFN · Transformer 块 · 参数量计算
想往下钻,按你愿意花的时间选
下面每条都经实测验证可访问、且讲清了「为什么」。速通、同质化转载已排除。建议顺序:先看档位1的三个可视化,再啃 Karpathy,最后用李沐/苏剑林补 √d_k 和 RoPE。
🎮 想更直观 10–30 分钟
🔬 想吃透原理 1–3 小时
📄 想读原文 按需
我替你排除了什么(也是筛选标准): 「10 分钟彻底搞懂 Transformer」类速通(不可能讲清 √d_k 动机)· B站「最全最细 59 集」这类注水标题党 · CSDN 上返回 521 的二手笔记转载 · StatQuest(靠比喻+无中文版,零基础应急可看但不进正式清单)· 各类”代码逐行解读”(只得到”怎么做”)。
另注:搜到的 2 个 Karpathy B站字幕版(av1405625318 / BV1r6421f736)已 404 失效——搜索结果里还在但打不开,所以只收了实测可用的 BV11yHXeuE9d。