策略模型的文本生成

在强化学习循环中,策略模型的核心任务是根据输入提示生成相应的回复。这个策略模型本质上是一个经过微调的大型语言模型(LLM)。在标准强化学习框架下,这一生成过程对应"动作"阶段——策略 $\pi_\theta$ 基于当前状态(输入提示)执行动作(生成文本)。

1.1 文本序列的生成机制

生成过程从一批提示开始,这些提示通常来源于奖励模型训练时使用的数据集,或者是专门设计的提示集合,旨在激发模型产生多样化的行为。对于批次中的每个提示 $x$,当前策略模型 $\pi_\theta$ 会生成对应的文本序列 $y$。

文本生成通常采用大型语言模型标准的自回归解码方法。但与普通推理不同的是,RLHF场景下有一个关键要求:探索性(Exploration)。不只需要概率最高(贪婪)的单一回复,而是要尝试多种可能的回复,从中找出能够从奖励模型获得更高评分的选项。

因此,生成过程主要采用采样技术,而非纯粹的贪婪解码。

1.1.1 常用采样策略

温度缩放 (Temperature Scaling)

在模型最后一层的logits(softmax前的分数)上应用温度参数 $T > 0$:

  • $T > 1$: 使概率分布更平滑,增加采样低概率词元的机会,提高多样性
  • $T < 1$: 使分布更尖锐,生成结果更接近贪婪解码
  • 典型值: $T \in [0.7, 1.0]$

概率计算公式:

$$ P(\text{token}_i | \text{context}) = \frac{\exp(\text{logit}_i / T)}{\sum_j \exp(\text{logit}_j / T)} $$

Top-k 采样

在每个生成步骤中,将词汇表限制为概率最高的 $k$ 个词元,然后仅从这个缩小的集合中采样。这样可以避免采样到极低概率的词元,同时保持一定的多样性。

  • 典型值: $k \in [20, 100]$

Top-p (核采样, Nucleus Sampling)

不固定选择前 $k$ 个词元,而是选择累积概率超过阈值 $p$ 的最小词元集合,然后从这个动态大小的集合中采样。这种方法能根据模型在每步的置信度自适应调整候选词元数量。

  • 典型值: $p \approx 0.9$ 或 $0.95$

这些采样方法常常组合使用(例如先进行温度缩放,再应用top-k或top-p)。此外,控制生成长度的参数(max_new_tokens)以及重复惩罚(repetition_penalty)等参数对于生成连贯且有用的回复也很重要。

1.2 生成流程图示

