强化学习(RL)是一种独特的机器学习形式,用于解决通常开放性的问题。它可以与监督学习(SL)进行比较,后者本质上是一个高维回归任务。在监督学习中,我们获得一个输入和输出的数据集 ,我们的任务是学习一个函数 来最小化数据集上的均方误差(MSE)。然而,许多有用的问题无法用这个框架来表达。假设你想教一个虚拟人形代理做一个后空翻。我们的数据集是什么?你如何收集这样的数据集?这并不简单。然而,简单的是知道代理是否真的翻转了。我们只需检查:
- 代理的脚是否离开了地面
- 代理的脚在某个时间点是否朝上
- 代理的脚是否回到了地面
我们看到,对于这个任务,成功可以很容易地定义,但成功所需的精确关节控制却无法定义。正是在这种情况下,我们使用强化学习。
在高层次上,强化学习是一种试错方法。在策略梯度(PG)中,我们从某个策略开始,尝试一堆策略,看看哪些成功了,然后引导策略更像成功的那些。在这篇文章中,我们将从头开始探索强化学习和策略梯度方法,最终达到PPO ( Schulman et al., 2017 Proximal Policy Optimization AlgorithmsarXiv preprint arXiv:1707.06347 ) ,它是并且一直是策略梯度方法的基础基准。我们还将涉及启发PPO的早期信任区域方法。
预备知识
马尔可夫决策过程
我们首先想要形式化代理、环境和奖励的概念,以便我们可以用数学方式处理它们。最常见的形式化是马尔可夫决策过程(MDPs) ( Bellman, 1957 A Markovian Decision ProcessJournal of Mathematics and Mechanics ; Sutton and Barto, 2018 Reinforcement Learning: An IntroductionThe MIT Press ) 。
MDP由以下内容定义:
- :环境所有可能状态的集合
- :代理可以执行的所有动作的集合
- :代理可能收到的所有奖励的集合
- :转移函数,它告诉我们:如果我们在状态 采取动作 ,获得奖励 并转移到下一个状态 的概率是多少?
- :时间折扣因子,即时间 的奖励比时间 的奖励价值低 倍。
- :初始状态分布
在由MDP定义的环境中,我们可能有一个策略 ,它告诉我们当前从中采样动作的概率分布。在本文的其余部分,我们将假设这是一个由权重 参数化的神经网络。
我们可以生成一个轨迹 ,它只是状态、动作和奖励随时间的序列
一旦你理解了所有这些变量,我们的目标应该很清楚。我们想要学习 的权重,使得预期未来折扣奖励(我们表示为 )最大化。形式上,我们想要最大化
由于我们想要在 上使用反向传播来优化这个目标,我们需要用 的梯度来表达 。然而, 操作是不可微的,即梯度无法通过从 采样的动作传回 。我们希望 在期望内部,而不是外部。
一旦我们注意到以下内容,这就成为可能
这让我们可以简单地从策略中采样轨迹,将轨迹的对数概率乘以其折扣总奖励,然后无问题地进行反向传播。
我们无法计算估计的梯度,但我们可以计算梯度的估计!
基础策略梯度
好了,理论够多了。让我们看看这东西是否真的有效。
我们将使用gymnasium的CartPole-v1环境,目标是尽可能长时间地保持连接在小车上的杆子处于直立位置(
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是负的,减少轨迹的似然
结果
结果…相当糟糕。如果你仔细看,曲线几乎没有上升趋势——但它仍然很差。这个算法有几个明显的问题。首先,这个环境只给正奖励,所以 总是正的。这意味着无论一次迭代中的轨迹有多差,算法总是会增加其似然。奖励唯一的区别是增加的幅度。其次,我们的信用分配是序列级的。轨迹中的每个动作都根据整个轨迹的成功获得同等奖励,所以算法缺乏对动作选择的细粒度控制。
让我们首先关注信用分配问题。
REINFORCE
与其用 奖励 中的每个动作,我们可以为每个动作 分配时间步级别的奖励。回顾我们的梯度估计,我们可以将其分解为时间步
为什么第二项是零?直观上,这是因为过去的奖励( 时的 )在时间 实际上是常数。一旦这被分解出来,期望就干净地计算为零。设 为时间 之前的历史。第二项等价于
我们注意到内部期望化简为0
整个表达式也是如此
因此,我们看到方程4等于
好的,这告诉我们什么?“如果我们使用奖励-待得(reward-to-go) ,策略梯度在步级信用分配下是保持的”。这让我们可以在不影响正确性的情况下进行更细粒度的信用分配。这个算法被称为REINFORCE ( 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()结果
仅此一项更改就似乎显著提高了性能。
这表明*信用分配很重要!*然而,我们距离解决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键与它竞争。
广义优势估计
从我们的实验中,我们看到添加基线对性能至关重要。然而,我们计算基线的方法不一定适用于所有任务。为了确定状态处所有动作的典型回报是什么,我们的方法简单地平均来自 个rollout组的经验回报。在高度随机或可变的任务中,这可能是一个高方差估计:预期回报不一定是状态 的函数。
在这种情况下,我们可以考虑学习一个价值函数 作为一个神经网络,该网络从训练期间遇到的真实回报中进行自举。这是估计器
它告诉我们给定我们在状态 并遵循策略 的预期未来奖励。然而,将这个精确函数作为神经网络 学习是不可行的,因为 在训练期间会发生变化。因此总会有非零误差
因此,我们正在用增加的估计偏差(因为我们放弃了MC采样)换取更低的方差。
好的,如果我们的基线是 ,那么我们应该优化什么量?一个合理的选择是优势 :
其中
它表示给定我们从状态 采取了动作 的预期回报。 告诉我们与从 采样的典型动作相比,采取 预期好多少。
均值基线是所有基线中最简单的。然而,它背后的思想更一般地表示为优势 ,它告诉我们与从 采样的典型动作相比,采取动作 预期能获得多少更多的奖励。
我们可以用 函数 来表达
它表示采取 后的预期未来奖励,以及价值函数
——给定我们在状态 的预期未来奖励。优势由下式给出
它量化了我们选择的动作比策略 下的典型动作好多少。
目前,在我们的设置中,我们只有一个网络 。为了
参考文献
- 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. ↩
- 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. ↩
- Sutton, R. S. and Barto, A. G. (2018). Reinforcement Learning: An Introduction. The MIT Press. https://mitpress.mit.edu/9780262039246/reinforcement-learning. ↩
- 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. ↩
- 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. ↩