跳到主要内容

SFT 从零写 — 训练信号怎么只落在回答上

这一章讲三件事: SFT 的监督信号到底是什么;一份「问题+回答」怎么被加工成机器能吃的形状; 以及怎么确认模型真的变了。 这一章跟着原书把训练循环亲手写一遍——每个循环不到一百行,单卡能跑,代码逐行讲。 读完你能回答:「为什么说 SFT 只是换数据的预训练?」

1. 先看现象:损失在降,但模型学对了吗

跑训练时屏幕上最显眼的是一个不断变小的数——损失(衡量「模型的输出离目标差多远」的一个数,下一节讲它怎么算)。损失在降,只说明优化器(负责挪动权重、让损失变小的角色,下一节还会见到它)在干活,不说明模型学的是你想教的东西

原书这一章的立意就是对付这件事:把训练循环拆到最简,让「训练信号怎么流进权重」看得见,再配两个探针直接看模型的输出1。这一章的主走查是一条只有一轮对话的数据:

问:「用一词总结:回答永远保持简洁。」 答:「Concise.」(简洁)

目标是 30 步训练后,模型对这句话的回答从「随机的续写」变成「Concise」。 下面每一节都在给这条走查铺一步。

2. 顶层全景

一轮对话:「问句 + 回答」
│ 对话模板:套上特殊 token,标出谁是用户、谁是回答

一条 token 序列 + 一份标签(prompt 位置改成 -100)
│ 五步循环,重复 30 次

模型前向算分 → 错位对齐 → 交叉熵(跳过 -100)→ 反向传播 → 优化器挪权重


探针①:这条示范的损失从高变低
探针②:贪婪解码的文本从「别的什么」变成「Concise」

图说:机制与预训练完全相同,唯一的差别是数据——
预训练猜「互联网的下一个词」,SFT 猜「示范回答的下一个词」。

整张图只有一个核心量在动:损失。下一节讲它怎么算。

先打个预防针,那里会用到(衡量一份消息有多出乎意料)——名字唬人,用法朴素。

还会用到对数(把大数字的连乘压缩成连加的数学记法)。到时候我当场演示。

3. 监督信号:预测下一个 token

打分:从 token 到概率

模型每读到一个位置,就对词表(它认识的全部 token 的清单,通常几万到十几万个)里的每一个 token 打一个分。这个没加工过的分数叫 logit

分数可正可负、大小没尺度,要再过一道 softmax:把分数压成概率——每个候选 token 各占多少可能性,加起来等于 1;分数越高,占比越大2

一条长度为 T 的序列(token 排成的一串),模型会在每个位置上各做一次这样的预测3

预测长什么样:每个位置吐一份概率分布(整张词表的可能性清单),猜的都是「下一个 token 是什么」。

打分怎么变成「错多少」:交叉熵

每个位置上,模型有一份预测,数据里有一个正确答案(示范里实际的下一个 token)。两者的差距叫交叉熵:它就是负对数——把「模型分给正确 token 的概率」取负对数——概率越接近 1,损失越接近 0;概率越小,损失越大4

两种极端一眼看清(书里的原话):模型自信且答对,损失近零;模型自信且答错,损失很大5

在示范样本上把这个损失压到最小,统计里叫最大似然(让真实数据出现的可能性最大)估计——说人话:让数据里真实出现的 token,在模型那里变得更可能6

这就是 SFT 的全部

这里要停一下,把一件事钉死:SFT 没有任何「好答案/坏答案」的概念。它唯一知道的是「示范里这个位置写的是这个 token,你刚才给它的概率太低,去调权重」。预训练和 SFT 用的是同一个机制、同一个损失,唯一的区别是数据换了7

为什么这么高效:teacher forcing

训练时每个位置用的前文都是示范里的真实 token,不是模型自己生成的——这个安排有个名字,叫 teacher forcing(老师强迫:每一步都按标准答案走,不让学生自由发挥)8

它的直接后果:一条 10 个 token 的回答,一次就给出 10 个「预测-对答案」的机会——每个 token 都产生一个训练信号,这就是 SFT 样本效率高的原因9

4. 只训回答:对话模板与掩码

一份 SFT 数据长什么样

一份 SFT 数据 = 一个 prompt(用户的话)+ 一个 completion(你想要的回答)。你要模型学会「看着 prompt 产出 completion」,而不想让它学会生成 prompt——因为真正使用时 prompt 是用户给的,轮不到模型自己写10

