PPO算法
要点
- 根据 OpenAI 的官方博客, PPO 已经成为他们在强化学习上的默认算法. 如果一句话概括 PPO: OpenAI 提出的一种解决 Policy Gradient 不好确定 Learning rate (或者 Step size) 的问题. 因为如果 step size 过大, 学出来的 Policy 会一直乱动, 不会收敛, 但如果 Step Size 太小, 对于完成训练, 我们会等到绝望. PPO 利用 New Policy 和 Old Policy 的比例, 限制了 New Policy 的更新幅度, 让 Policy Gradient 对稍微大点的 Step size 不那么敏感.
- 总的来说 PPO 是一套 Actor-Critic 结构, Actor 想最大化 J_PPO, Critic 想最小化 L_BL.
PG Add Constraint → PPO
简单来说,PPO就是Policy Gradient的”off-policy”版本。为了满足Importance Sampling的使用条件,即防止$p_{\theta}$和$p_{\theta_{old}}$两个概率分布相差太多,PPO提供了两个解决方案:
- TRPO(Trust Region Policy Optimization)在目标函数外使用KL Penalty (惩罚项)来限制策略更新,希望在训练的过程中,new Policy 和 old Policy 的输出不要相差太大(因为输出的 action 是概率分布,也即计算两个概率分布之间的差别)。但是这种方法实现起来很复杂,需要更多的计算时间。
$L^{PPO}(\theta)=E_{t}\left[r_t(\theta) * A_{t}\right]-\beta·KL[\pi_{\theta_{init}}|\pi_{\theta}]$
- PPO-Clip 在目标函数中使用 Clipped surrogate objective function 来直接裁剪概率比率。所要做的事情本质上和TRPO是一样的,都是为了让两个分布($θ$和$θ’$)之间的差距不致过大
$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]$
其中,$\beta$是可以动态调整的,称之为自适应KL惩罚(adaptive KL penalty);$r_t(\theta)$表示Ratio Function,指产生同样的 token,在 Policy Model 和 Alignment Model 下的概率比值(It’s the probability of taking action a_t at state s_t in the current policy divided by the previous one. )
$r_t(\theta)=\frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$
正如我们所看到的,$r_t(\theta)$表示当前策略和旧策略之间的概率比率,是估计旧策略和当前策略之间差异的一种简单方法。
- 如果$r_t(\theta)>1$,则在状态$s_t$下,动作$a_t$在当前策略中比旧策略更有可能执行。
- 如果$0<r_t(\theta)<1$,则在当前策略下执行该动作的可能性比旧策略下低。
PPO-Clip 算法直观理解
$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]$
$r_t(\theta)=\frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$
整个目标函数在$min$这个大括号里有两部分,最终对比两部分哪部分更小,就取哪部分的值。
在括号的第二部分中,
- 首先是裁剪函数$clip$:如果$p_{\theta}(a_t|s_t)$和$p_{\theta^k}(a_t|s_t)$之间的概率比落在范围$(1-ε)$和$(1+ε)$之外,$\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}$将被剪裁,使得其值最小不小于$(1-ε)$,最大不大于$(1+ε)$
- 然后是$clip$括号外乘以$A^{\theta’}(s_t,a_t)$:当$A>0$,则说明这是好动作,那么希望增大这个action的几率$p_{\theta}(a_t|s_t)$,但是又不希望两者差异,即比值$\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}$太悬殊,所以增大到比值为$1+ε$就不要再增加了;当$A<0$,则说明该动作不是好动作,那么希望这个action出现的几率$p_{\theta}(a_t|s_t)$越小越好,但$\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}$最小不能小过$(1-ε)$
换言之,这个裁剪算法和KL散度约束所要做的事情本质上是一样的,都是为了让两个分布之间的差距不致过大,但裁剪算法相对好实现,别看看起来复杂,其实代码很好写,示例如下:
1 |
|
简单 PPO 的代码解读
https://mofanpy.com/tutorials/machine-learning/reinforcement-learning/DPPO
我们用 Tensorflow 搭建神经网络, tensorboard 中可以看清晰的看到我们是如果搭建的:
图中的 pi 就是我们的 Actor 了. 每次要进行 PPO 更新 Actor 和 Critic 的时候, 我们有需要将 pi 的参数复制给 oldpi. 这就是 update_oldpi 这个 operation 在做的事. Critic 和 Actor 的内部结构, 我们不会打开细说了. 因为就是一堆的神经网络而已. 这里的 Actor 使用了 normal distribution 正态分布输出动作.
这个 PPO 我们可以用一个 Python 的 class 代替:
1 |
|
而这个 PPO 和 env 环境的互动可以简化成这样.
1 |
|
了解了这些更新步骤, 我们就来看看如何更新我们的 PPO. 我们更新 Critic 的时候是根据 刚刚计算的 discounted_r 和自己分析出来的 state value 这两者的差 (advantage). 然后最小化这个差值:
1 |
|
两种更新 Actor 的方式 KL penalty 和 clipped surrogate objective
1 |
|
好了, 接下来就是最重要的更新 PPO 时间了, 同样, 如果觉得我这些代码省略的很严重, 请直接前往我的 Github 看全套代码. 注意的是, 这个 update 的步骤里, 我们用 for loop 更新了很多遍 Actor 和 Critic, 在 loop 之前, pi 和 old pi 是一样的, 每次 loop 的之后, pi 会变动, 而 old pi 不变, 这样这个 surrogate 就会开始变动了. 这就是 PPO 的精辟.
1 |
|
最后我们看一张学习的效果图:
好了这就是整个 PPO 的主要流程了, 其他的步骤都没那么重要了, 可以直接在我的 Github 看全套代码 中轻松弄懂.
PPO Actor-Critic Loss
PPO Actor-Critic 风格的最终 Clipped Surrogate Objective Loss 看起来像这样,它是 Clipped Surrogate Objective 函数、Value Loss Function 和 Entropy bonus 的组合:
DeepMind 总结 OpenAI conference 上的 PPO 的伪代码
参考
李宏毅老师的深度强化学习课程
Unit 8. Introduction to PPO - Hugging Face
图解人工反馈强化学习(RLHF) - Hugging Face