Skip to content

第 31 章 把全书收成一次你能解释的实验 ​

读到这里,最有价值的结果不只是“见过 Transformer”。你应该能够拿到一个训练程序,沿着数据、预测、损失、梯度和更新追踪一遍,说明每个维度和分母为什么存在;当损失异常时,还能设计一个小实验把问题缩小。

这一章把前面的内容收成一条实践路线。目标不是在普通 CPU 上假装复现大模型,而是完成一组确实能运行、能复查、能暴露错误的实验。所有新增练习均为本书补充,课程本身与本书实验的界线保持不变。

31.1 每个阶段都留下一个可检查的结果 ​

标量自动求导让你知道梯度从哪里来。计数 bigram 让你知道语言模型首先是概率模型。神经网络 bigram 和 MLP 把概率表变成可训练函数。初始化、BatchNorm 和手写反向让你看到数值尺度及广播怎样影响训练。上下文树和 Transformer 改变了信息如何从其他位置传来。tokenizer 则决定“一个 token”究竟是什么。最后,GPT-2 复现把这些东西放进可训练、可评估的工程循环。

这条路线可以用一组验收问题连接,而不必靠记住一串类名。

阶段应该能解释的结果一个有用的检查
标量自动求导多条路径的梯度为何相加与有限差分或自动求导对照
计数与神经 bigram条件概率怎样转成损失行概率和为 1,正确标签概率影响 NLL
MLP上下文拼接后形状如何变化固定批次能否过拟合
初始化与归一化激活、梯度为什么消失或膨胀比较层间统计和训练/评估模式
手写反向广播与归约怎样反传各中间张量逐一比对梯度
层次上下文与 Transformer哪些位置可以影响当前预测修改未来 token 不应影响过去 logits
tokenizer编号与字节是否可逆encode/decode 回环、特殊符号边界
GPT-2 工程一次更新看了多少有效 token大批次与正确加权累积一致
助手工作流哪个环节提供知识或执行能力分别评估模型、检索、权限与工具结果

这些检查不能互相替代。损失下降并不证明标签移位正确,采样好看不证明泛化,梯度一致也不证明训练数据合法或没有泄漏。每个实验回答一个具体问题,结论才不会无限扩张。

31.2 先运行这组 CPU 实验 ​

配套代码 examples/part4/lab.py 完整定义了 Config、Attention、Block、TinyGPT、优化器、训练步骤及测试。它不访问网络,不下载权重,也不会连接外部模型服务。固定环境为 Python 3.12.14、PyTorch 2.6.0+cpu,CPU 线程数 1,随机种子 1337。数值等价测试使用 float64,最后的微型训练使用默认 float32。

从全书目录运行:

bash
source qa/environment/env.sh
python examples/part4/lab.py

如果是在另一个目录中解压配套源码,先按配套 README 安装相同版本并设置单线程;不要把环境脚本路径不存在误认为实验本身有错。环境文件记录的是本次实际运行环境,不保证所有平台都有相同 wheel 或硬件行为。

程序的主体刻意保持短小。输入是整数 token,先查词与位置 embedding;两个 pre-norm block 使用因果 SDPA;最终 LayerNorm 后用共享权重输出 logits。优化器把矩阵与一维参数分组,采用 AdamW;每步清零梯度、计算平均交叉熵、反向、全局范数裁剪,再更新。前几章已经逐段解释这些动作,完整文件让读者不必从零散代码块猜出遗漏的导入和初始化。

实际输出中的关键结果如下:

检查本次结果能支持的结论
唯一参数量7056微型结构与共享统计可核对
输入/输出权重同一存储True真正共享而非仅复制数值
转置导入代数关系误差0本例 Linear 存储方向转换正确
SDPA 前向/梯度最大误差4.44×10⁻¹⁶ / 4.88×10⁻¹⁵float64 CPU 小尺寸数值相符
正确加权累积误差5.55×10⁻¹⁷等长序列的 2/5、3/5 加权正确
错误等权累积误差0.052799反例确实能暴露分母错误
DDP 平均数学模拟误差5.55×10⁻¹⁷两份等量数据的梯度平均关系成立
CPU checkpoint 续训误差0本例保存状态足以恢复下一步

完整日志在 results.json。程序还检查因果性、日程边界、标签移位和续写 mask;不要只挑上表里好看的数字,忽略测试输入和断言。

31.3 一个确实跑过的微型训练 ​

训练任务使用 17 个 token。输入 token 为 j 时,标签为 (j+1) mod 17。例如 15 后面应该是 16,16 后面应该是 0。固定批次有 8 条长度为 6 的序列,总共 48 个目标位置。

为什么用这样简单的规则?因为模型若连这件事都学不会,就没必要用复杂语料掩盖问题。它不需要世界知识,也没有含糊的自然语言评判。我们可以直接算标签并观察损失。

更新索引更新前平均损失裁剪前梯度范数
02.8402980.995749
92.5020911.005588
491.5028690.771117
990.5490100.508345

第 99 次索引对应的更新完成后,损失为 0.535705。表里的数值是在更新前算的,因此最后一行与最终结果不应完全相同。这个时间点区别与 checkpoint 的 step 含义类似:记录指标时要说清它描述更新前还是更新后。