graph LR Input["输入提示
(状态 s)"] --> Policy["活跃策略模型
(π_θ)"] subgraph "生成配置" Policy --> Sampling["采样策略
(温度, Top-k/Top-p)"] end Sampling --> Output["生成回复
(动作 a)"] style Input fill:#b3e5fc,stroke:#03a9f4 style Policy fill:#e1bee7,stroke:#9c27b0 style Output fill:#c8e6c9,stroke:#4caf50

此阶段的输入是提示,输出是由当前策略模型(即通过PPO算法不断更新的模型)生成的回复。


奖励模型的评分机制

对策略模型生成的回复进行评分,是强化学习循环中的关键步骤。这一评估过程根据学习到的人类偏好,判断回复的质量。奖励模型(Reward Model, RM)负责执行这项评估,其输出为PPO算法更新提供必要的奖励信号。

2.1 奖励模型的工作原理

奖励模型是在回复对数据上训练得到的,它学会为给定提示下人类偏好的回复赋予更高的分数。在强化学习阶段利用这个已训练好的函数。

对于策略模型生成的每个提示-回复对,奖励模型接收提示和完整回复作为输入,输出一个标量分数:

$$ \text{分数} = R_\phi(\text{提示}, \text{回复}) $$

其中:

  • $R_\phi$ 表示参数为 $\phi$ 的奖励模型
  • 分数衡量了在给定提示下,回复符合人类偏好的程度(基于奖励模型训练时捕获的偏好)
  • 分数越高,表明回复越符合人类期望

2.1.1 数据流程图示

graph LR Prompt["提示(输入)"] --> Policy["策略模型(π_θ)"] Policy --> Response["生成的回复"] Prompt & Response --> RM["奖励模型(R_φ)"] RM --> Score["标量分数(奖励)"] style Prompt fill:#b3e5fc,stroke:#03a9f4 style RM fill:#e1bee7,stroke:#9c27b0 style Score fill:#c8e6c9,stroke:#4caf50

流程说明: 使用奖励模型获取奖励分数的数据流程。奖励模型同时接收原始提示和策略生成的文本作为输入。

2.2 从分数到PPO奖励

奖励模型生成的标量分数直接作为PPO算法的奖励信号。在RLHF的标准PPO配置中,优化目标是最大化该奖励,同时通过KL散度惩罚项约束模型不要过度偏离初始的监督微调(SFT)策略。

PPO更新中使用的每词元奖励通常包含两部分:奖励模型分数(应用于整个序列)和当前策略与参考SFT策略之间的KL惩罚。

核心思想为:

$$ \text{总奖励} = \text{奖励模型分数} - \beta \times \text{KL散度惩罚} $$

其中 $\beta$ 是控制KL惩罚强度的系数。

PPO算法利用这个总奖励(结合价值函数估计)计算优势函数,进而更新策略模型参数 $\theta$,使模型在未来生成能从奖励模型获得更高分数的回复,同时不会过度偏离SFT模型的行为。

2.3 实践中的考量:归一化与缩放

奖励模型的原始分数在不同训练阶段可能具有不同的范围或分布。直接将原始分数输入PPO可能导致训练不稳定或收敛缓慢。因此,在将奖励分数用于PPO更新之前,通常会在每个批次内对分数进行归一化或标准化

一种常用技术是奖励白化(Reward Whitening):减去批次均值,除以批次标准差。

$$ \text{归一化分数} = \frac{\text{分数} - \text{均值}(\text{批次分数})}{\text{标准差}(\text{批次分数}) + \epsilon} $$

其中 $\epsilon$ 是用于数值稳定性的小常数。

这个过程将奖励集中在零附近,并缩放至批次内具有单位方差,使PPO对奖励模型分数的绝对大小不那么敏感,从而提升训练稳定性。根据具体实现和观察到的训练行为,还可以应用其他缩放函数或截断策略。

价值模型的估计机制

在PPO算法中,价值模型(Value Model)是一个关键组件,用于估计给定状态的期望回报。价值模型与策略模型协同工作,通过估计状态价值来计算优势函数,从而指导策略的优化方向。在RLHF场景中,价值模型帮助策略模型更高效地学习如何生成高质量的回复。

3.1 价值模型的工作原理

价值模型的核心任务是预测从当前状态开始,遵循当前策略能够获得的期望累积奖励。在文本生成任务中,“状态"由提示和已生成的部分文本序列组成。

对于给定的提示-文本对,价值模型输出一个标量值,表示该状态的价值估计:

$$ V_\psi(\text{提示}, \text{部分回复}) = \mathbb{E}[\text{未来累积奖励}] $$

其中:

  • $V_\psi$ 表示参数为 $\psi$ 的价值模型
  • 输出的价值是从当前状态开始,按照当前策略继续生成,预期能获得的总奖励
  • 这个估计包括当前步骤的即时奖励以及未来所有步骤的预期奖励

3.1.1 价值模型的架构

在实践中,价值模型通常与策略模型共享相同的基础架构(Transformer编码器),但使用独立的输出层(通常是一个线性层)来预测标量价值。这种设计有两个主要优势:

  1. 参数共享:共享的底层表示可以提高样本效率,减少计算成本
  2. 一致性:确保价值估计与策略模型对状态的理解保持一致
graph TB Input["提示 + 部分回复
(状态 s)"] --> Shared["共享编码器层
(Transformer)"] Shared --> PolicyHead["策略输出头
(词汇表概率分布)"] Shared --> ValueHead["价值输出头
(线性层)"] PolicyHead --> Action["下一个词元
(动作 a)"] ValueHead --> Value["状态价值
V(s)"] style Input fill:#b3e5fc,stroke:#03a9f4 style Shared fill:#fff9c4,stroke:#fbc02d style PolicyHead fill:#e1bee7,stroke:#9c27b0 style ValueHead fill:#c5e1a5,stroke:#7cb342 style Value fill:#c8e6c9,stroke:#4caf50

架构说明: 策略模型和价值模型通常共享编码器层,但使用不同的输出头。这种设计既提高了效率,又保持了一致性。

3.2 价值模型在PPO中的作用

价值模型在PPO算法中扮演着核心角色,主要体现在以下几个方面:

3.2.1 优势函数计算

PPO的核心是优化基于优势函数的目标。优势函数 $A(s, a)$ 衡量在状态 $s$ 下采取动作 $a$ 相比平均水平的好坏程度:

$$ A(s_t, a_t) = Q(s_t, a_t) - V(s_t) $$

其中 $Q(s_t, a_t)$ 是状态-动作价值函数。在实践中,使用**广义优势估计(Generalized Advantage Estimation, GAE)**来计算优势:

$$ A_t = \sum_{l=0}^{\infty} (\gamma \lambda)^l \delta_{t+l} $$

其中 $\delta_t$ 是TD误差:

$$ \delta_t = r_t + \gamma V_\psi(s_{t+1}) - V_\psi(s_t) $$

参数说明:

  • $\gamma \in [0, 1]$ 是折扣因子,控制未来奖励的权重
  • $\lambda \in [0, 1]$ 是GAE参数,权衡偏差和方差
  • 典型值:$\gamma = 0.99$,$\lambda = 0.95$

3.2.2 方差减少

价值函数的引入显著减少了策略梯度估计的方差。通过从回报中减去基准值(baseline),优势函数能够更清晰地区分好动作和坏动作,使训练更加稳定高效。

无价值基准的情况

  • 所有获得正奖励的动作都会被强化,即使它们只是略好于平均水平
  • 高方差导致训练不稳定

有价值基准的情况

  • 只有显著优于预期的动作才会被强化
  • 低方差使训练更稳定,收敛更快

3.3 价值模型的训练

价值模型通过最小化预测价值与实际回报之间的差异进行训练。在每个PPO更新步骤中,价值模型与策略模型同时更新。

3.3.1 价值损失函数

价值模型的损失函数通常采用均方误差(MSE),衡量预测价值与目标回报的差距:

$$ L_{\text{value}} = \frac{1}{N} \sum_{i=1}^{N} (V_\psi(s_i) - R_i)^2 $$

其中:

  • $V_\psi(s_i)$ 是价值模型对状态 $s_i$ 的预测
  • $R_i$ 是实际观察到的回报(可以是蒙特卡洛回报或TD目标)
  • $N$ 是批次大小

为了防止价值函数变化过大,PPO通常使用裁剪价值损失

$$ L_{\text{value}}^{\text{clip}} = \frac{1}{N} \sum_{i=1}^{N} \max\left[(V_\psi(s_i) - R_i)^2, (\text{clip}(V_\psi(s_i), V_{\text{old}}(s_i) - \epsilon, V_{\text{old}}(s_i) + \epsilon) - R_i)^2\right] $$

其中 $\epsilon$ 是裁剪阈值,$V_{\text{old}}$ 是更新前的价值估计。

3.3.2 目标回报的计算

目标回报 $R_i$ 有多种计算方式:

蒙特卡洛回报: 使用完整轨迹的实际累积奖励:

$$ R_t = \sum_{k=0}^{T-t} \gamma^k r_{t+k} $$

TD目标: 结合即时奖励和下一状态的价值估计:

$$ R_t = r_t + \gamma V_\psi(s_{t+1}) $$

GAE目标: 结合优势估计和当前价值:

$$ R_t = A_t + V_\psi(s_t) $$

在RLHF实践中,通常使用GAE目标,因为它在偏差和方差之间取得了良好的平衡。

3.4 实践中的考量

3.4.1 价值归一化

与奖励归一化类似,价值目标也常常需要归一化以提高训练稳定性:

$$ \text{归一化目标} = \frac{R_t - \text{均值}({R_i})}{\text{标准差}({R_i}) + \epsilon} $$

这确保价值损失的尺度在训练过程中保持一致。

3.4.2 价值损失系数

在PPO的总损失函数中,价值损失通常会乘以一个系数 $c_1$:

$$ L_{\text{total}} = L_{\text{policy}} + c_1 L_{\text{value}} - c_2 H(\pi_\theta) $$

其中:

  • $L_{\text{policy}}$ 是PPO的策略损失(裁剪目标)
  • $L_{\text{value}}$ 是价值损失
  • $H(\pi_\theta)$ 是熵正则项,鼓励探索
  • 典型值:$c_1 = 0.5$,$c_2 = 0.01$

3.4.3 训练流程图示

graph TB Start["收集轨迹数据"] --> Compute["计算实际回报 R"] Compute --> Predict["价值模型预测 V(s)"] Predict --> CalcAdv["计算优势函数
A = R - V(s)"] Predict --> CalcLoss["计算价值损失
L_value = (V(s) - R)²"] CalcAdv --> UpdatePolicy["更新策略模型
(使用优势)"] CalcLoss --> UpdateValue["更新价值模型
(最小化损失)"] UpdatePolicy & UpdateValue --> NextIter["下一轮迭代"] style Start fill:#b3e5fc,stroke:#03a9f4 style CalcAdv fill:#ffe0b2,stroke:#ff9800 style CalcLoss fill:#ffe0b2,stroke:#ff9800 style UpdatePolicy fill:#e1bee7,stroke:#9c27b0 style UpdateValue fill:#c5e1a5,stroke:#7cb342 style NextIter fill:#c8e6c9,stroke:#4caf50

训练流程: 价值模型与策略模型在PPO迭代中同步更新,价值模型的预测用于计算优势函数,而优势函数又指导策略更新。

3.5 价值模型的初始化

在RLHF流程中,价值模型的初始化策略对训练效率有重要影响:

从头训练

  • 价值模型从随机初始化开始
  • 优点:完全独立,不受其他模型影响
  • 缺点:需要更长时间才能产生准确估计

从策略模型初始化

  • 复制SFT模型的权重作为价值模型的起点
  • 优点:快速获得合理的状态表示
  • 缺点:可能继承策略模型的偏差

从奖励模型初始化

  • 使用奖励模型的权重初始化价值模型
  • 优点:价值模型与奖励信号天然对齐
  • 缺点:奖励模型和价值模型的目标并不完全相同

在实践中,从策略模型初始化是最常用的方法,因为它能快速获得有用的状态表示,同时保持足够的灵活性来学习价值函数特有的特征。


三个模型的协同工作

在RLHF的强化学习阶段,策略模型、奖励模型和价值模型形成了一个完整的学习系统:

1. 动态学习者 (The Learners)

这一组模型的参数 ($\theta, \psi$) 会在 PPO 过程中通过梯度下降积极更新。

策略模型 (Actor / Policy Model, $\pi_\theta$)

  • 角色定义:最终想要得到的 LLM,也是 PPO 算法优化的核心对象。
  • 功能:接收提示词(Prompt),生成相应的文本回复(Action)。
  • 优化目标:它试图在以下两者之间寻找平衡:
    1. 最大化奖励:生成能从奖励模型获得高分的回复。
    2. 保持约束:避免与初始的 SFT 模型分布差异过大(由 KL 散度约束)。
  • 状态活跃 (Active),参数不断更新。

价值模型 (Critic / Value Model, $V_\psi$)

  • 角色定义:策略模型的辅助者,通常初始化自策略模型或奖励模型的架构,但在输出层是一个标量预测头。
  • 功能:估算给定状态(提示词 + 部分生成序列)的预期回报(Expected Return)。它并不生成文本,而是预测“这一步走得怎么样”。
  • 核心作用:用于计算优势函数 (Advantage Estimation)。通过将实际奖励与 Critic 的预测值进行对比,得出动作的相对优势,从而显著降低策略梯度估计的方差,稳定训练过程。
  • 优化目标:最小化预测值与真实回报之间的均方误差(MSE)。
  • 状态活跃 (Active),参数随策略模型一同更新。

2. 静态参考者 (The Judges)

这一组模型的参数在整个 PPO 阶段保持冻结 (Frozen),仅用于提供信号和参考基准,不参与梯度更新。

奖励模型 (Reward Model, RM)

  • 角色定义:人类偏好的代理人,RLHF 系统中的“裁判”。
  • 功能:在上一阶段(Reward Modeling)训练完毕。它接收“提示词 + 回复”作为输入,输出一个标量奖励信号。
  • 重要性:它是 PPO 优化的指挥棒,决定了策略模型生成的方向。
  • 状态冻结 (Frozen),作为一个固定的评估标准,防止评价标准随训练发生漂移。

参考策略模型 (Reference Model, $\pi_{\text{ref}}$)

  • 角色定义:SFT 模型的原始副本。
  • 功能:它作为“锚点”,用于计算 KL 散度。
  • 核心作用:防止策略模型为了通过欺骗奖励模型(Reward Hacking)获取高分而输出乱码或极端的文本。它强制新的策略 $\pi_\theta$ 不能偏离原始的语言能力太远。
  • 状态冻结 (Frozen),提供稳定的目标分布。

3. PPO 联合目标函数解析

这四个模型通过 PPO 的损失函数(目标函数)紧密结合在一起。一个典型的 PPO 混合目标函数可以表示为:

$$ L_{\text{PPO}} = \underbrace{L_{\text{clip}}(\pi_\theta)}_{\text{策略增益}} - c_{vf} \underbrace{L_{\text{VF}}(V_\psi)}_{\text{价值误差}} + c_{\text{entropy}} \underbrace{S[\pi_\theta]}_{\text{熵奖励}} - \beta \underbrace{D_{\text{KL}}(\pi_\theta || \pi_{\text{ref}})}_{\text{KL 惩罚}} $$

注:符号方向取决于具体实现是定义为“最大化目标”还是“最小化损失”。以上公式以最大化总收益为视角。

各组件详解:

  1. $L_{\text{clip}}$ (策略目标): 这是 PPO 的核心剪切目标,用于提升策略生成的概率,使其更有可能生成高优势(Advantage)的动作,同时限制更新步幅,防止训练崩溃。

  2. $L_{\text{VF}}$ (价值函数损失): 价值模型(Critic)的误差项。需要让 Critic 预测得越准越好,因此在总目标中通常表现为减去该误差(或在损失函数中加上该误差)。$c_{vf}$ 是权重系数。

  3. $S[\pi_\theta]$ (熵正则项): 鼓励策略保留一定的随机性(探索能力),防止模型过早收敛到单一模式。$c_{entropy}$ 是权重系数。

  4. $\beta D_{\text{KL}}[\pi_\theta || \pi_{\text{ref}}]$ (KL 散度惩罚): 这是参考模型发挥作用的地方。

    • 含义:计算当前策略 $\pi_\theta$ 生成的概率分布与参考策略 $\pi_{\text{ref}}$ 之间的相对熵。
    • 作用:这是一个惩罚项 (Penalty)。如果当前模型生成的文本分布与 SFT 模型差异过大,该项值会变大,从而拉低总奖励。
    • $\beta$ 系数:控制惩罚的力度。$\beta$ 越大,模型越保守(贴近 SFT);$\beta$ 越小,模型越自由(可能出现 Reward Hacking)。

模型交互总结图

graph TD subgraph "参数冻结 (Inference Only)" Ref[参考模型 π_ref] RM[奖励模型 RM] end subgraph "参数更新 (Training)" Actor[策略模型 π_θ] Critic[价值模型 V_ψ] end Actor --生成文本--> RM Actor --生成概率--> Ref Actor --状态--> Critic RM --奖励信号--> Loss[PPO Loss 计算] Ref --KL散度--> Loss Critic --优势估算--> Loss Loss --梯度更新--> Actor Loss --梯度更新--> Critic style Ref fill:#e0e0e0,stroke:#333 style RM fill:#e0e0e0,stroke:#333 style Actor fill:#bbdefb,stroke:#1976d2 style Critic fill:#ffccbc,stroke:#d84315

项目目录结构

一个典型的RLHF项目应包含以下主要目录和文件:

rlhf_project/
├── configs/                 # 配置文件(YAML/JSON格式)
│   ├── sft_config.yaml     # 监督微调配置
│   ├── rm_config.yaml      # 奖励模型训练配置
│   └── ppo_config.yaml     # PPO强化学习配置
│
├── data/                    # 数据集存储
│   ├── sft/                # SFT训练数据
│   ├── preferences/        # 偏好对数据
│   └── prompts/            # 强化学习用提示词
│
├── models/                  # 模型权重与检查点
│   ├── base_llm/           # 基础预训练模型(可选本地副本)
│   ├── sft_model/          # SFT模型检查点
│   ├── reward_model/       # 奖励模型检查点
│   ├── ppo_policy_final/   # 最终PPO策略模型
│   └── ppo_checkpoints/    # PPO训练中间检查点
│
├── src/                     # 核心源代码
│   ├── data_processing/     # 数据加载与预处理
│   │   ├── __init__.py
│   │   └── preference_dataset.py
│   │
│   ├── models/              # 自定义模型定义
│   │   ├── __init__.py
│   │   └── reward_model.py
│   │
│   ├── training/            # 训练逻辑实现
│   │   ├── __init__.py
│   │   ├── sft_trainer.py   # SFT训练器
│   │   ├── rm_trainer.py    # 奖励模型训练器
│   │   └── ppo_trainer.py   # PPO训练器
│   │
│   ├── evaluation/          # 评估脚本与指标
│   │   ├── __init__.py
│   │   └── evaluate_alignment.py
│   │
│   └── utils/               # 通用工具函数
│       ├── __init__.py
│       └── helpers.py
│
├── scripts/                 # 可执行脚本
│   ├── run_sft.py          # 运行SFT训练
│   ├── run_rm.py           # 运行奖励模型训练
│   ├── run_ppo.py          # 运行PPO训练
│   └── run_evaluation.py   # 运行评估
│
├── requirements.txt         # Python依赖包列表
└── README.md               # 项目文档
graph TB subgraph "配置层" Config["configs/
配置文件"] end subgraph "数据层" SFTData["data/sft/
SFT数据"] PrefData["data/preferences/
偏好数据"] Prompts["data/prompts/
提示词"] end subgraph "执行层" RunSFT["scripts/run_sft.py"] RunRM["scripts/run_rm.py"] RunPPO["scripts/run_ppo.py"] RunEval["scripts/run_evaluation.py"] end subgraph "核心逻辑层" DataProc["data_processing/
数据处理"] SFTTrain["sft_trainer.py"] RMTrain["rm_trainer.py"] PPOTrain["ppo_trainer.py"] Eval["evaluate_alignment.py"] Utils["utils/
工具函数"] end subgraph "模型层" BaseLLM["基础LLM"] SFTModel["SFT模型"] RMModel["奖励模型"] PPOModel["PPO策略"] end Config --> RunSFT Config --> RunRM Config --> RunPPO Config --> RunEval SFTData --> DataProc PrefData --> DataProc Prompts --> DataProc RunSFT --> SFTTrain RunRM --> RMTrain RunPPO --> PPOTrain RunEval --> Eval DataProc --> SFTTrain DataProc --> RMTrain DataProc --> PPOTrain SFTTrain --> Utils RMTrain --> Utils PPOTrain --> Utils Eval --> Utils BaseLLM --> SFTTrain SFTTrain --> SFTModel SFTModel --> RMTrain RMTrain --> RMModel SFTModel --> PPOTrain RMModel --> PPOTrain PPOTrain --> PPOModel PPOModel --> Eval style Config fill:#b3e5fc,stroke:#03a9f4 style SFTData fill:#fff9c4,stroke:#fbc02d style PrefData fill:#fff9c4,stroke:#fbc02d style Prompts fill:#fff9c4,stroke:#fbc02d style RunSFT fill:#e1bee7,stroke:#9c27b0 style RunRM fill:#e1bee7,stroke:#9c27b0 style RunPPO fill:#e1bee7,stroke:#9c27b0 style SFTTrain fill:#c5e1a5,stroke:#7cb342 style RMTrain fill:#c5e1a5,stroke:#7cb342 style PPOTrain fill:#c5e1a5,stroke:#7cb342 style PPOModel fill:#ffccbc,stroke:#ff5722

这是一个关于如何使用代码实现 RLHF 最小闭环的实战指南。为了使其更具可读性和操作性,我将内容重新组织为**“环境准备”“代码实现”“流程可视化”“结果分析”**四个部分,并优化了代码注释和说明文字。


运行简单的 RLHF

  1. SFT 模型:一个经过指令微调的模型(本例中使用 gpt2 作为演示替身)。
  2. 奖励模型 (RM):一个能对文本进行打分的模型(本例中将演示加载或使用伪造奖励)。
  3. Python 环境:已安装 transformers, torch, 和 trl 库。

环境配置与模型加载

首先,需要配置 PPO 的超参数,并加载策略模型(Actor)、参考模型(Ref)以及奖励模型。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLMWithValueHead, pipeline
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead

# 1. 配置 - 用于演示的最小PPO设置
ppo_config = PPOConfig(
    model_name="gpt2", # 或者您的特定SFT模型路径
    learning_rate=1.41e-5,
    batch_size=4,       # 用于演示的小批量大小
    mini_batch_size=2,
    gradient_accumulation_steps=1,
    log_with="tensorboard", # 可选:用于日志记录
    kl_penalty="kl",      # 使用KL惩罚
    target_kl=0.1,        # 目标KL散度
    init_kl_coef=0.2,     # 初始KL系数
    adap_kl_ctrl=True,    # 使用自适应KL控制
    ppo_epochs=4,         # 每批次的优化周期数
    seed=0,
)

# 2. 加载模型和分词器
# 策略模型(Actor/Critic):从SFT/基础模型初始化
# AutoModelForCausalLMWithValueHead 结合了LM头和价值头
policy_model = AutoModelForCausalLMWithValueHead.from_pretrained(ppo_config.model_name)
# 参考模型(用于KL散度):保留初始策略的副本
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained(ppo_config.model_name)
tokenizer = AutoTokenizer.from_pretrained(ppo_config.model_name)
# 确保为分词器设置了pad token
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

# 奖励模型(RM):单独加载,假设它是一个文本分类风格的pipeline
# 替换!
reward_model_name = "path/to/your/reward/model" # 替换!
# 注意:这可能需要自定义pipeline或直接加载模型,具体取决于您的RM
try:
    # 简化示例,假设是兼容的情感/奖励pipeline
    reward_pipe = pipeline("text-classification", model=reward_model_name, device=policy_model.device)
    print("Reward model loaded via pipeline.")
    # 定义一个函数来获取标量分数
    def get_reward_score(texts):
        # 处理文本,如果需要,可能将其格式化为 (query, response)
        # 这高度依赖于您的奖励模型的输入格式
        # 假设RM输出一个字典列表,如 [{'label': 'POSITIVE', 'score': 0.9}]
        results = reward_pipe(texts, return_all_scores=True) # 根据您的pipeline进行调整
        # 提取所需的分数(例如,“POSITIVE”的分数或特定的分数索引)
        # 此提取逻辑高度依赖于您的RM的输出
        scores = []
        for result in results:
            # 示例:查找特定标签的分数,或假设第一个分数是奖励
            # 根据您的奖励模型结构调整此逻辑
            score = 0.0 # 默认分数
            if isinstance(result, list): # 处理变化的pipeline输出
                 for label_score in result:
                     if label_score['label'] == 'POSITIVE': # 示例标签
                          score = label_score['score']
                          break
            elif isinstance(result, dict):
                 score = result.get('score', 0.0) # 简化回退
            scores.append(torch.tensor(score, device=policy_model.device))
        return scores

except Exception as e:
    print(f"Warning: Could not load reward model pipeline '{reward_model_name}'. Using dummy rewards. Error: {e}")
    # 如果RM加载失败,则回退到虚拟奖励函数
    def get_reward_score(texts):
        # 虚拟奖励:基于长度的分数(仅用于演示)
        return [torch.tensor(len(text) / 100.0, device=policy_model.device) for text in texts]

# 3. 初始化PPOTrainer
ppo_trainer = PPOTrainer(
    config=ppo_config,
    model=policy_model,
    ref_model=ref_model,
    tokenizer=tokenizer,
    # 在此手动循环示例中,可以省略数据集
    # 如果不使用 AutoModelForCausalLMWithValueHead,则value_model需要单独设置
)

print("设置完成。已准备好运行简化的RLHF循环。")

执行 RLHF 训练循环

接下来手动运行几个 PPO 步骤。这个循环包含了 RLHF 的三个核心动作:生成 (Generate) -> 评分 (Score) -> 更新 (Update)

# 定义一些示例查询
queries = [
    "Explain the concept of KL divergence in simple terms:",
    "Write a short poem about a robot learning:",
    "What are the main stages of RLHF?",
    "Suggest a name for a friendly AI assistant:",
]

# 对查询进行分词
query_tensors = [tokenizer.encode(q, return_tensors="pt").to(policy_model.device) for q in queries]

# 策略模型的生成设置
generation_kwargs = {
    "min_length": -1, # 允许提前停止
    "top_k": 0.0,
    "top_p": 1.0,
    "do_sample": True,
    "pad_token_id": tokenizer.pad_token_id,
    "max_new_tokens": 64, # 限制响应长度用于演示
}

# 运行几个PPO步骤(例如2步)
num_steps = 2
for step in range(num_steps):
    print(f"\n--- PPO Step {step + 1} ---")

    # 1. 策略执行:从策略模型生成响应
    response_tensors = []
    for query_tensor in query_tensors:
        # 生成响应;响应包含查询和生成的部分
        response = ppo_trainer.generate(query_tensor.squeeze(0), **generation_kwargs)
        response_tensors.append(response.squeeze())

    # 解码响应以进行奖励计算和日志记录
    decoded_responses = [tokenizer.decode(r.squeeze(), skip_special_tokens=True) for r in response_tensors]

    # 2. 奖励计算:使用RM对生成的响应进行评分
    # 如果需要,为奖励模型格式化文本(例如,组合查询+响应)
    # 此示例假设RM对包括提示在内的完整生成文本进行评分
    reward_texts = decoded_responses
    rewards = get_reward_score(reward_texts) # 张量标量列表

    # 3. PPO优化步骤
    # 为 ppo_trainer.step 准备输入
    # query_tensors 需要是 List[torch.Tensor]
    # response_tensors 需要是 List[torch.Tensor]
    # rewards 需要是 List[torch.Tensor](每个样本的标量奖励)
    stats = ppo_trainer.step(query_tensors, response_tensors, rewards)

    # 4. 日志记录
    print(f"Query examples: {[q[:50] + '...' for q in queries]}")
    print(f"Response examples: {[r[len(q):][:80] + '...' for q, r in zip(queries, decoded_responses)]}")
    print(f"Mean reward: {torch.mean(torch.stack(rewards)).item():.4f}")
    if 'ppo/kl' in stats:
      print(f"KL Divergence: {stats['ppo/kl']:.4f}")
    if 'ppo/loss/policy' in stats:
      print(f"Policy Loss: {stats['ppo/loss/policy']:.4f}")
    if 'ppo/loss/value' in stats:
      print(f"Value Loss: {stats['ppo/loss/value']:.4f}")

    # 可选:如果使用TensorBoard等日志记录器,则记录详细统计信息
    # ppo_trainer.log_stats(stats, queries, response_tensors, rewards)

print("\n简化的RLHF循环已完成。")

数据流可视化

下图清晰地展示了上述代码在单次迭代中的数据流向:

graph TD subgraph "输入阶段" Queries["输入查询 Batch"] end subgraph "策略交互 (Rollout)" Actor["策略模型 (Actor)"] Queries --> Actor Actor -->|生成| Responses["生成文本"] end subgraph "评估阶段" RM["奖励模型 (RM)"] Responses --> RM RM -->|打分| Rewards["标量奖励"] end subgraph "PPO 优化核心" Trainer["PPO Trainer"] Queries & Responses & Rewards --> Trainer Trainer -->|计算优势 & Loss| Optimization["梯度下降"] Optimization -->|更新权重| Actor Ref["参考模型"] -.->|计算 KL| Trainer end style Actor fill:#bbdefb,stroke:#1976d2 style RM fill:#e0e0e0,stroke:#616161 style Trainer fill:#ffccbc,stroke:#d84315

观察与分析

  1. 响应生成 (Generation Quality)

    • 在最初的几个 Step 中,模型的输出将非常接近原始 SFT 模型(或基础模型)。
    • 随着训练进行,如果奖励信号有效,你会发现生成的文本风格开始向高分方向偏移。
  2. 奖励趋势 (Mean Reward)

    • 这是最直观的指标。如果训练正常,mean_reward 应该呈现上升趋势。
    • 如果使用本例中的“长度奖励”,你会发现模型开始倾向于生成更长的废话。
  3. KL 散度 (KL Divergence)

    • ppo/kl 监控策略模型与参考模型的距离。
    • 如果 KL 激增:说明模型正在“崩坏”,生成的文本可能已经乱码或利用了奖励模型的漏洞(Goodhart’s Law)。
    • 如果 KL 为零:说明模型没有学到任何新东西。
    • 通常希望 KL 保持在一个小的正值范围内(如 0.05 - 0.1)。
  4. 损失函数 (Losses)

    • ppo/loss/policyppo/loss/value 会随着优化波动。不像监督学习那样 Loss 一定要下降,在 RL 中,Loss 的震荡通常是正常的,关键要看 Reward 是否上升。