指令微调 SFT


文档摘要

指令微调 SFT 本节摘要:基座模型只预测下一个 token,仅此而已。它不遵循指令、不回答问题、不拒绝有害请求。SFT(监督微调)是「token 预测器」与「有用助手」之间的桥梁——你用过的每个模型(Claude、GPT、Llama Chat)都走过这一步。本节带你实现 SFT:把基座模型变成遵循指令的助手。核心是一个大多数教程略过的技术细节——掩码损失(只在响应 token 上算损失,指令 token 只提供上下文)。你会理解为什么 27,000 个高质量样本就够了(预训练给了知识,SFT 只给「礼貌」),以及学习率为何要降 15 倍以防灾难性遗忘。 学习目标 阅读完本节,你应当能够: 实现监督微调(SFT),把基座语言模型变成遵循指令的助手。

指令微调 SFT

本节摘要:基座模型只预测下一个 token,仅此而已。它不遵循指令、不回答问题、不拒绝有害请求。SFT(监督微调)是「token 预测器」与「有用助手」之间的桥梁——你用过的每个模型(Claude、GPT、Llama Chat)都走过这一步。本节带你实现 SFT:把基座模型变成遵循指令的助手。核心是一个大多数教程略过的技术细节——掩码损失(只在响应 token 上算损失,指令 token 只提供上下文)。你会理解为什么 27,000 个高质量样本就够了(预训练给了知识,SFT 只给「礼貌」),以及学习率为何要降 15 倍以防灾难性遗忘。

学习目标

阅读完本节,你应当能够:

  1. 实现监督微调(SFT),把基座语言模型变成遵循指令的助手。
  2. 用系统/用户/助手角色的聊天模板格式化训练数据,并在非助手 token 上掩码损失。
  3. 解释为什么 SFT 必要:基座模型续写文本而非回答问题。
  4. 通过对比基座与微调模型在留出指令集上的响应,评估 SFT 质量。

一、问题与直觉

你在第 04 节训了个模型,能给定序列预测下一 token。喂「Transformer 架构」它可能续「彻底改变了自然语言处理」,作为下一 token 预测器很厉害。

现在试这个:喂「法国的首都是什么?」基座模型不答「巴黎」,它续写模式——可能产出「德国的首都是什么?西班牙的首都是什么?」(因为它学过含问题列表的文档),或产出「是一个很多人问的问题」(合理的下一 token 续写)。模型没有「回答」的概念,只知「续写」。

这就是 GPT-3(基座,2020 年 6 月)与 ChatGPT(指令微调,2022 年 11 月)的鸿沟。同样架构、同样预训练。差别是 2 万到 10 万对精心制作的(指令,响应)配对,教会模型遵循对话模式。

Stanford Alpaca 证明了你不需要百万样本。2023 年 3 月,他们用 52,000 对 GPT-3.5 生成的指令-响应对微调 Llama 7B,总成本 600 美元,数小时训练,得到一个能遵循指令、回答问题、对话的聊天机器人——虽不及 ChatGPT,但 600 美元能做到如此接近,令人震惊。Meta 的 Llama 2 Chat 初始 SFT 阶段只用了约 27,000 个高质量样本。关键洞见:质量重于数量,27,000 个熟练标注员写的样本胜过 100 万条从网上抓的噪声样本。

SFT 到底做了什么

监督微调延续预训练的同一训练循环——前向、算损失、反向、更新权重——但换了一种数据。不再是原始文本,而是结构化对话:

{"system":"你是有帮助的助手。","user":"法国的首都是什么?","assistant":"法国的首都是巴黎。"}

模型早知道巴黎是法国首都——这在维基百科、教科书、网页的预训练里学过。SFT 不教模型新事实,它教模型一种新行为:看到问题就产出答案,看到指令就产出完成,看到有害请求就产出拒绝。预训练给知识,SFT 给礼貌。

数据格式

业界三大格式,编码同样信息(谁说了什么),分隔符不同。Alpaca 格式(Stanford):instruction/input/output 三字段,52K 样本 600 美元,开启了开源指令微调运动。ShareGPT 格式(社区):conversations 列表,支持多轮对话,Vicuna 在 70,000 条 ShareGPT 对话上训练。ChatML 格式(OpenAI,许多开源模型用):用 <|im_start|><|im_end|> 特殊 token 分隔角色,Qwen、Yi 等用之。三种格式做同一件事:告诉模型「这是指令,这是响应,学这个模式」。

