第六章:DPO (直接偏好优化) 实战 (难点)

在SFT之后,我们的模型学会了“说话”,但它的回答可能仍然是“正确的废话”,或者在面对开放性问题时,其回答的安全性、有用性和真实性仍有待提高。传统的解决方案是强化学习(RLHF),即先训练一个奖励模型(RM),再用这个RM作为环境,通过复杂的强化学习算法(如PPO)来优化语言模型。然而,RLHF流程复杂、训练不稳定、且对计算资源要求极高,令许多开发者望而却步。

直接偏好优化 (Direct Preference Optimization, DPO) 的出现,如同一道曙光,彻底改变了这一局面。它以一种极其优雅和高效的方式,实现了与RLHF相媲美甚至更好的对齐效果,但训练成本和复杂度却大大降低。本章将深入剖析DPO的核心思想、重难点配置,并通过详尽的实战步骤,带你完整地跑通一个DPO训练流程,真正让你的模型“更懂人心”。

6.1 为什么需要 DPO? (轻理论:替代 PPO,让模型更符合人类偏好)

要理解DPO的革命性,我们首先要明白传统RLHF的痛点。

  • 传统RLHF的四步“炼丹法” (复杂且昂贵):
    1. SFT阶段:先对基座模型进行监督微调,得到一个能对话的基础策略模型 (SFT Model)。
    2. RM阶段:收集人类偏好数据 (prompt, chosen_response, rejected_response),训练一个奖励模型 (Reward Model),让它学会给回答打分。
    3. PPO阶段 (瓶颈):这是最复杂的一步。需要同时在GPU内存中加载四个模型
      • 正在训练的策略模型 (Policy Model)。
      • 用于计算奖励分数的奖励模型 (Reward Model)。
      • 一个SFT模型的冻结副本,用于计算KL散度惩罚,防止模型“走火入魔”。
      • 一个用于生成经验的生成模型(有时与策略模型是同一个)。
        这个阶段通过PPO算法,让策略模型生成回答,奖励模型打分,然后根据分数更新策略模型,整个过程涉及大量的采样和复杂的超参数调试,非常不稳定。
    4. 评估阶段:对优化后的模型进行人工或自动评估。
PPO 训练实战(可选,用于对比)

为了让你更直观地感受 PPO 训练的复杂性,我们在这里提供一个使用 Llama-factory 进行 PPO 训练的命令示例。请注意,运行此步骤的前提是你已经完成了 SFT 阶段和 RM 阶段的训练。

  • PPO 训练的前提:

    1. SFT 模型: 假设我们已有一个训练好的 SFT 适配器,存放在 saves/Qwen1.5-7B/lora/sql_generator_v1
    2. RM 模型: 假设我们已有一个训练好的奖励模型适配器,存放在 saves/Qwen1.5-7B/lora/sql_reward_model_v1
    3. PPO 数据: PPO 训练时,通常只需要一个包含一系列 prompt(指令)的数据集即可,模型会根据这些 prompt 生成回答,并由奖励模型打分。假设我们有一个 ppo_prompts.jsonl 文件,并已注册为 sql_ppo_prompts
  • PPO 训练脚本 (train_ppo_sql.sh):

    #!/bin/bash
    export CUDA_VISIBLE_DEVICES="0"
    
    llamafactory-cli train \
        --stage ppo \
        --do_train \
        --model_name_or_path Qwen/Qwen1.5-7B-Chat \
        --adapter_name_or_path saves/Qwen1.5-7B/lora/sql_generator_v1 \
        --reward_model saves/Qwen1.5-7B/lora/sql_reward_model_v1 \
        --dataset sql_ppo_prompts \
        --template default \
        --finetuning_type lora \
        --lora_rank 8 \
        --lora_alpha 16 \
        --lora_target all \
        --output_dir saves/Qwen1.5-7B/lora/sql_ppo_v1 \
        --per_device_train_batch_size 1 \
        --gradient_accumulation_steps 4 \
        --lr_scheduler_type cosine \
        --logging_steps 5 \
        --save_steps 100 \
        --learning_rate 1e-5 \
        --num_train_epochs 1.0 \
        --plot_loss \
        --fp16
    
  • 脚本核心参数解读 (与 DPO 对比):

    • --stage ppo: 切换到 PPO 模式。这是启动 PPO 训练的开关。
    • --adapter_name_or_path ...: 指定 SFT 模型的适配器,它将作为 PPO 训练的初始策略模型
    • --reward_model ...: 这是 PPO 独有的关键参数。你需要明确提供一个已经训练好的奖励模型 (RM) 的路径。在训练过程中,这个 RM 会被加载进来,用于给策略模型生成的回答打分,从而产生梯度信号。
    • 内存消耗:请注意,这个命令启动后,除了策略模型和其 SFT 参考模型外,还需要额外加载一个奖励模型。这就是我们在理论部分提到的 PPO 资源消耗大的原因之一。
    • 超参数:PPO 还有许多独特的超参数,如 kl_coeff (功能类似 DPO 的 beta)、ppo_score_norm 等,这些参数的调试通常比 DPO 更为复杂和敏感,进一步增加了训练的不稳定性。

