第 10 课:SFT 微调

SFT(监督微调)就是——用人工写好的问答样例继续训练模型,让它学会按指令回答。上一课我们把模型训出来了,但它只会"续写文本",不会"回答问题"。这一课讲清这条最短的路径:数据要多好、loss 该算在哪、多轮对话怎么拼、显存不够怎么办、训完怎么知道没学坏。

🎯 学完这 6 步你将能够:
① 说清 SFT 与预训练的唯一区别(目标函数相同、数据不同)
② 判断一份指令数据的质量,并解释 loss masking 为什么必须
③ 会算全参、LoRA(低秩适配:冻结原模型权重,只在少数层插入一组很小的可训练矩阵)、QLoRA(把冻结的基座权重进一步压到低比特表示,再训练上述那些很小的矩阵)三种方案的显存,并给出选型
🗺 本节推理链(6 步:从"基座为什么不会听话"到"怎么知道没学坏")
1要让一个只会预测下一个 token 的基座变成助手,必须先回答"它到底缺什么"——因为它缺的不是知识而是"把用户输入当成任务"的行为模式,所以要用指令-回答对做一次极小的继续训练,也就是 SFT。
↓
2既然只用几万条样本就能改变行为,这就要求样本本身足够好——因为少量低质样本会把模型的风格带偏,于是要按"人类撰写 / 强模型蒸馏 / 社区采集"三类来源权衡质量与成本,而不是只看 token 总量。
↓
3数据对了,但目标函数还要改——因为如果对问题部分也算损失,模型就会学着去"生成问题"而不是"回答问题",因此必须把问题部分的 logits(模型对词表里每个 token 打出的原始分数)排除在损失之外(loss masking)。
↓
4把问题部分从损失里摘掉之后,剩下的文本仍是多角色、多轮次混在一起的一长串,这就要求用显式分隔符标出"谁在说话、说到哪停",于是出现 chat template 与特殊 token(如 Llama 3 的 <|eot_id|>),否则模型分不清"谁在说话"。
↓
5样本与模板都定了,但全参微调要存完整的权重、gradient 与优化器状态,因此显存成为瓶颈,这就要求按模型大小在"全参 / LoRA / QLoRA"之间选型。
↓
6【支链 · 评估维度】最后要回答一个反向问题:微调会不会把原来的能力弄丢——因此必须在微调前后都测一遍基座能力(用 perplexity 与通用基准),于是才能区分"学会了新格式"和"忘掉了旧知识"。
SFTInstruction TuningLoss MaskingChat TemplateLoRAQLoRA
1
一个知识齐全的基座模型,为什么你问它问题,它却在"续写"?
提示:先想"续写文本"和"回答问题"是不是同一件事。如果问一个只会续写的系统"某国首都是哪",它会补出什么?
▾
知识背景
指令微调(Instruction Tuning)由 FLAN 命名并推广(Wei et al., arXiv:2109.01652):把一个 137B 模型在 60+ 个改写为自然语言指令的任务上微调后,在 25 个评测中的 20 个上超过了零样本的 175B GPT-3。InstructGPT(Ouyang et al., arXiv:2203.02155)进一步用约 13,000 条人类撰写的示范做 SFT,得到的 1.3B 模型被人类标注者偏好于 175B 的基座 GPT-3——参数量只有约 1/135。

上一课我们把预训练跑完了。但跑出来的模型只会做一件事:预测下一个 token。所以问它"法国首都是哪",它可能续写出"德国首都是哪?意大利首都是哪?"——为什么?

因为它学到的是"文本如何接续",而不是"请求需要被执行",所以必须补一次行为层面的训练。可以把这类比成请来一位读过全图书馆的学者:知识都在,但没人告诉他"有人提问时要回答"——SFT 就是那场入职培训。

🧮 差距账:知识够了,行为没教
Step 1:两个模型的差别在哪
维度基座模型SFT 之后
训练目标预测下一个 token完全相同(仍是预测下一个 token)
训练数据万亿级网页数万条指令-回答对
数据量级15.6T token约几十 M token(小 5~6 个数量级)
行为续写文本把输入当任务执行
关键点:SFT 不改目标函数,只改数据——这是本课最容易被忽略的一条。
Step 2:为什么"行为"比"规模"更划算
对照实验参数量结果
InstructGPT1.3B人类偏好优于 175B 基座 GPT-3
参数量对比1.3B vs 175B约 1/135
SFT 数据量约 13,000 条示范相对预训练语料可忽略
用 1/135 的参数量 + 几万条样本,换来人类更喜欢的输出——说明"会不会听指令"与"知道多少"是两个可以分开投资的维度。
Step 3:换个角度理解 SFT 的"小"
阶段数据量耗时学到了什么
预训练15.6T token数月、上万卡语言与知识
SFT约 10M~100M token小时到天、少量卡回答的格式与风格
量级比15.6T ÷ 约 50M ≈ 3 × 10⁵ 倍——
SFT 之所以这么便宜,是因为它不注入新知识,只调整表达方式——把已有的能力"对齐"到用户期待的输出形态上。
🎯 关键认知

