从0训练LLM · 现代大模型体系 实验 实验 ≈ 30 分钟 页内实验 ×5
C12 后训练:从续写到听话、推理、做事 实验课

实验课:在我们自己的模型上做一次 SFT

同一个预训练 checkpoint,只多 300 步 SFT、只在回答和结束符上算 loss:留出提示里 64 个 token 内停下来的比例从 0% 变成 98%,结束符的 loss 从 16.84 降到 0.37;代价是预训练验证集的 loss 涨了 0.27。第 1 课的每一条结论,都拿我们自己的小模型核对一遍。
🧪

这一课不讲新概念。第 1 课的结论——SFT 用同一个交叉熵、只在回答上算 loss、教的是格式和行为、结束符要进 loss——放到我们自己从 0 训练的 3.7M 参数模型上核对。所有数字直接读取实验导出的 c12-posttrain.json;组件下方的「数字口径」写明了脚本、配置与日期。

这一课先不碰:偏好与 RL(3.7M 的模型连指令都刚学会,RL 部分用第 2–5 课的教学世界演示)。

实验是怎么设计的

为什么这里换例子:我们的模型是在《三國志演義》上预训练的,词表只有 2,048 个 token,它不认识「小明把苹果给了小王」这样的现代汉语。指令数据于是就地取材:

从已有出发 · 后训练流水线 点已学的站看数据、损失与模型数
演进图 · 每一代都是被上一代的失败逼出来的本课高亮
  1. 第一代 SFT:用示范教格式 FLAN 2021 · LIMA 2023 你在这里
    解决了同一个交叉熵,只在回答 token 上算:Qwen3.8 模板里一条 87 token 的样本只有 24 个算 loss;LIMA 只用 1,000 条示范。
    卡在只有正例:教学世界里只示范 A,SFT 对另一个好回答 B 和拒答 F 的梯度逐位相同,两者概率永远相等(第 2 课浏览器实算)。
  2. 第二代 RLHF:奖励模型 + PPO Christiano 2017 → Ziegler 2019 → InstructGPT 2022
  3. 第三代 · 离线 DPO:不训奖励模型 Rafailov et al. 2023(Stanford)
  4. 第三代 · 在线 GRPO 系:去掉 value model DeepSeekMath 2024-02 → DAPO / Dr. GRPO / GSPO 2025
  5. 奖励来源 RLVR:可验证的奖励 Tülu 3 2024-11 命名 · DeepSeek-R1 2025-01 规模化
  6. 岔路 RL 扩展了能力,还是重排? Yue 2025 vs ProRL 2025
  7. 第四代 思考模型:想更久,也能控 o1 2024-09 · R1 2025-01 · Qwen3 2025-05
  8. 第五代 蒸馏:小模型直接继承 Hinton 2015 → GKD 2023 → Qwen3 2025
  9. 第六代 Agentic RL:在环境里做事 SWE-RL 2025-02 · Kimi K2 · GLM-4.5 slime · DeepSeek-V3.2

一、先猜,再看

🎯

先在心里写下三个预测,再往下看:① 300 步后,eos_rate 从 0% 变成多少?② 台词 token 的 loss 会降很多吗?(提示:台词能从上文推出来吗?)③ 预训练验证集的 loss 会变好、不变还是变差?

项目实测 · SFT 前后对照(c12-posttrain.json)

读表(2026-09-26 导出的数据,以组件显示为准):

二、它是什么时候学会的

项目实测 · SFT 300 步的逐步曲线

几乎所有变化都发生在前 50 步:第 50 步 eos_rate 已经是 100%,结束符 loss 从 16.84 降到 0.63;之后 250 步,回答 loss 只从 4.49 慢慢挪到 4.47,预训练 loss 却一直在涨(4.869 → 4.909)。学会格式很快,多训只是在付遗忘的代价——这正是 LIMA「1,000 条就够」在我们这个规模上的影子,也是决策 3(训多少步)的依据。

三、同一个提示,前后各生成了什么

