27届大模型岗面试准备(六):SFT 监督微调深挖——数据构造、损失细节与训练技巧全解析
27届大模型岗面试准备六SFT 监督微调深挖——数据构造、损失细节与训练技巧全解析SFTSupervised Fine-Tuning是校招面试里被问得最细的环节之一。原因很简单多数公司的大模型业务不做预训练但几乎都做微调——所以面试官会默认你对 SFT 有实操级理解。SFT 的 loss 和预训练有什么区别为什么要 mask 掉 prompt 部分多轮对话怎么组织成训练样本这三个问题答不好基本一面就结束了。这篇从数据格式讲到损失实现再到数据质量工程把 SFT 的完整知识面串起来。一、SFT 在训练管线中的位置与本质完整管线是预训练PT→ 监督微调SFT→ 偏好对齐RLHF/DPO。base 模型只会续写不会对话——你问它中国的首都是哪里它可能续写出这是一道常见的地理题。SFT 的本质是用少量高质量的指令回复对教会模型响应指令这一行为模式。关键认知Superficial Alignment Hypothesis来自 LIMA 论文模型的知识和能力几乎全部来自预训练SFT 只是把以助手口吻响应指令这个分布激发出来。所以 SFT 数据质量远比数量重要——LIMA 用 1000 条精选数据就达到了不错的对齐效果。这是面试必背结论但要会辩证补充复杂任务数学、代码、多步推理仍需要足够的数据量与多样性1000 条就够不能绝对化。二、数据格式Chat Template 与 Loss MaskSFT 数据的标准形态是多轮对话训练前要用 chat template 拼成一条序列。以 ChatML 格式Qwen 使用为例|im_start|system You are a helpful assistant.|im_end| |im_start|user 9.11 和 9.8 哪个大|im_end| |im_start|assistant 9.8 更大。比较小数时先看整数部分相同再比较十分位8 1所以 9.8 9.11。|im_end|核心考点loss 只算 assistant 部分。promptsystem user的 token 参与前向传播提供上下文但不计入损失——实现上就是把这些位置的 label 设为 -100PyTorch 交叉熵的 ignore_index。为什么因为我们不希望模型学习生成用户问题的分布只希望它学会在给定问题下生成回复。如果不 mask模型会浪费容量拟合用户输入分布且训练信号被稀释。多轮对话的组织有两种做法整段拼接、只在各轮 assistant 部分算 loss主流一条样本利用所有轮次拆成多条样本每条只保留最后一轮 assistant 算 loss数据量膨胀历史轮被重复编码效率低。追问点为什么 EOS/im_end 一定要算 loss因为模型必须学会什么时候停。漏了这个细节推理时模型会停不下来一直生成——这是真实事故高发点也是面试官爱挖的工程细节。三、SFT 与预训练的异同对比维度预训练PT监督微调SFT目标函数Next Token Prediction 交叉熵相同的交叉熵但仅在 response 部分计算数据规模数T token数万~数百万条对话数据形态原始文本(instruction, response) 结构化对话学习率峰值 3e-4 量级小 1~2 个数量级1e-5 ~ 2e-5训练轮数1 epoch数据不重复2~3 epochs序列组织文档拼接切块按对话组织 loss mask / packing主要风险loss spike、数据污染过拟合、灾难性遗忘、幻觉加重表里两个点常被追问。为什么 SFT 学习率要小SFT 数据量小且分布窄大学习率会迅速过拟合并冲掉预训练学到的通用能力灾难性遗忘。为什么 SFT 可能加重幻觉如果 SFT 数据里包含模型预训练中根本没见过的知识等于教模型在不知道的时候也要一本正经地回答这是 John Schulman 的著名观点——SFT 数据应尽量落在模型已有知识边界内超出边界的问题应教它说不知道。四、可运行代码从数据构造到 Loss Mask 的完整演示下面的代码不依赖 GPU用 PyTorch 完整演示 SFT 样本的构造chat template 拼接、tokenize、loss mask 生成以及带 ignore_index 的损失计算。这段代码的逻辑与 LLaMA-Factory、trl 等主流框架内部实现一致。import torch import torch.nn.functional as F # ---------- 玩具 tokenizer字符级仅为演示结构 ---------- class ToyTokenizer: def __init__(self): self.vocab {pad: 0, im_start: 1, im_end: 2} def encode(self, text): ids [] for ch in text: if ch not in self.vocab: self.vocab[ch] len(self.vocab) ids.append(self.vocab[ch]) return ids property def im_start(self): return 1 property def im_end(self): return 2 IGNORE_INDEX -100 def build_sft_sample(tokenizer, messages, max_len512): 把多轮对话拼成 (input_ids, labels)仅 assistant 内容算 loss。 input_ids, labels [], [] for msg in messages: role_ids [tokenizer.im_start] tokenizer.encode(msg[role] \n) content_ids tokenizer.encode(msg[content]) end_ids [tokenizer.im_end] input_ids role_ids content_ids end_ids if msg[role] assistant: # 角色头不算 loss内容和 im_end 都算模型要学会停止 labels [IGNORE_INDEX] * len(role_ids) content_ids end_ids else: labels [IGNORE_INDEX] * (len(role_ids) len(content_ids) len(end_ids)) return input_ids[:max_len], labels[:max_len] def sft_loss(logits, labels): 标准的 shift 一位交叉熵预测下一个 token。 shift_logits logits[:, :-1, :].contiguous() shift_labels labels[:, 1:].contiguous() return F.cross_entropy( shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ignore_indexIGNORE_INDEX, ) if __name__ __main__: tok ToyTokenizer() messages [ {role: system, content: 你是一个有用的助手。}, {role: user, content: 9.11和9.8哪个大}, {role: assistant, content: 9.8更大十分位81。}, {role: user, content: 谢谢}, {role: assistant, content: 不客气}, ] input_ids, labels build_sft_sample(tok, messages) n_total len(labels) n_train sum(1 for x in labels if x ! IGNORE_INDEX) print(f总 token 数: {n_total}, 参与 loss 的 token 数: {n_train} f(占比 {n_train/n_total:.1%})) # 模拟一个随机模型前向验证 loss 可正常计算 vocab_size len(tok.vocab) logits torch.randn(1, n_total, vocab_size) loss sft_loss(logits, torch.tensor([labels])) print(f随机初始化下的 SFT loss: {loss.item():.4f} f(理论值≈ln({vocab_size}){torch.log(torch.tensor(float(vocab_size))):.4f}))运行后能看到两个关键输出参与 loss 的 token 占比真实项目中这个比例太低说明 prompt 冗长、训练效率差随机模型的 loss 约等于 ln(vocab_size)这是验证训练代码正确性的经典手段——面试聊到怎么排查训练代码 bug时检查初始 loss 是否接近 ln(V)是非常加分的回答。五、SFT 数据工程好数据长什么样数据构造的主流方法Self-Instruct 路线用强模型GPT-4 级别从种子任务扩展生成指令与回复Alpaca 是开山之作。廉价但有天花板——学生模型学到的是教师模型的近似且容易继承教师的套话与偏见。进化式增强WizardLM 的 Evol-Instruct对已有指令做深度进化加约束、加推理步骤、复杂化和广度进化换主题系统性提升指令复杂度分布。人工精标贵但质量上限最高通常用于核心场景安全、价值观、公司业务数据。实践中是合成数据打底 人工精标点睛。质量过滤的可操作指标指令多样性用 embedding 聚类看覆盖度、回复长度分布过短的敷衍回复要清掉、IFD 指标Instruction Following Difficulty用模型自身 loss 筛选有信息量的样本、拒答比例控制拒答样本太多模型会变得过度保守。数据配比经验通用对话、代码、数学、多语言、安全各占一定比例业务数据不超过 30%——纯业务数据训练会让模型变笨通用能力回退。这条在业务落地面试题里几乎必问给你 5 万条客服数据怎么微调答案一定要包含混合通用数据防遗忘。六、训练技巧与常见坑Packing把多条短样本拼进一个 max_length 序列配合 attention mask 隔离或 Flash Attention 的 varlen 接口可以把训练吞吐提升数倍。追问点不隔离会怎样样本间会互相看见造成信息泄露虽然实践中影响常常不大但严谨做法必须隔离。NEFTune给 embedding 加均匀噪声的正则化技巧一行代码在多个 benchmark 上涨点面试提到会显得跟进前沿。学习率与 epoch2e-5、2~3 个 epoch 是 7B 模型全参 SFT 的常见起点LoRA 微调学习率要放大到 1e-4 量级可训练参数少、有效步长需求大。判断过拟合验证集 loss 回升、回复开始背诵训练集措辞、多样性下降。灾难性遗忘的缓解混入预训练数据replay、降低学习率、LoRA 等参数高效方法天然遗忘更少、模型融合SFT 后与 base 加权平均。七、面试答题框架被问如何为业务场景做一次高质量 SFT推荐框架界定目标明确任务类型与成功指标人工评估维度 自动指标数据侧业务数据清洗精标 开源/合成数据补多样性控制配比业务 ≤30%构造拒答与边界样本格式侧统一 chat template检查 loss mask 与 EOS长样本截断策略训练侧小学习率 2~3 epoch先跑 LoRA 快速验证数据价值再决定是否全参评估侧held-out 业务评测集 通用能力回归测试防遗忘A/B 上线迭代badcase 归因数据缺失 or 能力不足 or 幻觉针对性补数据。自检清单能解释为什么 mask prompt 吗EOS 为什么要算 loss初始 loss ≈ ln(V) 的原理业务微调为什么要混通用数据这四问过关SFT 环节就是你的得分点。下一篇进入对齐算法RLHF 三阶段与 PPO/DPO 的原理对比与手推。