策略梯度(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\) 的概率分布。
核心区别
- 基于价值:我知道每条路的分数,选分数最高的
- 基于策略:我直接学习"在这个路口应该以什么概率选哪条路"
二、策略梯度定理¶
2.1 优化目标¶
我们的目标是找到参数 \(\theta\),使得策略的期望回报最大化:
其中 \(\tau = (s_0, a_0, r_0, s_1, a_1, r_1, \ldots)\) 是一条完整轨迹。
2.2 策略梯度定理¶
其中 \(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)}\)
所以有:
于是:
又因为轨迹概率中只有策略部分含 \(\theta\)(环境转移概率 \(P\) 与 \(\theta\) 无关):

三、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\):
为什么基线不影响梯度的期望?
可以证明 \(\mathbb{E}[\nabla_\theta \log \pi_\theta(a|s) \cdot b(s)] = 0\),所以减去基线不改变梯度期望值,但显著降低方差。
最常用的基线:状态价值函数 \(V(s_t)\)
直觉理解
不基线时:拿到 +100 分的动作→增大概率,拿到 +90 分的→也增大概率——好动作坏动作都往上推。
有基线时(比如基线=95):+100 → 增大概率(比平均好),+90 → 减小概率(比平均差)。信号清晰多了!
4.3 解决方案二:奖励归一化¶
将回报标准化为均值 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 分布采样:
网络输出高斯分布的均值 \(\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)\) |