Skip to content

第 20 章 用一条可检查的训练链路跑通小 GPT ​

现在我们已经有字符表、错位标签、因果多头注意力、前馈层和残差块。本章把它们接成完整程序,重点不在让输出“看起来像一段文学作品”,而是知道每个结果来自哪里:损失为何下降,验证是否可信,生成为什么仍会出错。

完整文件为 examples/part3/tiny_gpt.py,它能在 CPU 上独立运行,不下载数据和权重。本书实现经过真实运行;原课程的莎士比亚训练配置与本书缩小配置分开列出。原课后来的工程化实现可对照 nanoGPT model.py 与 train.py。

20.1 forward 必须同时满足训练和生成 ​

模型的输入编号形状为 (B,T)。取 token embedding 后为 (B,T,C),加上位置向量,依次通过多个 Transformer block,最终归一化后映射到 (B,T,V)。输出层的每一行代表同一位置对 V 个下一字符候选的打分。

本书的 forward 总是返回三维 logits,并在有 targets 时另算标量 loss。训练计算 loss 时才临时展平,不改变返回 logits 的接口。原课脚本有些版本会在有 targets 时直接把 logits 变量展平,调用方要知道这个差异,不能仅凭名字推断形状。

词表表和输出层没有绑定权重,采用 PyTorch 默认初始化;它们与 GPT-2 或当前 nanoGPT 的初始化细节并不完全相同。固定课程仓库 gpt.py 后来加入了标准差 0.02 的初始化,代码注释明确说原视频未覆盖这一改动。本书把它记录为仓库后续差异,而不是补写成原视频当时已经讲过的内容。

20.2 AdamW 更新的是参数,不是输入编号 ​

优化器接收 model.parameters(),包含 embedding、投影、LayerNorm 的可训练参数等。因果遮罩用 register_buffer 保存,会随模型移到设备和进入 state_dict,但它不是需要梯度更新的参数。

AdamW 使用梯度的一阶、二阶矩估计,并将 weight decay 与梯度更新解耦。本书为了展示最短的训练链路,对所有参数统一设置 weight_decay=0.01;这不是所有项目的最佳分组方式。nanoGPT 会区分维数较高的权重与 bias、归一化等参数,再决定是否衰减。学习率也不是越大越省时:过大可能震荡或发散,过小则在有限步数里学不动。AdamW 原论文;PyTorch 2.6 AdamW

随机种子、取样和评估要分清。我们的训练窗口使用独立的 torch.Generator,评估用另一个固定生成器;这样添加一次评估不会悄悄消耗训练窗口的随机状态。模型 dropout 仍依赖 PyTorch 随机状态。相同种子也不保证跨版本、跨硬件、跨算子后端逐位一致,所以日志同时记录 PyTorch 版本和 CPU 线程数。

20.3 评估不能只看最后一个训练 batch ​

一个 batch 可能刚好容易,另一个可能含有稀有组合。评估函数在 eval 与 no_grad 下,从每个 split 抽取多个窗口,分别计算平均损失。这个均值仍是抽样估计,不是整份验证集的精确遍历结果。若需要严谨比较两个配置,应固定评估集合、随机种子、训练预算和 tokenizer。

本书用 8 个批次估计训练与验证损失;评估前记住模型模式,评估后恢复。恢复成原模式比无条件调用 train 更稳妥,因为调用方可能本来就在评估。

项目本书真实 CPU 实验固定版课程 gpt.py 配置
batch size1664
上下文长度16256
模型宽度32384
注意力头数46
块数26
dropout0.10.2
训练步数1205000
学习率0.0030.0003
数据两句原创短文本重复tiny Shakespeare

右列只是代码配置核对,不是本书运行结果。官方 Colab 的最终代码又使用更小的配置,因此“视频模型”不能用一个没有版本说明的数字概括。

20.4 本次真实输出,以及它不能说明什么 ​

运行命令如下,详细 JSON 记录在代码包 qa/part3/training.json:

bash
python examples/part3/tiny_gpt.py --steps 120
python examples/part3/check_attention.py

PyTorch 为 2.6.0+cpu,seed 为 1729,CPU 线程为 1。模型共有 26,702 个参数,词表 14 项。120 步之前,训练/验证估计损失分别为 2.817643 和 2.817066;之后分别为 0.068689 和 0.069291。

