策略优化入门

在本节中,将探讨策略优化算法的数学基础,并将其与示例代码联系起来。内容将涵盖策略梯度理论中的三个关键成果:

  • 描述策略性能相对于策略参数的梯度的最简方程,
  • 一个允许从该表达式中移除无效项的规则,
  • 以及一个允许向该表达式中添加有效项的规则。

最后,将把这些成果整合起来,并描述基于优势函数的策略梯度表达式——这也是在 Vanilla Policy Gradient 实现中所采用的版本。

推导最简单的策略梯度

这里考虑一种随机参数化策略 $π_θ$。目标是最大化期望回报 $J(π_θ) = E_{τ∼π_θ}[R(τ)]$。在本推导中,将采用 $R(τ)$ 作为有限期、无折扣的回报,但对于无限期、有折扣回报的设定,其推导过程几乎完全相同。

优化的方法是采用梯度上升,例如:

$$ θ_{k+1} = θ_k + α ∇_θ J(π_θ) $$

策略性能的梯度 $∇_θ J(π_θ)$ 被称为策略梯度,而通过这种方式优化策略的算法则被称为策略梯度算法。(例如 Vanilla Policy Gradient 和 TRPO。PPO 通常也被称为策略梯度算法,尽管这种说法略有不准确。)


要实际运用此算法,需要一个可以进行数值计算的策略梯度表达式。这包含两个步骤:1) 推导策略性能的解析梯度,该梯度最终会表现为一个期望值的形式;2) 构建该期望值的样本估计,这个估计值可以通过智能体与环境进行有限次交互的数据来计算。

在本小节中,将找出该表达式的最简形式。在后续小节中,将展示如何改进这种最简形式,以得到标准策略梯度实现中实际使用的版本。

首先从陈述一些有助于推导解析梯度的基本事实开始。

1. 轨迹的概率 (Probability of a Trajectory)。 在给定动作来源于策略 $π_θ$ 的条件下,一条轨迹 $τ = (s_0, a_0, ..., s_{T+1})$ 的概率是:

$$ P(τ|θ) = ρ_0(s_0) \prod_{t=0}^{T} P(s_{t+1}|s_t, a_t) π_θ(a_t|s_t). $$

2. 对数导数技巧 (The Log-Derivative Trick)。 对数导数技巧基于一个简单的微积分法则:$log x$ 对 $x$ 的导数是 $1/x$。经过整理并结合链式法则,可得到:

$$ ∇_θ P(τ|θ) = P(τ|θ) ∇_θ log P(τ|θ). $$

对任意可微标量函数 $f(\theta)$(且 $f(\theta) \ne 0$),有

$$ > \frac{d}{d\theta} \log f(\theta) = \frac{1}{f(\theta)} \frac{d}{d\theta} f(\theta). > $$

将上式两边乘以 $f(\theta)$,得

$$ > \frac{d}{d\theta} f(\theta) = f(\theta) \frac{d}{d\theta} \log f(\theta). > $$

将 $f(\theta) = P(\tau \mid \theta)$代入,即可得到

$$ > \nabla_\theta P(\tau \mid \theta) = P(\tau \mid \theta) \, \nabla_\theta \log P(\tau \mid \theta). > $$

轨迹

$$ > \tau = (s_0, a_0, s_1, a_1, \dots, s_T, a_T, s_{T+1}) > $$

的概率(在给定策略 $\pi_\theta$)是

$$ > P(\tau \mid \theta) = \rho_0(s_0) \prod_{t=0}^{T} P(s_{t+1} \mid s_t, a_t) \, \pi_\theta(a_t \mid s_t). > $$

取对数:

$$ > \log P(\tau \mid \theta) = \log \rho_0(s_0) + \sum_{t=0}^{T} \left( \log P(s_{t+1} \mid s_t, a_t) + \log \pi_\theta(a_t \mid s_t) \right). > $$

对 $\theta$ 求梯度。通常环境动力学 $P(s_{t+1} \mid s_t, a_t)$ 与 $\theta$ 无关(策略参数只影响动作选择),于是其梯度为 0,因此

$$ > \nabla_\theta \log P(\tau \mid \theta) = \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t). > $$

代回对数导数技巧 $\nabla_\theta P(\tau \mid \theta) = P(\tau \mid \theta) \, \nabla_\theta \log P(\tau \mid \theta)$,得到常用的结果:

$$ > \nabla_\theta P(\tau \mid \theta) = P(\tau \mid \theta) \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t). > $$