对话模板:给对话加上标点

现代模型靠对话模板(chat template)分辨角色:一套用特殊 token 标出「系统/用户/助手」边界的固定格式。各家族模板不同——Llama 一套、Qwen 另一套;训练库 TRL 替你把这些差异抽象掉。模板套错,训出来的模型会输出畸形格式,或者干脆无视系统提示词11

掩码:把损失按在回答上

做法是把 prompt 和 completion 拼成一条序列喂进去,但准备一份标签数组:它照抄输入的 token,唯独把 prompt 位置全部改写成 -100——这是 PyTorch 交叉熵约定好的「跳过此位」哨兵值。回答位置保留真值,参与损失;另外标签要错一位读:位置 t 的标签是 t+1 的 token,因为模型在每个位置猜的都是「下一个」12

拿主走查那条数据看掩码长什么样(序列形状为演示简化):

输入 token: [模板头] [用户:「…keep answers concise.」] [模板中] [助手:「Concise.」] [模板尾]
标签: [ -100 ] [ -100 -100 … -100 ] [ -100 ] [ 「C」「o」「n」…「.」] [ -100 ]

图说:损失只从「Concise.」那几个 token 上收。模型在回答的每个位置
都被要求猜对下一个 token,其他位置一律免考。

原书用三行代码把掩码做出来13:

labels = input_ids.clone() # 标签先照抄输入
assistant_masks = enc["assistant_masks"] # 模板标出的「哪些 token 属于助手回答」
labels[assistant_masks == 0] = -100 # 不属于回答的位置,全部免考

5. 循环五步,走完主走查

原书的循环一共五步:前向算分 → 错位对齐 → 交叉熵(跳过 -100)→ 反向传播(从损失倒推每个权重该往哪挪)→ 优化器步进14。把中间三个新词说清:

  • 反向传播:从损失出发,倒推「每个权重该往哪个方向挪、挪多少」——这一趟算完,几亿个权重各自的指引一次拿齐;

  • 梯度:反向传播算出来的那份指引清单——每个权重一行「往哪边挪、挪多少」;

  • 优化器:拿着梯度真正去挪权重的角色。书里用的是 AdamW,属于梯度下降(顺着梯度一小步一小步把损失压下去的算法)这一族,只是它给每个参数配了自适应的步长15

设置:学习率(每步允许挪的幅度)取 1e-5,即 0.00001——刻意极小,因为这是微调,只许轻轻纠偏,不许把预训练学的东西冲掉;重复 30 步16。核心代码(照原书,注释是我们的):

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)

for step in range(30):
logits = model(input_ids).logits # ① 前向:每个位置一份分布
shift_logits = logits[:, :-1, :] # ② 错位:丢掉最后一个位置
shift_labels = labels[:, 1:] # 丢掉第一个标签(位置 t 猜 t+1)
loss = F.cross_entropy( # ③ 交叉熵
shift_logits.reshape(-1, shift_logits.size(-1)),
shift_labels.reshape(-1),
ignore_index=-100, # -100 的位置不计入
)
loss.backward() # ④ 反向传播:算出全部权重的指引
optimizer.step() # ⑤ 挪一点
optimizer.zero_grad() # 清掉旧指引,进下一步

两个书里特意点破的细节:错位对齐必须显式做——位置 t 的预测对的是 t+1 的 token,不对齐就全教错了;对 logits——就是前面说的打分——求梯度,得到的恰好是「预测减去正确答案的独热编码」,传回去的就是「你猜的减去该猜的」17。另外 transformers 库有捷径 model(input_ids, labels=labels).loss 一行顶上面五步里的前三步,原书坚持手写,就是为了不让错位和掩码藏进黑盒18

6. 训完之后:两个探针

探针①:示范的损失。 把主走查那条数据在训练前后各过一遍交叉熵。训前损失高(模型给「Concise.」的概率很低),训后趋向零——这个数就是循环里压的那个数,直接读出「示范在模型眼里变得多可能」19

探针②:生成的文本。 用贪婪解码(每一步都挑概率最大的 token,不带随机)把用户问句单独喂给模型,训前训后各来一次,比较文本20

训前: 「Summarize in one word: …」 → "The word that captures the idea of
keeping answers brief would be…" (串是编的演示;书里只说
「生成的是别的东西」,没给具体串)
30 步后: → "Concise." (书里说:贪婪解码收敛到目标词)

