0%

大模型强化学习笔记

PPO

论文:Proximal Policy Optimization Algorithms

代码:https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py

背景知识

策略梯度方法

策略梯度方法直接对策略进行参数化,并通过最大化预期回报来优化策略参数。常用的策略梯度估计器为:

g^=E^t[θlogπθ(atst)A^t]{\hat{g}}={\hat{\mathbb{E}}}_{t} \Big[ \nabla_{\theta} \operatorname{l o g} \pi_{\theta} ( a_{t} \mid s_{t} ) {\hat{A}}_{t} \Big]

其中:

  • E^t[]\hat{\mathbb{E}}_t[⋅] 表示对采样数据的经验平均;
  • π表示随机策略;
  • A^t\hat{A}_t 是优势函数(Advantage Function)的估计,衡量动作ata_t相对于基准策略在状态sts_t 下的好坏。

信赖域方法(TRPO)

为了解决策略更新不稳定的问题,信赖域策略优化(Trust Region Policy Optimization,TRPO) 被提出,核心思想是在策略更新时加入约束,限制新旧策略之间的差异。

TRPO解决以下优化问题:

maximizeθE^t[πθ(atst)πθold (atst)A^t] subject to E^t[KL[πθold (st),πθ(st)]]δ\begin{array}{cl} \underset{\theta}{\operatorname{maximize}} & \hat{\mathbb{E}}_t\left[\frac{\pi_\theta\left(a_t \mid s_t\right)}{\pi_{\theta_{\text {old }}}\left(a_t \mid s_t\right)} \hat{A}_t\right] \\ \text { subject to } & \hat{\mathbb{E}}_t\left[\operatorname{KL}\left[\pi_{\theta_{\text {old }}}\left(\cdot \mid s_t\right), \pi_\theta\left(\cdot \mid s_t\right)\right]\right] \leq \delta \end{array}

其中:

  • πθoldπ_{θ_{old}}是旧的策略;
  • KL[⋅|⋅] 表示两个策略在状态st 下的KL散度(Kullback-Leibler Divergence);
  • δ 是预设的阈值,限制策略更新的幅度。

TRPO通过复杂的二阶优化方法(如共轭梯度算法)求解上述约束优化问题。虽然TRPO在实践中表现稳定,但其实现较为复杂,且不易与一些神经网络结构(如共享参数的网络、包含随机噪声的网络)结合。

重要性采样是一种数据利用技巧。首先用策略模型πθ去和环境交互,产生一系列轨迹,然后用新的相同策略模型πθ′和环境交互,产生一系列轨迹。然后用这些轨迹去多次更新之前的策略模型πθ。但是我们并不是直接用这些轨迹对我们需要求取的策略模型进行迭代,而是需要对采样动作的概率进行比例放缩。重要性采样的优势在于隔离采样的轨迹和目标策略模型的关系。

参考文章:https://zhuanlan.zhihu.com/p/1895169683969274027

近端策略优化(PPO)

PPO旨在保留TRPO性能稳定的优点,同时简化实现,并提高数据利用率。

截断的代理目标(Clipped Surrogate Objective)

PPO引入了截断的代理目标函数,限制策略更新的幅度,避免过大的策略变化。

首先,定义概率比率(Probability Ratio):

rt(θ)=πθ(atst)πθold(atst)r_t(\theta)=\frac{\pi_\theta\left(a_t \mid s_t\right)}{\pi_{\theta_{\mathrm{old}}}\left(a_t \mid s_t\right)}

在TRPO中,优化的代理目标函数为:

LCPI(θ)=E^t[πθ(atst)πθold (atst)A^t]=E^t[rt(θ)A^t].L^{C P I}(\theta)=\hat{\mathbb{E}}_t\left[\frac{\pi_\theta\left(a_t \mid s_t\right)}{\pi_{\theta_{\text {old }}}\left(a_t \mid s_t\right)} \hat{A}_t\right]=\hat{\mathbb{E}}_t\left[r_t(\theta) \hat{A}_t\right] .

