后训练 SFT——从预训练模型到助手模型 / Supervised Fine-Tuning from Pretrained Model to Assistant
📅 创建时间:2026-06-02 🏷️ 标签:#SFT #监督微调 #对话数据 #数据配比 #过拟合 #课程学习 #ChatML 📚 前置知识:[[05-pretraining]](预训练) 📚 相关知识:[[07-posttraining-rlhf]](RLHF/DPO) [[08-efficient-finetuning]](LoRA)
场景:预训练模型会续写但不会对话,需要 SFT
┌─────────────────────────────────────────────────────────────┐
│ │
│ 你拿到了一个训练好的 Llama-3-8B 预训练模型。 │
│ │
│ 你输入: │
│ "帮我写一首诗" │
│ │
│ 预训练模型的输出: │
│ "这首诗是关于春天的。春天来了,万物复苏。..." │
│ "春天是一年中最好的季节..." │
│ (模型在续写关于"春天"的文章,不是对话) │
│ │
│ 你需要的输出: │
│ "当然可以!这是一首关于春天的诗..." │
│ "春风又绿江南岸,明月何时照我还..." │
│ "希望你喜欢这首诗,如果想要其他风格..." │
│ (模型在"回应"用户请求,而不是续写文本) │
│ │
│ 问题出在哪? │
│ → 预训练目标是"预测下一个 token" │
│ → 模型学到的是"文本接龙",不是"对话响应" │
│ → SFT(监督微调)就是解决这个问题的 │
│ │
└─────────────────────────────────────────────────────────────┘第1节:SFT 原理——预训练和微调的区别
预训练 vs SFT 的目标差异
┌─────────────────────────────────────────────────────────────┐
│ 预训练 vs SFT:目标函数的根本差异 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 预训练(Pre-training): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目标:学习通用的语言能力和世界知识 │ │
│ │ │ │
│ │ 训练数据:互联网文本(万亿 token) │ │
│ │ 格式:直接拼接文本 │ │
│ │ loss:所有 token 的语言模型 loss │ │
│ │ │ │
│ │ text = "互联网是个好东西。人们用它来..." │ │
│ │ 训练目标:每个位置预测下一个 token │ │
│ │ │ │
│ │ 模型学到: │ │
│ │ → 语法结构、知识事实、推理能力 │ │
│ │ → "文本的统计规律" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ SFT(Supervised Fine-Tuning): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 目标:学会"按照指令响应" │ │
│ │ │ │
│ │ 训练数据:高质量对话数据(百万~千万条) │ │
│ │ 格式:指令 + 响应对 │ │
│ │ loss:只计算 assistant 部分的 loss(prompt 不计算) │ │
│ │ │ │
│ │ text = "<|im_start|>user\n帮我写诗<|im_end|>" │ │
│ │ "<|im_start|>assistant\n当然可以..." │ │
│ │ 训练目标:给定 user 输入,预测 assistant 的回复 │ │
│ │ │ │
│ │ 模型学到: │ │
│ │ → 指令遵循、对话格式、交互模式 │ │
│ │ → "如何响应指令" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘SFT 的关键设计:只训练 Assistant 部分
┌─────────────────────────────────────────────────────────────┐
│ SFT Loss Mask:只训练回复,不训练问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 为什么 prompt 部分不计算 loss? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 如果计算 prompt loss: │ │
│ │ → 模型会过拟合到 prompt 的表达方式 │ │
│ │ → "用英文提问" 和 "用中文提问" 会学到不同权重 │ │
│ │ → 模型失去了指令的泛化能力 │ │
│ │ │ │
│ │ 只计算 assistant loss: │ │
│ │ → 模型专注于学习"如何回复" │ │
│ │ → 指令的具体措辞不重要,重要的是意图 │ │
│ │ → 模型保持对各种指令格式的泛化 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 具体实现: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ tokens = tokenizer("user: 写诗\nassistant: 当然") │ │
│ │ │ │
│ │ text: [user: 写诗 assistant: 当然...] │ │
│ │ loss: [False False False True True...] │ │
│ │ ↑ ↑ ↑ │ │
│ │ 这些不计算 loss 这些计算 loss │ │
│ │ │ │
│ │ mask = [0, 0, 0, 1, 1, ...] # assistant 部分=1 │ │
│ │ loss = cross_entropy(logits, labels, reduction='mean', mask=mask) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 常见错误: │
│ → 忘记 masking,prompt 的 loss 也参与更新 → 模型退化 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:对话数据格式——ChatML 和 ShareGPT
工业标准的对话格式
┌─────────────────────────────────────────────────────────────┐
│ ChatML 格式(推荐工业标准) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <|im_start|>system │ │
│ │ 你是一个有帮助的助手。<|im_end|> │ │
│ │ <|im_start|>user │ │
│ │ 什么是量子计算?<|im_end|> │ │
│ │ <|im_start|>assistant │ │
│ │ 量子计算是一种利用量子力学原理进行计算的技术...<|im_end|> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键设计: │
│ 1. <|im_start|>/<|im_end|> 作为 role 的边界符 │
│ 2. 每个 turn(user/assistant)都有明确的 role 标签 │
│ 3. 模型学会:看到 user 标签后,输出 assistant 标签的内容 │
│ 4. system prompt 在最前面,作为全局上下文 │
│ │
│ 优势: │
│ → 格式清晰,易于解析和处理 │
│ → 支持多轮对话,role 信息不丢失 │
│ → tokenizer 训练时就包含这些特殊 token │
│ │
└─────────────────────────────────────────────────────────────┘ShareGPT 格式(数据共享格式)
┌─────────────────────────────────────────────────────────────┐
│ ShareGPT 格式(数据交换格式) │
├─────────────────────────────────────────────────────────────┤
│ │
│ JSON 格式,用于数据集交换: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [ │ │
│ │ { │ │
│ │ "conversations": [ │ │
│ │ {"from": "human", "value": "什么是AI?"}, │ │
│ │ {"from": "gpt", "value": "AI是..."}, │ │
│ │ {"from": "human", "value": "继续说"}, │ │
│ │ {"from": "gpt", "value": "另外..."} │ │
│ │ ] │ │
│ │ } │ │
│ │ ] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 转换到 ChatML(训练前必须转换): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ human → <|im_start|>user<|im_end|> │ │
│ │ gpt → <|im_start|>assistant<|im_end|> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 注意事项: │
│ → ShareGPT 的 "from" 字段可能不标准("human"/"user"混用)│
│ → 需要统一映射 │
│ → 多轮对话要保证 turns 数量正确 │
│ │
└─────────────────────────────────────────────────────────────┘格式兼容性问题
┌─────────────────────────────────────────────────────────────┐
│ 训练格式 vs 推理格式的兼容问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:训练时用的格式,必须和推理时完全一致 │
│ │
│ 训练格式(ChatML): │
│ <|im_start|>system\n系统信息<|im_end|>\n │
│ <|im_start|>user\n问题<|im_end|>\n │
│ <|im_start|>assistant\n回复<|im_end|> │
│ │
│ 推理格式: │
│ 如果推理时用了不同的格式,模型表现会退化 │
│ │
│ 常见错误: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 错误 1:tokenizer 没有特殊 token │ │
│ │ → ChatML 的 <|im_start|> 是特殊 token │ │
│ │ → 如果 tokenizer 没有训练过,模型无法正确识别 │ │
│ │ → 解决:确认 tokenizer 支持 ChatML,或用 HF 的 │ │
│ │ chat_template 自动处理 │ │
│ │ │ │
│ │ 错误 2:训练时用 system prompt,推理时不用 │ │
│ │ → 训练见过 system prompt,推理没给 │ │
│ │ → 模型行为可能不一致 │ │
│ │ → 解决:训练数据中混入 20-30% 无 system 的样本 │ │
│ │ │ │
│ │ 错误 3:多轮格式不一致 │ │
│ │ → 训练用 "Q1 A1 Q2 A2",推理用 "Q1 Q2 A1 A2" │ │
│ │ → 解决:统一格式,推理时用相同格式 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第3节:SFT 数据工程
数据来源与构建方式
┌─────────────────────────────────────────────────────────────┐
│ SFT 数据的三大来源 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 来源 1:人工标注(Gold Standard) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 优点:质量最高,格式最标准 │ │
│ │ • 缺点:成本高、速度慢、规模有限 │ │
│ │ • 代表数据集: │ │
│ │ - OpenAI InstructGPT 的 SFT 数据 │ │
│ │ - Anthropic HH-RLHF(Helpfulness + Harmlessness)│ │
│ │ - Anthropic Constitutional AI 数据 │ │
│ │ │ │
│ │ 成本:约 $0.1-1 / 条(取决于难度和质量要求) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 来源 2:LLM 生成(Synthetic Data) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 优点:成本低、速度快、可大规模生成 │ │
│ │ • 缺点:质量参差不齐,可能有幻觉 │ │
│ │ • 流程: │ │
│ │ 1. 设计 seed prompts(覆盖不同任务类型) │ │
│ │ 2. 用强模型(GPT-4 / Claude)生成回复 │ │
│ │ 3. 质量过滤(reward model 打分 / 规则过滤) │ │
│ │ 4. 去重、格式标准化 │ │
│ │ │ │
│ │ 代表:WizardLM / UltraChat / phi-2 训练数据 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 来源 3:公开数据集 + 清洗 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 优点:免费、数据多样 │ │
│ │ • 缺点:质量不一、格式不统一、需要大量清洗 │ │
│ │ • 代表: │ │
│ │ - ShareGPT Conversations(来自 ChatGPT 用户分享)│ │
│ │ - Anthropic HH-RLHF(可用作高质量数据源) │ │
│ │ - OpenOrca(Orca 论文的 FLAN 数据清理后) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘数据质量过滤
┌─────────────────────────────────────────────────────────────┐
│ SFT 数据质量过滤策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 规则过滤(Rule-based): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✓ 长度过滤: │ │
│ │ - 拒绝过短(<20 tokens)和过长(>2048 tokens) │ │
│ │ ✓ 格式过滤: │ │
│ │ - 包含特殊控制字符 → 过滤 │ │
│ │ - 回复格式混乱(如缺少标点)→ 过滤 │ │
│ │ ✓ 内容过滤: │ │
│ │ - 包含明显的政治敏感词 → 过滤(根据业务需求) | │
│ │ - 包含个人信息(PII)→ 过滤或脱敏 │ │
│ │ ✓ 回复质量: │ │
│ │ - 重复词过多("the the the")→ 过滤 │ │
│ │ - 回复为空或只有标点符号 → 过滤 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 模型过滤(Model-based): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✓ Reward Model 打分: │ │
│ │ - 用 RLHF 训练好的 reward model 打分 │ │
│ │ - 保留分数 > 阈值的样本 │ │
│ │ - 阈值选择:Top 70-90%(根据质量需求) │ │
│ │ │ │
│ │ ✓ Perplexity 过滤: │ │
│ │ - 用语言模型计算回复的 perplexity │ │
│ │ - 过低:过于简单/模板化 │ │
│ │ - 过高:语言混乱 │ │
│ │ - 保留中间段 │ │
│ │ │ │
│ │ ✓ 幻觉检测: │ │
│ │ - 用 LLM 判断回复是否包含事实错误 │ │
│ │ - 对知识密集型任务尤其重要 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘数据配比——平衡不同任务类型
┌─────────────────────────────────────────────────────────────┐
│ SFT 数据配比策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:不同任务类型的数据比例如何设计? │
│ │
│ 常见任务类型分类: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任务类型 │ 典型数据来源 │ 建议比例 │ │
│ │ ────────────────┼────────────────────┼─────────────│ │
│ │ 通用对话 │ ShareGPT 清洗 │ 40-50% │ │
│ │ 代码 │ Code Alpaca │ 15-20% │ │
│ │ 数学/推理 │ GSM8K / MATH │ 10-15% │ │
│ │ 指令遵循 │ FLAN / SELF-INSTR| 10-15% │ │
│ │ 创意写作 │ 人工标注 │ 5-10% │ │
│ │ 安全/对齐 │ Anthropic HH │ 2-5% │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 配比陷阱: │
│ 1. 代码数据过多 → 模型变得"理工直男",创意能力下降 │
│ 2. 数学数据过多 → 模型啰嗦,每步都要推导 │
│ 3. 安全数据过多 → 模型过于保守,拒绝正常请求 │
│ │
│ 调整策略: │
│ → 先用经验配比训一个模型 │
│ → 在评估集上测试各能力维度 │
│ → 针对性调整比例 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:SFT 训练实践
SFT vs 预训练的关键差异
┌─────────────────────────────────────────────────────────────┐
│ SFT 训练的参数设置差异 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 参数 │ 预训练 │ SFT │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ batch_size │ 极大(全局 4M+)│ 较小(1M 以下)│
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ learning_rate │ ~1e-4 │ ~1e-5(更小) │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ warmup_steps │ 占总步数 0.5-1% │ 可以跳过或极少 │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ total_steps │ 数百万步 │ 数千~数万步 │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ weight_decay │ 0.1 │ 0.01-0.05 │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ dropout │ 0(通常) │ 0.0-0.1 │
│ ├───────────────────┼──────────────────┼─────────────────┤
│ │ 数据量 │ 数万亿 token │ 数百万~千万条 │
│ │
│ 核心逻辑: │
│ → SFT 数据量小(相对预训练),大学习率会导致过拟合 │
│ → 小学习率 + 小 weight_decay:保留预训练知识,只微调风格 │
│ │
└─────────────────────────────────────────────────────────────┘SFT 训练过拟合检测
┌─────────────────────────────────────────────────────────────┐
│ SFT 过拟合的识别与处理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 过拟合信号(需要监控的指标): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 信号 1:Train Loss 持续下降,Val Loss 开始上升 │ │
│ │ → 最直接的过拟合信号 │ │
│ │ → 解决:提前停止(Early Stopping) │ │
│ │ │ │
│ │ 信号 2:训练步数增加,评估集指标不再提升 │ │
│ │ → 到达能力上限,继续训练无益 │ │
│ │ → 解决:记录最优 checkpoint,及时停止 │ │
│ │ │ │
│ │ 信号 3:模型开始"复读" │ │
│ │ → 模型记住训练数据,开始直接复述 │ │
│ │ → 解决:增加数据多样性,过滤重复内容 │ │
│ │ │ │
│ │ 信号 4:对话质量主观退化 │ │
│ │ → Loss 不变,但回答变得模板化/无创造性 │ │
│ │ → 需要人工评估 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Early Stopping 实现: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ best_val_loss = float('inf') │ │
│ │ patience = 3 # 连续 3 个 epoch 无改善则停止 │ │
│ │ no_improve = 0 │ │
│ │ │ │
│ │ for epoch in range(max_epochs): │ │
│ │ train_loss = train_epoch(model, train_loader) │ │
│ │ val_loss = evaluate(model, val_loader) │ │
│ │ │ │
│ │ if val_loss < best_val_loss: │ │
│ │ best_val_loss = val_loss │ │
│ │ save_checkpoint(model, 'best') │ │
│ │ no_improve = 0 │ │
│ │ else: │ │
│ │ no_improve += 1 │ │
│ │ if no_improve >= patience: │ │
│ │ print(f"Early stopping at epoch {epoch}") │ │
│ │ break │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘课程学习——先易后难的训练策略
┌─────────────────────────────────────────────────────────────┐
│ SFT 中的课程学习策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心思想:先训练简单样本,再训练复杂样本 │
│ → 模型先建立基础能力,再逐步挑战困难任务 │
│ │
│ 难度分级标准: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 维度 1:回复长度 │ │
│ │ → 简单:< 100 tokens │ │
│ │ → 中等:100-500 tokens │ │
│ │ → 困难:> 500 tokens(需要长程规划) │ │
│ │ │ │
│ │ 维度 2:任务复杂度 │ │
│ │ → 简单:事实问答、简单翻译、格式转换 │ │
│ │ → 中等:代码调试、数学推理、多步骤推理 │ │
│ │ → 困难:创意写作、长文档摘要、复杂编程 │ │
│ │ │ │
│ │ 维度 3:知识深度 │ │
│ │ → 简单:常见知识 │ │
│ │ → 中等:专业领域知识 │ │
│ │ → 困难:前沿/小众知识(模型可能"不知道") │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 两阶段训练策略(推荐): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 阶段 1:基础微调(Epoch 1-2) │ │
│ │ → 60% 简单 + 30% 中等 + 10% 困难 │ │
│ │ → 目标:建立指令遵循、基础对话能力 │ │
│ │ │ │
│ │ 阶段 2:能力提升(Epoch 3-5) │ │
│ │ → 20% 简单 + 50% 中等 + 30% 困难 │ │
│ │ → 目标:提升复杂任务能力 │ │
│ │ → 注意:减少简单样本避免遗忘 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 或者:难度加权采样(不严格分阶段): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ sampling_prob(difficulty) = difficulty^alpha / sum(...) │ │
│ │ │ │
│ │ alpha 控制偏向: │ │
│ │ → alpha=0:均匀采样 │ │
│ │ → alpha=0.5:中等难度偏多(推荐) │ │
│ │ → alpha=1.0:困难样本为主 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘升华:SFT 的工程哲学
┌─────────────────────────────────────────────────────────────┐
│ SFT 的核心工程哲学 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据质量 >> 数据数量 │
│ → SFT 数据量远小于预训练(百万 vs 万亿) │
│ → 质量差的百万条 SFT 数据,不如质量高的十万条 │
│ → LLM 生成 + RM 过滤是工业界主流 │
│ │
│ 2. 格式一致性是硬性要求 │
│ → 训练格式和推理格式必须完全一致 │
│ → special token 不一致 → 模型无法正确响应 │
│ → system prompt 有无不一致 → 行为退化 │
│ │
│ 3. 过拟合是 SFT 的主要风险 │
│ → SFT 数据量小,模型容易记住训练样本 │
│ → 必须监控验证集 loss,设置 early stopping │
│ → 数据多样性是防止"复读"的关键 │
│ │
│ 4. SFT 是对齐的第一步,不是最后一步 │
│ → SFT 后模型学会了"如何回答" │
│ → 但还没学会"什么不该说" │
│ → RLHF/DPO 才是解决安全对齐的关键 │
│ │
│ 一句话总结: │
│ SFT 是预训练模型"学会对话"的关键一步。 │
│ 数据质量、格式一致性、过拟合控制是三个核心。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ 具体的数据集名称和下载地址(ShareGPT、Anthropic HH 等)
✅ chat_template 的具体实现代码
✅ 各任务类型的精确配比数字
必须理解:
🔴 预训练(预测下一个 token)和 SFT(预测 assistant 回复)的目标差异
🔴 SFT Loss Mask:为什么只训练 assistant 部分,不训练 prompt
🔴 ChatML 格式的设计(im_start/im_end 作为边界符)
🔴 SFT 数据的三种来源:人工标注、LLM 生成、公开数据集清洗
🔴 为什么 SFT 用更小的学习率(1e-5 vs 1e-4)和更少的数据量
🔴 过拟合的识别方法:Train Loss ↓ + Val Loss ↑ 或能力不再提升
🔴 课程学习的思想:先简单后复杂,先建立基础再提升能力学习状态:🟡 开始学习