掩码损失(最重要的技术细节)

这是 SFT 中最重要的技术细节,大多教程略过。

预训练时,你在每个 token 上算损失——模型学着预测序列里每个下一 token。SFT 时,你只在响应 token 上算损失。指令 token 提供上下文,但模型不为「预测错」它们而受罚。为什么?因为你不想让模型学会生成指令,你想让它学会响应指令。若在指令 token 上算损失,等于训练模型去预测「法国的首都是什么?」仿佛它在提问——这浪费梯度信号,还会混淆模型对自己角色的认知。

实践中你造一个损失掩码:响应 token 为 1,指令 token 为 0,逐 token 损失乘掩码再平均。

token: [SYS] 你是有帮助的 [USER] 法国首都? [ASST] 巴黎是首都 [EOS] 损失掩码: 0 0 0 0 0 0 0 1 1 1 1 1 1

只有 [ASST] 之后的 token 贡献损失。前向时模型看到完整对话(它需要指令才能产出正确响应),但只按预测响应的好坏更新权重。

训练超参数

SFT 用与预训练截然不同的超参数。你不是从零训练,而是在调一个已经能用的模型。

参数 预训练(Llama 2 7B) SFT(Llama 2 Chat)
学习率 3e-4(峰值) 2e-5
轮数 1(过一遍数据) 2
批大小 4M token 64 样本
数据量 2T token 27,000 样本

学习率低 15 倍是关键——微调时高学习率会摧毁预训练知识,模型「忘掉」所学、过拟合到小数据集,这就是灾难性遗忘。三种缓解:(1) 低学习率(1e-55e-5);(2) 短训练(13 轮,小数据集超 3 轮就开始逐字背诵训练样本);(3) 混入预训练数据(Llama 2 Chat 把 2~5% 原始预训练数据混进 SFT 数据集,「提醒」模型通用能力)。

真实数字

7B 模型在 10,000 对高质量指令上微调,单张 A100 80GB 约需 1 小时:10,000 样本 × 512 token = 5.12M token,2 轮 = 10.24M token,A100 对 7B 微调约 3000 token/秒,合计约 3400 秒 ≈ 57 分钟。

二、从零实现

Step 1:指令数据集