上标 CPI (conservative policy iteration)指的是保守策略迭代。

为了避免策略的过度更新,PPO引入了截断函数,定义新的目标函数:

LCLIP(θ)=E^t[min(rt(θ)A^t,clip(rt(θ),1ϵ,1+ϵ)A^t)]L^{C L I P}(\theta)=\hat{\mathbb{E}}_t\left[\min \left(r_t(\theta) \hat{A}_t, \operatorname{clip}\left(r_t(\theta), 1-\epsilon, 1+\epsilon\right) \hat{A}_t\right)\right]

其中:

  • clip(rt(θ),1ϵ,1+ϵ)clip(r_t(θ), 1−ϵ, 1+ϵ) 表示将rt(θ)r_t(θ) 限制在[1−ϵ, 1+ϵ] 区间内;
  • ϵ 是超参数,控制截断的幅度,通常取值为0.1或0.2。

截断目标函数的作用:

  • 限制策略更新幅度:通过截断rt(θ)r_t(θ) ,防止策略在一次更新中发生过大的变化。
  • 保留优化灵活性:当优势AtA^t 为正时,允许rt(θ)r_t(θ) 增加,但不超过 1+ϵ ;当AtA^t 为负时,允许rt(θ)r_t(θ) 减少,但不低于 1−ϵ 。
  • 提高数据利用率:由于有了稳定的目标函数,可以对同一批数据进行多次更新,而不会导致策略崩溃。

自适应的KL散度惩罚(Adaptive KL Penalty Coefficient)

另一种控制策略更新的方法是加入KL散度惩罚项,并自适应调整惩罚系数。

定义目标函数:

LKLPEN(θ)=E^t[πθ(atst)πθold (atst)A^tβKL[πθold (st),πθ(st)]]L^{K L P E N}(\theta)=\hat{\mathbb{E}}_t\left[\frac{\pi_\theta\left(a_t \mid s_t\right)}{\pi_{\theta_{\text {old }}}\left(a_t \mid s_t\right)} \hat{A}_t-\beta \operatorname{KL}\left[\pi_{\theta_{\text {old }}}\left(\cdot \mid s_t\right), \pi_\theta\left(\cdot \mid s_t\right)\right]\right]

其中:

  • β 是KL散度的惩罚系数;
  • 通过调整β ,控制策略更新的幅度。

自适应调整β:

  • 当实际KL散度d 小于目标值d_targ 的1.5倍时,减小β ,允许更大的策略更新;
  • 当d 大于d_targ 的1.5倍时,增大β ,限制策略更新的幅度。

这样,β 会根据策略更新的情况,自适应地调整,维持策略变化在合适的范围内。

算法(Algorithm)

PPO算法的主要步骤如下:

  • 采样数据:使用当前策略πθoldπ_{θ_{old}} 与环境交互,收集数据(状态、动作、奖励等)。
  • 计算优势函数:使用 广义优势估计(Generalized Advantage Estimation,GAE) 等方法,计算优势函数A^t\hat{A}_t
  • 构建目标函数:根据选择,构建LCLIP(θ) 或LKLPEN(θ) 。
  • 多轮优化:使用随机梯度上升法(如Adam),对目标函数进行K 轮优化。
  • 更新策略:将θoldθ_{old} 更新为优化后的θ 。
  • 重复上述步骤:迭代进行,直到达到训练目标。

优势估计量是**广义优势估计(Generalized Advantage Estimation,GAE)**的截断版本,即

A^t=δt+(γλ)δt+1+++(γλ)Tt+1δT1, where δt=rt+γV(st+1)V(st)\begin{aligned} & \hat{A}_t=\delta_t+(\gamma \lambda) \delta_{t+1}+\cdots+\cdots+(\gamma \lambda)^{T-t+1} \delta_{T-1}, \\ & \text { where } \quad \delta_t=r_t+\gamma V\left(s_{t+1}\right)-V\left(s_t\right) \end{aligned}