3. 轨迹的对数概率 (Log-Probability of a Trajectory)。 一条轨迹的对数概率就是:

$$ log P(τ|θ) = log ρ_0(s_0) + \sum_{t=0}^{T} \left( log P(s_{t+1}|s_t, a_t) + log π_θ(a_t|s_t) \right). $$

4. 环境函数的梯度 (Gradients of Environment Functions)。 环境与参数 $θ$ 无关,因此 $ρ_0(s_0)$, $P(s_{t+1}|s_t, a_t)$ 和 $R(τ)$ 的梯度均为零。

5. 轨迹的对数概率梯度 (Grad-Log-Prob of a Trajectory)。 因此,轨迹的对数概率梯度为:

$$ \begin{align} ∇_θ log P(τ|θ) &= ∇_θ log ρ_0(s_0) + \sum_{t=0}^{T} \left( ∇_θ log P(s_{t+1}|s_t, a_t) + ∇_θ log π_θ(a_t|s_t) \right) \\ &= \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t). \end{align} $$

将所有这些整合在一起,进行如下推导:

$$ \begin{align} ∇_θ J(π_θ) &= ∇_θ E_{τ∼π_θ} [R(τ)] & \\ &= ∇_θ \int_{τ} P(τ|θ) R(τ) & \text{展开期望} \\ &= \int_{τ} ∇_θ P(τ|θ) R(τ) & \text{将梯度移入积分内} \\ &= \int_{τ} P(τ|θ) ∇_θ log P(τ|θ) R(τ) & \text{对数导数技巧} \\ &= E_{τ∼π_θ} [∇_θ log P(τ|θ) R(τ)] & \text{返回期望形式} \\ \therefore ∇_θ J(π_θ) &= E_{τ∼π_θ} \left[ \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) R(τ) \right] & \text{展开对数概率梯度} \end{align} $$

一个连续随机变量 $X$ 的期望 $\mathbb{E}[X]$ 定义为:

$$ > \mathbb{E}[X] = \int x \, p(x) \, dx > $$

这里的积分范围是 $x$ 所有可能的取值。

同样,$X$ 的某个函数 $g(X)$ 的期望 $\mathbb{E}[g(X)]$ 定义为:

$$ > \mathbb{E}[g(X)] = \int g(x) \, p(x) \, dx > $$

问题:在推导的第 3 步

$$ > \int_\tau \nabla_\theta P(\tau \mid \theta) \, R(\tau) \, d\tau, > $$

我们遇到了一个巨大的障碍。$P(\tau \mid \theta)$ 依赖于环境动态 $P(s_{t+1} \mid s_t, a_t)$。在绝大多数现实问题中(如机器人控制、游戏 AI),我们根本不知道环境的精确数学模型。因此,我们无法直接计算 $\nabla_\theta P(\tau \mid \theta)$。

解决方案:最终的公式中,$P(s_{t+1} \mid s_t, a_t)$ 这一项完全消失了!梯度计算只剩下 $\nabla_\theta \log \pi_\theta(a_t \mid s_t)$,这只与我们自己的策略 $\pi_\theta$ 有关,而策略是我们自己设计的(比如一个神经网络),所以它的梯度是可计算的。这意味着,我们不需要知道环境如何工作,只需要在其中互动、收集数据,就可以优化我们的策略。

这是一个期望值,意味着可以用样本均值来估计它。如果收集了一组轨迹 $D = \{τ_i\}_{i=1,...,N}$,其中每条轨迹都是通过让智能体在环境中使用策略 $π_θ$ 行动获得的,那么策略梯度可以用以下方式估计:

$$ \hat{g} = \frac{1}{|D|} \sum_{τ \in D} \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) R(τ), $$

其中 $|D|$ 是轨迹集 $D$ 中的轨迹数量(这里是 $N$)。

上面这个最终表达式就是所期望的、最简洁的可计算表达式。假设策略的表示方式允许计算 $∇_θ log π_θ(a|s)$,并且能够在环境中运行该策略来收集轨迹数据集,那么就可以计算出策略梯度并执行更新步骤。

实现最简单的策略梯度

