从0训练LLM(旧版 · 存档中) · M2 阅读+实验 ≈ 60 分钟 页内实验 ×3

手搭 Transformer:让 token 互相看见

M1 把文字变成了一串 token id。但这串数字里,每个 token 还是彼此孤立的。Transformer 干的唯一一件核心的事:让每个 token 去看其他 token,按相关度把它们的信息收集过来。这就是注意力。
① 主线定位

这一步在整条主线的哪里

目标:亲手从 0 训练一个属于你自己的小 LLM。

M1 结束时,我们的模型输入是一串 token id,比如 [104, 588, 461]。每个 id 会被查成一个向量(embedding)。但此刻有个致命问题:这些向量彼此之间毫无关联——「它」这个词的向量,完全不知道前面提到的是「猫」还是「政策」。

Transformer 要解决的就是这个。它由一堆相同的**块(block)**堆叠而成,每个块只做两件事:

🧭

上游: 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 做加权求和。谁的权重大,谁的信息就更多地进入我的新表示。

计算器 · 一个 Query 的注意力(真算术)

下面 6 个词各自有一个 4 维向量(简化演示,真实模型是几百维)。点一个词,把它设为 Query(提问者),看它如何对其余每个词打分、归一化、再汇集信息。

缩放因子:
① 打分 Query · Key 点积(越大越相关)
② 归一化 ÷√d_k 再 softmax → 加起来 = 1 的权重
③ 汇集 用权重对每个词的 Value 加权求和
🎯

试三件事:① 点不同的词当 Query,看谁被关注;② 切到「不缩放」,看②里的权重怎么塌缩成一个接近 1、其余接近 0——这就是梯度消失的样子;③ 勾上 causal mask,看排在 Query 之后的词权重全变 0。

从一行到整个矩阵:真实模型里的那次矩阵运算

上面的计算器一次只算一个 Query 的一行。真实模型不这么干——它把所有 token 的 Query 摞成矩阵 Q(N 行 × d_k 列,一行一个词),Key 摞成 K,Value 摞成 V,然后一次矩阵乘法算完全部。这就是整个 Transformer 最核心的那行公式:

缩放点积注意力 · Attention Is All You Need §3.2.1
= ( / )
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
加 causal mask 时:在 softmax 之前把分数矩阵的右上三角设为 −∞。下面亲手看这个矩阵。
矩阵视角 · 完整的 6×6 注意力矩阵(真算术)

每行 = 一个词当 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,全部实算。 看三张图有多不像。

默认不加——不然「追 → 老鼠」这种往右的关系会被切掉,看不出头 2 学了什么
最后一步:三个头的输出拼接起来,过 WO 融合成一个向量
头 1 输出4 维
⊕
头 2 输出4 维
⊕
头 3 输出4 维
拼接 →
拼接向量12 维
× WO (12×12) →
融合输出12 维

为什么最后还要乘一个 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°。 拖两个滑杆,盯住右下角那个「旋转后点积」。

实线 = 转过之后的向量 · 虚线 = 还没转的原始向量 · 灰弧 = 两者当前夹角
操作记录 —— 只要 m − n 那一列不变,最后一列就一个数都不变

为什么点积只剩下 m − n(一行推导)
(R(mθ)q) · (R(nθ)k) = qᵀ R(mθ)ᵀ R(nθ) k = qᵀ R(−mθ) R(nθ) k = qᵀ R((n−m)θ) k

关键是旋转矩阵是正交矩阵: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 层)。层一深,就会遇到两个老问题:梯度在反向传播时消失、每层输出的数值分布乱漂。两个零件解决它们:

🧩

一个 Transformer 块 = 多头注意力 +(残差+LayerNorm)+ FFN +(残差+LayerNorm)。把这个块堆 N 层,加上开头的 embedding 和结尾的输出层,就是一个完整的 GPT。下面第三个实验里,你可以在 3D 可视化里看到这些零件一个个真实地排布在一起。

④ 亲手实验

从示意到真实,三个实验层层加深

第一个(上面的计算器)是真算术;下面两个是跑真模型、看真结构。

真模型 · 在浏览器里跑一个真的 GPT-2

上面是 4 维的简化演示。Transformer Explainer 在浏览器里跑一个真实的 GPT-2 small(124M 参数),你输入任意句子,能看到 Embedding → Q/K/V 三路分叉 → 真实的 attention 矩阵 → 输出概率的完整数据流,还能调 temperature 看输出分布怎么变。

👀

看什么: ① Q/K/V 三条不同颜色的线从同一个 embedding 分叉——这一幕抵得上十分钟口头解释”为什么要三个矩阵”;② 逐个点开 12 个注意力头,看它们呈现出完全不同的注意力图案(这就是”多头”的意义);③ attention 矩阵是真实计算出来的,不是演示数据。

