CONTENTS

从零开始理解PPODraft

强化学习(RL)是一种独特的机器学习形式,用于解决通常开放性的问题。它可以与监督学习(SL)进行比较,后者本质上是一个高维回归任务。在监督学习中,我们获得一个输入和输出的数据集 ,我们的任务是学习一个函数 来最小化数据集上的均方误差(MSE)。然而,许多有用的问题无法用这个框架来表达。假设你想教一个虚拟人形代理做一个后空翻。我们的数据集是什么?你如何收集这样的数据集?这并不简单。然而,简单的是知道代理是否真的翻转了。我们只需检查:

  • 代理的脚是否离开了地面
  • 代理的脚在某个时间点是否朝上
  • 代理的脚是否回到了地面

我们看到,对于这个任务,成功可以很容易地定义,但成功所需的精确关节控制却无法定义。正是在这种情况下,我们使用强化学习。

在高层次上,强化学习是一种试错方法。在策略梯度(PG)中,我们从某个策略开始,尝试一堆策略,看看哪些成功了,然后引导策略更像成功的那些。在这篇文章中,我们将从头开始探索强化学习和策略梯度方法,最终达到PPO ( Schulman et al., 2017 Schulman et al. (2017) Proximal Policy Optimization AlgorithmsarXiv preprint arXiv:1707.06347 ) ,它是并且一直是策略梯度方法的基础基准。我们还将涉及启发PPO的早期信任区域方法。

预备知识

马尔可夫决策过程

我们首先想要形式化代理、环境和奖励的概念,以便我们可以用数学方式处理它们。最常见的形式化是马尔可夫决策过程(MDPs) ( Bellman, 1957 Bellman (1957) A Markovian Decision ProcessJournal of Mathematics and Mechanics ; Sutton and Barto, 2018 Sutton & Barto (2018) Reinforcement Learning: An IntroductionThe MIT Press )

MDP由以下内容定义:

  • :环境所有可能状态的集合
  • :代理可以执行的所有动作的集合
  • :代理可能收到的所有奖励的集合
  • 转移函数,它告诉我们:如果我们在状态 采取动作 ,获得奖励 转移到下一个状态 的概率是多少?
  • :时间折扣因子,即时间 的奖励比时间 的奖励价值低 倍。
  • :初始状态分布

在由MDP定义的环境中,我们可能有一个策略 ,它告诉我们当前从中采样动作的概率分布。在本文的其余部分,我们将假设这是一个由权重 参数化的神经网络。

我们可以生成一个轨迹 ,它只是状态、动作和奖励随时间的序列

一旦你理解了所有这些变量,我们的目标应该很清楚。我们想要学习 的权重,使得预期未来折扣奖励(我们表示为 )最大化。形式上,我们想要最大化

由于我们想要在 上使用反向传播来优化这个目标,我们需要用 的梯度来表达 。然而, 操作是不可微的,即梯度无法通过从 采样的动作传回 。我们希望 在期望内部,而不是外部。

一旦我们注意到以下内容,这就成为可能

这让我们可以简单地从策略中采样轨迹,将轨迹的对数概率乘以其折扣总奖励,然后无问题地进行反向传播。

我们无法计算估计的梯度,但我们可以计算梯度的估计!

基础策略梯度

好了,理论够多了。让我们看看这东西是否真的有效

我们将使用gymnasiumCartPole-v1环境,目标是尽可能长时间地保持连接在小车上的杆子处于直立位置( Towers et al., 2024 Towers et al. (2024) Gymnasium: A Standard Interface for Reinforcement Learning EnvironmentsarXiv preprint arXiv:2407.17032 )

在每个时间步,环境给我们它的状态——在这种情况下是一个包含杆角度、杆角速度、小车位置和小车速度的4维向量。然后我们通过策略网络运行状态,它输出两个动作的分布——向左推或向右推。我们每存活一个时间步就获得 的奖励。

这是伪代码。完整的可运行文件在这里

policy = Policy()  # 一个线性层 + softmax
optimizer = optim.Adam(policy.parameters(), lr=alpha)

for iteration in range(num_iterations):
    rewards, action_log_probs = [], []
    state = env.reset()  # 从 ρ_0 获取初始状态
    done = False
    while not done:
        log_probs = policy(state)  # (2,); 有梯度
        action_dist = Categorical(log_probs)
        action = action_dist.sample()
        log_prob = action_dist.log_prob(action)  # (1,); 有梯度
        state, r, done = env.step(action.detach())
        action_log_probs.append(log_prob)
        rewards.append(r)
    action_log_probs = torch.stack(action_log_probs)
    # 计算轨迹回报 ∑_{t=0}^T γ^t r_t
    R = 0.0
    for r in reversed(rewards):
        R = r + gamma * R
    # 为了最大化 J,我们最小化 -J
    loss = -(action_log_probs.sum() * R)  # 梯度只通过 log probs 流动
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