图说:损失降了是「优化器在干活」;解码文本变了才是「模型学会」。
权重动了没,要看输出,不能只看损失曲线。

「固定输入、训前训后各测一次、看输出」是原书要读者带全书的习惯——抓「循环在跑、但学错了东西」的最快办法21

7. 作者的判断与证据

书里给了证据的:

  • SFT 效果立竿见影:基础模型训完立刻按对话模板的格式说话——这是可以直接观察到的事实;
  • 一千条高质量示例训出来的模型,好过十万条噪声(又杂又互相打架)示例——作者把这表述为普遍经验(数据质量压倒数量),但这一章没有附对照实验,严格说是强论断而非证据22

是作者的方法论主张:

  • 「探针习惯」——任何训练都该配固定输入的前后对比。这是作者给出的工程纪律,不是实验结论;
  • 原书承认的边界:SFT 之后模型仍只会模仿,不会评估自己输出的好坏,也不会在多个正确选项里挑更好的那个——这道缺口是第 04 章偏好学习的入口23

8. 边界与局限

这一章的循环是刻意削出来的:「全书的纳米版」,每条循环不到一百行,砍掉了真实训练栈的基建换可读性24。它没覆盖的:多卡与分布式、日志(训练过程的运行记录)与实验管理、数据清洗——原书预告这些放到后续章节(TRL 各阶段那一部分),Early Release 还没出到。还有一个容易误读的点:30 步就能练到位,靠的是单条数据、一个极窄的目标;真实 SFT 是几千到几百万条数据上几千步的循环,「30 步」这个数不构成对训练时长的预期。

9. 可带走的

  1. SFT 的学习信号只有一个:把示范回答里每个 token 的概率往上提,没有别的;
  2. 机制与预训练完全相同,只换数据——理解了这句,就理解了 SFT 的一切;
  3. 交叉熵 = 正确 token 概率的负对数:自信且对≈0,自信且错很大;
  4. teacher forcing 让每个 token 都产生一个训练信号,这是 SFT 高效的原因;
  5. 损失掩码(-100)保证模型只学回答、不学复述问题;
  6. 对话模板套错,模型就废了:输出畸形或无视系统提示词——用 TRL 之类的库可以少踩;
  7. 微调学习率要小(示例 1e-5),目的是纠偏不是重学;
  8. 训完必做前后探针:固定输入,看生成文本,别只看损失曲线;
  9. SFT 的天花板是示范本身:它学不会「比示范更好」。

10. 原文地图

主题原书章原文位置
每条循环不到百行、纳米版Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:16(搜「under 100 lines」)
信号来源之别(全章主旨)Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:24(搜「SFT copies a signal you provide」)
模型与硬件设置(0.6B、16GB)Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:45(搜「600 million parameter」)
token/logit/softmaxChapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:69(搜「called a logit」)
交叉熵与最大似然Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:75(搜「negative log probability」)
自信且对/自信且错Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:77(搜「confident and wrong」)
机制不变只换数据Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:79(搜「mechanism does not change」)
teacher forcing 与密度Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:81(搜「teacher forcing」)
掩码与 -100Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:87(搜「sentinel value」)
标签错一位Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:89(搜「shift of one」)
主走查对话与掩码代码Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:98(搜「always keep answers concise」) · :119(搜「assistant_masks」)
循环五步Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:124(搜「five steps」)
训练代码(AdamW、30 步)Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:130(搜「AdamW」) · :133(搜「range(30)」)
梯度=预测减正确Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:150(搜「one-hot target」)
transformers 的捷径Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:154(搜「shifted cross-entropy」)
前后探针的动机Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:158(搜「probe the model before and after」)
探针代码与结果Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:180(搜「do_sample=False」) · :182(搜「converges on the target word」)
探针习惯Chapter 2. Speed Run Post-Training from First Principlestext/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:190(搜「learning the wrong thing」)
SFT 数据格式(对话/指令对)Chapter 1. Introduction to Post-Trainingtext/04-ch01-chapter-1-introduction-to-post-training.txt:136(搜「masked from the loss function」)
对话模板、模板错的结果Chapter 1. Introduction to Post-Trainingtext/04-ch01-chapter-1-introduction-to-post-training.txt:138(搜「malformed output or ignores its system prompt」)
数据质量压倒数量Chapter 1. Introduction to Post-Trainingtext/04-ch01-chapter-1-introduction-to-post-training.txt:140(搜「A thousand high-quality examples」)
SFT 只会模仿、不自评Chapter 1. Introduction to Post-Trainingtext/04-ch01-chapter-1-introduction-to-post-training.txt:142(搜「evaluate the quality of its own outputs」)