提供一个此版简单策略梯度算法的简短 PyTorch 实现,位于 spinup/examples/pytorch/pg_math/1_simple_pg.py

    # make environment, check spaces, get obs / act dims
    env = gym.make(env_name)
    assert isinstance(env.observation_space, Box), \
        "This example only works for envs with continuous state spaces."
    assert isinstance(env.action_space, Discrete), \
        "This example only works for envs with discrete action spaces."
    obs_dim = env.observation_space.shape[0]
    #obs_dim (observation dimension) 这个变量存储了状态向量的长度。在这里,obs_dim 的值会是 4。
    #这个数字将用来定义神经网络的输入层大小。我们的网络必须有4个输入神经元,才能接收 CartPole-v1 的状态。
    n_acts = env.action_space.n
    #n_acts (number of actions) 这个变量存储了可选动作的总数。在这里,n_acts 的值会是 2。
    #这个数字将用来定义神经网络的输出层大小。我们的网络必须有2个输出神经元,分别对应“向左”和“向右”这两个动作的概率(或 logits)。
  • 一维的 Box:就是一个线段,由 low (最低值) 和 high (最高值) 定义。这就像一个滑块的轨道。Box(low=0, high=1, shape=(1,))。
  • 二维的 Box:就是一个矩形,由左下角 [low_x, low_y] 和右上角 [high_x, high_y] 定义。你可以选择这个矩形内的任何一个点。Box(low=np.array([0,0]), high=np.array([1,1]), shape=(2,))。
  • N 维的 Box:就是一个超立方体 (Hyperrectangle)。它由 N 个维度的 low 和 high 边界定义。你可以选择这个“盒子”内部的任何一个点。

1. 构建策略网络

30  # make core of policy network
31  logits_net = mlp(sizes=[obs_dim]+hidden_sizes+[n_acts])
32  
33  # make function to compute action distribution
34  def get_policy(obs):
35      logits = logits_net(obs)
36      return Categorical(logits=logits)
37  #接收这些 logits,并在内部将它们转换成一个真正的概率分布。
38  # make action selection function (outputs int actions, sampled from policy)
39  def get_action(obs):
40      return get_policy(obs).sample().item()
41  # 随机采样输出数值

此代码块构建了用于前馈神经网络分类策略的模块和函数。logits_net 模块的输出可用于构造动作的对数概率和概率,而 get_action 函数则根据从 logits 计算出的概率来抽样动作。(注意:这个特定的 get_action 函数假定只提供一个 obs,因此只有一个整数动作输出。这就是它使用 .item() 的原因,该方法用于获取只有一个元素的 Tensor 的内容。)

这个例子中的很多工作都是由 L36 行的 Categorical 对象完成的。这是一个 PyTorch 的 Distribution 对象,它封装了与概率分布相关的一些数学函数。特别地,它有一个从分布中采样的方法(在 L40 行使用)和一个计算给定样本的对数概率的方法(稍后会使用)。

2. 构建损失函数

42  # make loss function whose gradient, for the right data, is policy gradient
43  def compute_loss(obs, act, weights):
44      logp = get_policy(obs).log_prob(act)
45      return -(logp * weights).mean()

步骤 1: get_policy(obs) - 得到概率分布

这部分本身又分为两小步:

1a. logits_net(obs) - 神经网络计算 logits

  • obs 张量被送入神经网络 logits_net。

  • 网络对批次中的每一个状态向量进行前向传播计算。

  • 因为动作空间大小为 2,所以网络的输出层有 2 个神经元。

  • 输出是一个形状为 (3, 2) 的张量,每一行都对应一个状态的 logits。

    logits = logits_net(obs)
    

    假设 logits 的计算结果是:

    logits = tensor([
        [-0.5,  1.2],  # 对应 状态 1 的 logits
        [ 0.8,  0.1],  # 对应 状态 2 的 logits
        [-1.0, -0.8]   # 对应 状态 3 的 logits
    ])
    
    • 解读第一行 [-0.5, 1.2]: 在状态1下,网络认为动作1 (向右) 的“分数” (1.2) 高于动作0 (向左) 的分数 (-0.5)。
    • 解读第二行 [0.8, 0.1]: 在状态2下,网络认为动作0的分数 (0.8) 略高于动作1的分数 (0.1)。

1b. Categorical(logits=logits) - 创建分布对象

  • 这个 logits 张量被传递给 Categorical。
  • Categorical 会在内部对 logits 的每一行独立地应用 Softmax 函数,将它们转换为概率。
    • 对于状态 1: softmax([-0.5, 1.2]) -> [0.15, 0.85] (大约)
    • 对于状态 2: softmax([0.8, 0.1]) -> [0.67, 0.33] (大约)
    • 对于状态 3: softmax([-1.0, -0.8]) -> [0.45, 0.55] (大约)
  • 最终,get_policy(obs) 返回一个 Categorical 分布对象,我们称之为 dist。这个 dist 对象现在封装了一整个批次的概率分布。你可以把它想象成一个包含了 3 个独立骰子的集合,每个骰子都有不同的概率偏向。

