ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

策略梯度从原理到实践:REINFORCE与Actor-Critic的演进路径

策略梯度从原理到实践:REINFORCE与Actor-Critic的演进路径

策略梯度(Policy Gradient)是强化学习里少数几条“直接优化策略”的路线,也是从DQN式的间接决策走向连续控制的关键一步。在之前几讲我们把值函数方法翻来覆去讲透了,从Q-learning到DQN,走的都是同一条路线:先估计“状态-动作”的价值,再在决策时对动作空间做最大值操作。这套逻辑在离散动作空间里没问题,可一旦进入机器人控制这类连续动作任务,就会碰到两堵硬墙。这一讲要解决的就是这两堵墙,核心内容是策略梯度的数学原理、REINFORCE落地实现,以及从REINFORCE平滑过渡到Actor-Critic的完整路径。无论你是正在被高维连续控制折磨的实践者,还是刚接触强化学习、想搞清楚“不学Q值怎么学策略”的新手,这篇都值得读完。

1. 值函数方法的两块天花板:为什么“间接选动作”会撞墙

1.1 离散动作里的“查表式”胜利

值函数方法在离散动作空间里非常好用。一个离散动作空间通常就是有限个选项,在某个状态下采用贪心策略时,只需要遍历一遍所有动作,逐个比较Q值就能选出最大的一项。经典教学任务“倒立摆”的动作通常只有左右两个,遍历成本几乎为零,于是很多入门教程都拿它演示DQN。也正因为如此,初学者很难体会到隐藏在max操作背后的代价。

Q-learning的核心是a* = argmax_a Q(s,a),早期表格型版本需要遍历整个动作空间,DQN虽然用网络逼近Q函数,但决策时仍然要做同样的扫描。动作数量少的时候这没问题,动作数量一旦上千,或者是连续量,这条路就开始吃力了。问题的本质在于:值函数方法把“决策”拆成了两步——先评估价值,再从价值中反推动作——这个两步流程把推理成本放到了决策阶段,而策略梯度把决策本身变成了网络的前向传播,动作直接由网络输出,推理时不需要任何优化循环。

这种差异在部署阶段尤其明显。实时控制系统每毫秒都在做决策,如果每一步都要对连续动作空间做一次内部优化,延迟会迅速累积到不可接受。策略网络则不同,输入状态,输出动作分布,一次前向就结束。单凭这一点,连续控制任务里策略梯度就有天然优势。

1.2 连续控制的硬约束与随机策略的缺失

连续控制是值函数方法真正的天花板。假设机械臂的关节力矩可以取任意实数,动作空间就是一个连续向量空间。这时argmax_a Q(s,a)不再是一个数组遍历,而是一个连续最优化问题。在高维连续空间里求解最优化本身就不便宜,更别提每次决策都要实时求解,这在部署时几乎不可行。

值函数方法还有第二个很少被讨论的问题:策略表达能力受限。值函数方法最终从Q函数导出一个确定性贪心策略——哪个动作的Q值高就选哪个。训练过程中固然可以用 ε-greedy 加探索,但训练收敛后的策略本质上是确定性的。很多环境里这够用,可一旦遇到“随机策略才是最优策略”的场景,比如石头剪刀布这类需要随机出拳才能最大化的博弈,确定性的值函数方法就很难表达正确的行为。

策略梯度走的是另一条路:网络直接输出动作概率分布 π_θ(a|s),随机性天然内嵌在输出里。探索不再是外挂的 ε 机制,而是策略本身的一部分。网络通过调整分布的均值、方差或类别概率,把“探索”和“利用”的权衡直接学出来。在部分可观测的场景里,随机策略往往能带来更平滑、更鲁棒的行为。理解这两块天花板,才能明白为什么我们非要“直接优化策略”。

2. 策略梯度定理的直觉和数学:为什么“轨迹梯度”只剩策略项

2.1 目标函数与log导数小技巧

策略梯度的目标是最大化期望回报,目标函数写成:

J(θ) = E_{τ ~ p_θ(τ)} [ R(τ) ]