其中:

  • rtr_t:当前奖励。
  • γ:折扣因子。
  • V(st)V(s_t):当前状态价值估计。
  • V(st+1)V(s_t+1):下一状态价值估计。
  • δt=rt+γV(st+1)V(st)δ_t=r_t + γV(s_t+1) - V(s_t) 是TD-error(Temporal Difference error,时序差分误差)。

价值函数

t时刻状态s的总收益 = 身处状态s能带来的即时收益 + 从状态s出发后能带来的未来收益

Vt=Rt+γVt+1V_t=R_t+\gamma V_{t+1}

其中:

  • VtV_t : t 时刻的总收益,注意这个收益蕴涵了“即时”和“未来”的概念
  • RtR_t : t 时刻的即时收益
  • Vt+1V_{t+1} : t+1 时刻的总收益,注意这个收益蕴涵了“即时”和“未来”的概念。而 Vt+1 对 Vt 来说就是“未来”。
  • γ :折扣因子。它决定了我们在多大程度上考虑将“未来收益”纳入“当下收益”。

TD-error(Temporal Difference error,时序差分误差) 是强化学习中衡量当前价值估计与一步引导(bootstrap)目标之间偏差的核心量,用来指导参数更新。

TD误差有两种主要形式:

  • ‌1-step TD误差‌:只考虑一步的未来奖励和价值,如上述公式所示。
  • ‌n-step TD误差‌:考虑多步的未来奖励和价值,例如n-step TD(λ)算法,可以更平滑地估计长期价值。

广义优势估计

优势就是在当前状态下,这次选这个动作,是不是比平常更赚。

优势 = 实际表现 − 预测表现

rt+γV(st+1)r_t + γV(s_t+1)表示实际表现,它表示当前奖励 + 折扣后的未来价值,V(st)V(s_t) 表示预测表现

优势计算:

def get_advantages_and_returns(self, values, rewards, start):
"""
Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134

没有引入GAE前的t时刻的优势值:
detal_t = r_t + gamma * V_t+1 - V_t
其中:
- r_t表示t时刻的即时收益
- V_t+1表示未来时刻的预期收益
- r_t + gamma * V_t+1可理解成t时刻的实际预期收益
- V_t可理解成t时刻的预估预期收益(是模型,例如critic model自己估算出来的)

引入GAE后的t时刻的优势值:
A_t = delta_t + gamma * lambda * A_t+1
粗暴理解为在t时刻时,不仅考虑当下优势,还考虑了未来的优势
为了知道A_t, 我们得知道A_t+1,所以在本算法中采取了从后往前做动态规划求解的方法,也即:
假设T是最后一个时刻,则有A_T+1 = 0, 所以有: A_T = delta_T
知道了A_T, 就可以依次往前倒推,把A_t-1, A_t-2之类都算出来了

引入GAE后t时刻的实际预期收益
returns_t = A_t + V_t
= delta_t + gamma * lambda * A_t+1 + V_t
= r_t + gamma * V_t+1 - V_t + gamma * lambda * A_t+1 + V_t
= r_t + gamma * (V_t+1 + lambda * A_t+1)

注意,这里不管是advantages还是returns,都只算response的部分
"""

# Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
lastgaelam = 0
advantages_reversed = []
length = rewards.size()[-1]
# 注意这里用了reversed,是采取从后往前倒推计算的方式
for t in reversed(range(start, length)):
nextvalues = values[:, t + 1] if t < length - 1 else 0.0
delta = rewards[:, t] + self.gamma * nextvalues - values[:, t]
lastgaelam = delta + self.gamma * self.lam * lastgaelam
advantages_reversed.append(lastgaelam)
advantages = torch.stack(advantages_reversed[::-1], dim=1) # 优势
returns = advantages + values[:, start:] # 实际收益
# values: 预期收益
return advantages.detach(), returns

每次迭代,N个并行的参与者收集T个时间步长的数据。然后计算在这些数据上的损失,并使用minibatch SGD对K epoch进行优化。