步骤 2: .log_prob(act) - 查询对数概率

现在,我们有了代表策略的概率分布 dist,以及我们实际执行过的动作 act。

  • 操作: dist.log_prob(act)
  • 含义: “嘿,dist 对象,请告诉我,根据你内部的概率分布,我实际执行的这些动作 act 的对数概率分别是多少?”

这个操作会进行元素级 (element-wise) 的查询

  • 对于第一个数据点 (状态1):
    • dist 中对应的概率是 [0.15, 0.85]。
    • act 中对应的动作是 1。
    • 查询动作 1 的概率,是 0.85。
    • 计算其对数:log(0.85) ≈ -0.16。
  • 对于第二个数据点 (状态2):
    • dist 中对应的概率是 [0.67, 0.33]。
    • act 中对应的动作是 0。
    • 查询动作 0 的概率,是 0.67。
    • 计算其对数:log(0.67) ≈ -0.40。
  • 对于第三个数据点 (状态3):
    • dist 中对应的概率是 [0.45, 0.55]。
    • act 中对应的动作是 1。
    • 查询动作 1 的概率,是 0.55。
    • 计算其对数:log(0.55) ≈ -0.60。
$$log π(act | obs)$$

计算过程:

log π(act | obs) 的计算原理是用当前的网络重新评估一次历史行为发生的概率,然后取对数

让我们用一个单数据点的例子来完整走一遍流程,假设环境是 CartPole(动作0:左,1:右)。

输入:

  • obs = tensor([0.1, -0.2, 0.0, 0.3]) (某个历史状态)
  • act = 1 (当时在这个状态下,我们实际采取的动作是“向右”)
  • logits_net (我们当前的神经网络)

步骤 1: 将 obs 输入网络,得到 Logits

网络进行一次前向传播,输出每个动作的原始分数。

logits = logits_net(obs)

假设输出是 logits = tensor([-0.5, 1.2])。

  • logits[0] = -0.5 是动作0(向左)的分数。
  • logits[1] = 1.2 是动作1(向右)的分数。 这些分数本身不是概率,但它们的大小关系代表了网络对动作的偏好。

步骤 2: 将 Logits 转换为概率 (Softmax)

为了得到一个有效的概率分布(所有概率相加为1),我们需要对 Logits 应用 Softmax 函数。

$$P(action_i) = exp(logit_i) / Σ exp(logit_j)$$$$P(action=0) = exp(-0.5) / (exp(-0.5) + exp(1.2)) > = 0.606 / (0.606 + 3.320) > = 0.606 / 3.926 > ≈ 0.154$$$$P(action=1) = exp(1.2) / (exp(-0.5) + exp(1.2)) > = 3.320 / (0.606 + 3.320) > = 3.320 / 3.926 > ≈ 0.846$$

现在我们得到了一个概率分布 [0.154, 0.846]。这就是 π( . | obs),它代表了以当前网络的“视角”来看,在 obs 状态下,采取各个动作的概率。 注意: 这一步在代码中是由 Categorical(logits=logits) 隐式完成的,你不需要手动计算。

步骤 3: 选取实际动作对应的概率

我们的历史记录告诉我们,当时实际采取的动作 act 是 1(向右)。 所以,我们从上一步计算出的概率分布中,选取动作 1 对应的概率。

π(act | obs) = P(action=1) = 0.846

步骤 4: 取自然对数 (Natural Logarithm)

最后一步,我们计算这个概率的自然对数。

log π(act | obs) = log(0.846) ≈ -0.167

这就是最终的结果!get_policy(obs).log_prob(act) 这行代码,对于这一对 (obs, act),最终计算出的值就是 -0.167。

在这个代码块中,构建了一个策略梯度算法的“损失”函数。当输入正确的数据时,这个损失的梯度就等于策略梯度。所谓正确的数据,指的是一组在当前策略下行动时收集的 (状态, 动作, 权重) 元组,其中状态-动作对的权重是它所属回合的回报。(尽管在后续小节中会展示,也可以用其他值作为权重,并且同样有效。)

尽管称之为损失函数,但它并不是传统监督学习意义上的损失函数。与标准损失函数有两个主要区别。

1. 数据分布依赖于参数。 损失函数通常定义在一个固定的数据分布上,这个分布独立于我们希望优化的参数。但这里不是这样,数据必须从最新的策略中采样得到。