在高层次上,这将

  • 使用当前策略进行一次rollout
  • 计算该rollout的似然
  • 最大化R乘以概率
    • 如果R是正的,增加轨迹的似然
    • 如果R是负的,减少轨迹的似然

结果

100次运行的平均奖励,每次运行1000次迭代。

结果…相当糟糕。如果你仔细看,曲线几乎没有上升趋势——但它仍然很差。这个算法有几个明显的问题。首先,这个环境只给正奖励,所以 总是正的。这意味着无论一次迭代中的轨迹有多差,算法总是会增加其似然。奖励唯一的区别是增加的幅度。其次,我们的信用分配是序列级的。轨迹中的每个动作都根据整个轨迹的成功获得同等奖励,所以算法缺乏对动作选择的细粒度控制。

让我们首先关注信用分配问题。

REINFORCE

与其用 奖励 中的每个动作,我们可以为每个动作 分配时间步级别的奖励。回顾我们的梯度估计,我们可以将其分解为时间步

为什么第二项是零?直观上,这是因为过去的奖励( 时的 )在时间 实际上是常数。一旦这被分解出来,期望就干净地计算为零。设 为时间 之前的历史。第二项等价于

我们注意到内部期望化简为0

整个表达式也是如此

因此,我们看到方程4等于

好的,这告诉我们什么?“如果我们使用奖励-待得(reward-to-go) ,策略梯度在步级信用分配下是保持的”。这让我们可以在不影响正确性的情况下进行更细粒度的信用分配。这个算法被称为REINFORCE ( Williams, 1992 Williams (1992) Simple Statistical Gradient-Following Algorithms for Connectionist Reinforcement LearningMachine Learning )

实现

我们只需要从基础PG实现中更改几行代码:

policy = Policy()
optimizer = optim.Adam(policy.parameters(), lr=alpha)

for iteration in range(num_iterations):
    rewards, action_log_probs = [], []
    state = env.reset()
    done = False
    while not done:
        log_probs = policy(state)
        action_dist = Categorical(log_probs)
        action = action_dist.sample()
        log_prob = action_dist.log_prob(action)
        state, r, done = env.step(action.detach())
        action_log_probs.append(log_prob)
        rewards.append(r)
    action_log_probs = torch.stack(action_log_probs)
    G = torch.zeros_like(action_log_probs)
    R = 0.0
    for r in reversed(rewards):
    for t, r in reversed(enumerate(rewards)):
        R = r + gamma * R
        G[t] = R
    loss = -(action_log_probs.sum() * R)
    loss = -(action_log_probs * G).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

结果

仅此一项更改就似乎显著提高了性能。

100次运行的平均奖励,每次运行1000次迭代。

这表明*信用分配很重要!*然而,我们距离解决CartPole还有一段距离,如果我们保持杆子直立500个时间步,或等价地获得500个轨迹奖励,就算解决了。我们需要的最后改进是基线。

基线

如果我们回顾一下,我们遇到的第二个问题是,一个动作被采取的概率总是增加。我们的算法本质上无法判断一个动作是否糟糕,只能表征它是多好的动作

一个常识性的方法来确定动作 是好是坏,就是简单地测量 相对于某个基线的表现。一种方法是在每次迭代中运行多个rollout,取 在rollout之间的平均值——称之为 ——并将该数量用作基线。

所以,我们新的策略梯度是

有一个注意事项:如果 不独立于 ,我们的PG估计将是有偏的——很容易看出 将是 的估计器,其中 是rollout的数量。因此,我们需要使用留一法(LOO)均值作为基线。这意味着,对于轨迹 ,我们只考虑所有其他rollout的平均回报。因此,我们的蒙特卡洛估计器变成

另一个注意事项:每个rollout不一定具有相同的长度,因为它们在不同的时间失败。因此,必须在 个rollout中的每个 处取平均值如果是有效的。这在代码中比在方程中更容易说明,所以我直接放在这里。

