预训练模型只会续写,怎么让它回答问题?
本课一问:一个预训练好的模型,你问它「「小明把苹果给了小王。」苹果现在在谁手里?」,它会接着往下编,而不是回答。怎么让它学会回答?
这一课先不碰:怎样的回答「更好」(第 2 课起);推理、思考模式(第 6 课);工具调用(第 8 课)。这一课只解决格式和行为:看到问题就作答,答完就停。
从已有出发
C0–C11 训出来的是一个「预测下一个 token」的模型。贯穿本章的提问,用 Qwen3.8 的真 tokenizer 切出 13 个 token;对预训练模型来说,这 13 个 token 和语料里任何一段文字没有区别——它的全部本事就是猜「后面最可能接什么」。网页上一个问句后面,常接的是另一个问句、一段评论、一篇文章的下一段,不一定是答案。
后训练流水线的第一站就是这一课。先看它在整章演进里的位置:
- 解决了同一个交叉熵,只在回答 token 上算:Qwen3.8 模板里一条 87 token 的样本只有 24 个算 loss;LIMA 只用 1,000 条示范。卡在只有正例:教学世界里只示范 A,SFT 对另一个好回答 B 和拒答 F 的梯度逐位相同,两者概率永远相等(第 2 课浏览器实算)。
先自己设计:原样把问题交给预训练模型
最省事的办法:什么都不训,直接把问题喂进去,顶多在前面放几个「问:… 答:…」的示例(few-shot)。我们自己从 0 训练的 3.7M 模型(《三國志演義》语料,C0–C11 的项目)正好可以试:给它一个「問:…說了什麼?答:」格式的提示,贪心生成 64 个 token。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
同一个 checkpoint,只多了 300 步 SFT(6,750 条「某某曰」对子、AdamW、学习率 3×10⁻⁴)。每条都在十几个 token 内停下来了;回答内容还谈不上对——台词本来就无法从上文推出,这个实验看的是格式。
两边的差距来自同一个事实:预训练数据里从来没有「这里该作答、这里该停」的信号 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条137B 模型在 60 多个 NLP 数据集上做指令微调,零样本在评测的 25 个数据集里 20 个超过 GPT-3 175B 零样本。只在论文的任务与规模上成立。 出处FLAN(Wei et al. 2021)摘要 ↗需联网 。few-shot 示例能让大模型「看出」格式,但它随示例变化、会跑偏,而且每次都要占上下文;想让模型稳定地按格式回答,得把这个行为训进参数里。
改进一:SFT——同一个目标函数,换一种数据
监督微调(SFT)不发明新的训练方法:优化器、学习率调度、混合精度,和预训练一模一样。变的只有两件事——数据换成「指令 → 回答」,loss 只在回答上求和:
「只在回答上求和」这件事是可以严格证明的等价:把 −100 位置的项从 Σ 里拿掉,和给这些项乘 0 完全相同,梯度里也就没有它们 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条交叉熵对被忽略位置的梯度恰为 0;我们项目的 tests/test_extensions.py::TestSFTMask 验证了提示 token 的梯度恰为 0。 出处待补 。需要多少数据?LIMA 只用 1,000 条精心挑选的示范微调 65B 的 LLaMA,在人工评测里对 GPT-4 有 43% 持平或更好、对 Bard 58%、对 DaVinci003 65%;同样 65B、在 52,000 条数据上训练的 Alpaca 复现反而不如它 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条300 个测试提示上的人工偏好评测;作者据此提出「表层对齐假设」:知识和能力几乎都在预训练里学到,对齐只是教会模型用哪一类格式和用户交互。 出处LIMA(Zhou et al. 2023) ↗需联网 。
SFT 教的是「表层行为」,不是知识。知识几乎全部在预训练阶段学好了;SFT 只教用什么格式、以什么角色、是回答还是续写。这一条同时解释了三件事:为什么预训练模型只会续写、为什么 1,000 条就够、以及为什么 SFT 有天花板(改不了知识和推理上限,所以后面还需要 RL)。
改进二:对话要有结构——聊天模板
纯文本没有「谁在说话」。对话需要标出系统提示、用户、助手的边界,每家模型都用一套自己的特殊 token 来标。下面是锚点模型 Qwen3.8 的官方聊天模板真实渲染出来的样子——不是示意字符串,而是用它的 tokenizer 和 chat_template.jinja 调 apply_chat_template 算出来的:
<|im_start|>assistant↵):
Qwen3.8 切成 3 个 token,GPT-2 切成 10 个:
<|im_start|>assistant↵。
特殊 token 是在词表里显式注册的:没注册过的词表只会把它当成一串普通字符。
试三件事:① 先看「裸文本」,再点「训练样本 · 思考模式」:同一句问题被包进了 <|im_start|>、<|im_end|>、<think> 这些粗框 token,每个都是词表里显式注册的一个 id(<|im_start|> 是 248045);② 看组件最下面:同一串 <|im_start|>assistant↵,GPT-2 的词表切成 10 块——它从没注册过这个 token,只能当普通字符拆;③ 点「推理提示 · 思考(xhigh)」,开头那段英文是模板自动插进 system 的推理力度指令(37 个 token),你在对话里根本没写它。
每家的模板都不一样,也没有统一标准。用错模板不会报错:模型照样生成,只是悄悄变笨——训练时见过的边界 token 不在了,它得靠猜。
改进三:只在回答上算 loss——先看朴素做法在哪失败
一条样本里有系统指令、用户问题、模板头,也有助手的回答。最省事的做法是整段都算 loss,和预训练一样:
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
先点上方的「整段都算 loss(朴素)」,看绿色下划线落在哪些 token 上。
回到「只算助手」:算 loss 的只剩 24 个位置——思考块、正文、结束符 <|im_end|> 和它后面的换行。结束符在里面,模型才学得会「答完就停」;提示部分模型照样看得见,只是不在那里付学费。
为什么不在用户的话上学?模型的容量有限,把梯度花在「预测用户会问什么」「背一段固定的系统指令」上,是在浪费;要教的是怎么回答。Gottesman 在 Universal-NER 上的对照实验里,屏蔽用户 token 的版本验证 loss 更好。
常见误解一:「只在回答上算 loss 一定更好。」
这是实验选择,不是定理。Shi et al. 2024 发现,当训练数据的指令很长、回答很短,或者样本很少(正好是 LIMA 那种设定)时,把指令部分也算进 loss 反而更好 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条作者归纳出两个影响因素:指令与回答的长度比、训练样本数;低资源时对指令也算 loss 的收益最明显。 出处Instruction Tuning With Loss Over Instructions ↗需联网 。我们项目的提示(上文 + 「某某說了什麼?」)比回答长得多、样本也只有 6,750 条,这正是章首页项目决策 2 要你拍板的地方;项目里的对照结果(回答 loss 在重跑噪声以内,预训练 loss 涨得更少)见实验课。
常见误解二:「loss mask 把用户的话遮住了,模型看不见。」
遮的是 loss,不是注意力。第 t 个位置预测时,前面的系统指令和用户问题全都在上下文里;只是这些位置自己不当预测目标。
一个新的静默失败:掩码是谁算的?
「只算助手」要由训练代码知道哪些 token 属于助手。HF 的做法是让聊天模板用 {% generation %} 标记把助手输出包起来,apply_chat_template(..., return_assistant_tokens_mask=True) 据此返回掩码。回到上面的组件,点「官方模板 + return_assistant_tokens_mask」:Qwen3.8 的官方模板没有这个标记,transformers 4.51.3 只打一行日志,返回的掩码是 0 / 87——如果训练代码直接信任它,这条样本等于没训练。
截至 2026-09,TRL 的 SFTTrainer 已经补上了这个坑:assistant_only_loss=True 而模板缺标记时,自动换上它自带的训练版模板(本页「只算助手」按钮就是用这份模板现算的,commit 31bfab1829);掩码全 0 时直接报错;结束符不在掩码里时警告「model may not learn to stop」——训练 loss 看着正常,推理时才发现停不下来 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条源码行为:has_generation_markers → get_training_chat_template;assistant_masks 全 0 时 RuntimeError;is_chat_template_stop_token_trained 为假时 warning。本页 2026-09-26 读取。 出处TRL sft_trainer.py(main) ↗需联网 。
🔍 一个经典陷阱:base 模型停不下来
拿 base 模型做 SFT 时,如果结束符没有对齐成模板里的那个 token(Qwen 系是 <|im_end|>),模型不知道什么时候该停,会一直生成下去。我们的项目用分词器自带、预训练语料里从没出现过的 <|endoftext|>(id 2047)当结束符,并让它进 loss:SFT 前结束符的 loss 是 16.84,300 步后 0.37(2026-09-26 导出,见实验课)。
再深一层:能不能不动整个模型?LoRA
SFT 的数据不多、要改的只是行为,那能不能不更新全部参数?全量微调要给每个参数存梯度和优化器状态(C9:每参数 16 字节),Qwen3.8 这个规模就是几百 GB。LoRA 的洞察是:微调时的权重更新是低秩的——不必动整个 d×d 的大矩阵,用两个瘦长矩阵 B、A 的乘积去逼近就够了:
d×d
❄️ 冻结
放到锚点模型上算一次(anchor-model.json 现算,覆盖全部线性层:16 个全注意力层的 q/k/v/o、64 层 FFN 的 3 个矩阵、48 个线性注意力层的 3 个投影):这些矩阵共有 24,350,556,160 个参数;r = 16 时 LoRA 只加 108,863,488 个可训练参数,是全模型 27,781,427,952 个参数的 0.39%;r = 64 时 435,453,952(1.57%)。
关于 rank r 的取舍,有一段值得看的认知演进:2024 年的结论是「LoRA 学得少、也忘得少」(学新知识不如全量微调,但更能保住原能力);2025 年 John Schulman 团队复现后修正为「配置对了,LoRA 能追平全量微调」——关键是用在所有权重矩阵上,而不只是注意力层,并且用更高的学习率 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条SFT 用 rank 256、RL 用 1–32 也够;只在他们测过的模型与任务上成立。 出处LoRA Without Regret ↗需联网 。同一个问题的认知在两年里演进,比背一个静态答案有价值。
🔍 QLoRA 与 NF4:再省一半
QLoRA 把冻结的 W₀ 用 4 位的 NF4 格式存(一种按正态分布分位数设计的 4 位数),只在计算时临时反量化;LoRA 的 B、A 仍用高精度训练。再加「双重量化」(量化常数本身也量化)和分页优化器,单卡能微调更大的模型。Raschka 的实测:比普通 LoRA 省约 33% 显存、慢约 39%。4 位格式本身怎么设计,见 C9 的岔路课。
再深一层:SFT 会不会伤到它没见过的 token?
2026 年 5 月,MiniMax 排查了一个现象:模型明明知道某位艺人是谁,却就是输出不了他名字里的「嘉祺」(词表里单独的一个 token)。官方博客的结论是:后训练数据里含这个 token 的样本不足 5 条;SFT 前后 vocab embedding 几乎没变,lm_head 对应的那一行却显著偏移——全词表 lm_head 与预训练版的余弦相似度平均 0.9837、最低 0.3290,4.9% 的 token 低于 0.95(日语 token 高达 29.7%);修复办法是混入覆盖全词表的「复读」合成数据,最低余弦回到 0.9711 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方自报,未经第三方复现;同一机制还解释了日语对话混入俄文(混杂率 47% → 修复后 1%)。 出处MiniMax 官方博客 2026-05-25 ↗需联网 。
机制可以用交叉熵的梯度直接推出来:对 lm_head 第 j 行,梯度是 (pj − 𝟙[j = 目标])·h。一个 token 只要不是目标,它那一行每一步都收到「把我的 logit 压低」的推力;输入 embedding 却只在它出现在输入里时才有梯度 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条softmax 交叉熵对 logits 的梯度是 p − onehot;lm_head 行的梯度 = 该行 logit 的梯度 × h。前提是 embedding 与 lm_head 不共享。 出处待补 。Qwen3.8 的 tie_word_embeddings = false,正是不共享的情形。下面在 5 个 token 的玩具模型上真跑一遍:
玩具模型:词表 5 个 token(曹操 / 孔明 / 说 / 曰 / 嘉祺),「预训练」权重由种子 1 的随机数生成(不是任何真模型的权重);隐藏向量共享一个公共方向 mu;SFT 数据 12 对,只含前 4 个 token;学习率 0.5 的 SGD,lm_head 与 embedding 一起训。所有数由浏览器现算。
拖到第 300 步:「嘉祺」的 lm_head 行与预训练时的余弦从 1 掉到约 0.17,在该说它的上下文里它的概率从 0.506 掉到 0.001,输入 embedding 行一动不动(余弦 1.0000);勾上「混入全词表复读数据」,余弦停在 0.89 左右,概率反而升到 0.72。玩具模型只演示机制的方向,不复现 MiniMax 的数值;五道口纳什的 E10(B 站 BV1qZRZBcEiH,需联网)从梯度视角讲了同一件事。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 预训练模型只会续写,不知道「这里该作答、答完就停」;SFT 把这个行为训进参数 |
| ② 数学上为什么可能有效? | SFT 就是在新数据上的条件交叉熵,只改求和范围 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条被屏蔽位置的梯度恰为 0。 出处待补 ;「知识来自预训练,SFT 只教格式」是 LIMA 提出的假设 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条Superficial Alignment Hypothesis;有实验支持,但不是证明。 出处LIMA §2 ↗需联网 |
| ③ 实验上为什么选了它? | FLAN 20 / 25、LIMA 1,000 条;我们的小模型 300 步后留出提示里 98% 在 64 token 内停下(SFT 前是 0%) 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条04-实践项目 exp_c12_posttrain.py,单种子、3.7M 参数、玩具指令数据,2026-09-26 导出;只说明「格式」学会了。 出处待补 |
一句话
SFT 用的还是「预测下一个 token」,只是换了数据、只在回答上算 loss:它教的是格式和行为,知识来自预训练。
先停在这里:记住「同一个交叉熵、换数据、只算回答、结束符要进 loss」,就可以去下一课。
再深一层:读「SFT 会不会伤到它没见过的 token」一节并拖一遍玩具模型,展开「base 模型停不下来」,再去实验课看我们模型的完整 SFT 曲线。
下一个缺口
SFT 只能模仿:示范里有的就学,没有的就不会;而且它从来不知道「这个回答不好」。好回答又很难写——写得最好的也只是标注者本人的水平。可人能比较两个回答哪个更好。好回答很难写出来,但人能比较两个回答——怎么把「比较」变成训练信号? → 第 2 课:RLHF
回扣:思考块 <think> 为什么出现在训练样本里、推理力度那段英文从哪来,第 6 课兑现;工具调用的轨迹怎么算 loss,第 8 课兑现。
你可能会问
- 「SFT 要训几个 epoch?」 InstructGPT 的 SFT 训了 16 个 epoch,验证 loss 在第 1 个 epoch 后就开始过拟合,但奖励模型打分和人工偏好继续变好(原文 §3.5)——SFT 的验证 loss 不是好指标。Raschka 在 LoRA 上的实测则是多轮次弊大于利。条件不同,结论不同。
- 「SFT 数据从哪来?」 人写(InstructGPT 约 13k 条提示的示范)、从现成数据集改写(FLAN 把 60 多个 NLP 数据集套上指令模板)、让模型自己生成再过滤(Self-Instruct),或者用更强的模型生成——最后一种就是第 7 课的蒸馏。
- 「思考模式的样本也用这个模板训吗?」 是的,
<think>…</think>就在助手的回答里,和正文一起算 loss;「关闭思考」时模板塞进一个空的思考块。第 6 课展开。