Algorithm 1 PPO, Actor-Critic Style
for iteration=1, 2, . . . do
for actor=1, 2, . . . , N do
Run policy πθold in environment for T timesteps
Compute advantage estimates Aˆ1, ... , AˆT
end for
Optimize surrogate L wrt θ, with K epochs and minibatch size M ≤ NT
θold ← θ
end for

在强化学习里trajectory / episode / rollout 都是指“状态–动作–奖励序列

实验与结果

在连续控制任务中,作者比较了以下几种方法:

  • 无截断或惩罚:直接优化LCPI(θ)L^{CPI}(θ)
  • 截断的目标函数:使用不同ϵ 值的LCLIP(θ)L^{CLIP}(θ)
  • 固定的KL惩罚:使用固定β 的LKLPEN(θ)L^{KLPEN}(θ)
  • 自适应的KL惩罚:动态调整β 的LKLPEN(θ)L^{KLPEN}(θ)

实验结果表明:

  • 使用截断的目标函数,尤其是ϵ=0.2 时,算法性能最好
  • 自适应KL惩罚的方法次之。
  • 固定KL惩罚的方法表现较差。
  • 无截断或惩罚的方法由于策略更新过大,导致性能不稳定,甚至下降。

附录

PPO其他实现:

参考文章:

GRPO

论文:DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models

代码:https://github.com/huggingface/trl/blob/main/trl/trainer/grpo_trainer.py

PPO 需加载‌策略模型、‌价值模型、‌参考模型和‌奖励模型(共4个),显存占用高。‌‌

GRPO 仅需策略模型、参考模型和奖励模型(共3个),资源消耗更低。‌‌

GRPO相比PPO优化:

  • 无价值模型优化:GRPO 通过比较组内的响应消除了对评论模型的需求,从而显著减少了计算开销。
  • 相对评估:GRPO 不依赖外部评估者,而是使用群体动力学来评估某个响应相对于同一批次中其他响应的表现如何。
  • 高效训练:通过关注基于群体的优势,GRPO 简化了奖励估计过程,使其更快、更适用于大型模型。

GRPO计算流程:

  • 采样一组输出并计算每个输出的奖励。
  • 对组内奖励进行归一化处理。
  • 使用归一化后的奖励计算优势函数。
  • 通过最大化目标函数更新策略模型。
  • 迭代训练,逐步优化策略模型。

PPO目标函数:

JPPO(θ)=E[qP(Q),oπθold (Oq)]1ot=10min[πθ(otq,o<t)πθold (otq,o<t)At,clip(πθ(otq,o<t)πθold (otq,o<t),1ε,1+ε)At],\mathcal{J}_{P P O}(\theta)=\mathbb{E}\left[q \sim P(Q), o \sim \pi_{\theta_{\text {old }}}(O \mid q)\right] \frac{1}{|o|} \sum_{t=1}^{|0|} \min \left[\frac{\pi_\theta\left(o_t \mid q, o_{<t}\right)}{\pi_{\theta_{\text {old }}}\left(o_t \mid q, o_{<t}\right)} A_t, \operatorname{clip}\left(\frac{\pi_\theta\left(o_t \mid q, o_{<t}\right)}{\pi_{\theta_{\text {old }}}\left(o_t \mid q, o_{<t}\right)}, 1-\varepsilon, 1+\varepsilon\right) A_t\right],

GRPO目标函数:

JGRPO(θ)=E[qP(Q),{oi}i=1Gπθold (Oq)]1Gi=1G1oit=1oi{min[πθ(oi,tq,oi,<t)πθold (oi,tq,oi,<t)A^i,t,clip(πθ(oi,tq,oi,<t)πθold (oi,tq,oi,<t),1ε,1+ε)A^i,t]βDKL[πθπref]},\begin{aligned} \mathcal{J}_{G R P O}(\theta) & =\mathbb{E}\left[q \sim P(Q),\left\{o_i\right\}_{i=1}^G \sim \pi_{\theta_{\text {old }}}(O \mid q)\right] \\ & \frac{1}{G} \sum_{i=1}^G \frac{1}{\left|o_i\right|} \sum_{t=1}^{\left|o_i\right|}\left\{\min \left[\frac{\pi_\theta\left(o_{i, t} \mid q, o_{i,<t}\right)}{\pi_{\theta_{\text {old }}}\left(o_{i, t} \mid q, o_{i,<t}\right)} \hat{A}_{i, t}, \operatorname{clip}\left(\frac{\pi_\theta\left(o_{i, t} \mid q, o_{i,<t}\right)}{\pi_{\theta_{\text {old }}}\left(o_{i, t} \mid q, o_{i,<t}\right)}, 1-\varepsilon, 1+\varepsilon\right) \hat{A}_{i, t}\right]-\beta \mathbb{D}_{K L}\left[\pi_\theta \| \pi_{r e f}\right]\right\}, \end{aligned}