INSTRUCTION_DATA = [ {"instruction":"法国的首都是什么?","response":"法国的首都是巴黎。"}, {"instruction":"用一句话解释引力。","response":"引力是使有质量的物体相互吸引的力。"}, # ... 共 8 条样本(生产中 27K~52K) ]

8 条很小(Alpaca 用 52K),但无论 8 还是 52K 机制相同:分词、掩码、只在响应上算损失。

Step 2:用聊天模板分词

把指令-响应对转成带特殊角色标记的 token 序列。

SPECIAL_TOKENS = {"INST_START":253, "INST_END":254, "RESP_START":255} def tokenize_instruction_pair(instruction, response, vocab_size=256): inst = [min(t, vocab_size-4) for t in instruction.encode("utf-8")] resp = [min(t, vocab_size-4) for t in response.encode("utf-8")] return [253] + inst + [254] + [255] + resp # [INST] 指令 [INST_END] [RESP] 响应 def create_loss_mask(tokens): mask = np.zeros(len(tokens), dtype=np.float32) in_resp = False for i, t in enumerate(tokens): if t == SPECIAL_TOKENS["RESP_START"]: in_resp = True; continue if in_resp: mask[i] = 1.0 # 只有 [RESP_START] 之后掩码为 1 return mask

Step 3:掩码交叉熵损失

标准交叉熵,但乘损失掩码,只有响应 token 贡献梯度。分母是响应 token 数而非序列长度——若除序列长度,长指令会稀释梯度信号。

def masked_cross_entropy_loss(logits, targets, loss_mask): log_softmax = ... # 标准数值稳定实现 per_token = -log_softmax[arange, targets] masked = per_token * loss_mask return masked.sum() / loss_mask.sum() # 除以响应 token 数

Step 4:SFT 训练循环

复用第 04 节的 MiniGPT,循环几乎与预训练相同,只是多了指令格式化与掩码损失。学习率 2e-5(Llama 2 Chat),对比预训练的 3e-4——小 15 倍。梯度被掩码:指令 token 产零梯度,只有响应 token 推权重。

Step 5:对比基座与 SFT 模型

SFT 的全部意义是行为改变。测模型如何响应指令格式输入,而非续写原始文本。小模型 8 样本上,响应不会有意义——预期之内。重要的是结构:模型学会了在响应标记后产出输出,而非继续生成更多指令。

Step 6:度量灾难性遗忘

对比 SFT 前后模型在原始文本上的下一 token 预测能力。若 SFT 损伤通用能力,原始文本损失会上升。真实微调中全程追踪此指标——若原始文本损失升超 10~15%,说明 SFT 太激进,降学习率或减轮数。

三、框架对比

HuggingFace TRL 的 SFTTrainer 把上述封装成几行:

from trl import SFTTrainer, SFTConfig trainer = SFTTrainer(model=model, train_dataset=ds, args=SFTConfig(...)) trainer.train()

它自动处理聊天模板、损失掩码、数据集打包。底层与我们手写的逻辑相同。生产中你用 TRL,但理解掩码损失才能诊断「为什么模型在生成指令而非响应」这类问题。

四、可复用产物

本节产出 outputs/prompt-sft-data-curator.md——一个帮你设计与策展 SFT 指令数据集的提示。给定目标能力(代码生成、数学、对话),它产出含格式规范、质量标准、多样性要求的数据采集计划。

五、练习

  1. (Easy)tokenize_instruction_pair 加系统提示支持,在指令前加系统消息。造 5 条不同系统提示(「你是诗人」「你是数学导师」)的样本。

  2. (Medium) 实现数据混合:SFT 数据集中 5% 是原始文本(不掩码)、95% 是指令对(掩码),跑 3 轮对比纯 SFT 的遗忘指标。

  3. (Medium) 构建数据质量打分器:对每对算响应 token 数、指令响应比、词汇多样性(唯一/总数),过滤响应<10 token 或多样性<0.3 的样本,看过滤如何影响最终损失。

  4. (Hard) 实现多轮对话训练:扩展分词处理 3 轮对话(用户-助手-用户-助手-用户-助手),损失掩码覆盖三个助手轮次。

  5. (Hard) 对比学习率:用 1e-4、2e-5、1e-6 各训一次画损失曲线。1e-4 初期降快但最终损失高(过拟合),1e-6 几乎不动,2e-5 是甜点。

本节要点回顾

  1. 基座只续写,不回答:喂「法国首都?」它续写问题列表而非答「巴黎」;GPT-3 与 ChatGPT 的鸿沟就在 SFT。
  2. SFT 教行为不教知识:模型早知巴黎是首都,SFT 教它「看到问题就回答」;预训练给知识,SFT 给礼貌。
  3. 质量重于数量:Llama 2 Chat 用 27K 高质量样本,Alpaca 用 52K 花 600 美元。
  4. 掩码损失是核心:只在响应 token 算损失,指令 token 只提供上下文,否则等于训练模型生成指令。
  5. 损失掩码除以响应 token 数:不除序列长度,否则长指令稀释梯度。
  6. 学习率降 15 倍:2e-5 vs 预训练 3e-4,防灾难性遗忘摧毁预训练知识。
  7. 三种格式同一目的:Alpaca(instruction/input/output)、ShareGPT(conversations)、ChatML(<|im_start|>),都是「这是指令这是响应」。
  8. 灾难性遗忘三种缓解:低学习率、短训练(13 轮)、混入预训练数据(25%)。
  9. 聊天模板必须精确:模型只认训练时的那一种格式,任何偏差都推出分布外。
  10. 27K 样本够用:不教英语不教事实,只教一个简单行为——响应指令;知识早已在那里。

下一节,SFT 之上的进阶:RLHF。SFT 教模型遵循指令,但教不了哪个响应更好——RLHF 把人类判断编码进模型行为。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U