Xiaohei's Blog
headpicBlur image

前言#

前两篇我们一直站在“值函数”的视角:学 Q(s,a)Q(s,a),再用 argmax\arg\max 导出策略。

策略梯度(Policy Gradient)反过来:直接优化策略 πθ(as)\pi_\theta(a|s)。这件事在实战里很“香”,一方面它天然适配连续动作(不用把动作硬离散、再用 DQN 去拟合),另一方面策略本身就是概率分布,探索不再是外部贴一层 epsilon 的补丁,而是模型输出的一部分。

这一篇我会按我自己写代码的顺序,把三类最常见的策略方法串成一条清晰的路线:先用 REINFORCE 把“策略分布 + log_prob + 回报”这套最小闭环跑通,再用 PPO 把更新变得克制和稳定,最后用 A2C 把采样效率与方差控制一起做到一个更均衡的状态。

我个人愿意花时间学这套东西,主要是因为它解决了我在值函数方法里反复遇到的两个尴尬:第一,连续动作任务里“离散化动作”会让策略变得很笨拙,很多时候你离散得再细也不如直接学一个连续分布;第二,很多环境的探索不是“偶尔随机一下”就够了,策略梯度把探索写进分布里以后,你会更容易用概率/熵去量化“我到底在不在探索”。

当然,策略方法也有自己的一堆坑:最常见的是分布参数化不对(比如 std 太小导致几乎不探索)、回报/优势算错(done 边界处理错一位就能让训练完全失真),以及 log_prob 和 action 对不上(这种 bug 最可怕,loss 还能降,但学到的东西是错的)。所以这篇我会把这些“我踩过的雷”也一并写下来。

策略梯度的关键:先把策略分布设计对#

策略梯度的公式可以写很长,但我真正开始写实现时,脑子里只留一句话:让网络输出一个“可采样、可求 log_prob、可反传”的分布。只要这件事做对了,后面无论是 REINFORCE 还是 PPO,本质都是在用 log_prob 去乘某个权重(回报/优势),然后反向传播。

实践里最常见的两类分布就是下面这两个。

1) Softmax(离散动作)#

网络输出 logits,策略分布:

πθ(as)=softmax(fθ(s))\pi_\theta(a|s) = \text{softmax}(f_\theta(s))

PyTorch 对应就是 Categorical(logits=...)

2) Gaussian(连续动作)#

常见做法是网络输出均值 μ(s)\mu(s),再配一个方差(或 log_std):

aN(μθ(s),σ2)a \sim \mathcal{N}(\mu_\theta(s), \sigma^2)

PyTorch 对应 Normal(loc=mu, scale=sigma)

如果你的动作空间只有两个动作(0/1),我更喜欢把它看成 Bernoulli:网络输出一个概率 p(0,1)p\in(0,1)(用 sigmoid),然后 action ~ Bernoulli(p)。这个写法会比 softmax(2) 更“直觉”,而且在调试时也更容易看策略到底在偏向哪一边。

REINFORCE(Monte-Carlo Policy Gradient):最小可跑的策略梯度#

REINFORCE 是我用来“打通策略梯度的第一块砖”。它的结构非常干净:采一整条轨迹,算每一步的折扣回报 GtG_t,然后用 GtG_t 去加权 log_prob 做梯度下降。你写完它,就会对“为什么要保存 log_prob、为什么要等回合结束”形成肌肉记忆。

回报 GtG_t:从后往前的动态规划#

一条轨迹 (st,at,rt)(s_t,a_t,r_t) 采完之后,从后往前算:

Gt=rt+γGt+1G_t = r_t + \gamma G_{t+1}

代码里通常这样写:

def compute_returns(rewards, gamma: float):
	G = 0.0
	returns = []
	for r in reversed(rewards):
		G = r + gamma * G
		returns.append(G)
	returns.reverse()
	return returns
python

损失:用 log_prob 加权#

REINFORCE 的一个常见写法:

L(θ)=tGtlogπθ(atst)\mathcal{L}(\theta) = -\sum_t G_t \cdot \log \pi_\theta(a_t|s_t)

工程上就是:

  • 采样时保存 log_prob
  • 回合结束后算 returns
  • 做一个加权求和再反传

PPO:Actor-Critic + 剪切(clip)让更新别太激进#

当我开始在稍复杂一点的环境里训练策略时,REINFORCE 的“不稳定”会很快把我劝退。PPO 是我最常用的替代方案:它仍然是 Actor-Critic,但它会用一个很朴素的思想约束更新——别一口气把策略改太猛