env = gym.make_vec("CartPole-v1", num_envs=args.n_rollouts)
for iter in range(iterations):
    done_envs = np.zeros((N,), dtype=bool)
    rewards, log_probs_history, entropies, alive_masks_history = [], [], [], []
    done = False
    while not done:
        alive_mask = torch.as_tensor(~done_envs, dtype=torch.float32)
        state_tensor = torch.as_tensor(state, dtype=torch.float32)
        log_probs = policy.forward(state_tensor)
        dist = torch.distributions.Categorical(logits=log_probs)
        action = dist.sample()
        state, reward, terminated, truncated, info = env.step(action.numpy())
        rewards.append(torch.as_tensor(reward, dtype=torch.float32) * alive_mask)
        log_probs_history.append(dist.log_prob(action) * alive_mask)
        entropies.append(dist.entropy().detach())
        alive_masks_history.append(alive_mask)
        done_envs |= terminated | truncated
        done = np.all(done_envs)

    T = len(log_probs_history)
    action_log_probs = torch.stack(log_probs_history)  # (T, N)
    alive_masks = torch.stack(alive_masks_history)  # (T, N)
    G = torch.zeros_like(action_log_probs)
    running = torch.zeros(N, dtype=action_log_probs.dtype)
    rewards = torch.stack(rewards)  # (T, N)
    for t in reversed(range(T)):
        running = rewards[t, :] + gamma * running
        G[t, :] = running

    # 留一法均值基线用于无偏梯度估计
    # 每个rollout的基线排除自己的回报以保持独立性
    alive_count = alive_masks.sum(dim=-1, keepdim=True)  # (T, 1)
    total_G = G.sum(dim=-1, keepdim=True)  # (T, 1)
    baseline = (total_G - G) / (alive_count - 1).clamp(min=1)
    A = G - baseline
    loss = -(action_log_probs * A).sum()

这个版本和之前版本之间的大部分代码差异来自使用向量化环境,这要求我们(有点繁琐地)跟踪并行运行的多个轨迹的done状态。

结果

各方法的平均总奖励。阴影区域是四分位距。

均值基线远远超过了其他两种方法!它通常在不到200步内解决任务(达到500奖励)。还要注意,虽然之前的图跨越1k步,但这个只有250步!

演示

这是用这个算法训练的策略,完全在你的浏览器中运行。按tab键与它竞争。

500 +
click to start
h/l or / · space start · tab toggle human tap sides to control · tap center to start

广义优势估计


从我们的实验中,我们看到添加基线对性能至关重要。然而,我们计算基线的方法不一定适用于所有任务。为了确定状态处所有动作的典型回报是什么,我们的方法简单地平均来自 个rollout组的经验回报。在高度随机或可变的任务中,这可能是一个高方差估计:预期回报不一定是状态 的函数。

在这种情况下,我们可以考虑学习一个价值函数 作为一个神经网络,该网络从训练期间遇到的真实回报中进行自举。这是估计器

它告诉我们给定我们在状态 并遵循策略 的预期未来奖励。然而,将这个精确函数作为神经网络 学习是不可行的,因为 在训练期间会发生变化。因此总会有非零误差

因此,我们正在用增加的估计偏差(因为我们放弃了MC采样)换取更低的方差

好的,如果我们的基线是 ,那么我们应该优化什么量?一个合理的选择是优势

其中

它表示给定我们从状态 采取了动作 的预期回报。 告诉我们与从 采样的典型动作相比,采取 预期好多少


均值基线是所有基线中最简单的。然而,它背后的思想更一般地表示为优势 ,它告诉我们与从 采样的典型动作相比,采取动作 预期能获得多少更多的奖励。

我们可以用 函数 来表达

它表示采取 后的预期未来奖励,以及价值函数

——给定我们在状态 的预期未来奖励。优势由下式给出

它量化了我们选择的动作比策略 下的典型动作好多少

目前,在我们的设置中,我们只有一个网络 。为了

参考文献

  1. Schulman, J., Wolski, F., Dhariwal, P., Radford, A., and Klimov, O. (2017). Proximal Policy Optimization Algorithms. arXiv preprint arXiv:1707.06347. https://arxiv.org/abs/1707.06347.
  2. Bellman, R. (1957). A Markovian Decision Process. Journal of Mathematics and Mechanics. Vol. 6, No. 5, pp. 679-684. doi:10.1512/iumj.1957.6.56038. https://doi.org/10.1512/iumj.1957.6.56038.
  3. Sutton, R. S. and Barto, A. G. (2018). Reinforcement Learning: An Introduction. The MIT Press. https://mitpress.mit.edu/9780262039246/reinforcement-learning.
  4. Towers, M., Kwiatkowski, A., Terry, J., Balis, J. U., De Cola, G., Deleu, T., Goulão, M., Kallinteris, A., Krimmel, M., KG, A., Perez-Vicente, R., Pierré, A., Schulhoff, S., Tai, J. J., Tan, H., and Younis, O. G. (2024). Gymnasium: A Standard Interface for Reinforcement Learning Environments. arXiv preprint arXiv:2407.17032. doi:10.48550/arXiv.2407.17032. https://arxiv.org/abs/2407.17032.
  5. Williams, R. J. (1992). Simple Statistical Gradient-Following Algorithms for Connectionist Reinforcement Learning. Machine Learning. Vol. 8, No. 3-4, pp. 229-256. doi:10.1007/BF00992696. https://doi.org/10.1007/BF00992696.
AI 透明度

Codex CLI和Claude Code编写了浏览器中的CartPole实现和绘图代码。其他内容均为本人原创。