← 返回首页
08_sft_viz.html
SFT:把 base 调成会答题
白皮书 讲了 SFT 的轮廓,这一关动手把它跑通 ——
直接拿 Phase 1 的 124M base 做监督微调。一句话打底:SFT 和预训练用同一个 loss ,只动三处 ——
模板 、
EOS 、
loss mask 。
配套代码 phase2-sft-lora/06_sft.py。
这一关
base 124M Phase1 训的底座
数据 16 条 玩具指令对
lr 2e-5 比预训练小30×
EOS 50256 教它停
① 起点:同一个问题,base 只会跑题、停不下来
Phase 1 的 124M 是个base ——
它只会"顺着上文接着写"。你问它问题,它不答,而是把你的问题当开头继续编,直到撞上长度上限才被强行截断 。
下面是 06_sft.py 训练前 对 base 的真实采样:
指令 → base 的"回答"(实测)
What is the capital of France?
→ "For the purposes of this article, we do not consider the capital of France as the capital of the states. In fact…"
没停 · 被长度上限截断
我们想要的样子
What is the capital of France? → "The capital of France is Paris." 答完 · 主动停
base 不是"笨",它只是没学过"被问就该答、答完就停"这件事 。这恰恰是 SFT 要教的。
而且注意:它续写里冒出来的 "capital / France" 说明知识预训练时就有了 ,SFT 不灌新知识,只拧"回答的姿态"。
↳ 下一步:要教它,先得把"指令 + 理想回答"拼成一条它能学的训练样本。
② 拼一条训练样本:对话模板 + EOS
SFT 的数据是「指令 → 回答」成对 的。要喂给模型,得先用一个固定模板 把它俩拼成一条序列,
并在回答末尾放一个 EOS 。点下面换不同样本,看它怎么被拼起来:
模板标记 标出"哪段是指令、哪段该我答"
指令
回答
EOS 答完就停
模板标记用什么文字不重要,重要的是"全程一致"。 真实 chat 模型常用 <|user|>/<|assistant|>;
06_sft.py 用的是 Alpaca 风格的纯文字模板 ### Instruction / ### Response ——
好处是不必给词表加新 token (vocab 保持 50304、权重绑定不变)。EOS 直接复用 GPT-2 的 <|endoftext|>(id 50256)。
↳ 代码:06_sft.py 的 PROMPT_TEMPLATE + build_example
↳ 下一步:整条都喂进去,但 loss 只能算"回答"那一段 —— 这是 SFT 唯一的新动作。
③ loss mask:整条都看,但只对"回答"算账
把拼好的序列喂进模型做前向 ,
每个位置都会预测"下一个 token"。但我们只对落在"回答段"的预测算 loss ,
指令/模板段的标签全标成 -100 —— PyTorch 的 cross_entropy 默认 ignore_index=-100,会自动跳过它们。
灰底 × = 指令/模板,标签 -100,不算 loss
橙底 ↑ = 回答,算 loss
为什么只算回答? 我们要教的是"给定指令,该怎么答 "。如果连指令段也算 loss,
模型会分心去学"怎么生成用户的指令",注意力被带偏、答题质量下降。所以指令段必须 mask 掉。
回答末尾的 EOS 也算 loss —— 这正是它学会"答完就停"的地方。
↳ 代码:06_sft.py 的 build_example 里 labels = [-100]*len(prompt_ids) + resp_ids
那个 "错位一格"(x = full[:-1], y = labels[1:])是怎么回事?
语言模型是"用第 i 个 token 预测第 i+1 个"。所以输入 x 取整条序列去掉最后一个,
标签 y 取整条往左挪一格。掩码也跟着挪:第一个"回答 token"由"最后一个指令 token"预测出来,
这一对要算 loss (它是"从指令跨进回答"的关键一步);再往前的、预测指令内部 token 的位置,才是 -100。
这和 Phase 1 里 x=chunk[:-1], y=chunk[1:] 的错位是同一套,只是 y 多了 -100 掩码。
↳ 下一步:有了带掩码的 (x, y),训练循环就和预训练几乎一模一样了。
④ 训练循环:就是预训练那个循环 + 一个掩码
SFT 的循环和 Phase 1 的 04_gpt2_124m.py 几乎逐字相同:
zero_grad → forward(算 loss) → backward → step。唯一的差别是 forward 里的 loss
只来自回答段 (靠上一步的 -100 掩码)。点"跑一个 epoch"看回答段 loss 怎么掉:
zero_grad梯度清零
→
forward算 loss(只回答段)
→
backward反向求梯度
→
step更新权重
▶ 跑一个 epoch
重置
实测起点 2.16 → 收敛到 ~0.15 (ckpt10b 上,几十秒)
和预训练比,SFT 只改两个旋钮:学习率小 1~2 个量级 (2e-5 vs 预训练 6e-4,别把底座学崩)、
只过几遍数据 (epoch 少,数据少容易过拟合)。其余 —— 优化器、反向、裁剪 —— 全照搬。
↳ 代码:06_sft.py 的 main 训练循环 + make_batches (右侧补位用 -100,不产生梯度)
↳ 下一步:训完再问一遍同样的问题 —— 看行为有没有真的变。
⑤ 训练前 vs 训练后:行为真的变了
下面是 06_sft.py 在 124M 上的真实采样 (同一指令、同一随机种子)。
点切换看每条指令训练前后的差别 —— 重点不在"答得多对",而在有没有套上格式、有没有在 EOS 处停下 。
达标 玩具级 · 看行为不刷榜
✓ 回答段 loss 明显下降 (实测 2.16 → 0.15)。
✓ 套上模板给指令,模型产出"回答形状"并在 EOS 处主动停下 (不再无限续写)。
✓ 在训过的指令上大致扣题;甚至对没训过 的 "capital of Germany" 也套用了格式(格式会泛化)。
诚实交代: 这是玩具级。翻译那条 "J'aime leur." 就不太准 —— 16 条数据、124M、只见过英文,
学到的是回答的姿态与格式 ,不是可靠的知识/能力。要能真聊天得换更大、含中文的底座(另一条线)。
↳ 代码:06_sft.py 的 chat (碰到 EOS 就 break)· 跑法:python 06_sft.py --ckpt ../phase1-124m/ckpt10b/latest.pt --epochs 30
Q&A 常见疑问
SFT 会不会把预训练学到的东西"洗掉"?
会有一点(叫"灾难性遗忘"),所以才用很小的学习率 + 很少的 epoch ,轻轻拧、别学崩。
从结果也看得出:SFT 后它还知道 Paris、Berlin —— 知识基本还在,只是被套上了"答题"的姿态。
为什么用 16 条就能看出变化?真实 SFT 要多少?
16 条是为了演示机制 :格式和"停"这种姿态 很容易学,几十步就上身。
真实的指令微调通常要几千到几十万条 高质量数据,才能覆盖各种任务、答得有用。这里只求"看见机制",不求能力。
接下来两关会做什么?
第 9 章 · 手搓 LoRA :这一关是
全量微调 (更新全部参数);下一关改成只训旁挂的低秩小矩阵,~1% 参数拿到同款效果。
第 10 章 · DPO :从"会答"进一步到"答得合人意"(偏好对齐)。机制见
白皮书的任务×手段表 。
🎉 SFT · 监督微调 · 通关
你已经把 base 亲手调成了"会答题、会停"的模型,并看懂了模板 / loss mask / EOS。下一关:第 9 章 · 手搓 LoRA 。
← 上一步
下一步 →