SFT 的第一性认识是:它和预训练用的是同一个目标函数,唯一区别是数据。所以"模型答不好问题"不是知识不够,而是行为没被教过——用 1/135 的参数、约 1/3×10⁵ 的数据量就能补上。这也解释了为什么 SFT 通常只跑 1~3 轮、几小时就结束。

AI 助教
👋 对 SFT 的定位有疑问?比如"SFT 会不会注入新知识?""为什么小模型能超过大基座?"

我会用具体数字帮你算。
2
指令数据只有几万条,那"质量"具体指什么?
提示:先想如果同样 1 万条样本,一批是专家写的、一批是网上随手抓的,训出来会差在哪。再想"多"能不能补上"差"。
▾
知识背景
训练数据的三种来源(成本与质量递减):人类撰写——InstructGPT 用了约 13,000 条专业标注者写的示范;强模型蒸馏——Alpaca 用 GPT-3.5 生成了 52,000 条;社区/开放数据集——规模可达几十万条但质量参差。极端案例是 LIMA(Zhou et al., arXiv:2305.11206):仅用 1,000 条精心策展的样本,就得到有竞争力的指令遵循能力。

上一张卡片我们看到 SFT 只需几万条样本。那问题就来了:同样是几万条,差别在哪?

因为SFT 样本直接示范"什么样的回答是好的",所以样本的风格就是模型的上限。可以把这类比成学做菜:跟 10 道大厨做的菜学,和跟 10000 份速冻食品学,前者学到的是手艺,后者学到的是"速冻食品长什么样"。

🧮 数据账:三种来源的质量与成本
Step 1:三种来源的对照
来源质量规模量级典型代表
人类专业撰写最高约 1 万条级InstructGPT(约 13,000 条)
强模型蒸馏高约 5 万条级Alpaca(52,000 条)
社区 / 开放集参差几十万条级各类开源指令集
精挑极少样本极高1,000 条LIMA
注意最后一行:1,000 条也能做出有竞争力的指令遵循能力——说明规模不是决定因素。
Step 2:为什么质量能压过数量
对比样本数学到的东西
少量精品1,000 条"好答案的标准"(格式、语气、推理痕迹)
大量杂样本100,000 条各种风格的平均值(含大量低质模式)
比值100,000 ÷ 1,000 = 100 倍但后者并不更强
机理是最大似然会去拟合样本的分布:样本里有多少比例是"敷衍回答",模型就有多少概率学会敷衍。
Step 3:"质量"具体由哪几项构成
维度合格不合格
正确性事实与推理都对编造、算错
格式遵循指令要求的格式答非所问
风格一致全数据集语气统一一部分口语一部分学术腔
多样性任务类型与长度(token 数)分布合理全是同一类问题
无泄漏不含评测题把 benchmark 答案抄进去
最后一项最隐蔽:把评测题混进训练集会让分数虚高,但真实能力没变——这叫数据污染。
🎯 关键认知

指令数据的质量压过数量:1,000 条精选样本(LIMA)能与 52,000 条蒸馏样本(Alpaca)竞争,13,000 条人工示范(InstructGPT)能让 1.3B 模型胜过 175B 基座。因为 SFT 拟合的是样本分布,样本里有多少"敷衍"、"编造"、"格式错乱",模型就会学多少——所以筛数据比堆数据更重要。

AI 助教
👋 对数据质量有疑问?比如"怎么自动筛掉低质样本?""蒸馏数据会不会有系统性偏差?"

我会用具体数字帮你算。
3
为什么算损失时要"假装看不见"问题部分?
提示:先想一条样本里问题和回答各占多少 token;再想如果两部分的损失都算,梯度主要在优化哪一部分。
▾
知识背景
Loss masking(损失掩码)是 SFT 的标准做法:输入序列 = 问题 token + 回答 token,但只对回答部分计算交叉熵。实现上把问题部分的标签置为 -100(PyTorch 的 ignore_index),只保留回答部分的标签。注意掩码只作用于损失,不作用于注意力——回答仍能看到问题。