监督学习:你的训练数据集(比如ImageNet)是固定不变的。无论你的模型参数 θ 是什么,你都用同一批图片来计算损失和梯度。你的目标很明确:在这个固定的数据集上表现得更好。

策略梯度:你的“数据集”(即通过与环境互动收集的轨迹)是由你当前的策略 πθ 生成的。当你更新了参数 θ 变成 θ’,你用来评估策略的“数据集”的分布也随之改变了,因为新策略 πθ’ 会让你访问到不同的状态,采取不同的动作。

2. 它不衡量性能。 损失函数通常评估我们关心的性能指标。在这里,我们关心的是期望回报 $J(π_θ)$,但这个“损失”函数即使在期望意义上也完全不近似于它。这个“损失”函数之所以有用,仅仅是因为当用当前参数生成的数据在当前参数下进行评估时,它具有与性能的负梯度相同的方向。

真正的优化目标:

$$ > J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ R(\tau) \right] > $$

REINFORCE 的 loss(只是为了调用自动微分框架):

$$ > L(\theta) = - \mathbb{E}_{(s,a) \sim \pi_\theta} \left[ \log \pi_\theta(a \mid s) \cdot R(\tau) \right] > $$

关键关系:

$$ > \nabla_\theta L(\theta) = - \nabla_\theta J(\theta) > $$
  • 梯度方向相反 → 能用于改进策略
  • 函数值完全不同 → 无任何性能意义
  • 更没有单调性!

若想用 $L(\theta)$ 直接逼近 $-J(\theta)$,必须满足:

$$ > L(\theta) = -J(\theta) + \text{常数} > $$

但它们依赖不同的分布:

分布依赖
$J(\theta)$取样于 $\pi_\theta$ 的真实轨迹分布
$L(\theta)$固定于给定 batch 的旧轨迹分布

这意味着:

当 $\theta$ 更新时,数据分布变了,但 loss 仍在衡量旧数据 —— 其数值变得毫无参考意义。

但是,在第一步梯度下降之后,它与性能之间就再无关联。这意味着,对于给定的数据批次,最小化这个“损失”函数完全不保证能提高期望回报。你可以将这个损失降到 $-∞$,而策略性能可能会崩溃;事实上,通常会这样。有时,深度强化学习的研究者可能会将这种情况描述为策略对一批数据“过拟合”。这种说法很形象,但不应从字面上理解,因为它指的不是泛化误差。

提出这一点是因为机器学习从业者通常会将损失函数在训练过程中的下降视为一个有用的信号——“如果损失下降了,一切都好。” 在策略梯度中,这种直觉是错误的,唯一需要关心的指标是平均回报。损失函数本身没有任何意义。

此处用于生成 logp 张量的方法——调用 PyTorch Categorical 对象的 log_prob 方法——可能需要一些修改才能适用于其他类型的分布对象。

例如,如果你正在使用 Normal 分布(用于对角高斯策略),调用 policy.log_prob(act) 的输出将是一个 Tensor,其中包含每个向量值动作的每个分量的独立对数概率。也就是说,你输入一个形状为 (batch, act_dim) 的 Tensor,然后得到一个形状为 (batch, act_dim) 的 Tensor,而构建强化学习损失函数所需要的是一个形状为 (batch,) 的 Tensor。在这种情况下,你需要将动作分量的对数概率相加,以得到动作的对数概率。也就是说,你需要计算:

logp = get_policy(obs).log_prob(act).sum(axis=-1)
  1. 离散动作空间 (Discrete Action Space)
    • 例子: CartPole(向左/向右),Atari游戏(上/下/左/右/开火…)。
    • 动作表示: 一个单独的整数,如 0, 1, 2。
    • 策略输出: 策略网络输出每个动作的logits,然后用 Categorical 分布来处理。
    • log_prob(act) 的结果:对于每个样本,返回一个标量 (scalar),即单个动作的对数概率。
  2. 连续动作空间 (Continuous Action Space)
    • 例子: Hopper(控制3个关节的力矩),机器人手臂控制。
    • 动作表示: 一个包含多个浮点数的向量,如 [0.5, -0.8, 0.1]。
    • 策略输出: 策略网络通常输出一个多元高斯分布 (Multivariate Gaussian Distribution) 的参数(均值μ和标准差σ)。然后用 torch.distributions.Normal 或 MultivariateNormal 来表示这个分布。
    • log_prob(act) 的结果:这里就是问题的关键所在!