项目实测 · 按固定间隔从留出集抽 6 条(不挑结果),贪心解码

SFT 前:模型把「答:」后面当成小说正文接着往下写,64 个 token 用完也不停(有的还陷入「踐踐踐」式的重复)。SFT 后:十几个 token 的一句台词,停在结束符上。台词和参考答案对不上,但形式完全对了。

四、把结论对回前面几课

课那一课的结论在我们的模型上看什么
第 1 课SFT 是同一个交叉熵,只改求和范围训练引擎、优化器与预训练完全相同,只有 targets = −1 的位置不同
第 1 课教的是格式和行为,不是知识eos_rate 0% → 98%,台词 loss 只降 0.56
第 1 课结束符要进 loss,模型才学得会停结束符 loss 16.84 → 0.37;SFT 前 100 条里没有一条停下来
第 1 课 · 深一层从不当目标的 token,输出行被一路压低SFT 前结束符 loss 16.84 > ln 2048 = 7.62
第 2–5 课偏好、RL、奖励作弊3.7M 的模型做不了;下面用教学世界把 SFT 与偏好学习放在一起再看一次
问题:「小明把苹果给了小王。」苹果现在在谁手里?(标准答案:小王)

教学世界:候选回答、初始概率、偏好对是手写的教学数据(不是任何模型的输出或真实标注);token 数来自 Qwen3.8 真 tokenizer;其余数字由浏览器按真算法现算。

只有示范时,SFT 对「另一个好回答」和「拒答」一视同仁(第 2 课);我们的项目数据里每个提示只有一句台词,情况一样——模型学会的是「写一句台词然后停」,而不是「哪句台词更好」。要教「更好」,得有比较或奖励。

五、为项目拍板

🧭

你要拍板的三件事(对应章首页的项目里程碑):

  1. 结束符用哪个?本实验用分词器自带、预训练里没出现过的 <|endoftext|>:不扩词表,代价是它的初始 loss 高达 16.84。另一条路是新增 <|im_end|> 这类特殊 token,要扩 embedding、改 vocab_size。
  2. 提示部分要不要算 loss?我们的提示(上文 + 「某某說了什麼?」)比回答长得多、样本只有 6,750 条——正是 Shi et al. 2024 说「对指令也算 loss」可能更好的条件。对照已经跑了(第一节表的第三列):回答 loss 分不出高下,预训练 loss 少涨约 0.07。你更看重哪一个?
  3. 训多少步?依据是上面那条曲线:格式在第 50 步就学会了,之后每一步都在涨预训练 loss。

验收标准:TestSFTMask 通过(提示 token 梯度恰为 0);SFT 后留出提示的 eos_rate ≥ 95%;预训练 val loss 的涨幅写进表里,任何改动都不能让它比本页基线(+0.27)更大;两个设置的差异小于噪声底线(C14:同配置 5 次重跑极差 0.016,3σ ≈ 0.020)时不下结论。

🔍 自己复现这个实验

在 04-实践项目/ 下运行 python3.12 experiments/exp_c12_posttrain.py(已有且配置一致会直接复用 runs/c12_sft/,加 —retrain 强制重训),导出到 知识平台/src/data/project/c12-posttrain.json,本页所有组件自动读它。代码在 minillm/sft.py:extract_pairs 抽对子、encode_example 构造 x / y 并把提示部分的 y 设为 −1、collate 右侧补齐(补齐位置同样是 −1)、answer_loss 分开统计台词与结束符的 loss。改一项、写下预测再看结果——这门课所有实验课的固定流程。

一句话

📌

同一个 checkpoint、只多 300 步 SFT:格式在 50 步内学会(eos_rate 0% → 98%),内容几乎没变,代价是预训练 loss 涨了 0.27——SFT 教行为,不教知识,而且不是免费的。

下一个缺口

RL 的 rollout 就是 decode:模型一个 token 一个 token 地生成,为什么这么慢、怎么变快? → C13:推理