上一张卡片我们备好了数据。但直接拿"问题 + 回答"算损失会出问题——为什么?

因为如果问题部分也计入损失,模型会被训练去"复述问题",所以必须把问题部分屏蔽掉。可以把这类比成批改作文:你只该按"学生的回答"打分,而不是把"题目"也抄一遍再打分——抄题目不是考核目标。

更早的做法是照搬预训练那一套——对整段文本算损失,毕竟预训练就是这么干的、也跑得通。可预训练语料是一段连续文本、每个 token 都该被预测;SFT 样本里问题占了大头(示例中约 80%),照搬过来,模型每学 1 份「怎么回答」,就顺带学 4 份「怎么复述问题」,训久了会养成"抢着替用户把问题续下去"的坏习惯。

🧮 掩码账:不屏蔽会浪费多少梯度
Step 1:一条样本的 token 构成
部分典型长度是否计入损失
系统提示 + 用户问题约 80 token否(置 -100)
助手回答约 20 token是
合计约 100 token——
这里用短回答举例——现实中回答通常更长,但短回答场景能把问题放大到最明显。
Step 2:不掩码会怎样
方案损失覆盖的 token梯度去向
不掩码100 个(含 80 个问题 token)约 80% 用于"复述问题"
正确掩码20 个(只回答)100% 用于"生成回答"
差异——约 5 倍的有效梯度被浪费
计算:100 ÷ 20 = 5 倍。也就是说,不掩码时模型每学 1 份"如何回答",就顺带学了 4 份"如何复述问题"。
Step 3:掩码在实现上长什么样
张量内容
input_ids[问题 token…, 回答 token…](共 100 个)
labels[−100 × 80, 回答 token…](前 80 个被屏蔽)
损失cross_entropy(logits, labels, ignore_index=-100)(logits = 模型对词表里每个 token 打的原始分数)
注意 input_ids 没有变——问题 token 仍在输入里,只是不算损失。所以回答仍然能"看到"问题,这正是我们要的:条件于问题,但不模仿问题。
🎯 关键认知

Loss masking 的判据是"你要模型做什么":SFT 要它生成回答,就只对回答算损失。不掩码时,被浪费的梯度比例 = 问题 token 占比(示例中约 80%)。关键区别在于掩码只作用于损失、不作用于注意力——模型依然能看到问题,只是不被训练去复述它。

AI 助教
👋 对 loss masking 有疑问?比如"多轮对话里 system 提示也要屏蔽吗?""屏蔽后为什么还能看到问题?"

我会用具体数字帮你算。
4
多轮对话和多个角色,模型怎么知道谁在说话、话说完了没?
提示:先想如果把"系统提示、用户问题、助手回答"直接拼成一段文本,模型靠什么判断边界。
▾
知识背景
Chat template 用一组特殊 token 显式标出角色与边界。以 Llama 3 为例:序列以 <|begin_of_text|> 开头,每一轮用 <|start_header_id|>角色名<|end_header_id|> 起头,用 <|eot_id|>(end of turn)收尾。这些特殊 token 需要在 tokenizer 里注册,模型由此学会"该我说话了"与"我该停了"。

上一张卡片解决了"算哪部分损失"。但一条多轮样本里有多段文本,模型怎么区分它们?

因为朴素拼接会让模型分不清"哪句是用户说的、哪句是自己该说的、说到哪停",所以必须用显式的边界标记。可以把这类比成剧本:台词前面要写角色名、每句结束要有句号——否则演员不知道该谁开口、什么时候停。

更早的做法就是把多段文本首尾直接拼接(像预训练语料那样不留任何标记)。这样模型学不到「轮次在哪结束」,最直接的症状是回答写完后不收尾、继续替用户编出下一句问答;而放进 <|eot_id|> 这类结束标记后,「该我停了」才第一次变成模型可以学习的显式信号。

