06 - SFT 指令微调
预训练产出的叫 base 模型。这一篇把它变成能对话的模型,产出 sft.py。
前置:05 篇跑完的预训练 checkpoint,01 篇训好的 tokenizer。这一篇不需要多卡,0.5B 做 SFT 单卡就够。
零、开始之前:base 模型为什么不能直接用
0.1 base 模型学会了什么
预训练的目标从头到尾只有一个:猜下一个 token。所以 base 模型学到的是这段文本后面接什么最自然。
它读了 10B token 的网页,语法、常识、写作风格都有了。但它不知道「有人在问你问题,你该回答」这回事,因为训练数据里从来没有这种结构。
0.2 拿它当聊天机器人会发生什么
给 base 模型输入「中国的首都是哪里?」,它可能这样接:
中国的首都是哪里?
A. 上海
B. 北京
C. 广州
D. 深圳
答案:B
解析:北京是中华人民共和国的首都……
它没有回答你,它在续写。因为在网页语料里,这样一句话后面最常见的就是选择题选项。
也可能接出这样的:
中国的首都是哪里?这是很多小学生都会问的问题。今天我们就来聊聊……
同样是续写,不是回答。
这就是 base 模型的状态:知识都在,但不知道自己该扮演一个回答问题的角色。
0.3 SFT 要解决的是格式问题,不是知识问题
这一点特别容易误解,说清楚:
SFT 教的是行为模式,不是知识。 模型的知识在预训练阶段就基本定型了,SFT 用的那几万条数据,相对 10B token 来说是九牛一毛,不可能塞进什么新知识。
SFT 让模型学会的是:看到 <|im_start|>user 开头的一段,后面跟 <|im_start|>assistant 时,我应该输出一个回答,然后停下来。
这也解释了一个常见现象:SFT 数据里出现的知识错误,模型会照单全收;但想靠 SFT 给模型灌输一个新领域的知识,基本没用。 前者是学格式时顺带记住了内容,后者是想用几万条覆盖百亿 token 建立的分布。
有篇叫 LIMA 的论文把这个观点推到极致,它只用 1000 条精心挑选的数据做 SFT,效果就不错。结论是:SFT 数据的质量远比数量重要,因为你本来就只是在教格式。
0.4 跟预训练的三处不同
| 预训练 | SFT | |
|---|---|---|
| 数据组织 | 所有文档首尾拼成一条 token 流 | 按样本组织,要 padding |
| loss 算在哪 | 每个 token 都算 | 只在 assistant 的回复上算 |
| 学习率 | 3e-4 | 2e-5,小一个数量级 |
| 跑多久 | 一遍 10B token | 同样的数据跑 2 到 3 轮 |
这一篇剩下的内容就是把这四行讲清楚。
一、对话模板
1.1 为什么需要模板
模型的输入是一串 token,本身没有「谁在说话」的概念。要让它区分用户和助手,必须在文本里用特殊标记把角色写出来。
这套标记方式就是对话模板。用哪套都行,但训练和推理必须用同一套,否则模型认不出来。
1.2 ChatML
我们用 ChatML,现在最通行的一种,Qwen、多数开源模型都用它:
<|im_start|>system
你是一个乐于助人的助手。<|im_end|>
<|im_start|>user
中国的首都是哪里?<|im_end|>
<|im_start|>assistant
北京。<|im_end|>
规则很简单:每一轮是 <|im_start|> + 角色名 + 换行 + 内容 + <|im_end|>。
推理的时候,把用户输入按这个格式拼好,最后补上 <|im_start|>assistant\n,模型就会接着往下生成回复,生成到 <|im_end|> 就停。
1.3 special token 现在派上用场了
<|im_start|> 和 <|im_end|> 就是 01 篇 4.8 节训 tokenizer 时留的那两个。
当时说「预训练根本用不上,但现在不加,将来加就得重训」,现在到了要用的时候。如果那时候没留,此刻加进去词表会从 32000 变成 32002,embedding 层形状对不上,预训练的权重全废。
代码里加了个断言,就是防这个:
assert self.im_start is not None and self.im_end is not None, (
"tokenizer 里没有 <|im_start|> / <|im_end|>,"
"回 01 篇 4.8 节,这两个必须在训 tokenizer 时就加进 special_tokens"
)
1.4 为什么要把这两个当成单个 token
如果不把 <|im_start|> 注册成 special token,BPE 会把它切成 <、|、im、_、start、|、> 一堆碎片。
坏处有两个。一是浪费,每轮对话多花十几个 token。二是不可靠,模型要学会「这七个碎片连在一起才表示角色开始」,比学一个独立符号难得多,而且用户在正常文本里打出 <|im_start|> 就能伪造角色,这是提示注入的一个口子。
注册成单个 token 之后,它在词表里有独立编号,跟任何正常文本都撞不上。