17 类均匀分布的损失为 ln(17)≈2.833213。初始实际损失约 2.840298,接近但不完全等于它,因为随机初始化的 logits 并非全部相同。训练后损失下降,说明正确后继的概率总体提高。它没有证明模型在从未见过的自然语言上有效;这个实验甚至反复使用同一批输入,本来就是用于检查学习循环。

31.4 怎样把一次失败变成有信息的实验 ​

假设你把梯度累积加入模型后,损失曲线变了。先不要同时改学习率、批大小和初始化。保留同一批输入、同一份初始参数,在 float64 中比较一次完整批次反向与累积反向。若梯度已经不同,先检查 loss 分母、zero_grad 的位置以及是否提前 step。若梯度相同但参数更新不同,再检查优化器状态和超参数。

假设生成文本突然包含无法解码的编号。先核对 logits 最后一维与 tokenizer 可解码范围,尤其是补齐词表维度之后。不要为了不报错,直接删除未知编号再把剩余文本当成正常结果;那会隐藏概率分布与词表契约不一致的问题。

假设恢复 checkpoint 后曲线跳变。先在一个固定 CPU 小实验中检查 optimizer、RNG、数据游标和 next_step,再扩大到目标硬件。若不能说明下一批数据是否完全相同,就无法把差异全部归因于非确定性 GPU 算子。

每次调试都先把问题缩到一个更新、一个批次、一个性质。能在小实验中重复出现的错误,比只在长跑后看到的一条异常曲线更容易解释。

31.5 下一组实验应该只增加一个难点 ​

下面给出四条可逐步完成的路线。它们是练习计划,尚未在本书中执行。

第一条研究上下文。固定参数规模和训练 token 预算,改变上下文长度,设计一个只有远处信息才能决定标签的合成任务。若数据本身只需看前一个 token,更长上下文没有收益并不奇怪。评估应区分“模型没有能力使用长上下文”和“任务没有要求使用它”。

第二条研究 tokenizer。对同一份小型公开文本,比较字符级和 byte-BPE 的序列长度、词表大小、训练速度与按字节归一化的损失。不要直接比较两个 tokenizer 的每 token 困惑度。需要同时记录压缩率和模型计算量,避免把单位变化误当成学习效果变化。

第三条研究优化。固定初始权重与每次更新的 token 集合,比较无累积与多份累积,再改变有效 token 数使分母问题出现。然后再加入 dropout,观察逐位等价为何不再自然成立。先确认公式,再研究随机性与性能。

第四条研究助手数据。先不训练模型,只手工编写几十个任务、示范与反例,明确用户输入、助手目标、屏蔽位置和验收标准。若标注者自己都无法一致判断正确答案,增加训练算力不会自动解决任务定义的问题。真要训练时,另设留出集,记录模型和数据授权,避免把私密记录随意上传外部服务。

每条路线都应该预先写下停止条件。例如“完成三个随机种子的短跑并报告均值与差异”,或者“找到并修复一个会让梯度对照失败的反例”。不要把“继续调到分数好看”为唯一目标,那会让测试集逐渐变成训练信号。

31.6 把实验记录写给未来的自己 ​

一个可复查的实验目录至少应保存配置、代码版本、环境、数据来源与处理方式、运行命令、原始日志、评估定义和结论边界。若使用别人的源码,还应保存许可证及修改说明。只保存最终权重,无法解释它为何得到这个结果。

本书最后一部分保留了固定版本课程源码供核对、原创微型实现、真实 CPU 输出、图表生成脚本和主题覆盖表。源码的 MIT 许可不自动覆盖视频、幻灯片或语料;本书没有把公开可观看的视频当成可任意转录的材料。课程原始来源与本书补充内容均分别标明。

综合练习与参考答案 ​

以下为本书补充练习。

  1. 在 lab.py 中故意把加权累积改成两份各除以 2。哪个断言应该失败?为什么这个失败比“最终 loss 高一点”更有定位价值?
  2. 把 checkpoint 中 optimizer 状态去掉,保留模型与 RNG。设计一次测试证明下一步不再严格一致。
  3. 把 causal attention 改成不遮挡未来,保留训练标签。为什么训练损失可能变好,生成能力却不能据此得到同样结论?
  4. 为一个新实验写一段不超过 150 字的结果说明,必须包含规模、数据、已验证结果与未验证范围。

参考答案:第 1 题 weighted_accumulation_error 应超过容限,失败直接指向目标均值的分母;第 2 题在保存前至少完成一次 AdamW 更新,再比较恢复后同一批数据的下一次参数更新;第 3 题训练时未来输入会泄露标签相关信息,生成时又拿不到这些未来 token,任务条件不一致;第 4 题可参考:“在 CPU 上用 7056 参数模型训练固定的模 17 后继任务,100 次更新后损失由 2.840298 降至 0.535705;已验证小尺寸梯度与恢复流程,未测试自然语言泛化、GPT-2 规模训练或 GPU 性能。”

本章来源与配套文件 ​

学完这条路线之后,再遇到一个新架构,可以先问:它改变了数据表示、信息传递、优化目标,还是执行方式?能把变化定位到其中一层,再设计一个可复查的小实验,你就有了继续学下去的具体方法。

保留各书来源与许可 · 示例运行条件以正文为准