第4章 策略梯度方法:REINFORCE算法推导、策略梯度定理、Baseline设计与方差缩减

好,咱们进入策略梯度这部分。说实话,这是我觉得强化学习里最优雅的一块内容之一。为什么?因为它直接对策略本身下手,不像值函数方法那样绕个弯子。

4.1 策略梯度定理——核心思想

先问个问题:我们为什么要用策略梯度?

你想想看,值函数方法(比如DQN)有个硬伤——它只能处理离散动作。连续控制怎么办?策略梯度直接输出动作概率分布,天然适合连续空间。我在做机器人抓取项目时,一开始用DQN,动作空间一离散化,精度就上不去。后来换成策略梯度,效果立竿见影。

策略梯度定理说白了就是:目标函数对策略参数的梯度,可以写成期望形式

数学上,我们定义目标函数:

J(θ) = E[ Σ γ^t * r_t ]   (从初始状态开始的累计回报期望)

策略梯度定理告诉我们:

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

这个公式看着简单,但背后有个关键点——它把梯度计算转化成了采样估计。你不需要知道环境模型,只需要采样轨迹,就能更新策略。这就是基于学习的端到端控制的核心优势。

重要理解:∇log π(a|s,θ) 是「得分函数」,它告诉我们在当前状态下,往哪个方向调整参数能增加动作的概率。Q(s,a) 则是「权重」,好的动作权重高,坏的动作权重低。

4.2 REINFORCE算法——最朴素的实现

REINFORCE是策略梯度里最基础的算法。我习惯叫它「蒙特卡洛策略梯度」。为什么?因为它用整条轨迹的回报来估计Q值。

算法流程其实很直观:

  1. 用当前策略 π(θ) 采样一条完整轨迹
  2. 计算每个时间步的累计回报 G_t
  3. 用 G_t 作为 Q(s_t, a_t) 的估计,更新策略参数

代码实现大概长这样:

def reinforce(env, policy, lr=0.001, gamma=0.99):
    for episode in range(1000):
        # 采样轨迹
        states, actions, rewards = [], [], []
        state = env.reset()
        done = False
        while not done:
            action = policy.sample_action(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 + gamma * G
            returns.insert(0, G)
        
        # 更新策略
        for s, a, G in zip(states, actions, returns):
            # ∇log π(a|s) * G
            policy.update(s, a, G, lr)

嗯,这里要注意:REINFORCE用的是整条轨迹的回报,这意味着它必须等到episode结束才能更新。我做过一个实验,在仿真环境里跑没问题,但换到真实机器人上,每次都要跑完完整轨迹才能更新,效率太低了。

避坑指南:我曾经在真实机器人上直接套用REINFORCE,结果发现采样效率极低。真实环境不像仿真,你不能随便跑几千个episode。后来我改用Actor-Critic架构,才解决了这个问题。

4.3 Baseline设计——方差缩减的关键

REINFORCE有个大问题——方差太大。为什么?因为单条轨迹的回报 G_t 波动很大。你想想看,同样的策略,这次运气好拿到高分,下次运气差拿到低分,梯度方向可能完全相反。

解决办法是什么?引入Baseline。

策略梯度定理可以改写为:

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

其中 b(s) 是Baseline,它只依赖于状态s,不依赖于动作a。数学上可以证明,引入Baseline不会改变梯度的期望值,但能降低方差。

最常用的Baseline是什么?状态值函数 V(s)

这时候,Q(s,a) - V(s) 就是优势函数 A(s,a)。它衡量的是「在这个状态下,选这个动作比平均好多少」。

直观理解:如果Baseline是平均表现,那么优势函数就是「超额收益」。正的优势意味着这个动作比平均水平好,应该增加概率;负的优势则相反。

我个人的习惯是,用同一个网络同时估计策略和值函数。这样既节省参数,又能让两者共享特征提取层。当然,这需要小心处理梯度冲突的问题。

4.4 方差缩减的实用技巧

除了Baseline,还有几个我常用的方差缩减技巧:

技巧 原理 我的经验
折扣因子 γ 降低远期回报的权重 γ=0.99 是常见选择,但任务周期短时可以设小点
GAE (广义优势估计) 平衡偏差和方差 λ=0.95 效果不错,我一般从0.9开始调
多个轨迹平均 并行采样取平均梯度 能并行就并行,方差直接除以√N
梯度裁剪 限制梯度范数 max_norm=1.0 是安全起点

说到GAE,我得多说两句。GAE的核心思想是:用多个时间步的TD误差加权平均来估计优势。公式是:

A_t = Σ (γλ)^l * δ_{t+l}

其中 δ_t = r_t + γV(s_{t+1}) - V(s_t) 是TD误差。当λ=0时,GAE退化为单步TD误差;当λ=1时,退化为蒙特卡洛回报。λ在中间取值,就能平衡偏差和方差。

实用建议:如果你刚开始调参,我建议先用GAE λ=0.95。这个值在大多数任务上表现稳健。我在机械臂控制任务上试过,从λ=0.9调到0.99,效果差异不大,但0.95是最稳定的。

4.5 实际项目中的坑与经验

最后分享几个我在项目中踩过的坑:

  • 学习率太敏感:策略梯度对学习率特别敏感。我习惯用Adam优化器,初始学习率设在1e-4到3e-4之间。如果训练不稳定,先降学习率。
  • 奖励归一化:不同任务的奖励尺度差异很大。我建议把每个batch的奖励归一化到均值为0、标准差为1。这能显著提升训练稳定性。
  • 策略熵正则化:防止策略过早收敛到确定性策略。加一个熵惩罚项,鼓励探索。系数一般设0.01左右。
  • 不要忽视Baseline网络:很多人只关注策略网络,Baseline网络随便写写。实际上,Baseline估计不准,优势函数就有偏差,整个训练都会受影响。

我曾经在一个四足机器人控制项目上,花了两周时间调参,结果发现是Baseline网络层数太少,拟合能力不够。换成两层256的隐藏层后,效果立竿见影。所以说,细节决定成败。

好了,这一章的内容就到这里。策略梯度是端到端控制的基础,REINFORCE虽然简单,但它的思想贯穿了整个策略梯度家族。下一章我们会讲Actor-Critic,那才是真正实用的方法。