策略模型的文本生成
在强化学习循环中,策略模型的核心任务是根据输入提示生成相应的回复。这个策略模型本质上是一个经过微调的大型语言模型(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 生成流程图示
(状态 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 数据流程图示
流程说明: 使用奖励模型获取奖励分数的数据流程。奖励模型同时接收原始提示和策略生成的文本作为输入。
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编码器),但使用独立的输出层(通常是一个线性层)来预测标量价值。这种设计有两个主要优势:
- 参数共享:共享的底层表示可以提高样本效率,减少计算成本
- 一致性:确保价值估计与策略模型对状态的理解保持一致
(状态 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 训练流程图示
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)。
- 优化目标:它试图在以下两者之间寻找平衡:
- 最大化奖励:生成能从奖励模型获得高分的回复。
- 保持约束:避免与初始的 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 惩罚}} $$注:符号方向取决于具体实现是定义为“最大化目标”还是“最小化损失”。以上公式以最大化总收益为视角。
各组件详解:
$L_{\text{clip}}$ (策略目标): 这是 PPO 的核心剪切目标,用于提升策略生成的概率,使其更有可能生成高优势(Advantage)的动作,同时限制更新步幅,防止训练崩溃。
$L_{\text{VF}}$ (价值函数损失): 价值模型(Critic)的误差项。需要让 Critic 预测得越准越好,因此在总目标中通常表现为减去该误差(或在损失函数中加上该误差)。$c_{vf}$ 是权重系数。
$S[\pi_\theta]$ (熵正则项): 鼓励策略保留一定的随机性(探索能力),防止模型过早收敛到单一模式。$c_{entropy}$ 是权重系数。
$\beta D_{\text{KL}}[\pi_\theta || \pi_{\text{ref}}]$ (KL 散度惩罚): 这是参考模型发挥作用的地方。
- 含义:计算当前策略 $\pi_\theta$ 生成的概率分布与参考策略 $\pi_{\text{ref}}$ 之间的相对熵。
- 作用:这是一个惩罚项 (Penalty)。如果当前模型生成的文本分布与 SFT 模型差异过大,该项值会变大,从而拉低总奖励。
- $\beta$ 系数:控制惩罚的力度。$\beta$ 越大,模型越保守(贴近 SFT);$\beta$ 越小,模型越自由(可能出现 Reward Hacking)。
模型交互总结图
项目目录结构
一个典型的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 # 项目文档
配置文件"] 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
- SFT 模型:一个经过指令微调的模型(本例中使用
gpt2作为演示替身)。 - 奖励模型 (RM):一个能对文本进行打分的模型(本例中将演示加载或使用伪造奖励)。
- 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循环已完成。")
数据流可视化
下图清晰地展示了上述代码在单次迭代中的数据流向:
观察与分析
响应生成 (Generation Quality):
- 在最初的几个 Step 中,模型的输出将非常接近原始 SFT 模型(或基础模型)。
- 随着训练进行,如果奖励信号有效,你会发现生成的文本风格开始向高分方向偏移。
奖励趋势 (Mean Reward):
- 这是最直观的指标。如果训练正常,
mean_reward应该呈现上升趋势。 - 如果使用本例中的“长度奖励”,你会发现模型开始倾向于生成更长的废话。
- 这是最直观的指标。如果训练正常,
KL 散度 (KL Divergence):
ppo/kl监控策略模型与参考模型的距离。- 如果 KL 激增:说明模型正在“崩坏”,生成的文本可能已经乱码或利用了奖励模型的漏洞(Goodhart’s Law)。
- 如果 KL 为零:说明模型没有学到任何新东西。
- 通常希望 KL 保持在一个小的正值范围内(如 0.05 - 0.1)。
损失函数 (Losses):
ppo/loss/policy和ppo/loss/value会随着优化波动。不像监督学习那样 Loss 一定要下降,在 RL 中,Loss 的震荡通常是正常的,关键要看 Reward 是否上升。