通过这个具体的训练命令,我们可以清晰地看到,相较于 DPO,PPO 流程不仅多了一个训练 RM 的步骤,而且在最终的对齐训练阶段,配置也更为复杂,需要管理的模型和路径更多。

  • DPO的“神来之笔”:
    DPO的作者们通过精妙的数学推导发现,整个复杂的PPO优化目标,可以等价地转换为一个简单的、类似SFT的分类损失函数

    核心思想 (高度简化):
    DPO认为,我们不需要一个显式的奖励模型。偏好数据 (chosen, rejected) 本身就隐式地定义了奖励。DPO的损失函数直接利用这一点,其目标是:增大模型生成 chosen 回答的概率,同时减小生成 rejected 回答的概率

    更具体地说,DPO通过计算模型对 chosenrejected 回答的对数概率差,并将其与一个由参考模型(即SFT模型)计算出的隐式奖励进行对比,构建了一个简单的损失函数。训练过程就是最小化这个损失,直接将偏好“注入”到模型中。

  • DPO的压倒性优势:

特性 传统RLHF (PPO) 直接偏好优化 (DPO)
训练流程 极其复杂 (SFT -> RM -> PPO) 极其简单 (SFT -> DPO)
模型需求 训练时需加载多个模型,内存占用巨大 训练时只需加载策略模型和参考模型,内存友好
稳定性 PPO训练不稳定,对超参数敏感,容易崩溃 非常稳定,本质上是监督学习,收敛性好
效率 采样过程耗时,训练速度慢 无需采样,训练速度远快于PPO
效果 效果好,但调试成本高 效果与PPO相当甚至更好,且实现简单

结论: DPO以其简洁、稳定、高效的特性,已成为当今大模型对齐的首选方案,极大地降低了让模型更符合人类偏好的技术门槛。


6.2 DPO 的数据准备 (重难点)

DPO在数据层面延续了它的优雅——它直接复用RM阶段的偏好数据,无需任何额外处理。这是DPO流程简洁性的关键所在。

复用 RM 的数据 (Query, chosen, rejected)
  • 数据格式: DPO训练所需的数据格式与第五章介绍的奖励建模 (RM) 完全一致。每一条数据样本都是一个JSON对象,包含一个指令 (instruction/input),以及一个包含两个元素的 output 列表,分别代表“更受欢迎的回答”(chosen)和“不太受欢迎的回答”(rejected)。

  • 实践示例:
    假设我们有一个名为 dpo_data.jsonl 的文件:

    {
      "instruction": "作为一名旅行规划师,请为一对希望进行为期一周的浪漫海岛游的夫妇推荐一个目的地,并简述理由。",
      "output": [
        {
          "content": "我强烈推荐马尔代夫。那里有一流的水上别墅,提供极致的私密性和奢华体验。你们可以享受清澈的潟湖、白色的沙滩和丰富的水下活动,是蜜月和浪漫之旅的完美选择。"
        },
        {
          "content": "去马尔代夫吧,那儿不错。"
        }
      ]
    }
    {
      "instruction": "如何修复一个漏水的水龙头?",
      "output": [
        {
          "content": "修复漏水水龙头通常分几步:1. 关闭总水阀。2. 用扳手拧开水龙头手柄下的压盖螺母。3. 取出内部的垫圈或O形圈,这是最常见的磨损部件。4. 更换新的垫圈后,按相反顺序重新组装。如果问题依旧,可能需要更换整个阀芯。"
        },
        {
          "content": "你得先关掉水,然后把它拆开,换掉坏了的零件,再装回去。"
        }
      ]
    }
    

    在这个例子中,chosen 的回答明显比 rejected 的回答更详细、更专业、更有用。DPO的目标就是让模型学会这种“品味”。

--dataset 参数的设置
  • 注册 dataset_info.json: 与RM阶段一样,你必须在 data/dataset_info.json 中注册你的DPO数据集,并且必须设置 "ranking": true 来告诉Llama-factory这是一个偏好数据集。

    // in data/dataset_info.json
    {
      // ... other datasets
      "my_dpo_dataset": {
        "file_name": "dpo_data.jsonl",
        "ranking": true
      }
    }
    
  • 在CLI或Web UI中指定:

    • CLI: --dataset my_dpo_dataset
    • Web UI: 在“数据集”下拉菜单中选择 my_dpo_dataset

