跳转至

策略梯度(Policy Gradient)

DQN 系列算法通过学习 Q 值间接得到策略,但只能处理离散动作。策略梯度方法另辟蹊径——直接学习策略函数本身,自然地支持连续动作空间


一、为什么需要策略梯度?

1.1 基于价值方法的局限

局限 解释
只能处理离散动作 DQN 输出每个动作的 Q 值,取 \(\arg\max\)——如果动作是"方向盘转 37.5°"呢?
确定性策略 \(\arg\max\) 总是选一个固定动作,无法表达"石头剪刀布"中的随机最优策略
Q 值微小变化导致策略突变 两个动作的 Q 值差 0.001 就会导致选择完全不同的动作

1.2 策略梯度的思路

直接参数化策略:\(\pi_\theta(a \mid s)\) —— 一个神经网络,输入状态 \(s\),输出动作 \(a\) 的概率分布。

\[ \text{基于价值:} s \xrightarrow{\text{网络}} Q(s, a) \xrightarrow{\arg\max} a \]
\[ \text{基于策略:} s \xrightarrow{\text{网络}} \pi_\theta(a|s) \xrightarrow{\text{采样}} a \]

核心区别

  • 基于价值:我知道每条路的分数,选分数最高的
  • 基于策略:我直接学习"在这个路口应该以什么概率选哪条路"

二、策略梯度定理

2.1 优化目标

我们的目标是找到参数 \(\theta\),使得策略的期望回报最大化:

\[ J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} [R(\tau)] = \mathbb{E}_{\tau \sim \pi_\theta} \left[\sum_{t=0}^{T} \gamma^t r_t \right] \]

其中 \(\tau = (s_0, a_0, r_0, s_1, a_1, r_1, \ldots)\) 是一条完整轨迹。

2.2 策略梯度定理

\[ \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot G_t \right] \]

其中 \(G_t = \sum_{k=t}^{T} \gamma^{k-t} r_k\) 是从时间步 \(t\) 开始的折扣回报。

这个公式怎么理解?

  • \(\nabla_\theta \log \pi_\theta(a_t|s_t)\):策略对参数的敏感方向——"参数怎么调会让这个动作更可能被选中"
  • \(G_t\):这个动作之后实际拿到了多少回报

两者结合的含义:

  • 如果动作 \(a_t\) 之后拿到了高回报\(G_t > 0\)),就增大选这个动作的概率
  • 如果动作 \(a_t\) 之后拿到了低回报\(G_t < 0\)),就减小选这个动作的概率

好的动作变得更可能,坏的动作变得更不可能——这就是策略梯度的精髓。

2.3 直觉推导

整个推导的关键是一个数学技巧——对数微分技巧(Log-Derivative Trick)

回忆一下微积分中对数函数的求导公式:\(\nabla_x \log f(x) = \frac{\nabla_x f(x)}{f(x)}\)

所以有:

\[ \nabla_\theta \pi_\theta(\tau) = \pi_\theta(\tau) \cdot \nabla_\theta \log \pi_\theta(\tau) \]

于是:

\[ \nabla_\theta J = \nabla_\theta \mathbb{E}_\tau [R(\tau)] = \mathbb{E}_\tau \left[R(\tau) \cdot \nabla_\theta \log \pi_\theta(\tau) \right] \]

又因为轨迹概率中只有策略部分含 \(\theta\)(环境转移概率 \(P\)\(\theta\) 无关):

20260415163001

\[ \nabla_\theta \log \pi_\theta(\tau) = \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \]

三、REINFORCE 算法

REINFORCE 是最经典的策略梯度算法,也叫蒙特卡洛策略梯度。

3.1 算法流程

初始化策略网络 πθ
循环每个回合:
  1. 用 πθ 采样一条完整轨迹:τ = (s₀, a₀, r₀, s₁, a₁, r₁, ..., sₜ)
  2. 对每个时间步 t,计算回报 Gₜ = Σ γ^k · r_{t+k}
  3. 计算策略梯度:
     ∇θ J ≈ Σₜ ∇θ log πθ(aₜ|sₜ) · Gₜ
  4. 更新参数:θ ← θ + α · ∇θ J

3.2 代码实现

import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Categorical

class PolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, action_dim),
            nn.Softmax(dim=-1)
        )

    def forward(self, x):
        return self.net(x)

# 训练循环
policy = PolicyNetwork(state_dim, action_dim)
optimizer = optim.Adam(policy.parameters(), lr=1e-3)

