pokemon qwen 微调系列(四):DPO 偏好优化实战:为什么 v1 跑通了但没有变强

May 6, 2026

pokemon qwen 微调系列(四):DPO 偏好优化实战:为什么 v1 跑通了但没有变强

本篇关注的核心内容

DPO 算法原理、偏好数据构造、TRL 训练实现、v1 指标复盘,以及下一轮 DPO v2 应该怎么做。

这篇是 pokemon qwen 微调系列(三):SFT 评估复盘 的后续。

上一篇的结论是:SFT v1 退化以后,不能急着做 DPO;必须先修 SFT 数据。现在 SFT v2 已经在同一套 30 题评测上超过 base,才进入这一步:尝试用 DPO 做偏好优化。

但这次 DPO v1 的结果也很重要:技术链路跑通了,指标没有变强。

这不是坏结果。它说明 DPO 不是“接在 SFT 后面的万能增强按钮”,而是一个很依赖偏好数据强度、训练信号和评测覆盖的精细对齐步骤。

主要依据:DPO 原论文提出用一个分类式目标直接优化偏好对,避免显式 reward model 和 PPO 训练;TRL 文档中 DPOTrainer 要求每条样本包含 prompt / chosen / rejected,并通过 policy 与 reference 的 log probability 差异计算偏好损失。[Rafailov et al., 2023, arXiv:2305.18290] [Hugging Face, 2026-05-06, TRL DPOTrainer]

TL;DR

问题结论证据
DPO 是什么不显式训练 reward model 的偏好优化方法直接用 chosen / rejected 对训练 policy
为什么接在 SFT v2 后面DPO 适合做偏好对齐,不适合补基础知识SFT v1 退化时应先修数据
DPO v1 数据多大146 对 synthetic / RLAIF preference pairsSFT_DPO/data/pokemon_dpo_v1.jsonl
DPO v1 是否跑通跑通smoke 5/5 steps,full training 19/19 steps
DPO v1 是否超过 SFT v2没有Accuracy 同为 63.3%,hallucination 同为 36.7%
主要问题训练信号几乎没动loss≈0.6931,reward margin 为 0.0
下一步先做 overfit sanity test,再扩 DPO v220-40 对高置信偏好,3-5 epochs
SFT v2已修复数据退化偏好数据chosen / rejectedDPO 训练policy vs reference固定评估30 prompts失败分析signal / data / eval

DPO 的目标不是继续背知识,而是让模型更偏向“已知更好”的回答。

1) DPO 要解决什么问题

SFT 的训练信号很简单:给模型一个 prompt 和标准 answer,让模型最大化标准 answer 的 likelihood。

形式上可以写成:

LSFT(θ)=logπθ(yx)\mathcal{L}_{SFT}(\theta) = - \log \pi_{\theta}(y \mid x)

这里:

  • xx 是 prompt;
  • yy 是训练集中给定的 assistant answer;
  • πθ\pi_{\theta} 是正在训练的语言模型。

SFT 的问题也很直接:它只告诉模型“模仿这个答案”,但不告诉模型“两个答案里哪个更好”。

例如同一个问题:

Prompt:
What is the Electric type weak to?
 
Good answer:
Electric-type Pokemon are weak to Ground-type moves.
 
Bad answer:
This type is weak to Flying, Water, and Grass.

SFT 只能学习某个单独答案的分布;DPO 学的是更强的比较信号:

在同一个 prompt 下,chosen 应该比 rejected 更可能被模型生成。

也就是说,DPO 的数据不是普通监督样本,而是偏好对:

字段含义
prompt用户问题
chosen更好的回答
rejected更差的回答

这也是 SFT_DPO/data/pokemon_dpo_v1.jsonl 的基本结构。

2) 从 RLHF 到 DPO:为什么它更简单

传统 RLHF 常见流程可以拆成三步:

  1. 先做 SFT,得到一个可用初始模型;
  2. 收集偏好数据,训练 reward model;
  3. 用 PPO 之类的强化学习方法优化 policy,同时用 KL 约束防止模型偏离原模型太远。