Footnotes

  1. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 16 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:16,搜「under 100 lines」)与第 158 段(搜「probe the model before and after」)。原文:代码刻意削减抽象,聚焦 SFT 与 GRPO 在 token 和参数层面真正做的事;损失降只说明优化器在工作,不直接说明学到了什么。

  2. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 69 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:69,搜「called a logit」)。原文:模型对词表中每个 token 输出一个分数,称为 logit;softmax 把这些 logits 变成下一个 token 的概率分布。

  3. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 69 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:69,搜「one after each position」)。原文:长度 T 的序列产生 T 个分布,每个位置一个,各自预测后继 token。

  4. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 75 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:75,搜「negative log probability」)。原文:标准损失是交叉熵,即模型分给正确 token 的概率的负对数。

  5. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 77 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:77,搜「confident and wrong」)。

  6. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 75 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:75,搜「maximum likelihood estimation」)。

  7. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 79 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:79,搜「mechanism does not change」)。原文:SFT 的全部学习信号就这些;没有好坏答案的概念;机制不变,只换数据。

  8. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 81 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:81,搜「teacher forcing」)。原文:每个位置都在真实 token 之上训练,而不是模型自己会生成的东西。

  9. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 81 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:81,搜「ten supervised predictions」)。原文:一个十 token 的回答提供十个受监督的预测,这种密度是 SFT 高效的原因。

  10. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 85 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:85,搜「the prompt is supplied by the user」)。

  11. 出处:「Chapter 1. Introduction to Post-Training」第 138 段(text/04-ch01-chapter-1-introduction-to-post-training.txt:138,搜「malformed output or ignores its system prompt」)。原文:各家模板不同,TRL 抽象了这些差异;模板套错会产生畸形输出或让模型无视系统提示词。

  12. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 87-89 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:87,搜「sentinel value」)与第 89 段(搜「shift of one」)。原文:-100 是 PyTorch 交叉熵「跳过此位」的哨兵;标签错一位读。

  13. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 112-119 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:119,搜「labels[assistant_masks」)。原文:apply_chat_template 渲染对话并返回 assistant_masks(标记哪些 token 属于助手回答);标签先照抄 input_ids,再把非助手位置改成 -100。

  14. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 124 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:124,搜「five steps」)。

  15. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 148 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:148,搜「decoupled weight decay」)。原文:AdamW 是带逐参数自适应步长和解耦权重衰减的梯度下降;loss.backward() 为每个权重填上梯度,optimizer.step() 沿它做一步梯度下降。「梯度=指引」的解释为我们的转述。

  16. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 130-133 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:130,搜「lr=1e-5」)与第 133 段(搜「range(30)」)。

  17. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 150 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:150,搜「one-hot target」)。原文:交叉熵对 logits 的梯度是预测分布减独热目标,每个位置传回的误差是「预测减正确」。

  18. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 154 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:154,搜「shifted cross-entropy」)。

  19. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 160 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:160,搜「demonstration’s loss」)。

  20. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 170-182 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:180,搜「do_sample=False」)与第 182 段(搜「converges on the target word」)。原文:训前贪婪解码产出的不是目标;三十步后损失趋零、贪婪解码收敛到目标词。训前的具体输出串书里没给,是演示编的。

  21. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 190 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:190,搜「learning the wrong thing」)。原文:无论训什么,都在固定输入上测训前训后,看输出不只看损失——这是抓住「技术上在跑、实际学错了」的最快办法。

  22. 出处:「Chapter 1. Introduction to Post-Training」第 140 段(text/04-ch01-chapter-1-introduction-to-post-training.txt:140,搜「A thousand high-quality examples」)。

  23. 出处:「Chapter 1. Introduction to Post-Training」第 142 段(text/04-ch01-chapter-1-introduction-to-post-training.txt:142,搜「evaluate the quality of its own outputs」)。

  24. 出处:「Chapter 2. Speed Run Post-Training from First Principles」第 16 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:16,搜「trading convenience for brevity」)与第 435 段(text/05-ch02-chapter-2-speed-run-post-training-from-first-pri.txt:435,搜「deliberately minimal」)。