SFT(一):从Next Token Prediction到指令微调
在手写 Mini-GPT中,也已经从底层走通过:
Prompt
-> Tokenizer
-> Embedding
-> Causal Transformer
-> LM Head
-> logits
-> sampling
-> next token
但是理解推理流程后,新的问题随之出现:
一个只会续写文本的 Base Model,为什么经过训练后会开始理解“用户提问”和“助手回答”,并按照指令组织答案?
这就是本文要讨论的 Supervised Fine-Tuning,监督微调,简称 SFT。
本文不会从“调用一个 Trainer”开始,而会先把 SFT 还原成我已经熟悉的 next-token prediction。等把数据、标签和损失看清楚后,下一篇再用 Hugging Face Transformers 亲手实现,第三篇再进入 TRL SFTTrainer + LoRA。
1. SFT 到底是什么
SFT 的完整名称是 Supervised Fine-Tuning。
它表示:
从一个已经预训练的模型出发,使用带有目标答案的监督数据继续训练,让模型学习特定任务、输出格式或交互方式。
对于聊天模型,一条最简单的数据可以写成:
{
"prompt": "请解释什么是过拟合",
"completion": "过拟合是模型在训练数据上表现很好,但在未见数据上泛化较差的现象。"
}
也可以写成对话形式:
{
"messages": [
{"role": "system", "content": "你是一名耐心的机器学习助教。"},
{"role": "user", "content": "请解释什么是过拟合。"},
{"role": "assistant", "content": "过拟合是模型在训练数据上表现很好,但在未见数据上泛化较差的现象。"}
]
}
训练时,assistant 的回答不是模型现场生成出来再打分,而是数据集已经提供的标准答案。
因此 SFT 中的“监督”来自:
- 输入指令是已知的。
- 目标回答也是已知的。
- 每一个需要学习的目标 token 都可以直接计算交叉熵。
2. SFT 在 LLM 训练流程中的位置
一个简化的大语言模型训练流程可以写成:
随机初始化参数
-> 大规模预训练 Pretraining
-> 得到 Base Model
-> 监督微调 SFT
-> 得到能够遵循指令的 SFT Model
-> 可选:偏好对齐 DPO / PPO / GRPO 等
-> 安全评估、部署与持续迭代
2.1 预训练解决什么问题
预训练数据通常来自大规模自然文本、代码、书籍和网页。
模型学习的目标仍然是预测下一个 token:
$$
\mathcal{L}{\text{PT}}(\theta)
=-
\sum{t=1}^{T}
\log p_{\theta}(x_t\mid x_{<t})
$$
预训练让模型获得:
- 语言规律。
- 词语和概念之间的关系。
- 一部分世界知识。
- 代码和文本模式。
- 根据上下文续写的能力。
但“会续写”不一定等于“会按用户要求回答”。
2.2 SFT 解决什么问题
SFT 数据通常更小,但结构更明确,例如:
- 指令与回答。
- 多轮对话。
- 摘要任务。
- 信息抽取。
- 代码生成。
- 特定 JSON 格式输出。
- 某个领域中的标准问答。
SFT 主要让模型学习:
- 什么内容是用户输入。
- 什么位置应该由助手开始回答。
- 指令与答案之间的映射。
- 回答的风格、长度和结构。
- 在何处结束回答。
2.3 偏好对齐解决什么问题
一条指令可能存在多个语法正确的答案,但人类通常更喜欢其中某些答案。
例如两个回答可能都没有事实错误,但其中一个:
- 更清晰。
- 更有帮助。
- 更符合安全规范。
- 更少废话。
- 更遵守格式要求。
DPO、PPO、GRPO 等方法处理的是“不同回答之间哪个更好”或“什么行为能得到更高奖励”。这与 SFT 直接模仿标准答案并不相同。
3. SFT 不会自动改变模型架构
这是我学习 SFT 时最先需要稳定下来的概念。
假设微调前加载:
model = AutoModelForCausalLM.from_pretrained(model_id)
SFT 后通常仍然是同一个 AutoModelForCausalLM 架构:
Tokenizer
-> Token Embedding
-> 多个 Decoder-only Transformer Block
-> Final Norm
-> LM Head
-> vocab logits
SFT 一般改变的是参数值,而不是把 Decoder-only 模型改成另一种网络。
如果使用 LoRA,则主干参数可以保持冻结,只新增一组小型可训练矩阵;但前向计算的基础架构仍然是同一个 Causal LM。
因此要区分:
| 概念 | 回答的问题 |
|---|---|
| Architecture | 网络由哪些层组成 |
| Config | 层数、隐藏维度、头数、词表大小等配置是什么 |
| Checkpoint | 当前参数具体取值是什么 |
| SFT | 用什么监督数据和目标继续优化参数 |
| LoRA | 训练时允许哪些附加参数更新 |
SFT 是训练阶段和目标;LoRA 是参数高效更新方法。两者不是同义词。
4. SFT 和普通 Fine-tuning 有什么关系
“Fine-tuning”是一个更宽泛的概念。
例如前一篇 Hugging Face 入门文章使用 BERT 完成中文文本分类:
文本
-> Encoder
-> 分类头
-> 两个类别 logits
-> 分类标签
这同样是监督微调。
但 LLM 语境中的 SFT 通常特指:
指令 / 对话
-> Causal Language Model
-> 每个位置的词表 logits
-> 目标回答 token
两者都使用监督数据和梯度下降,区别主要在于:
- 模型头不同。
- 标签形状不同。
- 损失计算位置不同。
- 推理方式不同。
文本分类通常每条样本只有一个类别标签;Causal LM SFT 通常每条样本包含一串 token 标签。
5. Instruction Tuning 是不是 SFT
Instruction Tuning 可以看作 SFT 的常见形式。
如果数据主要是:
Instruction -> Response
那么这就是指令微调。
但 SFT 的范围可以更广:
- 让模型适应某个领域文本。
- 学习固定输出格式。
- 学习多轮客服对话。
- 学习工具调用格式。
- 学习把自然语言转换为 SQL。
因此更准确的关系是:
Fine-tuning
└─ Supervised Fine-Tuning
├─ Instruction Tuning
├─ Dialogue Tuning
├─ Domain Task Tuning
└─ Structured Output Tuning
具体术语在不同项目中可能被宽松使用,判断时应看数据和损失,而不是只看名称。
6. SFT 与继续预训练不要混淆
继续预训练常写作 Continued Pretraining 或 Domain-Adaptive Pretraining。
假设我有大量金融研报:
{"text": "某公司本季度营业收入同比增长……"}
直接让模型在这些连续文本上继续做全序列 next-token prediction,更接近继续预训练。
如果我把数据整理成:
{
"prompt": "根据下面的财报解释营业收入增长原因:……",
"completion": "营业收入增长主要来自……"
}
这更接近 SFT。
二者底层都可能使用 Causal LM 交叉熵,但数据分布和训练目标位置不同。
| 对比项 | 继续预训练 | SFT |
|---|---|---|
| 常见数据 | 连续领域文本 | 指令、问题、回答、对话 |
| 常见目标 | 学习整个文本分布 | 学习输入到目标输出的映射 |
| Loss 范围 | 通常覆盖大部分非 Padding token | 可只覆盖 completion / assistant |
| 主要目的 | 增强领域语言和知识建模 | 增强任务与指令遵循 |
7. 从 messages 到模型真正看到的 token
模型不会直接理解 Python 字典中的 role 字段。
下面的结构:
messages = [
{"role": "system", "content": "你是一名机器学习助教。"},
{"role": "user", "content": "什么是梯度下降?"},
{"role": "assistant", "content": "梯度下降是一种沿损失函数负梯度方向更新参数的方法。"},
]
必须先经过 Chat Template,变成一段线性 token 序列。
不同模型使用的格式可能不同。为了说明原理,假设模板产生:
<|system|>
你是一名机器学习助教。<|end|>
<|user|>
什么是梯度下降?<|end|>
<|assistant|>
梯度下降是一种沿损失函数负梯度方向更新参数的方法。<|end|>
再经过 Tokenizer:
文本模板
-> input_ids
-> attention_mask
-> labels
role 的意义最终由 <|user|>、<|assistant|> 等控制 token 表达。
这也解释了为什么不能随意把 A 模型的数据模板拿给 B 模型:即使文本内容相同,角色边界 token 也可能不同。
8. 训练模板与生成模板有什么区别
训练样本中已经包含 assistant 的完整答案:
user 消息
assistant 消息及完整答案
因此训练格式通常使用:
tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=False,
)
推理时只有用户输入,需要告诉模型“接下来应该由 assistant 说话”:
tokenizer.apply_chat_template(
messages_without_answer,
tokenize=True,
add_generation_prompt=True,
)
两种情况的区别是:
训练:序列中已经有 assistant 开头和标准答案
推理:只补 assistant 开头,答案由 generate() 产生
如果训练时额外增加一个新的 assistant 开头,却又没有对应答案,就可能制造一个无意义的尾部片段。
9. SFT 最关键的三个 Mask
我以前学习 Attention 时已经见过 Padding Mask 和 Causal Mask。SFT 又引入了标签屏蔽,因此很容易混淆。
9.1 Causal Mask
Causal Mask 决定一个位置能看见哪些历史位置。
对于长度为 5 的序列:
$$
M_{causal}=
\begin{bmatrix}
1&0&0&0&0\
1&1&0&0&0\
1&1&1&0&0\
1&1&1&1&0\
1&1&1&1&1
\end{bmatrix}
$$
它防止模型在预测当前 token 时偷看未来答案。
9.2 Attention Mask
attention_mask 通常区分真实 token 和 Padding:
input_ids: [11, 25, 39, 2, 0, 0]
attention_mask: [ 1, 1, 1, 1, 0, 0]
它回答:
哪些输入位置是真实序列,哪些只是为了组成 batch 而补齐?
9.3 Label Mask
Label Mask 决定哪些位置进入损失。
Hugging Face Causal LM 通常使用 -100 表示忽略某个标签:
labels: [-100, -100, -100, 52, 81, 2]
它回答:
哪些目标 token 应该产生训练梯度?
重要的是,被设为 -100 的 Prompt token 仍然保留在 input_ids 中,也仍然可以成为回答 token 的上下文;只是它们对应的预测误差不计入 loss。
10. 三种常见 Loss 范围
假设一条序列是:
<system> 规则 <user> 问题 <assistant> 回答 <eos>
10.1 Full-sequence loss
除了 Padding 外,几乎所有 token 都参与 loss:
labels:
规则 <user> 问题 <assistant> 回答 <eos>
这会训练模型复现整段对话,包括用户消息和系统消息。
10.2 Completion-only loss
Prompt 部分被忽略,只训练 Completion:
labels:
-100 -100 -100 -100 回答 <eos>
这常用于明确的 prompt / completion 数据。
10.3 Assistant-only loss
只训练所有 assistant 消息,忽略 system 与 user 消息:
system: -100
user: -100
assistant: 参与 loss
user: -100
assistant: 参与 loss
它适合多轮 conversational 数据。
三种策略没有脱离 next-token prediction,只是决定了求和集合 $\mathcal{T}_{loss}$:
$$
\mathcal{L}{SFT}(\theta)
=-
\frac{1}{|\mathcal{T}{loss}|}
\sum_{t\in\mathcal{T}{loss}}
\log p{\theta}(x_t\mid x_{<t})
$$
11. Label Shift 到底在哪里发生
假设 Tokenizer 得到:
input_ids = [BOS, A, B, C, EOS]
labels = [BOS, A, B, C, EOS]
模型输出的 logits 形状是:
[batch_size, sequence_length, vocab_size]
损失比较时会错开一个位置:
logits at position: 0 1 2 3
target token: A B C EOS
可以理解为内部执行:
shift_logits = logits[..., :-1, :]
shift_labels = labels[..., 1:]
因此在调用 Hugging Face AutoModelForCausalLM 时,通常把与 input_ids 等长的 labels 直接传入即可:
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
)
loss = outputs.loss
不应该在外部又手动右移一次,否则会发生双重错位。
12. 手算一次 Assistant-only SFT Loss
为了让计算可以手工完成,假设一条对话经过模板和 Tokenizer 后只有 8 个 token:
| 位置 | token | 区域 |
|---|---|---|
| 0 | <user> |
Prompt |
| 1 | 2 |
Prompt |
| 2 | + |
Prompt |
| 3 | 3 |
Prompt |
| 4 | =? |
Prompt |
| 5 | <assistant> |
Prompt 边界 |
| 6 | 5 |
Assistant answer |
| 7 | <eos> |
Assistant answer |
12.1 构造 input_ids
假设它们的 id 是:
input_ids = [10, 21, 22, 23, 24, 11, 25, 2]
所有 token 都是真实输入:
attention_mask = [1, 1, 1, 1, 1, 1, 1, 1]
12.2 构造 labels
只训练 assistant 的答案 5 <eos>:
labels = [-100, -100, -100, -100, -100, -100, 25, 2]
由于内部会进行 label shift:
- 位置 5 的 logits 用来预测位置 6 的 token
5。 - 位置 6 的 logits 用来预测位置 7 的 token
<eos>。
虽然 <assistant> 自己的 label 被屏蔽,但它的隐藏状态仍然负责预测答案第一个 token。
12.3 假设模型给出的概率
假设:
$$
p_{\theta}(\text{`5’}\mid\text{完整 Prompt})=0.60
$$
$$
p_{\theta}(\text{<eos>'}\mid\text{Prompt + 5’})=0.80
$$
只对两个目标 token 求平均:
$$
\mathcal{L}_{SFT}
=-
\frac{1}{2}
(\log 0.60+\log 0.80)
$$
取自然对数:
$$
\log 0.60\approx -0.5108
$$
$$
\log 0.80\approx -0.2231
$$
所以:
$$
\mathcal{L}_{SFT}
\approx
-\frac{-0.5108-0.2231}{2}
=0.3670
$$
12.4 一次梯度更新后会发生什么
理想情况下,更新后模型对正确 token 的概率上升,例如:
P("5") : 0.60 -> 0.68
P("<eos>") : 0.80 -> 0.84
新损失为:
$$
-\frac{1}{2}(\log 0.68+\log 0.84)\approx 0.280
$$
Loss 下降并不表示模型“记住了一个完整字符串”这么简单。梯度会经过:
LM Head
-> Final Norm
-> MLP
-> Attention
-> Embedding / LoRA 参数
相似指令和上下文可能共享一部分表示,因此模型有机会把行为泛化到未见样本。
13. Prompt 被 Mask 后,模型还看得到 Prompt 吗
看得到。
这是 attention_mask 与 labels 最容易混淆的地方。
input_ids:
<user> 2 + 3 = ? <assistant> 5 <eos>
attention_mask:
1 1 1 1 1 1 1 1 1
labels:
-100 -100 ... -100 5 <eos>
预测 5 时,模型仍然能够通过 Causal Attention 查看:
<user> 2 + 3 = ? <assistant>
-100 只告诉交叉熵不要评价那些位置,并没有从输入中删除 Prompt。
14. 为什么训练时叫 Teacher Forcing
在 SFT 训练中,预测第 $t$ 个回答 token 时,模型看到的是数据集中真实的历史 token:
$$
p_{\theta}(y_t\mid x,y_{<t}^{*})
$$
其中 $y_{<t}^{*}$ 表示标准答案的前缀。
例如标准回答是:
梯度 下降 是 ...
训练到“是”时,输入历史中使用的仍然是真实的“梯度 下降”,即使模型前一个位置最可能预测成了别的 token。
这使得所有位置可以并行计算 loss:
一次 forward
-> 得到所有位置 logits
-> 同时计算多个目标 token 的交叉熵
15. SFT 训练与 generate() 是两个阶段
训练时:
完整标准序列
-> 一次并行 forward
-> token-level loss
-> backward
-> optimizer.step()
推理时:
只有 Prompt
-> forward
-> 选择 next token
-> 拼回输入
-> 再次 forward / 使用 KV Cache
-> 循环直到 EOS 或长度上限
训练阶段并不需要在每个 token 上调用一次 generate()。
Trainer 的核心路径是:
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
而不是:
generated = model.generate(...)
16. GenerationConfig 会参与 SFT Loss 吗
通常不会。
GenerationConfig 中常见参数包括:
GenerationConfig(
max_new_tokens=128,
do_sample=True,
temperature=0.8,
top_p=0.9,
)
它们控制的是:
logits -> 怎样选择下一个 token
SFT 训练控制的是:
logits + labels -> 怎样计算交叉熵并更新参数
因此:
| 参数 | 典型作用阶段 |
|---|---|
learning_rate |
训练 |
gradient_accumulation_steps |
训练 |
labels == -100 |
训练 loss |
temperature |
生成 |
top_p |
生成 |
do_sample |
生成 |
max_new_tokens |
生成 |
在训练后的评估阶段,如果要生成回答并计算 ROUGE、BLEU 或任务成功率,才会再次用到生成配置。
17. SFT 后 Config、Tokenizer 和 GenerationConfig 哪些会变
17.1 Model Config
模型层数、隐藏维度和注意力头数通常不变。
但某些配置可能需要同步,例如:
- 增加特殊 token 后的
vocab_size。 pad_token_id。bos_token_id。eos_token_id。- 推理时的
use_cache。
17.2 Tokenizer
如果沿用模型已有 Chat Template 和控制 token,词表可以完全不变。
如果新增特殊 token:
num_added = tokenizer.add_special_tokens(...)
model.resize_token_embeddings(len(tokenizer))
新增 token 对应的 embedding 一开始没有预训练语义,需要在 SFT 中学出来。
17.3 GenerationConfig
SFT 不会自动推导出“最佳 Temperature”。
训练后可以单独保存一个适合任务的生成配置,但它是推理策略,不是 SFT 权重的一部分。
18. Base Model 还是 Instruct Model
18.1 从 Base Model 开始
优点:
- 能清楚观察从续写模型到指令模型的变化。
- 行为更少受到已有对话风格干扰。
- 适合研究自己的 Chat Template 和后训练配方。
缺点:
- 需要更多高质量数据。
- 小数据很难建立稳定的通用指令能力。
- 对安全和拒答行为需要额外设计。
18.2 从 Instruct Model 开始
优点:
- 已经会基本对话和遵循指令。
- 少量领域数据就可能产生明显适配效果。
- 更适合资源有限的业务实验。
缺点:
- 容易覆盖或破坏原有能力。
- 必须保持原模型 Chat Template 一致。
- 很难仅凭结果判断能力来自原模型还是自己的数据。
教学系列中,我会分别使用:
- 原生
Transformers篇:小型 Base Causal LM,加深对 labels 的理解。 TRL + LoRA篇:按照当前 TRL 的标准对话数据与 Chat Template 流程实现。
19. 数据格式比 API 名字更重要
SFT 数据常见四种表示。
19.1 标准 Language Modeling
{"text": "天空是蓝色的。"}
19.2 对话 Language Modeling
{
"messages": [
{"role": "user", "content": "天空是什么颜色?"},
{"role": "assistant", "content": "晴朗天气下通常呈蓝色。"}
]
}
19.3 标准 Prompt-completion
{
"prompt": "天空是",
"completion": "蓝色的。"
}
19.4 对话 Prompt-completion
{
"prompt": [
{"role": "user", "content": "天空是什么颜色?"}
],
"completion": [
{"role": "assistant", "content": "晴朗天气下通常呈蓝色。"}
]
}
选择格式时应先回答:
- 数据是一整段语言模型文本,还是明确的输入输出对?
- 是否包含多轮角色结构?
- Loss 应覆盖全序列、Completion,还是只有 assistant?
20. 多轮对话的 Loss 怎么计算
一条多轮数据可能是:
system: 你是一名助教
user: 什么是过拟合
assistant: ...
user: 如何缓解
assistant: ...
使用 assistant-only loss 时:
system -> -100
user 1 -> -100
assistant 1 -> 参与 loss
user 2 -> -100
assistant 2 -> 参与 loss
第二个 assistant 回答仍然能够看到第一轮对话,因为第一轮 token 没有从 input_ids 中删除。
但需要警惕:
- 一条样本过长导致后半段被截断。
- system prompt 在不同样本中互相矛盾。
- 对话角色没有严格交替。
- assistant 答案中混入用户文本。
- 末尾没有正确的 turn-end 或 EOS token。
21. 截断不是简单取前 max_length 个 token
假设:
Prompt 长度 = 500
Answer 长度 = 80
max_length = 512
如果直接保留前 512 个 token,最终只有 12 个回答 token 能产生 loss。
更糟糕的是,可能完整答案和 EOS 都被截掉。
因此 SFT 应统计:
- Prompt token 长度分布。
- Completion token 长度分布。
- 完整序列长度分布。
- 被截断样本比例。
- 截断后可训练回答 token 数量。
对于明确的 Prompt-completion 数据,可以制定策略:
优先保留回答
-> Prompt 太长时从左侧或中间压缩上下文
-> 回答太长时再截断回答
-> 始终尽量保留 EOS
具体策略取决于任务。文档问答可能必须保留问题和关键证据,代码任务则可能不能随意从中间删除。
22. Padding token 与 EOS token 可以相同吗
部分 Causal LM 没有单独的 Padding token,常见处理是:
if tokenizer.pad_token_id is None:
tokenizer.pad_token = tokenizer.eos_token
这不表示所有 EOS 都会被当作 Padding。
三组张量会共同区分:
真实 EOS:
input_id = eos_id
attention_mask = 1
label = eos_id
Padding:
input_id = eos_id
attention_mask = 0
label = -100
真正需要确认的是 collator 是否正确同步构造 attention_mask 和 labels。
23. Packing 是什么
如果大量训练样本很短,而每个样本都 Padding 到 512:
样本 A: 80 token + 432 Padding
样本 B: 60 token + 452 Padding
样本 C: 90 token + 422 Padding
计算会大量浪费在 Padding 上。
Packing 会把多个短样本装入较长序列:
[样本 A][EOS][样本 B][EOS][样本 C][EOS][少量 Padding]
它提高 token 利用率,但要确认:
- 样本边界如何隔开。
- 不同样本之间是否允许互相 Attention。
- position id 如何处理。
- label mask 是否与拼接后位置对齐。
- 评估集是否也适合 Packing。
初学阶段应先关闭 Packing,看懂单条样本;确认结果正确后再优化吞吐。
24. SFT 的梯度到底更新哪些参数
24.1 Full Fine-tuning
所有允许求梯度的模型参数都更新:
Embedding
Attention projections
MLP
Normalization
LM Head
优化器还需要保存参数对应的一阶和二阶状态,因此显存消耗远大于只加载权重进行推理。
24.2 LoRA Fine-tuning
主干权重冻结,只训练低秩增量:
$$
W’=W+\Delta W
$$
$$
\Delta W=BA
$$
其中:
$$
A\in\mathbb{R}^{r\times d_{in}},\qquad
B\in\mathbb{R}^{d_{out}\times r}
$$
当 $r$ 远小于输入输出维度时,可训练参数显著减少。
但无论 Full Fine-tuning 还是 LoRA,只要数据和目标是监督答案 token,它们做的都可以是 SFT。
25. 怎样判断 SFT 是否有效
只看训练 loss 下降远远不够。
25.1 验证 Loss
验证 loss 能观察模型对未见标准答案 token 的拟合程度。
如果:
train loss 持续下降
eval loss 先下降后上升
通常意味着开始过拟合。
25.2 生成评估
实际任务最终要看生成结果:
- 是否遵循指令。
- 是否正确结束。
- 是否符合目标格式。
- 是否出现事实错误。
- 是否泄漏训练答案。
- 对未见表达是否仍然有效。
25.3 任务指标
根据任务选择:
- Exact Match。
- F1。
- ROUGE。
- BLEU。
- JSON 解析成功率。
- SQL 执行正确率。
- 单元测试通过率。
- 人工偏好评估。
25.4 基线对比
至少比较:
Base / 原 Instruct Model
vs
SFT 后模型
并固定同一份 Prompt、Chat Template 和 GenerationConfig,否则 Sampling 波动可能被误认为训练收益。
26. 数据质量为什么比堆 Epoch 更重要
SFT 是模仿学习。数据中的模式会直接进入模型行为。
低质量数据常见问题:
- 答案事实错误。
- 指令与回答不匹配。
- 同一问题存在互相冲突的答案。
- 大量机械重复模板。
- 回答无意义地过长。
- 混入隐私或无授权内容。
- 训练集和测试集重复。
- system prompt 规则相互冲突。
- 工具调用 JSON 无法解析。
一个可靠的数据流程至少包括:
来源与许可证检查
-> 字段标准化
-> 空值与乱码清理
-> 去重
-> 长度统计
-> 规则校验
-> 训练/验证/测试划分
-> 人工抽样复核
-> 训练后错误分析
27. SFT 不能保证模型获得新知识
SFT 可以教模型如何表达、调用和组合已有能力,也可以让它记住一部分新样本。
但少量 SFT 数据不一定能稳定注入大量知识。
例如只用十条医学问答训练后:
- 训练问题可能回答得很好。
- 相似改写可能有效。
- 更广泛医学知识未必可靠。
- 原有能力还可能因过拟合而下降。
知识密集型需求可能还要考虑:
- 更高质量和更充分的数据。
- 继续预训练。
- 检索增强生成 RAG。
- 外部工具。
- 更严格的事实评估。
28. 常见误解
28.1 SFT 就是给模型加一个分类头
LLM SFT 通常仍使用 LM Head,输出每个位置上的完整词表 logits。
28.2 SFT 必须使用 LoRA
不是。LoRA、QLoRA 和 Full Fine-tuning 都可以实现 SFT。
28.3 Prompt 位置设成 -100 后模型看不到 Prompt
错误。它只是不对 Prompt 目标计算 loss。
28.4 训练时要设置 Temperature
普通交叉熵 SFT 不通过 Sampling 选择目标 token;Temperature 主要属于生成阶段。
28.5 labels 必须在外部手动右移
Hugging Face Causal LM 通常在 loss 内部完成错位。再次手动右移会出错。
28.6 Chat Template 只是为了显示好看
错误。它决定角色边界和特殊 token,是模型训练分布的一部分。
28.7 Loss 越低,模型就一定越有帮助
Loss 只衡量对目标 token 的拟合。数据偏差、事实性、安全性和真实任务效果还要单独评估。
28.8 只保存 LoRA Adapter 就可以独立推理
Adapter 通常还需要对应 Base Model;除非将权重合并并保存为完整模型。
29. 把 SFT 映射回我已经学过的 Hugging Face API
现在可以把已有知识重新排列:
Dataset
-> 提供 prompt / completion / messages
Tokenizer + Chat Template
-> 构造 input_ids
Data Collator
-> 动态 Padding
-> 构造 attention_mask
-> 用 -100 补 labels
AutoModelForCausalLM.forward
-> 输出 [B, T, V] logits
-> 内部 shift labels
-> 计算 token-level loss
Trainer / SFTTrainer
-> backward
-> AdamW
-> scheduler
-> checkpoint
GenerationConfig + generate
-> 训练完成后生成回答
30. 接下来两篇会实现什么
第二篇:原生 Transformers 实现
下一篇会刻意不使用 SFTTrainer,亲手完成:
本地 prompt-completion 数据
-> 格式化 Prompt
-> 分开 Tokenize Prompt 与 Answer
-> 拼接 input_ids
-> Prompt labels 设为 -100
-> 自定义动态 Padding Collator
-> AutoModelForCausalLM
-> Trainer
-> 保存并 generate
这样可以直接检查每个张量。
第三篇:TRL SFTTrainer + LoRA
理解底层后,再把重复工作交给 TRL:
messages 数据
-> Chat Template
-> assistant-only loss
-> SFTTrainer
-> PEFT LoRA
-> Adapter 保存、加载与可选合并
31. 总结
SFT 并没有创造一种新的语言模型目标。
它仍然可以还原成:
$$
\text{根据历史 token,提升正确下一个 token 的概率}
$$
真正需要掌握的是:
- 数据结构:哪些内容属于 Prompt,哪些属于目标回答。
- Chat Template:角色如何变成模型真实看到的 token。
- 三种 Mask:Causal Mask、Attention Mask 和 Label Mask 各管什么。
- Loss 范围:全序列、Completion-only 或 Assistant-only。
- 训练与生成的边界:SFT 优化交叉熵,Sampling 决定推理时怎样选 token。
- 参数更新方式:Full Fine-tuning 和 LoRA 都能承载同一个 SFT 目标。
如果只记住一句话,可以记成:
SFT 是把高质量的输入输出示范变成一串 Causal LM token,并只在希望模型学习的位置计算 next-token loss。
下一篇将把这句话变成真正的 input_ids、attention_mask、labels 和 Hugging Face Trainer 代码。
参考资料
- Hugging Face TRL:SFT Trainer
- Hugging Face TRL:Dataset formats
- Hugging Face Transformers:Chat templates
- Hugging Face Transformers:Causal language modeling
- Hugging Face PEFT:LoRA
- Training language models to follow instructions with human feedback
- Self-Instruct: Aligning Language Models with Self-Generated Instructions