这里 τ 是一条完整轨迹,也就是s0, a0, r0, s1, a1, r1, ...的组合;R(τ) 是该轨迹的累积折扣回报。我们的目标是找到一组策略参数 θ,使 J(θ) 最大。问题在于 R(τ) 由环境给出,策略参数 θ 是通过影响轨迹出现的概率来影响期望的,梯度无法直接穿透环境。

解决这个问题的数学工具是 log 导数技巧。对任意一个关于 θ 的概率分布 p_θ(τ),都有恒等式:

∇_θ p_θ(τ) = p_θ(τ) · ∇_θ log p_θ(τ)

这个式子本身只是“先取对数再求导,再乘回原函数”的逆运算,但它把一个难以计算的梯度转化成了“概率乘以某个得分函数”的形式。代入 J(θ) 的期望表达式后,原来对轨迹分布求梯度的困难,变成了对策略函数取对数的梯度,这一步是整个策略梯度方法的基石。

2.2 轨迹概率拆开之后发生了什么

把轨迹的概率 p_θ(τ) 完全展开,会得到:

p_θ(τ) = p(s0) · Π_t p(s_{t+1} | s_t, a_t) · π_θ(a_t | s_t)

这个乘积由三部分组成:初始状态分布、环境状态转移概率、策略概率。关键就在这里——取对数之后再对 θ 求梯度,初始状态分布和环境状态转移概率这两项都不含 θ,梯度直接归零。剩下的只有策略项:

∇_θ log p_θ(τ) = Σ_t ∇_θ log π_θ(a_t | s_t)

所以策略梯度定理可以写成:

∇_θ J(θ) = E_{τ ~ p_θ(τ)} [ (Σ_t ∇_θ log π_θ(a_t | s_t)) · R(τ) ]

这个结果的意义怎么强调都不过分:我们需要知道环境模型吗?不需要。只需要自己策略的梯度,和一条采出来的轨迹回报,就能估计出目标函数的梯度。这也是“无模型强化学习”的核心来源。很多同学第一次看到这个式子会觉得太简单,但它确实就是这么漂亮:对数概率提供了“应该往哪个方向调整动作概率”,回报提供了一个标量权重,告诉算法“这条轨迹值不值得让概率更大”。

2.3 一个容易误解的点:回报凭什么乘在整个轨迹梯度上

实现策略梯度时,最常见的两种写法分别是把整条轨迹回报 R(τ) 乘在所有时间步的梯度上,或者把“从当前时刻开始的未来回报”乘在当前时刻的梯度上。后者通常被称为 reward-to-go:

∇_θ J(θ) ≈ Σ_t ∇_θ log π_θ(a_t | s_t) · (Σ_{t'≥t} γ^{t'-t} r_{t'})

直觉上容易绕弯子:既然回报是整条轨迹的,为什么可以只取当前时刻之后的回报?答案是因果性。当前时刻 t 的动作只影响 t 之后的回报,不影响 t 之前的回报。从期望的角度看,把前面的回报项去掉,期望值不变,但方差会明显降低。去掉的这部分相当于“已知的过去”,不包含任何关于当前决策的信息,只会引入噪声。

实操中我建议从第一步就写成 reward-to-go 的形式,而不是先写整条轨迹回报再“优化”。因为策略梯度的方差本身已经很大,reward-to-go 几乎是零成本的白送减方差手段,先用起来再谈其他优化。

3. REINFORCE:最直接的策略梯度落地算法

3.1 标准REINFORCE执行流程

REINFORCE 是策略梯度家族里最朴素的实现,就是拿蒙特卡洛采样的方式去估计策略梯度。它的流程大致如下:

# 伪代码,框架细节省略 for episode in range(max_episodes): trajectory = [] state = env.reset() while not done: action, log_prob = policy.sample_with_log_prob(state) next_state, reward, done, _ = env.step(action) trajectory.append((log_prob, reward)) state = next_state # 从后往前计算折扣回报 returns = [0] * len(trajectory) G = 0 for t in reversed(range(len(trajectory))): log_prob, reward = trajectory[t] G = reward + gamma * G returns[t] = G # 策略梯度更新(最大化 J,框架里习惯做最小化,所以取负号) loss = 0 for log_prob, G in zip(log_probs, returns): loss = loss - log_prob * (G - baseline) loss = loss / len(trajectory) loss.backward() optimizer.step() baseline = moving_average(returns)

为什么 loss 是负号?因为策略梯度原本是梯度上升,而深度学习框架通常只提供梯度下降优化器,于是我们把目标函数取负,最大化期望回报变成最小化负的期望回报。这个符号问题看起来简单,实际写代码时如果搞反,策略会稳定地变差,而且你很难第一时间察觉。

3.2 Baseline为什么可行:期望不变的数学依据

REINFORCE 虽然实现简单,方差却往往大到训练无法收敛。一个必要的改良是引入 baseline。算法不再直接使用回报 G,而是使用G - b(s),其中 b(s) 是一个只与状态有关的函数,最常用的就是状态价值函数的估计值。

证明很简单,对任意状态相关 baseline b(s):

E_{a~π}[ ∇_θ log π_θ(a|s) · b(s) ] = Σ_a π_θ(a|s) · b(s) · ∇_θ log π_θ(a|s) = b(s) · Σ_a ∇_θ π_θ(a|s) = b(s) · ∇_θ (Σ_a π_θ(a|s)) = b(s) · ∇_θ 1 = 0

这个推导说明了为什么 baseline 不会给梯度带来偏差:它对梯度期望的贡献恒为零。但它的方差影响很大——G 和 b(s) 越相关,相减后的剩余方差就越小。实操里最简单的 baseline 是维护一个回报的滑动平均,进阶做法是训练一个 Critic 网络去估计 V(s),这正是从 REINFORCE 走向 Actor-Critic 的起点。

3.3 REINFORCE的方差问题和缓解手段

REINFORCE 用完整轨迹的回报作为梯度权重,完整轨迹包含大量随机性:策略采样随机性、环境转移随机性、未来决策随机性,全部叠加在一起,导致 G 的方差非常大。直观理解就是同样一个状态下,同一条策略跑十次,得到的累计回报可能从 -50 到 +80 剧烈波动。

缓解方差的手段可以排成一串,按性价比从高到低排列:第一,用 reward-to-go 替代完整回报;第二,减去一个 baseline;第三,一次采样多条轨迹取平均,相当于在多个蒙特卡洛样本之间平均;第四,对 advantage 做标准化,也就是减均值再除以标准差。第四步严格来说会让梯度带有一定偏差,因为它引入了批内统计量,但在实践中通常能大幅提升稳定性,属于工程上可接受的近似。我的建议是先做一到三,确认收敛趋势后再考虑第四步,否则容易掩盖算法本身的问题。

4. 训练现场最容易翻车的三个细节:学习率、奖励尺度与熵正则

4.1 学习率:随机梯度的脾气比想象中差

策略梯度的梯度是一个蒙特卡洛估计,方差天然比监督学习的梯度大。监督学习里梯度方向通常比较稳定,学习率稍大或者稍小影响有限;策略梯度里一个过大的学习率可能让策略在几十个 episode 内直接“变疯”,从正常探索变成原地打转,甚至彻底退化。

我自己的经验是:策略梯度的学习率通常要比同量级的监督任务小一个数量级起步。拿分类任务的手感去调策略梯度,十有八九会翻车。调试时优先把学习率固定在一个保守值,比如 3e-4 到 1e-3,然后观察 return 曲线的走势。如果曲线只是噪声大但趋势向上,说明学习率基本可用;如果曲线一开始就剧烈下探,先别急着加复杂度,把学习率再降一半试试。与此同时打开梯度裁剪,把梯度范数限制在一个合理范围内,这一步能挡住大部分偶发的梯度爆炸。

4.2 奖励尺度不一致:同一套代码换环境就失效

策略梯度的梯度大小受回报绝对尺度影响很大。同一个网络、同一个学习率,在一个奖励动辄上百的环境里可能正常收敛,换到另一个奖励始终在 ±0.1 之间的环境,梯度就会小到更新几乎无效。

这不是网络的问题,而是 reward 的尺度没有对齐。常见的处理方法有三种:一是对 advantage 做标准化,把均值拉到 0、方差拉到 1;二是维护一个可学习的 baseline,让网络自己去适应奖励尺度;三是对原始奖励做 clip 或 scale,使奖励数值落在合理范围。第一个方法最直接,但要注意批内统计量带来的偏差;第二个方法更自然,但需要额外维护一个 Critic 网络;第三个方法最粗暴,不过在这里经常也够用。

我曾经在连续控制任务里被这个问题坑过很久:算法在某一个仿真环境里跑得很稳,换到另一个环境后完全不动。后来把每个 episode 的回报打印出来才意识到,新环境奖励的整体量级比旧环境小了两个数量级。处理完奖励尺度之后,同一个算法几乎没有再改任何超参数就恢复了正常。所以遇到“代码没变但结果崩了”的情况,先去看奖励分布。

4.3 熵正则:防止策略过早“上头”

策略梯度还有一个常见病:策略熵在训练早期就急速下降,很快变成接近确定性的分布。表面上看这似乎说明“策略已经很有信心了”,但实际上很可能是过早收敛到了局部最优,探索不足,后续不管怎么更新都很难跳出去。

对策是加入熵正则项。策略熵 H(π_θ(·|s)) 越大,策略越随机;越小,越接近确定性分布。把负熵加入损失函数,等于在“最大化回报”和“保持探索”之间做一个显式的折中:

loss = -Σ_t log π_θ(a_t|s_t) · A_t - β · H(π_θ(·|s_t))

β 是熵系数,通常从 0.01 量级开始试。训练时应该持续监控策略熵的平均值:如果熵在前几百个 episode 内就趋近于零,大概率是 β 太小或学习率太大,策略在真正探索到足够多区域之前就被“锁死”了。正常的训练里熵应该是缓慢下降的,而不是雪崩式归零。记住这一点,可以帮你省下大量的调试时间。

5. 从REINFORCE到Actor-Critic:用价值函数压缩方差

5.1 蒙特卡洛回报的方差来源

REINFORCE 里梯度权重是完整轨迹的累计回报,这条轨迹从起点到终点经历了无数次随机决策。即使从同一个状态出发,后续的探索路线也会完全不同,因此累计回报的波动极大。REINFORCE 虽然是无偏的,但高方差导致梯度信噪比很低,需要大量采样才能得到稳定的更新方向。

解决思路是找一个更稳定、方差更小的量来替代累计回报。一个自然的候选是状态价值函数 V(s),它表示“从这个状态出发,期望能得到多少回报”。V(s) 本身就是一种期望,天然比单次采样的累计回报稳定得多。我们不需要精确知道未来会发生什么,只需要一个对未来的期望估计,就能告诉策略“当前这个动作相对平均水平好还是差”,这就是 advantage 的思想。

5.2 Actor-Critic如何工作

Actor-Critic 包含两个组件:Actor 是策略网络,输出动作分布;Critic 是价值网络,输出状态价值估计 V(s)。Critic 给出 advantage:

A_t = r_t + γ·V(s_{t+1}) - V(s_t)

这个式子是一个单步时序差分形式。如果用多步折现再加 GAE 之类的扩展,能得到更平滑的 advantage 估计,但核心思想不变:用“实际奖励加上未来价值的估计”减去“当前状态的估计”,差值就是对这个动作好坏的即时评价。Actor 的更新从“让整条轨迹的总回报变大”变成“让当前动作的 advantage 变大”,两者期望一致,但方差大幅下降。

训练时 Critic 通过回归学习逼近真实价值函数,回归目标就是 TD 目标r_t + γ·V(s_{t+1})。注意这个目标里包含 V(s_{t+1}),而 V(s_{t+1}) 本身就是 Critic 的输出,如果让梯度直接回传到 V(s_{t+1}),会出现自举导致的发散问题。所以训练 Critic 时,目标里的 V(s_{t+1}) 必须做 detach,也就是只当常数、不回传梯度。

5.3 容易被忽略的detach问题

在 Actor-Critic 实现里,梯度流管理是最容易出错的地方。advantage A_t 依赖 V(s_t),而 V(s_t) 又是 Critic 的输出。如果这个 advantage 直接进入 Actor 的损失函数,同时又参与了 Critic 自身的回归更新,那同一个梯度就会同时影响两个网络,训练很容易不稳定。

我的处理习惯是:所有进入 Actor loss 的 advantage 都必须 detach,让 Actor 只把它当做一个标量权重,不去反向影响 Critic;Critic 的 TD target 中的 V(s_{t+1}) 也必须 detach,防止价值网络对着自己的输出做自举。伪代码示意如下:

# 伪代码,框架细节省略 for step in range(max_steps): action, log_prob = actor.sample(state) next_state, reward, done, _ = env.step(action) value = critic(state) next_value = critic(next_state) if not done else 0.0 advantage = (reward + gamma * next_value.detach() - value).detach() actor_loss = -(log_prob * advantage).mean() critic_loss = mse(value, (reward + gamma * next_value.detach()).detach()) actor_loss.backward() actor_optimizer.step() critic_loss.backward() critic_optimizer.step()

分开两个优化器是更稳妥的做法,能避免两个网络共享梯度时互相干扰。如果你只有一个优化器,也要确保 actor 和 critic 的梯度流互不串门。这个 detach 的细节写起来只差一行代码,但它决定了整个训练过程是稳定收敛还是来回震荡。

6. 调试策略梯度模型时我自己沉淀的土办法

6.1 调试三板斧:固定噪声、盯梯度范数、对比baseline

调了几年策略梯度相关模型后,我沉淀出一套简单但很有效的调试顺序。第一板斧:在连续控制任务里先把探索噪声固定住,只让策略均值参与学习。具体做法是暂时不更新策略分布的方差参数,把它固定成单位阵的某个倍数,先看均值能不能把轨迹推向高回报区域。均值学不动,说明问题出在策略主体;均值能学动、放开方差后反而崩溃,问题就出在探索机制或者熵系数。这个切分方法能迅速缩小排查范围。

第二板斧:每次梯度更新前打印梯度范数。如果某次更新前梯度范数突然比平时高了几十倍,这通常不是网络结构问题,而是采样方差爆发。应对办法是第一时间打开梯度裁剪,把范数限制在 1.0 到 5.0 之间,同时降低学习率。第三板斧:对比“减去 baseline 前后”的 return 曲线。如果加了 baseline 之后性能反而更差,大概率不是 baseline 本身错了,而是 Critic 网络学习率过高,价值估计震荡太大,反而往 Advantage 里注入了额外噪声。可以先给 Critic 单独设一个更小的学习率试试。

6.2 几个可以立马上手的诊断指标

我在训练策略梯度模型时最少会盯四个指标:滑动平均 return、滑动平均策略熵、梯度范数、advantage 分布的统计量。return 看整体趋势,熵看探索是否健康,梯度范数看更新是否稳定,advantage 分布看算法是否在有效学习。

如果 return 持续上涨但熵掉得特别快,说明策略正在快速确定化,此时需要警惕它只是抓住了短期高回报,后续可能卡在局部最优;如果 return 几乎不动但 advantage 均值一直为负且绝对值很大,提示策略在持续退化,这时候先检查目标函数的符号是否写反,再检查 advantage 的 detach 是否写对——这两个错误在策略梯度实现里出现的频率远比你想象的更高。

最后照例分享一点个人体会:我早期折腾策略梯度,花时间最多的不是设计网络结构,而是调那一堆看起来和算法无关的参数——学习率、熵系数、梯度裁剪阈值、baseline 的软更新系数。策略梯度理论本身并不复杂,复杂的是它把概率分布、随机采样和优化器放在一起后产生的各种偶发不稳定。建议你从 REINFORCE 开始亲手写一遍,把 baseline 加上去,再切到 Actor-Critic,走完这一遍之后再看 PPO、TRPO 这些进阶算法,心里会踏实得多。下一讲进入信赖域策略优化时你会看到,很多改良本质上都是为了解决这一讲反复提到的方差和更新步长问题。

返回列表