3. 运行一轮训练

50  # for training policy
51  def train_one_epoch():
52      # make some empty lists for logging.
53      batch_obs = []          # for observations
54      batch_acts = []         # for actions
55      batch_weights = []      # for R(tau) weighting in policy gradient
56      batch_rets = []         # for measuring episode returns
57      batch_lens = []         # for measuring episode lengths
58
59      # reset episode-specific variables
60      obs = env.reset()       # first obs comes from starting distribution
61      done = False            # signal from environment that episode is over
62      ep_rews = []            # list for rewards accrued throughout ep
63
64      # render first episode of each epoch
65      finished_rendering_this_epoch = False
66
67      # collect experience by acting in the environment with current policy
68      while True:
69
70          # rendering
71          if (not finished_rendering_this_epoch) and render:
72              env.render()
73
74          # save obs
75          batch_obs.append(obs.copy())
76
77          # act in the environment
78          act = get_action(torch.as_tensor(obs, dtype=torch.float32))
79          obs, rew, done, _ = env.step(act)
80
81          # save action, reward
82          batch_acts.append(act)
83          ep_rews.append(rew)
84
85          if done:
86              # if episode is over, record info about episode
87              ep_ret, ep_len = sum(ep_rews), len(ep_rews)
88              batch_rets.append(ep_ret)
89              batch_lens.append(ep_len)
90
91              # the weight for each logprob(a|s) is R(tau)
92              batch_weights += [ep_ret] * ep_len
93
94              # reset episode-specific variables
95              obs, done, ep_rews = env.reset(), False, []
96
97              # won't render again this epoch
98              finished_rendering_this_epoch = True
99
100             # end experience loop if we have enough of it
101             if len(batch_obs) > batch_size:
102                 break
103
104     # take a single policy gradient update step
105     optimizer.zero_grad()
106     batch_loss = compute_loss(obs=torch.as_tensor(batch_obs, dtype=torch.float32),
107                                act=torch.as_tensor(batch_acts, dtype=torch.int32),
108                                weights=torch.as_tensor(batch_weights, dtype=torch.float32)
109                                )
110     batch_loss.backward()
111     optimizer.step()
112     return batch_loss, batch_rets, batch_lens

train_one_epoch() 函数运行策略梯度的一个“轮次”(epoch),这里定义为:

  1. 经验收集步骤 (L67-102),智能体使用最新的策略在环境中行动一定数量的回合,
  2. 接着是一个单一的策略梯度更新步骤 (L104-111)。

算法的主循环只是重复调用 train_one_epoch()

如果你对 PyTorch 中的优化还不熟悉,请注意在 104-111 行中执行一次梯度下降步骤的模式。首先,清除梯度缓冲区。然后,计算损失函数。接着,对损失函数执行反向传播;这将新的梯度累积到梯度缓冲区中。最后,用优化器执行一步更新。

期望梯度对数概率引理 (Expected Grad-Log-Prob Lemma)

EGLP 引理。 假设 $P_θ$ 是一个关于随机变量 $x$ 的参数化概率分布。那么:

$$ E_{x \sim P_θ} [∇_θ log P_θ(x)] = 0. $$

证明

回想一下,所有概率分布都是归一化的:

$$ \int_x P_θ(x) = 1. $$

对归一化条件的两边求梯度:

$$ ∇_θ \int_x P_θ(x) = ∇_θ 1 = 0. $$

使用对数导数技巧得到:

$$ \begin{align} 0 &= ∇_θ \int_x P_θ(x) \\ &= \int_x ∇_θ P_θ(x) \\ &= \int_x P_θ(x) ∇_θ log P_θ(x) \\ \therefore 0 &= E_{x \sim P_θ} [∇_θ log P_θ(x)]. \end{align} $$

别让过去分散你的注意力

审视一下最近得到的策略梯度表达式:

$$ ∇_θ J(π_θ) = E_{τ∼π_θ} \left[ \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) R(τ) \right]. $$

使用这个梯度进行一步更新,会以与 $R(τ)$(即所有获得过的奖励之和)成正比的方式,提升每个动作的对数概率。但这并没有太大意义。

智能体真正应该做的,是基于一个动作的后果来强化它。在采取一个动作之前获得的奖励,与该动作的好坏无关:只有之后获得的奖励才重要。

事实证明,这种直觉在数学上也是成立的,并且可以证明策略梯度也可以表示为:

$$ ∇_θ J(π_θ) = E_{τ∼π_θ} \left[ \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) \right]. $$

在这种形式下,动作只根据它们之后获得的奖励进行强化。

“一个动作的好坏,只应该由它未来的后果来评判。”

  • Causality (因果性): 过去的事件无法被未来的行为所改变。因此,在 $t$ 时刻做出的动作 $a_t$,不应该对 $t$ 时刻之前已经获得的奖励 $r_1, r_2, \dots, r_{t-1}$ 负责。
  • Credit Assignment (功劳分配): 我们应该把功劳(或过失)精确地分配给导致它的那个动作。动作 $a_t$ 的“功劳”应该是从它发生之后,一直到回合结束所获得的所有奖励。

基于上述直觉,数学家和研究者写成一种更好的形式:

$$ > \nabla_\theta J(\pi_\theta) = \mathbb{E} \left[ \sum_{t=0}^{T} \left( \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot \hat{R}_t \right) \right] > $$
  • 关键变化: $R(\tau)$ 被 $\hat{R}_t$替换了。

$\hat{R}_t$ 是什么?

$$ > \hat{R}_t = \sum_{t'=t}^{T} r_{t'+1} > $$

(注意:奖励下标通常比状态和动作晚一个,即在 $(s_t, a_t)$ 之后获得 $r_{t+1}$。)

它被称为 “未来回报 (Reward-to-Go)”,代表了从时间步 $t$ 之后一直到回合结束,所能获得的所有奖励的总和。

我们将这种形式称为“奖励到未来 (reward-to-go)”策略梯度,因为在轨迹中某一点之后的所有奖励之和,

$$ \hat{R}_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}), $$

被称为从该点开始的奖励到未来,而这个策略梯度表达式依赖于状态-动作对的未来奖励。

但这为什么更好呢?策略梯度的一个关键问题是,需要多少样本轨迹才能获得一个低方差的样本估计。我们开始时使用的公式包含了与过去奖励成比例的强化项,这些项的均值为零,但方差非零:因此,它们只会给策略梯度的样本估计增加噪声。通过移除它们,可以减少估计策略梯度所需的样本轨迹数量。

这一论断的一个证明可以在这里找到,它最终依赖于 EGLP 引理。

实现“奖励到未来”策略梯度

提供一个“奖励到未来”策略梯度的简短 PyTorch 实现,位于 spinup/examples/pytorch/pg_math/2_rtg_pg.py

1_simple_pg.py 相比唯一的变化是,现在在损失函数中使用了不同的权重。代码修改非常微小:增加一个新函数,并更改另外两行。新函数是:

17  def reward_to_go(rews):
18      n = len(rews)
19      rtgs = np.zeros_like(rews)
20      for i in reversed(range(n)):
21          rtgs[i] = rews[i] + (rtgs[i+1] if i+1 < n else 0)
22      return rtgs

然后,将旧的 L91-92 行:

91      # the weight for each logprob(a|s) is R(tau)
92      batch_weights += [ep_ret] * ep_len

调整为:

98      # the weight for each logprob(a_t|s_t) is reward-to-go from t
99      batch_weights += list(reward_to_go(ep_rews))

策略梯度中的基线 (Baselines)

EGLP 引理的一个直接推论是,对于任何只依赖于状态的函数 $b$,

$$ E_{a_t \sim π_θ} [∇_θ log π_θ(a_t|s_t) b(s_t)] = 0. $$

这允许我们在策略梯度表达式中加或减任意数量这样的项,而不会改变其期望值:

$$ ∇_θ J(π_θ) = E_{τ∼π_θ} \left[ \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) \left( \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t) \right) \right]. $$

任何以这种方式使用的函数 $b$ 都被称为基线 (baseline)

最常见的基线选择是在策略价值函数 (on-policy value function) $V^π(s_t)$。回想一下,这是智能体从状态 $s_t$ 开始,然后在其余生中根据策略 $π$ 行动所能获得的平均回报。

经验上,选择 $b(s_t) = V^π(s_t)$ 具有降低策略梯度样本估计方差的理想效果。这会带来更快、更稳定的策略学习。从概念角度看,它也很有吸引力:它编码了一种直觉,即如果智能体得到的回报符合预期,它应该“感觉”中性。

实践中,$V^π(s_t)$ 无法被精确计算,因此必须对其进行近似。这通常通过一个神经网络 $V_ϕ(s_t)$ 来完成,它与策略网络同时更新(这样价值网络总是近似于最新策略的价值函数)。