这个流程有效,但工程复杂度很高:

阶段需要训练什么主要风险
SFTpolicy数据质量差会直接学坏
Reward Model单独的奖励模型reward hacking、偏好泛化不稳
PPOpolicy训练不稳定、超参数敏感、成本高

DPO 的核心想法是:不单独训练 reward model,也不显式跑 PPO,而是把 RLHF 目标重写成一个可以直接优化的分类式损失。

直观理解:

如果 chosen 比 rejected 更好,
那么 policy 相对 reference 应该更偏向 chosen。

这里的 reference 通常就是 DPO 开始前的 SFT 模型。它的作用不是继续训练,而是作为“不要偏离太远”的锚点。

RLHF / PPOSFT policyReward ModelPPOAligned policyDPOSFT policyPreference pairsprompt / chosen / rejectedDPO lossAligned policy

3) DPO 的数学形式

每条 DPO 样本可以记作:

(x,yw,yl)(x, y_w, y_l)

其中:

  • xx 是 prompt;
  • ywy_w 是 preferred / chosen answer;
  • yly_l 是 dispreferred / rejected answer。

DPO 比较的不是单独的 log probability,而是 policy 相对 reference 的偏好差。

先定义 chosen 的相对优势:

rθ(x,yw)=β[logπθ(ywx)logπref(ywx)]r_{\theta}(x, y_w) = \beta \left[ \log \pi_{\theta}(y_w \mid x) - \log \pi_{ref}(y_w \mid x) \right]

再定义 rejected 的相对优势:

rθ(x,yl)=β[logπθ(ylx)logπref(ylx)]r_{\theta}(x, y_l) = \beta \left[ \log \pi_{\theta}(y_l \mid x) - \log \pi_{ref}(y_l \mid x) \right]

DPO 希望 chosen 的相对优势大于 rejected:

rθ(x,yw)rθ(x,yl)>0r_{\theta}(x, y_w) - r_{\theta}(x, y_l) > 0

最终 loss 可以写成:

LDPO(θ)=logσ(β[logπθ(ywx)πref(ywx)logπθ(ylx)πref(ylx)])\mathcal{L}_{DPO}(\theta) = -\log \sigma \left( \beta \left[ \log \frac{\pi_{\theta}(y_w \mid x)}{\pi_{ref}(y_w \mid x)} - \log \frac{\pi_{\theta}(y_l \mid x)}{\pi_{ref}(y_l \mid x)} \right] \right)

这里最关键的是四个量:

含义
πθ(ywx)\pi_{\theta}(y_w \mid x)当前 policy 对 chosen 的概率
πθ(ylx)\pi_{\theta}(y_l \mid x)当前 policy 对 rejected 的概率
πref(ywx)\pi_{ref}(y_w \mid x)reference 对 chosen 的概率
πref(ylx)\pi_{ref}(y_l \mid x)reference 对 rejected 的概率

如果 policy 相比 reference 更偏向 chosen,括号里的值会变大,loss 会下降。

如果 policy 对 chosen 和 rejected 没有拉开差距,sigmoid 输入接近 0,loss 就接近:

log(0.5)0.6931-\log(0.5) \approx 0.6931

这正好对应这次 DPO v1 的训练现象:loss 长期停在 0.6931 附近,reward margin 几乎为 0.0

4) beta 是什么

DPO 里的 beta 控制偏好信号强度,也可以理解为 policy 偏离 reference 的力度旋钮。

beta 情况直观效果风险
太小更新很弱,模型几乎不动指标无变化
适中chosen / rejected 被稳定拉开理想状态
太大偏好信号过强,容易过拟合偏好对事实回归或风格变窄

这次配置里:

beta: 0.1
learning_rate: 5.0e-7
num_train_epochs: 1

beta=0.1 本身是常见保守起点,真正更可疑的是整体训练强度太弱:学习率低、数据少、epoch 少,总步数只有 19