GRPO目标函数主要由三部分组成:

  • 策略比值 (Policy Ratio):衡量新旧策略的变化幅度。
  • 裁剪目标 (Clipped Objective):防止策略更新过大,确保稳定性。
  • KL 散度正则化 (KL Divergence Regularization):防止新策略偏离参考策略过远。

优势估计

A^i,t=r~i=rimean(r)std(r)\hat{A}_{i, t}=\widetilde{r}_i=\frac{r_i-\operatorname{mean}(\mathbf{r})}{\operatorname{std}(\mathbf{r})}

参考文章:

DPO

论文:Direct Preference Optimization: Your Language Model is Secretly a Reward Model

代码:https://github.com/eric-mitchell/direct-preference-optimization

DPO(Direct Preference Optimization)将 RLHF 的 2 阶段(训练 reward model和训练 PPO)多个模型的训练简化为了 1 阶段的 SFT 训练。

算法基于PPO的RLHF基础上进行了大幅简化,跳过了奖励建模步骤,直接使用偏好数据优化语言模型,解决了RLHF三个阶段的训练(SFT->RM->PPO)过程较长,更新迭代较慢的问题。DPO只有策略模型和参考模型,不再需要奖励模型。

DPO 提出了一种使用二进制交叉熵目标来精确优化大模型的方法,以替代基于RLHF的优化目标,从而大大简化偏好学习流程。

目标函数:

θLDPO(πθ;πref)=βE(x,yw,yl)D[σ(r^θ(x,yl)r^θ(x,yw))higher weight when reward estimate is wrong [θlogπ(ywx)increase likelihood of ywθlogπ(ylx)decrease likelihood of yl]],\begin{aligned} & \nabla_\theta \mathcal{L}_{\mathrm{DPO}}\left(\pi_\theta ; \pi_{\mathrm{ref}}\right)= \\ & -\beta \mathbb{E}_{\left(x, y_w, y_l\right) \sim \mathcal{D}}[\underbrace{\sigma\left(\hat{r}_\theta\left(x, y_l\right)-\hat{r}_\theta\left(x, y_w\right)\right)}_{\text {higher weight when reward estimate is wrong }}[\underbrace{\nabla_\theta \log \pi\left(y_w \mid x\right)}_{\text {increase likelihood of } y_w}-\underbrace{\nabla_\theta \log \pi\left(y_l \mid x\right)}_{\text {decrease likelihood of } y_l}]], \end{aligned}

其中:

r^θ(x,y)=βlogπθ(yx)πref(yx)\hat{r}_{\theta} ( x , y )=\beta\operatorname{log} \frac{\pi_{\theta} ( y | x )} {\pi_{\mathrm{ref}} ( y | x )}

参数β是控制策略模型(policy model)与参考模型(reference model)之间的 KL 散度约束强度。

为了方便分析,把log里面的分式展开,然后β设为1,并且忽略前面的log_sigmoid,那么目标函数可以简化为:

[logp(yw)logpref(yw)][logp(yl)logpref(yl)][\operatorname{log} p(y_{w})-\operatorname{log} p_{ref}(y_{w})]-[\operatorname{log} p(y_{l})-\operatorname{log} p_{ref}(y_{l})]