真结构 · 把整个 GPT 拆成 3D 立体

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 的预告。

⑤ 项目推进 · 从 0 搭

给我们的模型,搭出 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 commitM3 会直接拿这个架构去训练
⚠️

为什么坚持手写注意力: 直接调框架的 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 维)

这一模块你应该带走的

💬

有不明白的? 随时在对话里问我(比如「为什么 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 分钟

这三个加起来不到一小时,看完再学后面事半功倍
20-30 min英文(文字量小)需联网
把一个完整 GPT 拆成 3D 立体,每个 token 逐层穿过 Embedding → LayerNorm → 自注意力 → MLP,能看到每一次加法和乘法落在哪个格子上。
为什么值得玩:全网唯一能让“参数量”变成可以用眼睛数的物理体积。右上角切 nano-gpt / GPT-2 / GPT-3,体积暴涨的冲击力比任何数字都有效。
10-20 min英文需联网 论文 arXiv 2408.04619
浏览器里跑一个真的 GPT-2 small,输入任意句子看 Embedding → Q/K/V 分叉 → attention 矩阵 → 输出概率的完整数据流。
为什么值得玩:Q/K/V 三条不同颜色的线从同一 embedding 分叉那一幕,抵得上十分钟口头解释“为什么要三个矩阵”。12 个头可逐个点开,正好讲多头。
26 minB站官方双语字幕需联网 87.9 万播放
用贯穿全片的例子讲清 attention 本质是一次学出来的加权求和:权重来自 Q/K 点积经 softmax 归一化。
为什么必看:唯一把“点积大=对齐强”做成动画让你看见的资源。mask 部分明确讲“设成负无穷”,正好解释 causal mask 为什么不是“删掉”。3B1B 官方号,非民间搬运。

🔬 想吃透原理 1–3 小时

Karpathy 是主心骨,李沐和李宏毅补它讲快的地方
1h56m中英字幕精译需联网
从空 .py 文件开始,跟着论文一行行写出并训练 Transformer,最终得到 nanoGPT 核心。每加一个组件都先讲清“不加它会怎么坏”再加。
为什么是主心骨:覆盖本模块全部概念,且方法论与本课程“遇坑解坑”完全同构——先写出会偷看未来的 attention,训练诡异,才引出 causal mask;先堆深层发现训不动,才引出残差。Karpathy 是让你先疼一次再给药。
1h27m中文需联网 177 万播放
逐段读原论文,穿插“这句话作者其实想说什么”“这里没写清楚但实际上是……”。
补 Karpathy 讲快的两块:① √d_k 的动机(全网少有人认真讲);② LayerNorm vs BatchNorm(为什么变长序列不能用 BatchNorm)。注:搜索摘要说 53 分钟是错的,实际 87 分钟。排在 Karpathy 之后看。
2.5-3h中文需联网
从“输入是一排向量该怎么办”出发,一步步推出为什么需要 self-attention → 多头 → 位置编码。
“位置编码为什么必须存在”讲得最扎实的中文资源——从“把句子打乱结果一样”这个反证入手。李沐是读论文(自上而下),李宏毅是推公式(自下而上),偏好推导的选它。
40-60 min中文需联网 RoPE 发明人本人
从一个恒等式出发反推出 RoPE——先立目标“内积后自动只剩相对位置”,然后解这个方程,解出来就是旋转矩阵。
最符合“讲清为什么而非怎么做”的一篇。全网 99% 是倒着讲结论,他正着推——读完你会知道 RoPE 不是被发明的,是被解出来的。

📄 想读原文 按需

PDF 已下载到本地论文库,点击直接打开
Transformer 起点 Vaswani 等 · 2017-06
本模块的全部概念都源自此文。最该精读 §3.2.1 的脚注——“为什么除以 √d_k”的原始出处只有短短一句。配李沐精读一起读,别硬啃。
RoPE 出处 苏剑林 等 · 2021-04
LLaMA / Qwen / 几乎所有现代 LLM 实际在用的位置编码,你自己搭的小 LLM 也会用。强烈建议先读苏剑林中文博客再读论文——反过来读会痛苦。
🚫

我替你排除了什么(也是筛选标准): 「10 分钟彻底搞懂 Transformer」类速通(不可能讲清 √d_k 动机)· B站「最全最细 59 集」这类注水标题党 · CSDN 上返回 521 的二手笔记转载 · StatQuest(靠比喻+无中文版,零基础应急可看但不进正式清单)· 各类”代码逐行解读”(只得到”怎么做”)。
另注:搜到的 2 个 Karpathy B站字幕版(av1405625318 / BV1r6421f736)已 404 失效——搜索结果里还在但打不开,所以只收了实测可用的 BV11yHXeuE9d。

← M1 分词:把文字变成数字 M3 预训练 →