5) Pokemon DPO v1 的数据怎么构造

这次 DPO 数据集不是人工标注数据,而是 synthetic / RLAIF 风格的学习数据。

核心文件:

SFT_DPO/data/pokemon_dpo_v1.jsonl
SFT_DPO/DPO_DATASET_CARD.md
SFT_DPO/src/build_dpo_dataset.py

数据规模:

指标
preference pairs146
chosen 含 $effect_chance0
rejected 含 $effect_chance5

类别分布:

CategoryCount
factoid18
semi_structured33
reasoning79
long_form16

来源分布:

SourceCount含义
canonical_type_chart54基于类型克制表构造
eval_sft_rejected29SFT 评测失败输出作为 rejected
eval_winner_pair24base / SFT 一方胜出的 pair
canonical_pokemon_profile20基于 Pokemon 类型和弱点构造
eval_base_rejected15base 失败输出作为 rejected
synthetic variants4placeholder、style、ability confusion

一个真实样本长这样:

{
  "prompt": "What is the Electric type weak to?",
  "chosen": "Electric-type Pokemon are weak to Ground-type moves.",
  "rejected": "flying, water, grass",
  "category": "factoid",
  "source": "eval_sft_rejected"
}

这个 pair 很适合 DPO,因为 chosen 和 rejected 的差异不是风格差异,而是明确的事实正确性差异。

再看一个不那么强的 pair:

{
  "prompt": "Explain what Levitate does in Pokemon battles.",
  "chosen": "In Pokémon battles, the Levitate ability allows a Pokémon to become immune to Ground-type moves and Terrain types...",
  "rejected": "Nullifies effects of being grounded.",
  "category": "factoid",
  "source": "eval_winner_pair"
}

这个 pair 方向大体正确,但 chosen 本身也不完美:它把 Terrain 相关表述混进来了,可能让偏好信号变脏。

这就是 synthetic preference data 的主要风险:不是每个 pair 都像“Ground vs Flying/Water/Grass”那么干净。

DPO v1 category distribution146 preference pairs,reasoning 占比最高。reasoning79semi_structured33factoid18long_form16目标:补强 SFT v2 仍弱的规则应用。风险:小数据 + 弱 pair 可能无法形成梯度信号。

6) 训练实现:policy adapter 和 reference adapter

训练入口在:

SFT_DPO/src/train_dpo.py
SFT_DPO/src/modal_dpo.py
SFT_DPO/configs/dpo_qwen25_7b_lora_v1.yaml

核心配置:

Base modelQwen/Qwen2.5-7B-Instruct
SFT adapter/outputs/pokemon-qwen25-7b-instruct-qlora-v2/adapter
DPO datasetSFT_DPO/data/pokemon_dpo_v1.jsonl
Output/outputs/pokemon-qwen25-7b-instruct-dpo-v1
max seq length1024
learning rate5e-7
epochs1
batch1
grad acc8
beta0.1
LoRAr=16, alpha=32, dropout=0.05
quantization4bit NF4 + double quant + bf16
GPUA100

这次实现里有一个关键点:policy 和 reference 都从同一个 SFT v2 adapter 初始化,但只有 policy 继续训练。

代码逻辑可以简化成:

model = PeftModel.from_pretrained(
    model,
    sft_adapter_path,
    is_trainable=True,
    adapter_name="policy",
)
 
model.load_adapter(
    sft_adapter_path,
    adapter_name="reference",
)

然后在 DPOConfig 里指定:

model_adapter_name="policy"
ref_adapter_name="reference"

这个设计对应 DPO 的数学形式:

角色工程对象是否训练
πθ\pi_{\theta}policy adapter
πref\pi_{ref}reference adapter

这样可以避免额外加载一整份 reference model,比较适合 LoRA / QLoRA 场景。

7) Modal 链路

Modal 入口:

SFT_DPO/src/modal_dpo.py