数据质量是DPO成功的基石。高质量的偏好对应该具备:

  • 清晰的偏好差异: chosenrejected 之间的优劣应该是一目了然的。如果两个回答质量相近,会让模型感到困惑。
  • 多样性: 覆盖多样的指令类型、主题和偏好维度(如事实性、安全性、创造性、详细程度等)。
  • chosen回答的质量: chosen 回答本身也应该是高质量的。如果 chosen 回答也存在事实错误,模型在学习偏好的同时也会学到这些错误。

6.3 DPO 的训练配置 (重难点)

这是本章最核心、最关键的部分。DPO的配置有其独特性,理解这些配置是成功进行DPO训练的前提。

关键: DPO 训练需要 两个 模型 (训练中的模型 + SFT 后的参考模型)

正如6.1节理论部分所述,DPO的损失函数中,需要一个固定的参考模型 (Reference Model) 来衡量当前策略模型 (Policy Model) 的变化程度,以防止其偏离原始SFT模型太远。

  • 策略模型 (Policy Model):

    • 角色: 这是我们正在训练和优化的模型。它的参数会在每个训练步骤中被更新。
    • 起点: 策略模型的初始状态,必须是已经完成SFT的模型。我们不能用一个原始的基座模型来做DPO,因为它首先需要具备基本的指令跟随能力。
  • 参考模型 (Reference Model):

    • 角色: 这是一个权重被冻结、不参与训练的模型副本。它只用于在前向传播中计算 chosenrejected 回答的对数概率,为损失函数提供一个基准。
    • 来源: 参考模型就是SFT模型本身
  • Llama-factory 中的实现机制:
    Llama-factory 的设计非常巧妙,它简化了这个“双模型”的配置:

    1. 你通过 --model_name_or_path 参数,加载你已经训练好的SFT模型
    2. Llama-factory 在内部会用这个路径加载两次模型
      • 一次作为策略模型,并附加LoRA权重(如果你用LoRA的话),使其变为可训练状态。
      • 另一次作为参考模型,并将其权重完全冻结。
    3. 这样,你就无需手动管理两个模型的加载,框架会自动处理。
--dpo_beta 参数的实践意义

dpo_beta 是DPO训练中最重要的一个超参数

  • 是什么? beta 是DPO损失函数中,控制KL散度惩罚项权重的系数。KL散度衡量的是策略模型和参考模型在输出概率分布上的差异。

  • 实践意义 (调参关键): beta 的值决定了DPO训练的**“保守”程度**。

    • 较低的 beta (如 0.01, 0.05):
      • 含义: 对KL散度的惩罚较弱。模型会更“大胆”地去拟合偏好数据,即更专注于拉开 chosenrejected 的概率差距。
      • 优点: 可能在你的目标偏好上学得更充分,模型风格变化更明显。
      • 风险: 容易过拟合偏好,导致模型忘记了SFT阶段学到的通用语言能力。可能会生成一些符合偏好但重复、啰嗦或不自然的文本,这就是所谓的“KL塌陷”。
    • 较高的 beta (如 0.5, 1.0):
      • 含义: 对KL散度的惩罚很强。模型在学习偏好的同时,被一股强大的力量“拽”着,不允许它离SFT参考模型太远。
      • 优点: 训练更稳定,能很好地保持SFT模型的通用性和语言风格,在其基础上进行“微调”。
      • 风险: 可能学习偏好不充分,chosenrejected 之间的差异不够明显,模型提升有限。
  • 如何选择?

    • 黄金起点: 原始DPO论文和大量实践证明,beta = 0.1 是一个极其鲁棒和优秀的默认值。对于绝大多数任务,你都应该从 0.1 开始。
    • 调参建议:
      • 如果DPO训练后,你发现模型在偏好任务上表现很好,但通用对话能力下降,或者说话变得很奇怪,尝试增大 beta (如 0.2 or 0.3)。
      • 如果DPO训练后,你感觉模型和SFT版本相比没什么变化,提升不明显,可以尝试减小 beta (如 0.05),让它学得更“激进”一些。

6.4 实战:跑通一个 DPO 训练

现在,我们将所有理论知识串联起来,一步步完成一个完整的DPO流程。我们的目标是,在第五章SFT训练出的SQL生成模型的基础上,进一步通过DPO让它更偏爱格式规范、带有注释的SQL语句。

步骤 1:先 SFT 一个基础模型