学习 $V_ϕ$ 的最简单方法,也是在大多数策略优化算法(包括 VPG、TRPO、PPO 和 A2C)的实现中采用的方法,是最小化一个均方误差目标:

$$ > \phi_k = \arg \min_{\phi} E_{s_t, \hat{R}_t \sim \pi_k} \left[ (V_{\phi}(s_t) - \hat{R}_t)^2 \right], > $$

其中 $π_k$ 是在第 $k$ 轮的策略。这是通过一步或多步梯度下降完成的,从上一轮的价值参数 $\phi_{k-1}$ 开始。

为什么可以:

  1. 统计近似: 我们不是在求数学上的精确解,而是在做一个统计上的拟合。我们用有限的、带噪声的样本 $R̂_t$ 作为训练标签,来近似无限样本下的期望 $V^π(s_t)$
  2. 函数近似器的力量: 神经网络是一个强大的通用函数近似器。它能从杂乱的数据点中学习到潜在的、平滑的规律,并泛化到未见过的输入。
  3. 大数定律: 当我们使用足够多的样本时,$R̂_t$ 的平均值会收敛于其期望值 $V^π(s_t)$。通过最小化均方误差,神经网络实际上是在学习去预测这个平均值。
  4. 迭代改进: 在 Actor-Critic 框架中,价值函数和策略函数是同步迭代,共同进步的。即使每一步的拟合都不完美,但总体趋势是向着更优的策略和更准确的价值估计演进。

策略梯度的其他形式

到目前为止所看到的是,策略梯度具有如下通用形式

$$ ∇_θ J(π_θ) = E_{τ∼π_θ} \left[ \sum_{t=0}^{T} ∇_θ log π_θ(a_t|s_t) \Phi_t \right], $$

其中 $\Phi_t$ 可以是以下任何一种:

$$ \Phi_t = R(τ), $$

或者

$$ \Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}), $$

或者

$$ \Phi_t = \sum_{t'=t}^{T} R(s_{t'}, a_{t'}, s_{t'+1}) - b(s_t). $$
  1. $\Phi_t = R(\tau)$ (总回报)

    • 优点: 简单,理论上正确。
    • 缺点: 方差极大,学习效率低。不符合因果关系。
  2. $\Phi_t = \hat{R}_t$ (未来回报 / Reward-to-Go)

    • 优点: 引入了因果性,降低了方差。
    • 缺点: 仍然可能全是正数,导致“所有动作都被鼓励”的问题。
  3. $\Phi_t = \hat{R}_t - b(s_t)$ (未来回报 - 基线)

    • 优点: 引入了相对评价,进一步降低方差,使学习信号更清晰。

所有这些选择都导向同一个期望策略梯度,尽管它们的方差不同。事实证明,还有另外两个对权重 $\Phi_t$ 的有效选择,了解它们很重要。

1. 在策略动作价值函数 (On-Policy Action-Value Function)。 选择

$$ \Phi_t = Q^{π_θ}(s_t, a_t) $$

也是有效的。可参阅此页面获取该论断的证明。

2. 优势函数 (The Advantage Function)。 回想一下,一个动作的优势 (advantage),定义为 $A^π(s_t, a_t) = Q^π(s_t, a_t) - V^π(s_t)$,描述了该动作比(相对于当前策略的)平均水平好或差多少。这个选择,

$$ \Phi_t = A^{π_θ}(s_t, a_t) $$

也是有效的。其证明等同于使用 $\Phi_t = Q^{π_θ}(s_t, a_t)$,然后使用一个价值函数基线,这是我们总是可以自由做的。

使用优势函数的策略梯度形式非常普遍,并且有许多不同的方法来估计不同算法中使用的优势函数。

要更详细地了解这个主题,应该阅读关于泛化优势估计 (Generalized Advantage Estimation, GAE) 的论文,该论文在背景部分深入探讨了 $\Phi_t$ 的不同选择。

该论文接着描述了 GAE,一种在策略优化算法中近似优势函数的方法,并得到了广泛应用。例如,Spinning Up 的 VPG、TRPO 和 PPO 的实现都使用了它。因此,强烈建议学习它。

优势函数是核心,而学习GAE是如何精确且高效地估计优势函数的

回顾总结

在本章中,描述了策略梯度方法的基本理论,并将一些早期成果与代码示例联系起来。可以从这里继续学习,研究后续成果(价值函数基线和策略梯度的优势函数形式)如何转化为 Spinning Up 的 Vanilla Policy Gradient 的实现。