工程实现上,我通常把它拆成两块网络:Actor 输出策略分布(支持 sampling 和 log_prob),Critic 输出 V(s)V(s) 作为 baseline,用于优势估计。

PPO 的核心:update#

PPO 的实现细节有很多版本,这里我只抓住我认为最“管用”的核心差异:

  • 采样/预测和之前一样,但要输出概率(或 log_prob)
  • update() 里用 PPO 的目标函数更新 Actor,并用 value loss 更新 Critic

如果你在实现里容易迷路,可以用一句话自检:PPO 的 update 吃进去的是 rollout(states/actions/rewards/dones + old_log_probs + values),吐出来的是更新后的 actor/critic 参数。 只要数据形状对齐、优势算对、log_prob 对齐旧策略,PPO 就能跑起来。

PPO 专用的“经验回放”#

PPO 里也会有一个“buffer”,但它和 DQN 的 replay 完全不是一回事:DQN 是 off-policy,buffer 可以很大、数据可以存很久;PPO 更接近 on-policy,通常只保留最近一段 rollout,拿这段数据更新若干 epoch 后就丢。

因此 PPO rollout buffer 多保存:

  • states, actions, rewards
  • old_log_probs
  • values(critic 输出)
  • dones

A2C:优势 Actor-Critic(常见做法:并行环境 + n-step)#

A2C(Advantage Actor-Critic)在我心里像是“更实用的 REINFORCE”:同样是用采样数据去更新策略,但它用 Critic 做 baseline 来降方差,并且常常配合并行环境一次收集更多轨迹来提高吞吐。

实现上它仍然是 Actor + Critic 两个网络,更新 Actor 时常用优势函数 At=GtV(st)A_t = G_t - V(s_t),更新 Critic 时回归到 return 或 bootstrap target。很多坑都集中在回报/优势的计算:done 的边界、最后一个 state 是否 bootstrap,以及并行环境里 time dimension 和 env dimension 的整理。

训练函数可以拆成几块理解#

我更喜欢把 A2C 的训练代码按职责拆开(不然很容易越写越乱):

  1. 参数与环境设置:动作/状态维度、初始化网络、优化器
  2. 训练循环初始化:准备各种缓存数组
  3. 收集轨迹(固定步数)+ 定期评估/记录
  4. 计算回报与优势
  5. 计算 loss 并更新参数,返回 reward 曲线

小结#

这一篇我想传达的核心其实就三件事:第一,策略梯度写对的第一步是“分布建模”,要可采样、可求 log_prob、可反传;第二,REINFORCE 用一条轨迹打通最小闭环,但方差大;第三,PPO/A2C 通过 value/advantage 这些工程手段,把训练的波动压下来,让你能在更复杂的任务上持续迭代。

我调试策略梯度时一定会看的 7 个信号#

策略梯度最折磨人的地方是:你“看起来”什么都在动(loss 有值、梯度在回传、参数在更新),但策略可能就是不变好。下面这些是我最常用的自检清单:

  1. log_prob 是否有限:一旦出现 inf/NaN,优先查分布参数(尤其是 std/log_std)有没有爆。
  2. entropy(熵)是否在合理范围:熵很快掉到接近 0,通常意味着策略过早变得确定(探索没了);熵一直很高又说明策略基本在乱采。
  3. std/log_std 的数值:连续动作里 std 太小 ≈ 不探索;太大 ≈ 动作到处乱飞。很多实现会 clamp log_std。
  4. 回报/优势的量级:我会打印 return 的均值/方差,并且经常做 normalize(尤其是 REINFORCE 和 PPO 的 advantage)。
  5. done 边界与 bootstrap:A2C/PPO 里优势和回报的边界非常敏感,done 处理错一位就能让优势估计整体偏移。
  6. action 与 log_prob 是否匹配同一次采样:不要在采样后又对 action 做 clip/变换但忘了同步 log_prob(SAC 里尤其常见)。
  7. 先从最简单环境验证闭环:CartPole 这类任务能快速验证“分布采样 + log_prob + 更新”是否正确,别一上来就怼高维连续控制。

下一篇进入连续控制的三件套:DDPG / TD3 / SAC。它们和 PPO/A2C 的共同点是都有 Actor-Critic,但一个更偏 off-policy,一个更偏熵正则化。

强化学习算法程序实践(3):策略梯度与 Actor-Critic
https://xiaohei94.github.io/blog/rl-algorithm-3
Author 红鼻子小黑
Published at May 1, 2025
Comment seems to stuck. Try to refresh?✨