附录 D — notebook 环境与作业
这本书的所有数字都能在你自己的机器上跑出来——包括一台没有显卡的笔记本。
这一章说清楚三件事:环境怎么装、每个脚本跑多久、作业怎么做。
D.1 环境:一台 CPU 笔记本就够
全书只依赖两个库:PyTorch 和 NumPy。
不需要 transformers、不需要 diffusers、不需要下载任何预训练权重。 所有模型都是几十万参数的玩具模型,从零训练。
D.1.1 安装
# 用 conda 或 micromamba 建一个环境
micromamba create -n tdm python=3.10 -c conda-forge
micromamba activate tdm
# 安装 PyTorch(CPU 版即可)
pip install torch numpy验证:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"输出 2.x.x False 也没关系——False 表示没有 GPU,全书照样能跑。
D.1.2 为什么 CPU 够用
因为模型小。下面是实测数字(不是估算):
| 任务 | 规模 | CPU 用时 |
|---|---|---|
| 第 1–4 章的字符级 GPT | 42 万参数、1500 步 | 约 1.1 分钟(150 步实测外推) |
| 第 5 章的扩散模型 | 3000 步训练 | 6.7 秒 |
| 第 5 章的采样 | 1000 个点 × 1000 步 | 0.9 秒 |
| 第 8 章的文本条件模型 | 6000 步训练 | 25.6 秒 |
| 第 8 章的采样 | 400 个样本 × 1000 步 | 1.4 秒 |
这些数字是在一台普通服务器 CPU 上测的。 你的笔记本可能慢 2–3 倍,但仍然在”泡杯茶”的量级。
正文标注的耗时是 RTX 3090 上的。
GPU 快在”大矩阵、大 batch”;这些玩具模型的矩阵太小, GPU 的并行优势发挥不出来,有时甚至比 CPU 还慢(kernel 启动开销占比高)。
所以:如果你的 CPU 跑出来比书里的 GPU 数字还快,不要惊讶。
D.1.3 把 device 改成 CPU
tiny_gpt.py 的默认 device 是 "cpu",开箱即用。
tiny_diffusion.py 的默认值是 "cuda:1"(作者机器的第二块卡)。 在你的机器上,把它改成 "cpu":
from tiny_diffusion import Config
cfg = Config(device="cpu")或者用环境变量批量改(不推荐,容易忘):
# 只影响当前 shell
export TDM_DEVICE=cpu最稳的做法:每个脚本开头显式写 Config(device="cpu")。
D.2 目录结构
ai-mechanisms-book/
├── chapters/ ← 正文(.qmd)
├── appendix/ ← 附录 A–E
├── notebooks/ ← 全部可运行代码
│ ├── tiny_gpt.py 第 1–4 章的模型
│ ├── tiny_diffusion.py 第 5–8 章的扩散骨架
│ ├── conditional_diffusion.py 第 7 章的条件模型
│ ├── text_conditioning.py 第 8 章的文本条件
│ ├── run_chN_case.py 每章的主实验
│ └── ...
├── data/ ← 语料和实验结果(JSON)
├── experiments/ ← 15 个交互实验(HTML)
├── assets/ ← 插图、样式、静态快照
└── qa/ ← 生成图、截图、lint 的脚本
三个原则:
notebooks/里只有代码,没有数据——数据在data/- 每个脚本都能单独运行——不依赖 notebook 的执行顺序
- 结果写成 JSON——正文的数字和
data/里的文件一一对应
D.3 每章跑什么
| 章 | 主脚本 | 依赖 | 产出 | CPU 量级 |
|---|---|---|---|---|
| 1 | run_ch1_case.py |
tiny_gpt.py |
data/ch1_case.json |
分钟级 |
| 2 | run_ch2_case.py |
tiny_gpt.py |
data/ch2_case.json |
分钟级 |
| 3 | run_ch3_case.py |
tiny_gpt.py |
data/ch3_case.json |
分钟级 |
| 4 | run_ch4_case.py、run_ch4_extra.py |
tiny_gpt.py |
data/ch4_*.json |
几分钟 |
| 5 | run_ch5_case.py、run_ch5_schedule.py |
tiny_diffusion.py |
data/ch5_*.json |
秒级 |
| 6 | run_ch6_case.py、run_ch6_bias_grid.py、run_ch6_grid.py |
tiny_diffusion.py |
data/ch6_*.json |
分钟级 |
| 7 | run_ch7_case.py、run_ch7_extra.py、run_ch7_more.py |
conditional_diffusion.py |
data/ch7_*.json |
几分钟 |
| 8 | run_ch8_case.py、run_ch8_ablation.py、run_ch8_ablate2.py、run_ch8_capacity.py |
text_conditioning.py |
data/ch8_*.json |
约十分钟 |
“量级”不是精确值——你的机器可能差两三倍。 精确的单项数字在 D.1 那张表里,那些是实测的。
为什么第 8 章反而久? 因为它要训五个模型、每个都采样几千个样本。 批小、步多,GPU 的并行优势发挥不出来,CPU 和 GPU 的差距会缩小。
如果你想确认环境没问题,先跑:
cd notebooks
python run_ch5_case.py它只要一分钟,而且会打印一组你能和正文对上的数。 跑通了,再跑别的。
D.4 复现书里的数字
每一步都按”跑脚本 → 看 JSON → 对正文”的顺序。
D.4.1 第 1 步:确认输出位置
所有脚本都把结果写到 data/,不覆盖原文件。 如果目录里已经有同名 JSON,脚本会覆盖它—— 想保留原结果,先备份。
D.4.2 第 2 步:对照三个数
每个脚本都会打印几个关键数。 以第 8 章为例:
mean RB: left_blue 0.455 right_blue 0.55
cross RB: left_blue 0.0 right_blue 1.0
cross: 对调后最大逐元素差 = 9.722e-01
和正文对不上时,按这个顺序查:
- 种子:脚本里有没有
torch.manual_seed? - 设备:CPU 和 GPU 的浮点结果可能差 10^{-7},不影响结论
- 版本:PyTorch 2.x 的不同小版本,初始化可能不同
- 步数:
steps参数有没有被改过?
D.4.3 第 3 步:允许的误差
下面这些差异是正常的,不算复现失败:
| 量 | 允许的差异 | 原因 |
|---|---|---|
| 训练损失 | \pm 0.005 | 初始化、浮点顺序 |
| 准确率 | \pm 0.03 | 采样噪声 |
| 对调差 | 量级相同即可 | 具体值依赖模型 |
| 注意力熵 | \pm 0.05 bit | 同上 |
真正不能接受的是”定性相反”: 如果均值池化在你的机器上绑定了,那一定是哪里错了。
D.5 作业
五组,对应五个部分。每组 3–4 题,建议 2–3 小时。
D.5.1 作业一 注意力与 Block(第 1–2 章)
1.1 验证 \sqrt{d_k}
用 NumPy 生成 q,k\sim\mathcal{N}(0,1),取 d=4,64,512, 各算 10000 次点积,报告标准差。
要求:画出”标准差 vs d“的图,并在同一张图上画 \sqrt d 的曲线。 问:实测斜率是多少?和理论差多少?
1.2 把 bug 加回去
把 Config(scaled=False) 打开,训 1500 步。
报告:训练损失、验证损失、注意力熵(每层每头平均)。 问:和正常版本差多少?为什么正文说”小模型上看不出差别”?
1.3 改头数
把 n_head 从 4 改成 8,d_model 保持 128。
先手算:参数量变不变?每个头的维度变不变? 再实测:训练 1500 步,比较 loss。
1.4 RoPE 外推
用 rope_extrapolation.py,在长度 64 上训练、在 128 上推理。
报告:两种位置编码(学习式 vs RoPE)在外推长度上的 loss。 问:哪一个崩得更厉害?为什么?
D.5.2 作业二 训练、采样与可解释性(第 3–4 章)
2.1 顺序陷阱
实现”先截断后调温”和”先调温后截断”两种采样。
报告:固定 (\tau,p)=(0.7,0.9),两种顺序的候选集大小。 问:差多少倍?哪个更保守?
2.2 数一数 KV 缓存
不跑代码,先手算:生成长度 N=100、提示词 P=10, 无缓存和有缓存各要做多少次 QK^\top 的行计算?
再用 exp-3-3 验证。
2.3 消融热图
用 run_ch4_extra.py 的消融数据,画一张”层 × 头”的 loss delta 热图。
报告:最重要的三个头、最不重要的三个头。 问:最重要的头,它的注意力图有什么共同模式?
2.4 随机对照
把训练好的模型和随机初始化的模型并排, 比较它们的注意力熵。
问:随机模型的熵更高还是更低?这说明什么?
D.5.3 作业三 扩散的数学与采样(第 5–6 章)
3.1 手推后验
不看附录,自己推一遍 q(x_{t-1}\mid x_t,x_0) 的均值和方差。
再用代码数值验证:取 x_0=1、t=500,采样 10000 次 x_t, 统计 x_{t-1} 的均值和方差,和公式比。
3.2 实现 DDIM
从 DDPM 的采样循环改出 DDIM(\eta=0)。
报告:20、50、100 步下的样本均值和协方差。 问:多少步之后,结果和 1000 步的 DDPM 无法区分?
3.3 偏差实验
复现实验 6-2:给完美去噪器注入偏差 b。
报告:b=0.01,0.05,0.10,0.20 时输出的均值和标准差。 问:为什么均匀偏差只导致塌缩,不导致发散?
3.4 三种参数化
实现 \varepsilon-预测、x_0-预测、v-预测。
报告:三者在一组固定的 t 上的损失量级。 问:哪一个的动态范围最小?和正文的 8500 倍对得上吗?
D.5.4 作业四 CFG(第 7 章)
4.1 三段曲线
跑 run_ch7_case.py 的 w 扫描。
画三张图:准确率、样本标准差、越界率。 标出:欠引导 / 正常 / 过引导三段的分界。
4.2 负向引导
用 w=-1 复现负引导。
报告:准确率、多样性、越界率。 问:为什么负引导会让准确率掉到 0.05 左右?
4.3 PCA 有效维度
采 1000 个 w=40 的样本,自己算协方差矩阵的特征值。
报告:PC1 占比、有效维度 (\sum\lambda)^2/\sum\lambda^2。 问:去掉最大的 1% 样本后,结论变不变?
4.4 不丢弃标签
训练时不丢弃标签(drop=0),再扫 w。
报告:w=5 时的越界率。 问:为什么没有空条件,CFG 就崩了?
D.5.5 作业五 文本条件(第 8 章)
5.1 三个数
跑 run_ch8_ablation.py。
报告:RB/BR 准确率、两者之和、对调差。 问:为什么”和”比单个准确率更能说明问题?
5.2 去掉位置嵌入
把 variant="no_pos" 打开,重训。
先预测:准确率会是多少?对调差会是多少? 再实测,解释 0.602 这个数是怎么来的。
5.3 自己训一个 BPE
用 bpe_demo.py,换一份你自己的语料(比如你的课程笔记)。
报告:合并 1000 次后,77 个 token 能装多少汉字。 问:和书里的 74 个差多少?为什么?
5.4 空条件
分别用”学出来的 NULL token”和”全零向量”当空条件,扫 w=1,3,5。
报告:越界率、多样性。 问:为什么零向量会崩,而 NULL token 不会?
每题按四档:
| 档 | 标准 |
|---|---|
| A | 数字对、解释对、能指出边界 |
| B | 数字对、解释基本对 |
| C | 数字对,但解释错或缺失 |
| D | 数字不对,或没跑 |
“解释对”比”数字对”更难,也更值钱。 如果数字对不上但你能说清楚为什么,给 B。
D.6 大作业(三选一)
D.6.1 题目 A:复现一篇论文的核心图
从下面三篇里选一篇,复现它的一张核心图:
- DDIM(采样步数与质量)
- CFG(引导强度与多样性)
- MMDiT(联合注意力 vs 交叉注意力的成本)
要求:
- 用本书的玩具模型,不要求复现原论文的规模
- 报告”哪些结论复现了、哪些没有”
- 必须有一张图是你自己算的,不能截图
D.6.2 题目 B:设计一个新的条件接口
给扩散模型加一种新条件:点击、骨架、布局、音频包络——任选。
要求:
- 按第 8 章”六个问题”回答设计决策
- 在 2D 玩具数据上验证:这个条件真的被用上了吗?
- 必须做对调实验(换条件、看输出)
评分重点:你有没有证明”条件生效”,而不是”图画得好看”。
D.6.3 题目 C:写一个条件诊断工具包
输入:一个训练好的条件模型 + 一对镜像条件。 输出:三档判断(绑定成功 / 部分 / 没绑定)+ 数字依据。
要求:
- 至少三个指标(准确率、和、对调差、熵,任选)
- 能在第 7 章和第 8 章的模型上跑通
- 附一份”误判分析”:什么情况下它会判错
D.7 作业提示
只给方向,不给答案。 照着做之前,先自己想十分钟。
D.7.1 作业一
1.1 标准差应该正比于 \sqrt d。先画散点再拟合斜率, 不要只报一个数——看图能发现”大 d 时点积的分布是不是还近似高斯”。
1.2 重点不是 loss 差多少,是注意力熵差多少。 如果熵几乎一样,说明这个规模下 softmax 没饱和——回去看第 1 章的方差计算。
1.3 参数量的答案:不变。 因为 d_{\text{model}} 不变,多头只是把同一个矩阵切成更多份。 如果你算出来变了,检查是不是把每个头的投影矩阵当成了独立的。
1.4 RoPE 的外推会”渐变”,学习式位置编码会”突崩”。 报告崩溃发生在第几个位置,比报告平均 loss 有信息量。
D.7.2 作业二
2.1 先截断后调温:候选集是”先砍掉尾巴,再重新分配概率”。 先调温后截断:温度改变了分布形状,再砍。 两种顺序的候选集大小比,应该和实验 3-2 对得上。
2.2 无缓存:第 i 步要算 P+i 个位置,总量是 \sum_{i=1}^{N}(P+i)。 有缓存:每步只算 1 个新位置,加上注意力的 P+i 次点积。 把”矩阵乘法”和”点积”分开数,别混在一起。
2.3 热图的颜色用 \Delta loss,不要用原始 loss。 原始 loss 的层间差异会把头的差异盖住。
2.4 随机模型的熵通常更高(更接近均匀)。 如果它更低,先检查初始化尺度——这本身就是一个值得报告的发现。
D.7.3 作业三
3.1 数值验证的关键是固定 x_0 和 t,只对 \varepsilon 采样。 别把 x_0 也采样了,否则你验证的是边缘分布,不是后验。
3.2 DDIM 的更新式在附录 A.10。 先实现 \eta=0,再实现 \eta=1 验证它能退回 DDPM。
3.3 “均匀偏差”指 \hat\varepsilon=\varepsilon+b,b 和 t、x 都无关。 先预测输出均值会偏移多少,再实测。
3.4 动态范围用”分位数”比用”最大最小值”稳。 报告 1%–99% 分位数的比值。
D.7.4 作业四
4.1 三段的分界不是固定的,它随模型质量移动。 报告”你的模型”的三段分界,而不是抄书里的 2 和 20。
4.2 负引导的准确率掉到 0.05,不是”变差了”,是”被推向了反面”。 看混淆矩阵,不要只看准确率。
4.3 有效维度对极端值敏感。 先算一遍全样本,再算一遍去掉最大 1%,两个数都报。
4.4 drop=0 时,空条件从未被训练。 CFG 的 w>1 会放大一个”模型没见过的输入”的输出—— 这正是第 7 章”零向量”那个坑的另一个版本。
D.7.5 作业五
5.1 “和”接近 1 说明两个类互补。 如果你的和是 1.2,先检查是不是采样种子没对齐。
5.2 预测的时候写下两个数:准确率和对调差。 对调差是你的”真相判据”,准确率不是。
5.3 语料越小,BPE 越容易”记住”而不是”学会”。 用留出集测,不要用训练集。
5.4 零向量崩的原因是”分布外”,不是”零”。 验证方法:把零向量换成一个小范数的随机向量,看是否也崩。
D.8 大作业评分表
三个题目共用一张表,五个维度,每维 0–4 分,总分 20。
| 维度 | 0 分 | 2 分 | 4 分 |
|---|---|---|---|
| 问题定义 | 没写清楚要回答什么 | 有目标但不可检验 | 一句话能说清,且能设计实验 |
| 实验设计 | 只有演示,没有对照 | 有对照但缺一个关键变量 | 对照完整,能排除替代解释 |
| 数据与复现 | 数字来源不明 | 有脚本但跑不通 | 一条命令复现,含随机种子 |
| 解释 | 只报数不解释 | 解释与数据基本一致 | 能指出边界和反例 |
| 表达 | 图看不懂 | 图能看懂但缺标注 | 图自解释,结论一句话 |
不是”结果不好”,是”没有对照”。
一个跑了三天的实验,如果没有 baseline, 在评分表上是 0 分的”实验设计”。
反过来,一个两小时的玩具实验, 如果有完整的对照和一句诚实的边界,可以拿满分。
D.9 常见问题
Q:torch.cuda.is_available() 是 False,能跑吗?
能。 全书不需要 GPU。把 Config(device="cpu") 显式写上即可。
Q:报错 Expected a 'cpu' device type for generator
这是生成器和张量不在同一个设备上。 torch.Generator() 默认在 CPU;如果你的张量在 GPU 上,要么把生成器也放到 GPU, 要么把数据放回 CPU 再取 batch。
Q:matplotlib 画图时中文变成方块
设置字体:
import matplotlib
matplotlib.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei"]
matplotlib.rcParams["axes.unicode_minus"] = FalseQ:第一次运行很慢,后面很快
正常。第一次要导入 PyTorch、初始化线程池。第二次开始才是真实速度。
Q:结果和书里差一点
先看 D.4 的”允许的误差”。差 10^{-3} 是浮点,差 0.1 是 bug。
Q:内存不够
把 batch_size 减半。这些模型的 batch 从 256 降到 64,结论不变。
Q:能不能用 Jupyter Notebook?
能,但书里的脚本是 .py,不是 .ipynb。 用 %run run_ch5_case.py 或者在 IDE 里直接跑。 这样做的原因:.py 能 diff、能 lint、能被 CI 跑; .ipynb 的 JSON 结构不适合版本控制。
这套代码的设计目标是:一个没有显卡的学生,一个下午能跑完全书。 如果你花了超过半天还没跑通第一个脚本,那是环境问题,不是你的问题。