由于最初loss前面是有个负号的,所以优化目标是让本简化公式最大,即我们希望左半部分和右半部分的margin越大越好。

简化目标函数存在下面三种变化:

  1. 左边变大,右边变小,理想情况,good response概率提升,bad response概率下降
  2. 左边变小,右边更小,good response概率下降,但是bad response概率下降的更多,生成的时候还是倾向于good response
  3. 左边变的更大,右边只大了一点点,和2)同理

“Reward hacking”是指在RLHF过程中,模型发现奖励函数中的漏洞或偏差,从而通过非预期行为获得高奖励的现象。

Q:为什么DPO里Chosen和Rejected概率会同时下降(防止hacking)?

在DPO优化过程中, 对于chosen优化的方向是有不确定性的,DPO实际上优化保证整体增大 ,而不是单一的让chosen prob增大。

解决方案:

  • 为chosen/rejected ratio设定不同参数β
    • β⁺·logπ(y⁺) − β⁻·logπ(y⁻)(其中 β⁺ > β⁻,如 β⁺=0.6、β⁻=0.2)
  • 我们在DPO优化时,可以确定性的优化使得正例reward>0,如DPO-P
    • max(0, −r(y⁺|x)),强制模型必须让 r (y⁺|x) > 0,即logπ(y⁺|x) > −C/τ(τ 为温度系数,C 为常数)。
  • 在DPO优化时,同时对正例增加SFT优化
    • SFT(监督微调)的核心是最小化 y⁺的负对数似然损失(L_SFT = −logπ(y⁺|x)),其目标是直接最大化 y⁺的生成概率,具有极强的 “单向提升” 确定性。将其与 DPO 损失联合优化,可形成互补;
    • 联合损失公式:L = L_DPO + α·L_SFT(α∈[0.1, 0.5],控制 SFT 的权重)。
  • 切换为 KTO:重构目标为 “确定性优化单个策略”,从根源消除比值的不确定性

Q:DPO训练的模型效果如何评估?

核心评估维度:

  • 偏好对齐度(核心)
    • 人工偏好对比测试:赢率、偏好准确率
    • 自动评估(奖励模型 RM):平均奖励得分、排序准确率
  • 生成质量(避免能力损失)
    • 流畅度:Perplexity(困惑度)
    • 任务适配性:BLEU/ROUGE/METEOR(摘要 / 翻译等)
    • 知识准确性:FactScore、TruthfulQA 正确率
  • 安全性(规避有害内容)
    • 对抗性测试:有害内容拒绝率(避免过度拒绝)
    • 基准数据集:HarmBench、RealToxicityPrompts 毒性 / 偏见分数

代码实现:

def preference_loss(policy_chosen_logps: torch.FloatTensor,
policy_rejected_logps: torch.FloatTensor,
reference_chosen_logps: torch.FloatTensor,
reference_rejected_logps: torch.FloatTensor,
beta: float,
label_smoothing: float = 0.0,
ipo: bool = False,
reference_free: bool = False) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]:
pi_logratios = policy_chosen_logps - policy_rejected_logps
ref_logratios = reference_chosen_logps - reference_rejected_logps

if reference_free:
ref_logratios = 0

logits = pi_logratios - ref_logratios # also known as h_{\pi_\theta}^{y_w,y_l}

if ipo:
losses = (logits - 1/(2 * beta)) ** 2 # Eq. 17 of https://arxiv.org/pdf/2310.12036v2.pdf
else:
# Eq. 3 https://ericmitchell.ai/cdpo.pdf; label_smoothing=0 gives original DPO (Eq. 7 of https://arxiv.org/pdf/2305.18290.pdf)
losses = -F.logsigmoid(beta * logits) * (1 - label_smoothing) - F.logsigmoid(-beta * logits) * label_smoothing

chosen_rewards = beta * (policy_chosen_logps - reference_chosen_logps).detach()
rejected_rewards = beta * (policy_rejected_logps - reference_rejected_logps).detach()

return losses, chosen_rewards, rejected_rewards

参考文章:

欢迎关注我的其它发布渠道