关键点有四个:

  1. 使用 python_version="3.11"
  2. pin 住 trl==0.25.0
  3. 限制 transformers>=4.57.0,<5.0.0
  4. 训练结束后调用 outputs_volume.commit(),确保 adapter 持久化。

依赖片段:

.uv_pip_install(
    "torch",
    "transformers>=4.57.0,<5.0.0",
    "datasets>=2.20.0",
    "peft>=0.12.0",
    "trl==0.25.0",
    "mergekit",
    "accelerate>=0.33.0",
    "bitsandbytes>=0.43.0",
    "pyyaml>=6.0",
    "pydantic>=2.6.0",
)

训练完成后,实际可用于评估的 policy adapter 路径是:

/outputs/pokemon-qwen25-7b-instruct-dpo-v1/adapter/policy

不是父目录:

/outputs/pokemon-qwen25-7b-instruct-dpo-v1/adapter

这个路径差异很容易踩坑,因为 PEFT 多 adapter 保存时会把 adapter name 变成子目录。

8) DPO v1 的评估结果

DPO v1 完整跑通:

  • smoke training:5/5 steps;
  • full training:19/19 steps;
  • output persisted 到 pokemon-qlora-outputs
  • 评估使用同一套 30 题 Pokemon eval set;
  • scoring output:SFT/outputs/eval_scored_dpo_v1.json

总体指标:

ModelAccuracyHallucination rate
Base50.0%50.0%
SFT v263.3%36.7%
DPO v163.3%36.7%

也就是说:

DPO v1 没有比 SFT v2 更好。

这不是“DPO 算法失败”,而是“这次 DPO 实验没有产生可见收益”。

30-prompt evaluationDPO v1 与 SFT v2 在 aggregate metric 上完全持平。AccuracyBase 50.0%SFT v2 63.3%DPO v1 63.3%Hallucination rateBase 50.0%SFT v2 36.7%DPO v1 36.7%

9) 为什么 DPO v1 没有变强

训练日志里最关键的信号是:

loss ≈ 0.6931
rewards/chosen = 0.0
rewards/rejected = 0.0
rewards/margins = 0.0
rewards/accuracies = 0.0

这说明 policy 没有把 chosen 和 rejected 拉开。

从 DPO loss 看,0.6931 不是随机数字。它接近:

log(0.5)-\log(0.5)

也就是模型面对 chosen / rejected 的偏好分类时,几乎还在二选一随机状态。

9.1 训练设置太保守

这是最可能的直接原因。

当前 DPO v1:

dataset size146 pairs
learning rate5e-7
epochs1
optimizer steps19
batch1
grad acc8

对于一个 7B 模型上的 LoRA adapter 来说,这个更新强度非常弱。

保守设置有合理动机:不想破坏 SFT v2 已经修好的事实能力。但结果说明它可能保守过头,连高置信偏好也没有明显学进去。

9.2 偏好数据有用,但不够强

DPO 对 pair 质量非常敏感。

强 pair 应该长这样:

PromptChosenRejected
Electric weak to?GroundFlying / Water / Grass
Fire vs Rock?Not very effectiveSuper-effective
Thunderbolt effect?Electric damage + may paralyze$effect_chance%

这类 pair 的优点是:chosen 和 rejected 的差异明确、可验证、非风格化。

弱 pair 则可能只是:

  • chosen 更长,rejected 更短;
  • chosen 更像解释,rejected 也不完全错;
  • rejected 只是缺字段,不是事实错误;
  • chosen 自身带有不够干净的额外信息。

如果弱 pair 比例过高,DPO 就很难形成稳定、方向一致的更新。

9.3 评测集太小,可能看不到局部收益

这次仍然用 30 题固定评测集。

它适合做工程闭环,但不适合证明 DPO 的全部收益或失败。

可能出现这种情况:

DPO 改善了某些偏好 pair 附近的输出,
但这 30 题没有覆盖到;
或者改善幅度太小,关键词 scorer 看不出来。

所以不能只根据 aggregate metric 否定 DPO。更准确的结论是:

