策略梯度:不学价值,直接学策略
第 9 章留了一句话:策略比价值先定下来,价值只是中间品。这一章顺着那句话走到底——干脆跳过价值,直接推着策略往好的方向挪。这条路会打开一扇 DQN 永远打不开的门。
换一个思路
前面所有算法的路线都是:学 Q → 取 argmax → 得到策略。
这条路有三个卡点,而且都不小:
- 那个 argmax 要求动作可枚举。连续动作(关节力矩、方向盘角度)直接出局。
- 它只能给出确定性策略。可有些问题的最优策略本来就是随机的(石头剪刀布,或者任何对手会摸清你套路的场合)。
- Q 值差一点点,argmax 就跳变。0.51 和 0.49 之间挪一下,策略整个换了——这让训练很不稳。
策略梯度的做法是:把策略本身参数化,然后直接对「期望回报」求梯度。
π(a|s; θ) —— 一个网络:状态进去,各动作的概率出来
目标:max J(θ) = E[ 这一局的总回报 ]
θ
做法:θ ← θ + α ∇J(θ)
没有 Q,没有 max,没有表。就是普通的梯度上升。
但那个梯度怎么求
这里有个真实的困难,值得停一下:J(θ) 是「按这个策略走一局,期望能拿多少」——而这个期望依赖于环境的转移概率 P,你不知道 P。
怎么对一个你写不出来的东西求梯度?
答案是策略梯度定理,它的结论漂亮得有点不像话:
∇J(θ) = E [ G · ∇ log π(a|s; θ) ]
t θ
P 消失了。
为什么能消失?因为环境的转移概率不依赖于 θ——你怎么调策略,物理规律都不变。求导的时候它就成了常数,被约掉了。剩下的部分全是你能算的:G 是实际拿到的回报,∇log π 是你自己网络的梯度。
然后用第 10 章那一招:期望算不出来?采样求平均。
θ ← θ + α · G · ∇ log π(a|s)
θ
逐项翻译:
∇ log π(a|s):「把动作 a 的概率调高」的方向。这纯粹是个方向,和好坏无关。G:这一局实际拿了多少分。它是个标量,充当缩放系数。
两个一乘:G 是正的大数 → 大幅提高这个动作的概率;G 是负数 → 反向,降低它的概率。
整句话就是:「这局分高?那就把这局里做过的每一件事都变得更可能发生。」
朴素到近乎天真,但它是对的——而且它是后面 A2C、TRPO、PPO、以及 ChatGPT 那个 RLHF 的共同祖先。
跑一下
看两个数:末 100 局平均回报(越接近最优 0.705 越好),和梯度信号的标准差(越小越稳)。
它的毛病:方差大到离谱
那个天真的做法有个直接后果。
注意 G 是整局的总回报。这意味着:这一局里的每一个动作,共享同一个缩放系数。
假设一局走了三十步,其中第 12 步是个好动作,第 25 步是个烂动作,但这局总分不错。REINFORCE 会把这两个动作的概率一起调高。
它靠什么区分好坏?靠统计——跑很多很多局,好动作平均出现在高分局里的次数多一点。信号淹在噪声里,只能靠海量样本捞出来。
更糟的一层:如果所有回报都是正的
假设某个环境里,回报总是 +10 到 +100 之间。
那么 G 永远是正数,于是每一个被采样到的动作,概率都在被调高——包括那些糟糕的动作。
它靠什么学?靠「好动作被调得多一点」这种相对差异。这能奏效,但效率极低,而且噪声巨大。
那个救命的小改动:减基线
解法简单得让人怀疑它有没有用:从 G 里减掉一个基线 b。
θ ← θ + α · (G - b) · ∇ log π(a|s)
b 可以是「历史平均回报」,也可以是一个学出来的 V(s)。关键性质是:
数学上:只要 b 不依赖于动作 a,那么 E[b · ∇log π] = 0。所以减掉它之后,梯度的期望完全不变——估计仍然是无偏的。
但方差降了,而且降得很实在。因为现在的含义变了:
从「这局拿了多少分」变成了「这局比平均好多少」。
比平均好 → 正数 → 调高概率。比平均差 → 负数 → 调低概率。
终于有负反馈了。而在此之前,所有动作都只在被往上推。
引擎跑出来的数字:梯度信号的标准差从 0.527 降到 0.433,末 100 局平均回报从 0.640 提到 0.696——而理论最优是 0.705。它几乎学到了最优。
那个基线,就是 Critic
现在把基线从「历史平均」升级成「一个学出来的 V(s)」,会发生一件事:
G - V(s) ≈ A(s,a) ← 这个叫「优势」(advantage)
「在状态 s 做动作 a,比这个状态的平均水平好多少。」
这一下就精确了。不再是「这一局好不好」,而是「这一步好不好」——第 10 章末尾那个信用分配问题,在这里得到了正面回答。
于是结构变成了两个网络:
- Actor π(a|s;θ):负责做动作
- Critic V(s;w):负责回答「这个状态平均能拿多少」
这就是 Actor-Critic。而回头看第 9 章那张表——它就是策略迭代:Critic 在做评估,Actor 在做改进,只不过两边都改成了「用梯度挪一点点」。
优势 A 怎么估?这里又回到了第 10、11 章那条谱线:
1 步: A ≈ r + γV(s') - V(s) ← 偏差大,方差小 n 步: A ≈ r + …… + γⁿV(s⁽ⁿ⁾) - V(s) 整局: A ≈ G - V(s) ← 无偏,方差大
GAE(广义优势估计)把所有 n 按 λ 的幂加权平均,一个参数 gae_lambda 就在这条谱线上滑动。
这就是你在 PPO 配置里看到的那个 gae_lambda=0.95。它和 γ 是两个不同的旋钮,一个常见的混淆点:
- γ:agent真正在乎多远的未来(这是问题定义的一部分)
- λ:你愿意用多长的实际轨迹去估优势(这是估计方法的选择)
调 γ 是在改问题,调 λ 只是在改你怎么估计。前者要慎重,后者可以放心试。
策略梯度最重要的能力,是它从头到尾没有出现过 max。
所以策略网络可以直接输出连续值——比如输出一个高斯分布的均值和方差,然后从里面采样一个力矩:
class GaussianPolicy(nn.Module):
def forward(self, state):
mu = self.mu_net(state) # 均值:4 个关节各一个力矩
std = self.log_std.exp() # 标准差:控制探索的幅度
return Normal(mu, std) # 一个连续分布
dist = policy(state)
action = dist.sample() # 采样一个连续动作
logp = dist.log_prob(action).sum() # ← 这就是那个 log π
机器人、走路、开车、机械臂——所有连续控制,全在这条路上。
顺带,那个 std 值得注意:它就是探索。训练初期它大(动作随机),后期自动变小(动作确定)。第 3、4 章那个 ε 在这里被吸收进了策略本身——不再是外挂的,是策略的一部分。
① 它是 on-policy 的,样本用完就扔。
那个梯度公式里的期望,是在当前策略下的期望。策略一更新,之前收集的数据就不再代表它了。
所以不能用经验回放,样本效率天生比 DQN 低一个数量级。这是第 13 章那张表里那一行的来源,也是 PPO 要用「多个环境并行采样」的原因。
② 一步迈太大就崩,而且崩了回不来。
这个比第一点更要命。策略更新过头 → 走到很差的区域 → 采到的全是烂数据 → 用烂数据算出的梯度更烂 → 正反馈,一路崩到底。
监督学习没有这个问题:数据集是固定的,学坏了下一轮还是同一批数据,能救回来。强化学习里,你的数据是你自己采的——策略崩了,数据源也就毁了。
这个问题催生了 TRPO 和 PPO。下一章就在解它。
它值得敲一遍,因为它是所有现代策略梯度方法的最小骨架:
import torch, gymnasium as gym
from torch.distributions import Categorical
env = gym.make("CartPole-v1")
policy = torch.nn.Sequential(
torch.nn.Linear(4, 64), torch.nn.ReLU(),
torch.nn.Linear(64, 2)) # 2 个动作的 logits
opt = torch.optim.Adam(policy.parameters(), lr=1e-2)
for ep in range(1000):
s, _ = env.reset()
logps, rewards = [], []
while True:
dist = Categorical(logits=policy(torch.tensor(s)))
a = dist.sample()
logps.append(dist.log_prob(a)) # ← log π(a|s)
s, r, term, trunc, _ = env.step(a.item())
rewards.append(r)
if term or trunc: break
# 倒着算回报
G, returns = 0, []
for r in reversed(rewards):
G = r + 0.99 * G
returns.insert(0, G)
returns = torch.tensor(returns)
returns = (returns - returns.mean()) / (returns.std() + 1e-8) # ← 减基线
loss = -(torch.stack(logps) * returns).sum() # 负号:梯度上升写成下降
opt.zero_grad(); loss.backward(); opt.step()
那句 (returns - mean) / std 就是减基线的最省事版本(顺便还归一化了尺度)。去掉它试试——你会看到收敛慢一大截,而且不稳。
那个负号也值得注意:PyTorch 的优化器只会做梯度下降,所以要最大化 J 就得最小化 −J。这是策略梯度代码里最常见的一处困惑来源。
那个学走路的小人,用的几乎肯定是这条路上的算法(PPO 或 SAC)。原因就是上面那个 max:它的动作是四个连续的力矩,DQN 那一系根本处理不了。
而你在训练早期看到的抽搐,现在有了更准确的解释:那是高斯策略的标准差还很大,每一帧的力矩都在大范围随机采样。
随着训练推进,std 自动收缩,动作变得平滑、连贯、可预测。「从抽搐到走路」这个过程,在参数上就是那个 std 从大变小的过程。
这一章的一句话
不学价值直接学策略,绕开了 max,也就绕开了「动作必须可枚举」。代价是方差极大——而治它的那个基线,正好长成了 Critic。
下一章:解掉「一步迈太大就崩」那个问题。答案简单到能用十几行写完,却稳到从机器人一路用到了 ChatGPT——相信这批数据,但只相信一步之内。