下面是实际采样结果的一部分,保留了错误,没有人工润色:

text
t. the dog ran.
the cat sat. the dog ran.
the cat sat. the dat. rathe cat sat. the dog ran.
the cat s

低损失并不使每一次随机生成都正确。“the dat” 这样的输出可以来自分布尾部被采到,也可能来自模型尚未充分学习。初始提示只有 t,它的下一个字符在语料的不同上下文中本来就未必唯一。

更重要的是,验证数据和训练数据重复了相同短句。这个实验说明前向、反向、参数更新和生成链路连通,不能说明模型获得了通用语法能力。训练语料稍微有结构,模型就可能把损失降得很低;评估是否有价值,取决于验证题是否真的新。

20.5 生成每一步都要裁剪上下文 ​

生成的序列会越来越长,但位置表和遮罩只支持最多 context 个位置。因此每一步使用:

python
logits, _ = model(ids[:, -model.context:])
last = logits[:, -1, :] / temperature
next_id = torch.multinomial(last.softmax(-1), 1)
ids = torch.cat([ids, next_id], dim=1)

只裁剪送给模型的输入,不丢弃用户要得到的完整输出。裁剪后本例位置编号重新从 0 开始,这是一种固定窗口生成策略;它不具备跨窗口的额外记忆,也没有实现 KV cache。每生成一个字符都重新计算窗口中的所有位置,简单但重复工作多。

温度必须大于 0。温度小于 1 会拉大相对分数差,使分布更集中;大于 1 会变平。温度本身不提供知识,不能靠调温度修复没学到的事实。若需要贪心解码,应直接取 argmax,而不是把温度设为零造成除零。

20.6 从这个小 GPT 到 ChatGPT,还缺什么 ​

本课展示的是自回归预训练的核心目标:用前面的 token 预测下一个。Transformer 是网络结构,GPT 是使用该类解码器结构进行生成式预训练的一条模型路线。一个随机初始化的小 GPT 经过莎士比亚文本训练,不会自动变成遵循指令的聊天助手。

课程末尾讨论了预训练、监督微调和基于人类反馈的训练。可以把它们分别理解为学习广泛文本分布、学习如何响应示范任务、依据偏好调整行为的不同阶段。ChatGPT 的产品能力还涉及数据、训练规模、对话格式、安全处理和推理系统。这里没有复现这些阶段,也不能以 26,702 参数小实验推断大型系统的全部机制。GPT-3 原论文;InstructGPT 原论文

课程练习:把“跑出来”改成可检验的问题 ​

课程 EX2 建议尝试自己的数据,甚至加法;EX3 建议先在大数据预训练,再在莎士比亚微调;EX4 建议实现论文中的一项结构改动。下面提供检查方法,不能把尚未执行的研究题写成已完成实验。

EX2:加法。 先生成整数对,按数值或长度划分训练与测试;只计算答案部分损失。把答案数字倒序可能与进位算法的方向更一致。答案不应只报告 token loss,还要把生成结果还原成数字,检查整题准确率,尤其测未见长度。提示:遇到一个错误例子时,记录是数位错误、进位错误,还是结束位置错误。

EX3:预训练再微调。 对照组必须有相同微调数据与评估集合,记录预训练额外计算量。先保存模型、优化器和 tokenizer 信息,再用较小学习率微调。答案边界:本书没有大规模预训练条件,因此不能给出“必然优于从头训练”的实测结论;这项练习的结果必须由实际对照实验回答。

EX4:结构改动。 可以尝试把 ReLU 换成 GELU,或改变位置编码,但一次只改一个主要因素。参数量、步数、随机种子、验证集合需一起报告。若结果仅变化很小,至少增加几个随机种子,避免把抽样噪声当作模型改进。

补充题与答案。 为什么高训练损失时不应先追求更好看的采样?因为采样只观察少量输出,而且可以通过低温度掩盖部分错误。应先检查标签错位、mask、归一化轴、梯度是否存在、学习率和数据,再用固定评估量比较训练进展。

本章主要来源:固定版 nanoGPT 训练循环;AdamW 原论文;InstructGPT 原论文。实现和实验差异已在对应小节注明。

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