for episode in range(num_episodes):
    states, actions, rewards = [], [], []
    state = env.reset()

    # 1. 采样完整轨迹
    while not done:
        probs = policy(torch.FloatTensor(state))
        dist = Categorical(probs)
        action = dist.sample()

        next_state, reward, done, _ = env.step(action.item())

        states.append(state)
        actions.append(action)
        rewards.append(reward)
        state = next_state

    # 2. 计算折扣回报
    returns = []
    G = 0
    for r in reversed(rewards):
        G = r + gamma * G
        returns.insert(0, G)
    returns = torch.FloatTensor(returns)

    # 3. 计算损失并更新
    log_probs = []
    for s, a in zip(states, actions):
        probs = policy(torch.FloatTensor(s))
        log_probs.append(Categorical(probs).log_prob(a))
    log_probs = torch.stack(log_probs)

    loss = -(log_probs * returns).mean()  # 梯度上升 = 最小化负的目标
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

四、高方差问题与解决方案

4.1 为什么 REINFORCE 方差大?

\(G_t\) 是整条轨迹的累积奖励,受到轨迹中每一步随机性的影响:

  • 动作选择是随机的
  • 环境转移是随机的
  • 奖励也可能有噪声

结果:不同轨迹的 \(G_t\) 差异巨大 → 梯度估计的方差很高 → 训练极不稳定,收敛极慢。

4.2 解决方案一:基线(Baseline)

引入一个基线 \(b(s_t)\),用 \(G_t - b(s_t)\) 替代 \(G_t\)

\[ \nabla_\theta J \approx \sum_t \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot (G_t - b(s_t)) \]

为什么基线不影响梯度的期望?

可以证明 \(\mathbb{E}[\nabla_\theta \log \pi_\theta(a|s) \cdot b(s)] = 0\),所以减去基线不改变梯度期望值,但显著降低方差

最常用的基线:状态价值函数 \(V(s_t)\)

\[ \nabla_\theta J \approx \sum_t \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot \underbrace{(G_t - V(s_t))}_{\text{优势函数 } A(s_t, a_t)} \]

直觉理解

不基线时:拿到 +100 分的动作→增大概率,拿到 +90 分的→也增大概率——好动作坏动作都往上推。

有基线时(比如基线=95):+100 → 增大概率(比平均好),+90 → 减小概率(比平均差)。信号清晰多了!

4.3 解决方案二:奖励归一化

\[ G_t \leftarrow \frac{G_t - \mu_G}{\sigma_G + \epsilon} \]

将回报标准化为均值 0、标准差 1,简单有效。

4.4 解决方案三:引入 Critic → Actor-Critic

用另一个神经网络来学习 \(V(s)\),作为基线——这就不再是纯策略梯度方法了,而是进入了 Actor-Critic 架构。


五、策略梯度方法对比总结

方法 使用的信号 方差 偏差 备注
REINFORCE \(G_t\)(完整回报) 无偏 最基础
+ Baseline \(G_t - b(s)\) 无偏 常用 \(b = V(s)\)
Actor-Critic \(r + \gamma V(s') - V(s)\) 有偏 下一章详解

六、离散动作 vs 连续动作的输出

网络输出各动作的概率,用 Categorical 分布采样:

probs = policy(state)           # [0.1, 0.7, 0.15, 0.05]
dist = Categorical(probs)
action = dist.sample()           # 采样一个离散动作
log_prob = dist.log_prob(action) # 用于计算梯度

网络输出高斯分布的均值 \(\mu\) 和标准差 \(\sigma\),用 Normal 分布采样:

mu, sigma = policy(state)        # 均值和标准差
dist = Normal(mu, sigma)
action = dist.sample()           # 采样一个连续值
log_prob = dist.log_prob(action) # 用于计算梯度

连续动作是策略梯度的天然优势

DQN 无法处理连续动作(你不可能输出无穷多个 Q 值),但策略梯度可以自然地输出连续分布。

DQN 的底层逻辑是“为每一个可能的动作打分(计算 Q 值),然后选得分最高的那一个”,如果动作无穷多,神经网络的输出层就需要有无限个神经元,这在物理上是不可能实现的。

策略梯度跳出了“给每个动作打分”的思维定势。它不输出具体的动作得分,而是输出一个概率分布的形状。无论动作空间有多么连续、多么复杂,神经网络最后一层只需要输出 两个数值:一个 \(\mu\),一个 \(\sigma\)


关键公式速查

名称 公式
优化目标 \(J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}[R(\tau)]\)
策略梯度定理 \(\nabla_\theta J = \mathbb{E}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot G_t\right]\)
带基线 \(\nabla_\theta J = \mathbb{E}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \cdot (G_t - b(s_t))\right]\)
优势函数 \(A(s, a) = Q(s, a) - V(s) \approx G_t - V(s_t)\)