🧮 模板账:特殊 token 花在什么地方
Step 1:一条单轮样本的实际结构
片段内容作用
<|begin_of_text|>1 个 token序列起点
<|start_header_id|>system<|end_header_id|>若干 token声明系统角色
系统提示正文n₁ 个 token行为约束
<|eot_id|>1 个 token本轮结束
user 段 + 正文 + <|eot_id|>n₂ + 若干用户输入
assistant 段 + 正文 + <|eot_id|>n₃ + 若干要学的那一段
结构 token 的绝对数量不多,但缺了它们模型就无法界定边界——这是"少而关键"的那类 token。
Step 2:多轮样本里损失落在哪几段
轮次片段是否计入损失
第 1 轮system / user否
第 1 轮assistant是
第 2 轮user否
第 2 轮assistant是
规则很统一:所有 assistant 段计入损失,其余全部屏蔽。所以多轮样本的损失是"若干个回答段之和",而不是整段文本。
Step 3:短样本怎么办——打包
方案利用率风险
一条样本一个序列,不足则填充低(大量 padding 浪费)无
打包:多条短样本拼进同一序列高需保证跨样本不互相注意
示例:4 条 1000 token 的样本4 × 1000 = 4000 token省约 4 倍序列数
打包时必须在样本边界加注意力隔离(各自独立),否则第二条样本会"看到"第一条——那等于制造了假的多轮对话。
🎯 关键认知

Chat template 的作用是把"谁在说话、说到哪停"变成可学习的显式信号:角色标记界定说话者,<|eot_id|> 界定轮次结束。没有它,模型学不到"该我停下来了"——这也是"模型不会停、一直续写"的主要成因之一。而打包能把短样本的利用率提上来,前提是做好样本间的注意力隔离。

AI 助教
👋 对 chat template 有疑问?比如"训练和推理的模板必须一致吗?""特殊 token 怎么加进词表?"

我会用具体数字帮你算。
5
全参微调要多少显存?LoRA 和 QLoRA 省在哪?
提示:先算一个 7B 模型做全参微调时,权重、梯度和优化器状态各占多少字节;再想如果只训练"加装的小矩阵",需要存哪些东西。
▾
知识背景
LoRA(Hu et al., arXiv:2106.09685):冻结原权重,只在选定层注入低秩矩阵 ΔW = BA(A 是 r×d、B 是 d×r,两者相乘即得到 d×d 的增量),把可训练参数减少约 10,000 倍、显存减少约 3 倍,且推理时无额外延迟(可合并回原权重)。QLoRA(Dettmers et al., arXiv:2305.14314, NeurIPS 2023):把基座量化到 4-bit NF4(4 位 NormalFloat:按正态分位设计的 4 比特编码)并冻结,在 16-bit 适配器上反传,配合双重量化(再省约 0.37 bit/参数)与分页优化器,让 65B 模型在单张 48GB 卡上微调——而 16-bit 全参微调该模型需要 >780GB。

上一张卡片把样本格式定好了。现在要真正开始训练——但显存够吗?

因为全参微调要存完整的权重、梯度和优化器状态,所以模型一大就跑不动,必须选更省的方式。可以把这类比成装修:全参=把整栋楼的墙都重刷一遍;LoRA=只在墙上贴几张便利贴;QLoRA=连墙都换成轻质材料再贴便利贴。

更早、更自然的做法就是全参微调(预训练怎么训、微调就怎么训),但它先撞的其实是优化器状态这道墙:以 7B 为例,Adam 状态要 84 GB,是权重(14 GB)的 6 倍,加上梯度合计 112 GB——单张 80GB 的卡根本放不下,必须切上两张以上。

🧮 显存账:三种方案差在哪
Step 1:全参微调的显存构成(以 7B 为例)
项目每参数字节7B 总计
BF16(训练常用的半精度浮点格式)权重214 GB
BF16 梯度214 GB
Adam 优化器状态(fp32 主权重 + 一阶 + 二阶矩)1284 GB
合计16≈ 112 GB
还没算激活值——所以单张 24GB 消费卡完全不可能跑 7B 全参微调。
Step 2:LoRA 把可训练参数压到多少
项目计算结果
注入位置q / k / v / o 四个投影 × 2 个矩阵8 个矩阵/层
单个矩阵参数(A:d×r,r=16)4096 × 1665,536
每层合计8 × 65,536524,288
32 层合计524,288 × 32≈ 16.8M
占 7B 的比例16.8M ÷ 7000M≈ 0.24%
只需为这 0.24% 的参数存 gradient(梯度)与优化器状态——基座只占 14 GB(冻结、无需梯度和优化器状态),所以 7B 的 LoRA 微调约 16 GB 就够(其中注入位置 q/k/v/o 为常见扩展配置,原论文默认只注入 q、v)。
Step 3:QLoRA 再压一层,以及三档对照
方案7B 需要显存65B 需要显存相对全参
全参微调(BF16,含梯度与优化器状态)≈ 112 GB> 780 GB1×
LoRA(基座 BF16,含适配器训练态)≈ 16 GB≈ 130 GB(仅基座权重,65B × 2 字节;不含适配器训练态)约 1/7
QLoRA(基座 4-bit,含适配器训练态)≈ 6 GB< 48 GB(含适配器训练态)约 1/19
从 780GB 到 48GB 是 16 倍的压缩——这是 QLoRA 让"单卡微调大模型"成为现实的直接原因(论文报告 65B 单张 48GB 且质量与 16-bit 全参微调相当)。
Step 4:怎么选
场景建议理由
基座能装进显存、追求最好效果LoRA比 QLoRA 快约 20%~40%(示意,需人工核实;无逐次反量化开销)
基座装不下或显存紧张QLoRA显存约为 LoRA 的 1/2.7(16GB → 6GB),质量损失通常 < 0.5 分(示意,需人工核实;论文只称质量相当)
需要改变模型"底子"(如换语言能力)全参微调LoRA 的表达能力受低秩约束
🎯 关键认知

