3. 策略梯度方法(PG):从理论到实践的避坑指南

策略梯度方法,说白了就是让智能体自己学会「什么动作该多做,什么动作该少做」。我刚开始接触这个算法时,总觉得它像个黑盒子——明明没有价值函数,怎么就能直接优化策略呢?后来踩了几个坑才明白,核心就在那个梯度公式里。

3.1 策略梯度定理推导

我们先从最直观的角度理解。假设你有一个策略 π(a|s),参数是 θ。你的目标是最大化期望累积奖励 J(θ)。

那么梯度怎么算?直接对 J(θ) 求导?不行,因为期望里面有个「采样」过程,没法直接求导。这里就要用到那个经典的技巧——log 导数技巧。

核心公式:

∇J(θ) = E[∇log π(a|s) * Q(s,a)]

这个公式的意思是:用当前策略采样,如果某个动作的 Q 值高,就增加它的概率;反之就减少。

推导过程其实不复杂。我习惯这样记:

  1. 先把 J(θ) 写成积分形式
  2. 把梯度挪到积分里面
  3. 用 ∇log π = ∇π / π 这个恒等式
  4. 最后就得到了那个漂亮的期望形式

嗯,这里要注意:推导中假设了策略是可微的。如果你用离散动作,softmax 策略就很合适。连续动作的话,高斯策略是标配。

3.2 REINFORCE 算法实现

REINFORCE 是最朴素的策略梯度算法。它的思路很简单:采样完整轨迹,然后用整条轨迹的累积奖励作为 Q 值的估计。

我曾经在项目中用 REINFORCE 训练一个简单的 CartPole 任务,结果发现收敛特别慢。后来才意识到,问题出在「整条轨迹」上——如果轨迹很长,方差会爆炸。

下面是一个最小实现:

def reinforce(env, policy, episodes=1000):
    for ep in range(episodes):
        states, actions, rewards = [], [], []
        state = env.reset()
        done = False
        
        # 采样一条完整轨迹
        while not done:
            action = policy.sample(state)
            next_state, reward, done, _ = env.step(action)
            states.append(state)
            actions.append(action)
            rewards.append(reward)
            state = next_state
        
        # 计算累积奖励
        G = 0
        returns = []
        for r in reversed(rewards):
            G = r + 0.99 * G
            returns.insert(0, G)
        
        # 更新策略
        for s, a, G in zip(states, actions, returns):
            policy.update(s, a, G)

避坑指南:我曾经在实现时忘记对奖励做归一化,结果梯度爆炸,策略直接崩了。建议每次更新前,把 returns 做标准化处理。

3.3 Baseline 技巧

为什么要加 Baseline?说白了就是为了降低方差。你想想看,如果所有动作的 Q 值都是正数,那策略更新时所有动作的概率都会增加——这显然不合理。

Baseline 的核心思想是:从 Q 值中减去一个基准值,让部分动作的「优势」变成负数,这样就能抑制不好的动作。

最常用的 Baseline 是状态价值函数 V(s)。于是就有了:

∇J(θ) = E[∇log π(a|s) * (Q(s,a) - V(s))]

这里的 Q(s,a) - V(s) 就是优势函数 A(s,a)。

我的经验:Baseline 不需要太精确。我试过用简单的线性函数做 Baseline,效果已经比不用好很多。关键是它要能捕捉到状态的基本价值水平。

3.4 方差缩减方法

方差缩减是策略梯度方法的核心痛点。我总结了几种常用方法:

方法 原理 我的评价
Baseline 减去状态价值 简单有效,必用
TD 误差 用时序差分代替整条轨迹 方差小但有偏差
GAE 加权平均多个 TD 误差 平衡方差和偏差,推荐
重要性采样 用旧策略数据更新新策略 小心权重爆炸

我个人最常用的是 GAE(Generalized Advantage Estimation)。它的公式长这样:

def compute_gae(rewards, values, gamma=0.99, lam=0.95):
    gae = 0
    advantages = []
    for t in reversed(range(len(rewards))):
        delta = rewards[t] + gamma * values[t+1] - values[t]
        gae = delta + gamma * lam * gae
        advantages.insert(0, gae)
    return advantages

这里的 λ 参数控制偏差和方差的权衡。λ=0 时就是 TD 误差,λ=1 时就是蒙特卡洛估计。我一般取 0.95,效果比较稳定。

曾经踩过的坑:有一次我把 λ 设成了 0.99,结果方差太大,训练直接发散。后来改成 0.95 就好了。记住:λ 越大,方差越大,但偏差越小。要根据任务调。

3.5 知识体系总览

下面这张图总结了策略梯度方法的核心脉络:

策略梯度方法知识体系 策略梯度定理 REINFORCE 算法 Baseline 技巧 方差缩减方法 整条轨迹采样 累积奖励计算 状态价值函数 优势函数估计 GAE 方法 TD 误差 核心目标:在偏差和方差之间找到平衡

从这张图可以看出,策略梯度方法的核心就是围绕「如何更准确地估计梯度」展开的。REINFORCE 是最基础的版本,Baseline 和方差缩减方法则是在不同维度上做优化。

我的建议:初学者先从 REINFORCE 入手,跑通一个简单环境。然后加上 Baseline,看看效果提升。最后再尝试 GAE 等高级技巧。一步到位反而容易出问题。

好了,这一章的内容就到这里。策略梯度方法虽然看起来公式多,但核心思想其实很直观——用好动作的概率,抑制坏动作的概率。下一章我们会讲 Actor-Critic 方法,那是把策略梯度和价值学习结合起来的高级玩法。

可可精品资料库