在当前 146 对 synthetic preference pairs、当前超参数、当前 30 题评测下,
DPO v1 没有产生可见收益。

10) DPO 不应该承担什么任务

这次实验再次确认一个原则:

SFT 负责把基础行为和领域知识学对;
DPO 负责在“都能回答”的基础上偏向更好的回答。

DPO 不适合用来做大规模知识注入。

如果模型不知道:

  • Electric 弱 Ground;
  • Ground 免疫 Electric;
  • Gyarados 是 Water/Flying;
  • Levitate 是 ability 而不是 move;

那应该回到 SFT 数据、RAG 数据或基础知识源,而不是指望 DPO 靠少量 rejected answer 把知识补齐。

SFT学习领域事实、回答格式、基础规则prompt → target answer适合修:知识缺失、模板污染、字段缺失DPO学习偏好排序、拒绝坏答案、稳定风格prompt + chosen + rejected适合修:二选一偏好、明显坏输出、回答质量排序

如果 SFT 还没学对事实,DPO 只会把问题包装得更像“偏好问题”。

11) 下一步:先做 overfit sanity test

现在不应该直接扩到几千条 DPO 数据。更好的下一步是先做一个 overfit sanity test。

目标:

验证当前 DPO 实现和超参数是否能让 adapter 真的移动。

建议设置:

建议
pair 数20-40
pair 类型高置信事实纠错
learning rate1e-65e-6
epochs3-5
generationdeterministic

选择 pair 时优先满足:

  1. chosen 明确正确;
  2. rejected 明确错误;
  3. 差异不是单纯语气或长度;
  4. rejected 包含事实错误、placeholder、方向混淆或关键字段缺失;
  5. prompt 与 eval failure 有直接关系。

预期结果分三种:

结果解释下一步
loss 仍停在 0.6931,margin 仍为 0实现、adapter trainability 或超参数有问题查 TRL / PEFT adapter 交互
margin 能拉开,但 30 题不变训练能动,但评测覆盖或数据覆盖不足扩评测、扩高质量 pair
margin 能拉开,目标题输出改善DPO 链路有效构建 DPO v2

12) DPO v2 应该怎么做

DPO v2 不应该追求“大而杂”,应该追求“小而硬”。

建议目标:

300-800 对高置信 preference pairs

数据分层:

Layer用途
factual correction修事实错误,例如类型、技能、进化
rule reasoning修类型克制、免疫、双属性倍率
semi-structured修字段缺失、profile 顺序、comparison
style control修 flavor text 泄漏、啰嗦、答非所问

同时每条数据最好保留:

category
source
error_bucket
confidence

这样评估时就能回答更细的问题:

  • DPO 是否真的改善 type chart reasoning;
  • 是否只改善了 style;
  • 是否牺牲 factoid accuracy;
  • 是否把回答变短但漏了字段;
  • 是否降低 hallucination。

13) 当前关键路径

SFT_DPO/data/pokemon_dpo_v1.jsonl
SFT_DPO/DPO_DATASET_CARD.md
SFT_DPO/DPO_V1_ANALYSIS.md
SFT_DPO/configs/dpo_qwen25_7b_lora_v1.yaml
SFT_DPO/src/build_dpo_dataset.py
SFT_DPO/src/train_dpo.py
SFT_DPO/src/modal_dpo.py
SFT/outputs/eval_scored_dpo_v1.json

14) 一句话总结

DPO v1 的价值不是“让 Pokemon Qwen 立刻变强”,而是证明了从 SFT v2 adapter、偏好数据、TRL DPOTrainer、Modal A100 训练到固定评估的链路已经打通;真正的问题转向了更核心的部分:偏好数据是否足够硬,训练信号是否足够强,评测是否足够敏感。

下一步不该盲目扩大训练,而应该先用 20-40 条高置信 pair 做 overfit sanity test。只有当 loss、reward margin 和目标输出都能动起来,DPO v2 才值得扩大到 300-800 条高质量偏好数据。