这是DPO的绝对前提。我们必须先有一个具备基础能力的SFT模型。我们复用第五章的SFT脚本,并假设其输出保存在 saves/Qwen1.5-7B/lora/sql_generator_v1

  • SFT脚本 (train_sft_for_dpo.sh):
    #!/bin/bash
    # (此脚本已在第五章运行过,这里仅作回顾)
    llamafactory-cli train \
        --stage sft \
        --do_train \
        --model_name_or_path Qwen/Qwen1.5-7B-Chat \
        --dataset sql_gen_custom \
        --finetuning_type lora \
        --output_dir saves/Qwen1.5-7B/lora/sql_generator_v1 \
        # ... 其他SFT参数 ...
    ```**关键产物**: `saves/Qwen1.5-7B/lora/sql_generator_v1` 文件夹,包含了LoRA适配器权重。
    
    
步骤 2:使用该 SFT 模型作为起点来启动 DPO

现在,我们进入DPO阶段。

  • DPO数据准备:
    假设我们创建了 dpo_sql_preference.jsonl,内容如下,并已注册为 sql_dpo_custom

    {
      "instruction": "查询'用户表'中所有年龄大于30岁的用户的姓名和邮箱。",
      "output": [
        { "content": "-- 查询30岁以上用户的核心信息\nSELECT \n  name, \n  email \nFROM \n  user_table \nWHERE \n  age > 30;" },
        { "content": "SELECT name, email FROM user_table WHERE age > 30;" }
      ]
    }
    

    这里,chosen 的回答包含了注释和更好的格式,是我们希望模型学习的偏好。

  • DPO训练脚本 (train_dpo_sql.sh):

    #!/bin/bash
    export CUDA_VISIBLE_DEVICES="0"
    
    llamafactory-cli train \
        --stage dpo \
        --do_train \
        --model_name_or_path Qwen/Qwen1.5-7B-Chat \
        --adapter_name_or_path saves/Qwen1.5-7B/lora/sql_generator_v1 \
        --dataset sql_dpo_custom \
        --template default \
        --finetuning_type lora \
        --lora_rank 8 \
        --lora_alpha 16 \
        --lora_target all \
        --output_dir saves/Qwen1.5-7B/lora/sql_dpo_v1 \
        --per_device_train_batch_size 1 \
        --gradient_accumulation_steps 4 \
        --lr_scheduler_type cosine \
        --logging_steps 5 \
        --save_steps 100 \
        --learning_rate 1e-5 \
        --num_train_epochs 1.0 \
        --plot_loss \
        --fp16 \
        --dpo_beta 0.1
    
  • 脚本核心参数解读 (重中之重):

    • --stage dpo: 切换到DPO模式。这是启动DPO训练的开关,Llama-factory会自动加载DPO Trainer和对应的损失函数。
    • --model_name_or_path Qwen/Qwen1.5-7B-Chat: 注意!这里依然是原始的基座模型
    • --adapter_name_or_path saves/Qwen1.5-7B/lora/sql_generator_v1: 这才是DPO的真正起点。Llama-factory会先将这个SFT阶段训练好的LoRA适配器加载到基座模型上,形成完整的SFT模型。然后,以此为基础,创建策略模型和参考模型。
    • --learning_rate 1e-5: DPO的学习率通常需要设置得比SFT更小。因为偏好对齐是一个更精细的微调过程。1e-55e-6 是一个很好的范围。
    • --dpo_beta 0.1: 设置核心超参数 beta 为推荐的默认值 0.1
    • --output_dir: DPO训练产物的输出目录,不要和SFT的目录混淆。
  • 监控训练过程:
    当你运行此脚本后,观察终端输出的日志。除了 loss 之外,你应该重点关注以下几个指标:

    • rewards/chosen: 策略模型赋予 chosen 回答的平均奖励分数。
    • rewards/rejected: 策略模型赋予 rejected 回答的平均奖励分数。
    • rewards/accuracy: 关键指标。表示在当前batch中,模型正确地给予 chosen 回答比 rejected 回答更高分数的样本比例。一个健康的DPO训练,这个值应该稳定在50%以上,并逐渐提升,通常能达到70%~90%。
    • rewards/margins: chosenrejected 奖励分数的平均差值。这个值应该是正数并逐渐增大,表示模型正在成功地拉开好坏回答的差距。

至此,你已经完整地掌握了从理论到实践的DPO全流程。通过先SFT再DPO的两步走策略,你可以将一个通用的语言模型,先塑造成一个特定领域的“专家”,再进一步教会它“品味”,使其回答不仅“正确”,而且“优秀”,更符合人类的期望和偏好。下一章,我们将讨论如何将我们辛辛苦苦训练出的模型(无论是SFT还是DPO的产物)真正地应用起来,包括模型合并、部署推理和评估。

Llama-factory 详细学习笔记 目录

以下是整个系列的8章目录,点击章节标题即可跳转阅读,可直接访问:

Logo

更多推荐