
1. 策略梯度里的方差和偏差先把账算明白做强化学习的人十有八九都见过这种画面策略梯度算法跑了几千步loss 曲线像心电图一样上蹿下跳偶尔还突然变成一条直线。基于策略梯度的强化学习核心思想谁都懂——让“好动作”出现的概率变大让“坏动作”出现的概率变小但真正决定训练能不能跑下去的往往不是这个思想本身而是方差和偏差这一对冤家。这一篇是强化学习理论第 3 篇的第一部分我想把策略梯度里这两个最磨人的东西拆开来看它们从哪里来、有哪些常用的压制手段、压力过大时又该怎么发现。1.1 从 REINFORCE 谈起无偏估计为什么天生高方差策略梯度的原始形式并不难写。假设一条轨迹是 \tau(s_0,a_0,r_0,...,s_{T},a_T,r_T)总回报是 R(\tau)\sum_{t0}^T r_t目标函数是 J(\theta)E_{\tau\sim\pi_\theta}[R(\tau)]那么梯度可以写成\nabla_\theta J(\theta)E_{\tau}\left[\sum_{t0}^T \nabla_\theta \log \pi_\theta(a_t|s_t) R(\tau)\right]这个式子里的关键动作是“把 \nabla_\theta \pi_\theta(a_t|s_t) 改写成 \pi_\theta(a_t|s_t)\nabla_\theta\log \pi_\theta(a_t|s_t)”也就是所谓的 log 导数技巧。它最厉害的地方在于我们根本不需要对环境的动态做任何求导只要知道一条轨迹里每个动作的概率密度变化方向就能估计梯度方向。但代价也藏在同一个地方这个梯度估计是拿“采样到的轨迹”当全体轨迹来用的。如果某个动作只被采样到两次一次回报是 100一次回报是 -100那么这两次给出的梯度方向几乎完全相反。粗暴理解就是拿一个人的口味判断全城人的口味样本越少判断越飘。更麻烦的是回报 R(\tau) 是一长串奖励的累积值假设每一步奖励的随机性都很大几十步之后累加出来的回报方差会被放大很多倍。REINFORCE 用的正是这种蒙特卡洛回报所以它是无偏的但方差高得让人头疼。1.2 Critic 一进来偏差就跟着出现了既然纯蒙特卡洛回报方差大最自然的想法就是让一个价值函数来做向导。如果有一个准确的 V(s)那么可以定义优势函数 A(s,a)Q(s,a)-V(s)然后用优势值代替 R(\tau) 作为梯度权重。优势值的核心意义是“这个动作比平均水平好多少”它围绕 0 分布量级通常比原始回报小很多因此方差会明显下降。可是这里藏着一个公平交易V(s) 是估计出来的不是真实的价值所以优势估计本身已经带上误差。Critic 训练得越差这个误差就越大策略梯度就可能被“带偏”。这就是经典的偏差-方差权衡一个无偏但高方差的估计器换来了一个有偏但低方差的估计器。你可能会问能不能找到一个既无偏又低方差的完美估计器在模型未知、只能靠采样的情况下这个目标通常很难同时满足。所以从这一篇往后看到的各种改进方法本质上都是在调这台天平。2. 降方差的三个基本手段基线、Critic 和折扣因子方差和偏差不是一次性能解决的但有一批最基础、也是最常用的手段几乎每个策略梯度实现里都会用到。把它们理解透后面再看 GAE、PPO 这类复杂算法会轻松很多。2.1 减一个基线方向不变方差变小最早也最简单的降方差手段是在梯度权重里减掉一个只依赖状态、不依赖动作的函数 b(s)。修改后的梯度是\nabla_\theta J(\theta)E_{\tau}\left[\sum_t \nabla_\theta \log \pi_\theta(a_t|s_t) \left(R(\tau)-b(s_t)\right)\right]为什么减去 b(s_t) 不改变期望因为对单个状态求和时\sum_a \pi_\theta(a|s)\nabla_\theta\log\pi_\theta(a|s)b(s)b(s)\nabla_\theta\sum_a\pi_\theta(a|s)0b(s) 在“按概率加权求和”之后会被消掉所以期望梯度不变。但样本方差会变尤其当 R(\tau) 量级很大而 b(s) 又靠近真实均值时每个样本的权重会从“一个很大的数”变成“一个不大不小的差值”波动幅度明显下降。实践中用得最多的基线就是状态价值函数 V_\theta(s)。减去 V(s) 之后权重变成了 Q(s,a)-V(s)也就是优势函数。这样做还有个额外好处权重的重心被拉回 0 附近训练初期的梯度更新不会因为某个样本回报特别大而出现“一步改过头”。我自己调参时经常看到没加基线的 REINFORCE 前期容易把策略熵一下子推到接近 0加上基线之后训练平滑很多。2.2 用 Critic 估计 TD 误差用“意外”程度当权重用 Critic 估计价值之后优势可以换成 TD 误差的形式。如果当前策略的 Critic 是 V_\theta那么单步 TD 误差是\delta_tr_t\gamma V_\theta(s_{t1})-V_\theta(s_t)这里 \delta_t 表达的是“当前这一步实际发生的结果比 Critic 预期的好多少”。如果 \delta_t 是正的说明这一步比预期更好策略梯度会提高这个动作的概率如果 \delta_t 是负的说明比预期更差概率会降低。这种做法的好处很明显它不再需要等一整条轨迹走完才更新而是每步都能学方差大幅下降。代价则是 Critic 自身的偏差会直接传染给策略梯度。还有一种变体是用 n 步回报A_t^{(n)}\sum_{l0}^{n-1}\gamma^l r_{tl}\gamma^n V_\theta(s_{tn})-V_\theta(s_t)n 越大越接近蒙特卡洛回报偏差越小但方差越大n 越小越依赖 Critic 的预测偏差越大但方差越小。所以 n 本身就是一个偏差-方差旋钮。2.3 折扣因子 \gamma 不只是在定义任务也在决定偏差很多人习惯把 \gamma 当成一个“看起来差不多就能用”的超参数但 \gamma 对偏差和方差的影响其实非常直接。\gamma 越小算法越不关心远期回报累计回报里的随机项越少方差自然越低但代价是策略变得短视可能丢掉有长期价值的动作。\gamma 越接近 1目标函数越接近真实总回报偏差越小但训练方差会随任务长度增长。一个可用的直觉公式是“有效时间尺度” 1/(1-\gamma)。\gamma0.95 时算法大约只看未来 20 步内的影响\gamma0.99 时大约看未来 100 步。对生命周期短、奖励步步都有的任务\gamma 可以在 0.95 到 0.98 之间对稀疏奖励或长周期任务\gamma 常常要设到 0.99 以上。实际调参时我习惯先把 \gamma 固定下来再去调别的参数因为 \gamma 改变的是优化目标本身如果边调 \gamma 边调 \lambda出了问题很难定位是哪个参数导致的。手段主要影响偏差变化方差变化典型配置减去基线降低权重量级期望不变降低明显用 V(s) 作基线使用 Critic 优势用 TD 误差替代累计回报增大降低明显单步/ n 步 TD减小 \gamma缩短有效视野增大降低有效步数短的任务增大 \gamma延长有效视野降低增大稀疏长周期任务3. 把偏差和方差做成一个旋钮GAE 与 \lambda 退火如果说基线和 Critic 是两把常用的螺丝刀那么广义优势估计 GAE 就是一个精细的旋钮。它能在一个统一的框架里把从“一步 TD 高偏差低方差”到“蒙特卡洛低偏差高方差”的整个光谱都覆盖出来。3.1 GAE 的构造逻辑让不同的优势估计互相加权GAE 的做法非常直白先算每个时间步的 TD 误差 \delta_tr_t\gamma V(s_{t1})-V(s_t)然后对所有后续步的 \delta 做指数加权平均A_t^{GAE}\sum_{l0}^{T-t}(\gamma\lambda)^l \delta_{tl}当 \lambda0 时只保留 \delta_t也就是一步 TD 优势Critic 的偏差会直接影响策略但方差很小。当 \lambda1 时所有步的 \delta 都按 \gamma^l 被保留下来展开后等价于“蒙特卡洛回报减去 V(s_t) 基线”偏差很小但方差又回到较高水平。\lambda 在 0 到 1 之间取中间值就是在两种极端之间做平衡。我刚开始学 GAE 的时候总觉得这个公式是凭空冒出来的。后来换个角度理解就顺了n 步优势分别有自己的偏差-方差特性GAE 只是给它们分配了指数衰减的权重距离当前步越近的误差越可信权重越大越远的误差越不确定权重越小。这有点像做多尺度平滑越近的细节保留越多越远的细节越模糊。3.2 \gamma 和 \lambda 的分工不一样别把它们混在一起经常有人把 \gamma 和 \lambda 当成一回事实际上两者管的是不同环节。\gamma 是任务层面的折扣决定“我们要优化的长期回报到底有多长”\lambda 是估计器层面的平滑参数决定“优势估计在多少步内对噪声进行衰减”。即使 \gamma 固定成 0.99\lambda 从 0.9 变成 0.99也会明显改变训练曲线。从实验感觉来说如果每个 step 都有稠密奖励\lambda0.95 是一个很稳的起点。如果任务奖励稀疏滞后回报要在十几步之后才能出现\lambda 可以调到 0.97 到 0.99让优势估计覆盖更长的区间。如果环境奖励本身噪声很大我会把 \lambda 降到 0.9 附近宁可吃一点 Critic 的偏差也要先把梯度噪声压住。3.3 我常用的 \lambda 调法梯度方向比数值大小更重要固定 \lambda 之外也可以让它在训练过程中变化但我个人不太建议一开始就做“花式退火”。更实用的做法是先用 \lambda0.95 跑通一个小规模任务观察两项东西。第一项是策略梯度更新后的平均熵如果熵下降过猛说明权重噪声太大可能把某些随机出现的低概率动作意外放大第二项是优势的均值如果优势均值明显偏离 0说明 Critic 的估计可能存在系统性偏差。此时把 \lambda 朝 0.9 方向降一点压一压噪声如果训练曲线很平稳但策略一直不进步则把 \lambda 朝 0.98 方向升一点让优势估计更逼近真实回报。每次调整幅度控制在 0.01 到 0.05 之间一次只改一个参数。4. 实操中的稳定训练伪代码骨架与超参数经验理论讲再多最后还是要落到训练循环里。我在这里给出一套策略梯度加 GAE 的最小伪代码骨架并解释每个关键选择的理由。# 伪代码策略梯度 GAE 单轮更新 def compute_gae(rewards, values, dones, gamma, lam): gae 0 advantages [] for t in reversed(range(len(rewards))): if dones[t]: next_value 0 else: next_value values[t 1] delta rewards[t] gamma * next_value - values[t] gae delta gamma * lam * gae advantages.insert(0, gae) return advantages for rollout in collect_rollouts(env, policy, num_steps): values critic(rollout.states) advantages compute_gae( rollout.rewards, values, rollout.dones, gamma0.99, lam0.95 ) # 对优势做标准化减少不同奖励尺度的影响 advantages (advantages - advantages.mean()) / (advantages.std() 1e-8) for _ in range(update_epochs): for batch in minibatch(rollout): logits policy(batch.states) logp log_prob(logits, batch.actions) policy_loss -logp * batch.advantages.detach() value_loss mse(critic(batch.states), batch.returns) entropy_loss entropy(logits) total_loss ( policy_loss value_coef * value_loss - entropy_coef * entropy_loss ) update_parameters(total_loss)这段伪代码里最容易出错的地方有三个。第一是 compute_gae 必须倒着遍历因为每一步的 gae 依赖后一步的 gae。第二是策略损失里用的 advantage 要 detach否则梯度会穿过优势去更新 Critic导致两个网络互相拉扯。第三是 returns 要在算完 GAE 之后立刻恢复出来通常做法是 returns advantages values然后用这个目标去训练 Critic。4.1 超参数范围与推荐起点不同任务对超参数敏感程度不同但有几个相对稳定的起点。我常用的是 \gamma0.99、\lambda0.95、学习率 3e-4、minibatch 大小 256、value loss 系数 1.0、entropy 系数 0.01。优势标准化基本每次都做尤其是奖励尺度变化大的任务标准化能把梯度量级固定在一个合理范围避免初始化稍微不同训练结果就天差地别。超参数推荐起点主要影响备注\gamma0.99任务视野稀疏奖励可加到 0.995\lambda0.95优势估计偏差方差噪声大时降到 0.9学习率3e-4更新步长过大容易策略崩溃value_coef1.0Critic 损失权重Critic 发散时调小entropy_coef0.01探索强度过早收敛时调大advantage 标准化开启梯度量级建议始终开启4.2 稳定训练的三个小技巧第一gradient clipping 不是可选项而是保险丝。尤其在新任务上刚开始的策略完全是随机的优势估计可能突然出现一个极端值不加 clipping 的话一次更新就能让策略崩塌。我习惯把全局梯度范数限制在 0.5 到 1.0 之间。第二Critic 和 Actor 可以用不同的学习率Critic 的任务更简单学得太快反而会在后期反复震荡让优势估计不稳定。第三entropy bonus 要定期检查不要只看 reward。如果一轮更新后策略熵从 0.8 掉到 0.3说明探索能力正在快速消失即使当前 reward 还在上升后劲也会不足。5. 常见问题与排查技巧实录调参过程中总会碰到一些看起来玄学、实际有规律的问题。我把最常见的几种列出来并附上我自己的定位思路。现象可能原因处理建议训练开始 loss 剧烈震荡优势尺度太大、没有标准化开启 advantage normalization策略一两千步后突然停滞\lambda 过高或熵系数太小降 \lambda增大 entropy_coef优势值越来越大Critic 未收敛或发散调低 value 学习率、加入梯度裁剪reward 上升但策略熵快速下降偏差主导优势估计不准减小 value_coef 或增大 \lambda更新一次后策略直接崩掉学习率过大、GAE 值异常降学习率检查 returns 计算5.1 我踩过的两个坑detach 忘记写\gamma 和 \lambda 一起调第一次实现 GAE 时策略损失里没有对 advantage 做 detach结果 Actor 和 Critic 之间多了一条梯度路径。训练初期看着还行几百步之后价值函数的梯度开始影响策略概率等于一边学一边改自己的基线策略更新方向越来越怪。后来把所有参与策略损失的 advantage 全部 detach 掉曲线立刻正常了。第二个坑是调参时同时改了 \gamma 和 \lambda。当时任务奖励稀疏我一股脑把 \gamma 从 0.99 调到 0.995、\lambda 从 0.95 调到 0.99训练曲线变好了但完全不知道是因为哪个参数起的作用后续换任务时没有任何可迁移的经验。后来我养成了一个习惯每次实验只动一个超参数并且把改动原因写在实验记录里。这个方法很笨但长期下来效率反而最高。5.2 什么时候该怀疑“偏差太大了”有时候训练曲线很平稳reward 也在涨但涨得非常慢。这种“平稳的慢”往往不是方差问题而是偏差问题。我常用两个信号来判断。第一优势均值长期不等于 0。GAE 算出来的优势在标准情况下应该是一个均值接近 0 的量如果它在几百个 batch 里始终大于 0.5说明 Critic 对价值的估计存在系统性偏移。第二对比 1 步 TD 和 n 步回报估计出来的价值差距如果两者持续差出 20% 以上说明 bootstrap 偏差已经大到不能忽视这时候应该升高 \lambda让优势估计多看几步真实奖励。6. 写在最后调参之外更值得养成的习惯策略梯度里的方差和偏差之争并不是一个“调一次就结束”的问题。环境变化、奖励函数变化、网络结构变化都会让原本平衡的天平重新倾斜。我在实际项目里最大的体会是先把基础实现做对再谈花式技巧。检查一下你的优势估计是否 detachRLoss 计算中是否把终止状态正确处理reward-to-go 是否真的等于 GAE 加 value这些细节对训练稳定性的影响往往比调 \lambda 大得多。另外我开始给自己的每次实验写“卡点记录”。不用写长就写三行当前任务是什么、我改了哪个超参数、改之前和之后曲线有什么变化。这套习惯坚持下来之后再回头看那些让人头疼的方差爆炸和偏差偏移大部分都能在半小时内定位。最后再分享一个小技巧跑新任务前先降低奖励尺度并开启优势标准化如果这样还爆炸那就不是数值问题而是逻辑问题。