微调与对齐:从"会续写"到"会对话"
基座模型只会补全文本。要变成 ChatGPT 那样的助手,需要两步:SFT 教会它"指令 → 回答"的格式,偏好对齐(RLHF/DPO)教会它"什么样的回答更好"。
一、SFT:监督微调
数据格式
用对话模板(chat template)把样本组织成模型要学的样子。以 ChatML 为例:
<|im_start|>system
你是一个有帮助的助手。<|im_end|>
<|im_start|>user
解释一下梯度消失<|im_end|>
<|im_start|>assistant
梯度消失是指……<|im_end|>
关键技巧:只计算回答部分的 loss
对 prompt(system+user)部分的 token 把 label 置为 -100(PyTorch 交叉熵自动忽略),只让模型学怎么回答,不学怎么提问:
def build_labels(input_ids, prompt_len):
labels = input_ids.clone()
labels[:prompt_len] = -100
return labels
SFT 数据来源与配比
| 来源 | 例子 | 注意 |
|---|---|---|
| 人工标注 | OpenAI 早期 1.3 万条 | 贵但质量天花板 |
| 强模型蒸馏 | Alpaca、ShareGPT | 便宜量大,注意许可 |
| 自我改进 | Self-Instruct、Evol-Instruct | 由弱到强自动扩增 |
| 人类反馈改写 | 对模型输出人工精修 | 对齐效果最好 |
经验:1 万条高质量 > 100 万条噪声(LIMA 论文的"表面对齐假说":模型能力在预训练已获得,SFT 只是教格式)。配比上覆盖:问答、写作、代码、数学、拒答(安全样本)、多轮对话。
超参
lr: 1e-5 ~ 2e-5(比预训练小 1-2 个数量级)
epoch: 2-3(多于 3 容易过拟合、灾难性遗忘)
batch: 32-128 条样本
全参数 vs LoRA:LoRA 冻结基座,只训练注入的低秩矩阵 W' = W + BA(B: d×r, A: r×d, r=8~64),显存省 2/3,适合单卡微调 7B;但追极致效果或继续预训练仍用全参数。
from peft import LoraConfig, get_peft_model
cfg = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj","k_proj","v_proj","o_proj"],
lora_dropout=0.05, task_type="CAUSAL_LM")
model = get_peft_model(base_model, cfg)
二、RLHF:用人类偏好训练
SFT 后模型会答了,但不知道"哪个答案更好"。RLHF 三步走:
1. 收集偏好数据
对同一 prompt 采样多个回答,标注员排序 → 得到 (prompt, chosen, rejected) 三元组。
2. 训练奖励模型(RM)
奖励模型 = 基座 + 一个标量输出头,用 Bradley-Terry 损失:
L_RM = -log σ(r(x, y_chosen) - r(x, y_rejected))
直觉:好回答的奖励分要比坏回答高,sigmoid 把分差变成概率。
def rm_loss(r_chosen, r_rejected):
return -F.logsigmoid(r_chosen - r_rejected).mean()
3. PPO 强化学习
用 RM 的分数作为奖励,微调策略模型,同时加 KL 惩罚防止跑偏:
目标 = E[ r(x,y) - β · KL(π_θ ‖ π_ref) ]
π_ref是冻结的 SFT 模型,KL 惩罚防止模型为骗高分而输出乱码(reward hacking);- PPO 需要同时在线跑 4 个模型(策略、参考、奖励、价值),工程复杂、调参敏感——这是它被 DPO 大量替代的原因。
三、DPO:不用强化学习的偏好对齐
DPO 的天才之处:把 RLHF 的目标解析地解出来,直接用偏好对训练,不需要奖励模型和在线采样。损失函数:
L_DPO = -E log σ( β·( log[π_θ(y_c|x)/π_ref(y_c|x)] - log[π_θ(y_r|x)/π_ref(y_r|x)] ) )
翻译成人话:让 chosen 回答相对参考模型的对数概率增幅,大于 rejected 的增幅。
def dpo_loss(policy_c, policy_r, ref_c, ref_r, beta=0.1):
# policy_c/r: 策略模型对 chosen/rejected 的序列对数概率
logits = beta * ((policy_c - ref_c) - (policy_r - ref_r))
return -F.logsigmoid(logits).mean()
def seq_logprob(model, input_ids, labels):
logits = model(input_ids).logits[:, :-1]
tgt = labels[:, 1:]
mask = tgt != -100
lp = torch.log_softmax(logits, -1).gather(-1, tgt.clamp(min=0).unsqueeze(-1)).squeeze(-1)
return (lp * mask).sum(-1)
DPO 稳定、简单、效果好,已成为中小团队对齐的首选;前沿实验室在 DPO 之上再叠在线 RL(RLHF/GRPO)冲极限(如 DeepSeek-R1 的推理强化学习路线)。
四、评估对齐效果
| 维度 | 工具 |
|---|---|
| 指令遵循 | IFEval、AlpacaEval(胜率) |
| 综合能力 | MT-Bench、Arena 人类偏好评分 |
| 安全 | 越狱攻击集、有害请求拒答率 |
| 遗忘检查 | 通用基准(MMLU)对齐前后对比,防"对齐税" |
五、实战路线建议
- 先用开源 SFT 数据(如 Tulu-3 混合集)跑通 SFT;
- 用强模型对自己的 badcase 生成 corrected 回答,构造偏好对;
- 上 DPO(beta=0.1,lr=5e-7,1 epoch);
- 评估对齐税,必要时回退混数据比例。
最后一步:让模型高效地跑起来服务用户 → 推理与部署。