三档的本质是"训练哪些参数、以什么精度存基座":全参要存全部 16 字节/参数的训练状态(7B 需 112GB);LoRA 只训 0.24% 的参数,基座 14GB 冻结(7B 约 16GB);QLoRA 再把基座压到 4-bit(7B 约 6GB,65B < 48GB)。选型判据很简单:基座装得下就用 LoRA,装不下就用 QLoRA,需要改底子才上全参。

AI 助教
👋 对 PEFT 选型有疑问?比如"r 和 alpha 该怎么定?""QLoRA 的精度损失能接受吗?"

我会用具体数字帮你算。
6
微调之后,怎么知道模型没有把原来的本事弄丢?
提示:先想一个只在"客服问答"上微调过的模型,做数学题还灵不灵。再想该在微调前后各测什么才能发现这件事。
▾
知识背景
灾难性遗忘(Catastrophic Forgetting):在新数据上继续训练会让模型在原有任务上的能力下降。缓解手段包括:用 LoRA 等参数高效方法(只动少量参数,对原能力的扰动小)、控制学习率(全参 SFT 常用 1e-5~2e-5,与预训练峰值 8e-5 更接近;LoRA 常用 1e-4~2e-4,可高出约一个数量级)、减少轮数(通常 1~3 轮)、以及混合通用数据做重放。评估上必须在微调前后各测一遍通用能力。

上一张卡片选好了训练方式。但还有一个反向风险:微调会不会把原有能力弄丢?

因为梯度只服务于新数据的目标,所以在窄领域上训久了,通用能力会退化。可以把这类比成让一位全科医生去专科进修半年:专科能力上去了,但全科手感可能生疏——所以进修前后都要考一次全科。

🧮 遗忘账:怎么发现、怎么防
Step 1:评估要在前后各测一遍
时间点测什么作用
微调前通用能力基准 + 验证集 perplexity(困惑度 = 模型对文本的"意外程度",越低表示预测越准)建立基线
微调后同样的基准 + 目标任务评测净变化 = 微调后分数 − 微调前分数
只看微调后只有目标任务的分数无法发现遗忘
最常见的错误是只报目标任务涨了多少——不测基线就永远看不到代价。
Step 2:三条缓解手段的取舍
手段做法代价
用 LoRA只训练 0.24% 的参数表达能力受低秩约束,改不了"底子"
控制学习率全参 1e-5~2e-5 / LoRA 1e-4~2e-4收敛更慢
减少轮数1~3 轮可能欠拟合任务
混合通用数据指令数据中掺入通用语料做重放稀释目标任务的信号
四条可以叠加。最安全的组合是"LoRA + 低学习率 + 少轮数"——它们的共同点是"尽量少动原权重"。
Step 3:为什么轮数通常只有 1~3
轮数训练损失通用能力判断
1~3下降基本保持推荐区间
5~10继续下降开始退化已过拟合样本风格
> 10很低明显退化开始"背答案"
机制是样本量太小:几万条样本重复多轮,模型从"学规律"滑向"记样本"——损失继续降,但泛化在变差。(上表的轮数-退化区间为经验区间、示意,具体拐点随数据量与任务而变,需实测。)
🎯 关键认知

微调的代价是可能在原能力上退化,而发现它的唯一方法是微调前后都测同一套基准。工程上的默认组合是"LoRA + 受控学习率(LoRA 档 1e-4~2e-4)+ 1~3 轮",共同逻辑都是尽量少动原权重。只报目标任务的提升、不报通用能力的变化,是最常见的自欺。

AI 助教
👋 对遗忘与评估有疑问?比如"怎么构造通用能力基线?""重放数据要掺多少?"

我会用具体数字帮你算。