原文:
towardsdatascience.com/how-does-ppo-with-clipping-work-eff71a7a974a
·发表于 Towards Data Science ·9 min 阅读·2023 年 10 月 7 日
--
图片由 Tamanna Rumee 提供,Unsplash
在强化学习中,近端策略优化(PPO)通常被引用作为策略方法的例子,相对于 DQN(基于价值的方法)和包括 TD3 和 SAC 在内的大量 actor-critic 方法。
我回想起之前第一次学习时,我感到不信服。许多老师采用了一种模糊的方法。我不接受这种做法,你也不应如此。
在这篇文章中,我将尝试解释 PPO 如何工作,通过直观和代码来支持数学。你可以尝试不同的场景,亲自验证它不仅在原则上有效,而且在实践中也有效,并且没有 cherry picking。
PPO 和其他 SOTA 模型可以在几分钟内使用 stable-baselines3(sb3)实现。任何遵循文档的人都可以运行它,而无需了解底层模型。
然而,无论你是实践者还是理论家,基础知识确实重要。如果你仅仅把 PPO(或任何模型)当作一个黑箱,你怎么指望你的用户对你提供的结果有信心呢?
我将在本月晚些时候进行详细的代码讲解,编写一个包装器,使任何环境,无论是来自 Gymnasium 还是你自己的环境,都能与任何 sb3 模型兼容,无论空间是‘Discrete’还是‘Box’。(上个月,我展示了如何从 TD(λ)派生出 Monte Carlo、SARSA 和 Q-learning,所有这些都是通过一套代码实现的。)
够了,明天再谈,现在就让我们在这里吧!
Vanilla 策略梯度是基于策略方法中的最基本情况,其中策略是直接学习和更新的,而不是从某些价值函数推导出来的。缺点是策略更新的方差很高,这对收敛性是一个问题,特别是在奖励稀疏的环境中。
TRPO(Trust Region Policy Optimization)确保新的策略(其中“新”指的是更新后)不会偏离旧策略太远。这是通过施加一个约束来实现的,即新策略相对于旧策略的 KL 散度不超过某个阈值δ。
PPO 的目标。公式由作者输入,参考OpenAI Spinning Up。
注意每个策略π{θ}本身是一个分布。*D{KL}*,带有“帽子”,是相对于旧策略下访问的状态的 KL 散度的(加权)平均值。KL 散度本身是概率比的对数的平均值,根据第一个分布加权。
KL 散度公式用于两个离散分布p和 q 之间。对于连续分布,我们将有概率密度和积分替代求和。公式由作者提供。
目标函数是替代优势,它是一个比率(新策略下的动作概率除以对应于旧策略的概率)乘以优势。这个优势是相对于某些基准的期望回报,基准可以简单地是相应状态值的移动平均。
我们有一个约束优化问题,使用拉格朗日乘子和共轭梯度法来解决。目标和约束通过泰勒展开线性化,因此解决方案是近似的。为了确保满足约束,使用回溯线搜索来调整策略更新中的步长。
(这是我设定的界限,不再深入探讨。上述内容对应于 Level 7xx 课程,实践者可以跳过;了解描述的方向在我看来已经足够了。)
-
如果一个动作是有利的,即A > 0,我们希望增加其概率。更新策略后,它应该被更频繁地选择。
-
如果一个动作是不利的,即A < 0,我们希望降低其概率。L是负的,我们希望它更不负,以最大化目标。
-
小的 KL 散度意味着在旧政策和新政策下每个动作的概率保持接近。在极端情况下,我们有 log(1),这使得 KL 散度为零。
解决约束优化问题涉及回溯线搜索(即每次用较小的步长重复计算,直到满足约束)以及计算包含二阶偏导数的 Hessian 矩阵。我们可以简化这一过程吗?
TRPO 论文的第一作者在 2015 年提出了改进模型,并在 2017 年作为第一作者发表了 PPO。即便在今天(2023 年),PPO 仍然被广泛使用。
我将减少数学内容,所以请继续跟随我!有不同的 PPO 变体,我们将在这里讨论‘clip’版本,这也是 sb3 使用的版本。
不通过对 KL 散度施加约束,而是修改目标函数,使其不受政策大幅变化的影响。我们仍然尝试增加有利动作的概率(并减少不利动作的概率),但概率比大幅高于(或低于)1 的影响被裁剪所限制。
ϵ 通常选择 0.2。这个值也在原始论文中使用。
TRPO 的目标。作者编写的方程,参考 OpenAI Spinning Up。
论文的图 1(第 3 页)显示了 (1 + ϵ) 或 (1 — ϵ) 的因子,根据优势是正还是负,二者都对目标函数设置了上界。下面的图示更直观:
简化方程,取决于优势 A 的值。
不要被‘min’运算符搞混。我们希望优化目标 L 使其尽可能高(积极)。‘min’运算符仅用于 (1 + ϵ) 或 (1 — ϵ) 的作用形成上限。
-
如果A > 0,当概率比高于 1 时,我们的目标函数 L 较大(更积极),即新策略更频繁地选择有利的动作。然而,由于 (1 + ϵ),存在一个上界。
-
如果A < 0,当概率比低于 1 时,我们的目标函数 L 较大(较少负值),即新策略更频繁地选择不利的动作。然而,由于 (1 - ϵ),存在一个上界。
-
梯度更新可以使用常规的 pytorch 或 tensorflow 轻松完成。
首先让我们说服大家,梯度更新是有效的,不涉及任何裁剪。
我们建立了一个简单的单层神经网络,输入维度为 1 的状态,输出在 3 个可能选择之间的动作概率。
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
torch.manual_seed(0)
class PolicyNetwork(nn.Module):
def __init__(self, n_in=1, n_out=3):
super(PolicyNetwork, self).__init__()
self.fc = nn.Linear(n_in, n_out)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
x = self.fc(x)
return self.softmax(x)
你可以有多维状态,甚至是图像,概念依然有效——对于某个给定状态,神经网络给出总和为 1 的概率。
现在,让我们指定一个状态。对于本练习,它可以是任何常数。在实际操作中,这个状态是通过对环境的观察获得的,并且完全独立于参数θ,即在计算梯度时将其视为常数。
优势可以通过广义优势估计(如有兴趣,请参见上述引用的 Schulman et al. 2017 年论文的公式 11)来计算。它结合了来自多个时间区间的信息,试图平衡偏差和方差之间的权衡,类似于 TD(λ)。
policy = PolicyNetwork()
optimizer = optim.Adam(policy.parameters(), lr=0.01)
state = torch.tensor([[1.]])
pi = policy(state)
print("Before: ", pi)
advantages = torch.tensor([[-2., 0., 0.]], requires_grad=True)
loss = -torch.sum(pi * advantages)
optimizer.zero_grad()
loss.backward()
optimizer.step()
pi = policy(state)
print("After: ", pi)
对于本练习,我们任意指定了第一行动的负面优势,模拟了第一次行动带来负面优势的情况。这是为了验证参数是否更新,使得第一行动的概率减少。
上面代码单元的输出,其中第一次行动导致负面收益
实际上,在梯度更新后,策略输出了较低的第一行动概率和较高的其他行动概率。
现在,我们将实现剪切方面。为了首先检查正确性,将进行一次更新。这次,假设第二行动被执行,估计的优势为 1.8。
注意,在求和乘积时添加了负号,以得到损失(在此基础上将进行反向传播)。这是因为我们希望最大化目标函数,并进行梯度下降。
epsilon = 0.2
old_policy = PolicyNetwork()
old_policy.load_state_dict(policy.state_dict())
pi_old = old_policy(state)
r = pi / pi_old
advantages = torch.tensor([[0., 1.8, 0.]], requires_grad=True)
clipped_ratio = torch.clamp(r, 1 - epsilon, 1 + epsilon)
ppo_objective = torch.min(r * advantages, clipped_ratio * advantages)
loss = -torch.sum(ppo_objective)
print("Probabilities before update:", policy(state))
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Probabilities after update:", policy(state))
上面代码单元的输出,其中第二行动导致了正面收益
到目前为止,一切顺利。当采取一种有利的行动时,更新策略会增加该行动的概率。现在,我们将考虑许多迭代的净效果,其中每种不同的行动都被多次执行。
你可能会说,在现实中,优势是有噪声的,使用常数是不公平的。你说得对!让我们通过np.random.randn()
添加随机噪声。
num_iterations = 500
advatange_dict = {
0: [-2., 0., 0.],
1: [0., 1.8, 0.],
2: [0., 0., 0.1],
}
np.random.seed(2023)
for i in range(num_iterations):
pi = policy(state)
pi_old = old_policy(state)
r = pi / pi_old
old_policy.load_state_dict(policy.state_dict())
noisy_advantange = [
x+np.random.randn() for x in advatange_dict[i%3] if x != 0
]
advantage = torch.tensor(noisy_advantange, requires_grad=True)
ppo_objective = torch.min(
r * advantage,
torch.clamp(r, 1 - epsilon, 1 + epsilon) * advantage
)
loss = -torch.sum(ppo_objective)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("After: ", pi)
经过 500 次迭代,包括噪声,我们在给定状态下得到了以下行动概率。(当然,我们可以轻松扩展这个环境以考虑不同的状态。)
上面代码单元的输出,其中多次执行行动,并添加了噪声
我们可以看到,所有更新的净效果使得第二种行动的概率增加,而其他行动的概率减少。(我们开始时的概率为[0.2149, 0.5257, 0.2594]。)
尽管最后一个动作具有正的均值优势,这种情况仍然存在。这是因为梯度不仅依赖于优势的符号,还依赖于其大小。每次执行最后一个动作时,概率在更新后会增加,但在执行第二个动作时,更新的影响会抵消这一增加。
最后,让我们看看剪切的实际效果。重复上述单元(其中epsilon
ϵ 设置为 0.2),但这次将其设置为更小的值,比如 0.02。
以较小的 epsilon 值重新运行代码时的输出
很好!我们看到剪切确实有效!通过使用更小的ϵ,策略的变化幅度确实较小,尽管仍朝着期望的方向变化。
在这篇文章中,我们探讨了 PPO 的起源。数学部分仅保留了最基本的内容——足以让从业者向其他利益相关者展示这些思想,而不会被详细的推导所淹没。我们从数学中获得了关键的直觉,以理解 PPO 的工作原理。
此外,你现在拥有了代码来证明 PPO 的基本组件确实实现了其承诺。我们看到,即使添加了噪声,最有利的动作的概率也有所增加。此外,我们发现,通过收紧剪切,得到的策略变化的幅度较小。