首页 / 模块 2 · 概率统计、信息论与最优化 / 第 10 课(共 10 课)

强化学习速成 II——策略梯度到 PPO

从零到前沿 ML 自学课程 · 阶段0:数学与工具基础 · 能力点:强化学习 II——策略梯度→PPO(直通 RLHF/GRPO)

上一课我们用价值函数与贝尔曼方程刻画了"一个状态/动作有多好",并通过"估价值 → 贪心选动作"间接得到策略。这一课换一条主线:不绕道价值,直接把策略本身参数化、对参数求梯度往上爬。这条路通向今天大模型对齐的核心——RLHF 里的 PPO。

读完这一课,你将能够

  • 用一句话说清策略梯度定理的直觉,并写出 \(\nabla_\theta J=\mathbb{E}[\nabla_\theta\log\pi_\theta(a\mid s)\,G]\);
  • 证明加基线 \(b(s)\) 不改变梯度期望,并说清承重的是哪两步、它为何能降方差;
  • 区分优势函数 \(A=Q-V\)、actor–critic 与 GAE,说清各自解决什么问题;
  • 解释 PPO 裁剪代理目标为什么要把重要性比 \(r\) 限制在 \([1-\epsilon,1+\epsilon]\),逐字说清 \(A>0\) 与 \(A<0\) 两种情形各裁掉了什么,以及它与 KL 惩罚的等价直觉;
  • 画出 RLHF 的优化目标 \(\text{reward}-\beta\,D_{\mathrm{KL}}(\pi_\theta\|\pi_{\text{ref}})\),并说清 DPO / GRPO 各省掉了哪一步。

1. 为什么要"直接优化策略"?

价值方法(上一课的 Q-learning 思路)有个隐含假设:先精确估出每个动作的价值,再取 \(\arg\max\)。可一旦动作是连续的(机械臂的力矩)或巨大离散(语言模型下一个 token 有几万种选择),"对所有动作取 max"就变得不现实。

换个思路:把策略写成一个带参数的概率分布 \(\pi_\theta(a\mid s)\)(比如 softmax 或高斯),定义目标为期望回报 \[ J(\theta)=\mathbb{E}_{\tau\sim\pi_\theta}\!\left[\,G(\tau)\,\right],\qquad G(\tau)=\sum_{t=0}^{T}\gamma^{t} r_t, \] 其中 \(\tau=(s_0,a_0,r_0,s_1,\dots)\) 是一条采样轨迹。我们想做的事极其朴素:对 \(\theta\) 做梯度上升 \(\theta\leftarrow\theta+\eta\,\nabla_\theta J\)。问题只剩一个:\(J\) 是对"按 \(\pi_\theta\) 采样的轨迹"求期望,而采样分布本身依赖 \(\theta\),这个梯度怎么算?

直觉先行。 想象你在调一个"出招概率表"。你试了一整局,结果赢了——你不知道具体哪一步是关键,但一个安全的赌注是:把这一局里用过的所有招式,概率都调高一点;输了就调低一点。赢得越多调得越狠。策略梯度就是这句话的数学化。

2. 策略梯度定理:好结果就抬高导致它的动作的对数概率

核心是一个叫 log-derivative trick(对数求导技巧)的小恒等式。对任意依赖 \(\theta\) 的概率 \(p_\theta\): \[ \nabla_\theta p_\theta = p_\theta\,\frac{\nabla_\theta p_\theta}{p_\theta}=p_\theta\,\nabla_\theta\log p_\theta . \] 这一步把"对概率求梯度"换成了"概率 × 对数概率的梯度",而后者正好能写回期望里。把它用在整条轨迹上:一条轨迹的概率是初始分布乘上每一步"策略 × 转移"的连乘,取 \(\log\) 把连乘变连加, \[ \log p_\theta(\tau)=\log\rho(s_0)+\sum_t\Big[\log\pi_\theta(a_t\mid s_t)+\log P(s_{t+1}\mid s_t,a_t)\Big]. \] 对 \(\theta\) 求梯度时,初始分布 \(\rho(s_0)\) 与环境转移 \(P(s_{t+1}\mid s_t,a_t)\) 都不含 \(\theta\),梯度为 0,只剩下 \(\sum_t\nabla_\theta\log\pi_\theta(a_t\mid s_t)\)。这就是"为何转移会消失"的完整机制。代回去就得到策略梯度定理

\[ \boxed{\;\nabla_\theta J(\theta)=\mathbb{E}_{\tau\sim\pi_\theta}\!\left[\Big(\sum_{t}\nabla_\theta\log\pi_\theta(a_t\mid s_t)\Big)\;G(\tau)\right]\;} \]

读这条式子:\(\nabla_\theta\log\pi_\theta(a_t\mid s_t)\) 是"朝着让动作 \(a_t\) 更可能的方向",整条轨迹的回报 \(G(\tau)\) 当作这一批动作共同的权重。\(G\) 大且为正,就大步抬高这些动作的概率;\(G\) 为负,就压低。这就是第 1 节那句"赢了全调高"的严格版。下一节的 pitfall 会把这个"整条回报"收紧成更省方差的形式。

策略梯度直觉:高回报轨迹提高动作概率,低回报轨迹降低概率策略梯度直觉:按回报调整动作概率高回报轨迹a₀a₁a₂s₀s₁s₂G(τ)=+8提高π(aₜ|sₜ)低回报轨迹a₀a₁a₂s₀s₁s₂G(τ)=−3降低概率θ J = E[ θ log π(a|s) · G ]∇log π = 方向G = 权重力度 ∝ 回报,方向沿 ∇log π
策略梯度直觉:采样一条轨迹,若回报高,就把这条轨迹用到的所有动作的概率往上推(绿色↑箭头);回报低则往下压(红色↓箭头)。推/压的力度正比于回报,方向沿 ∇log π。

ML 和 ML 的联系

这个形式你其实见过。监督学习里最小化交叉熵,梯度是 \(\nabla_\theta\big(-\log\pi_\theta(y\mid x)\big)\)——"抬高正确标签的对数概率"。策略梯度几乎一模一样,只是把"正确标签"换成"实际采到的动作",并用回报当作这条样本的权重(软标签)。所以可以把 REINFORCE 理解成"加权的最大似然":表现好的轨迹权重大,被更用力地模仿。

REINFORCE 算法(最朴素的策略梯度)

  1. 用当前 \(\pi_\theta\) 跑一整条(或一批)轨迹,记录 \((s_t,a_t,r_t)\);
  2. 算每步的未来回报(reward-to-go)\(G_t=\sum_{k\ge t}\gamma^{k-t}r_k\);
  3. 用 \(\hat g=\frac1N\sum_t \nabla_\theta\log\pi_\theta(a_t\mid s_t)\,G_t\) 估计梯度;
  4. 更新 \(\theta\leftarrow\theta+\eta\,\hat g\),回到第 1 步。
易错:用未来回报 \(G_t\) 还是整条 \(G(\tau)\)? 上面方框定理为忠于第 1 节的 \(J\),给每步都乘了整条轨迹回报 \(G(\tau)\)。但严格来说第 \(t\) 步只应被"它之后的回报"\(G_t\) 加权——因为当前动作影响不了过去的奖励(这叫 causality / reward-to-go)。可以证明把权重从 \(G(\tau)\) 换成 \(G_t\) 期望不变(过去那段奖励与当前 \(\nabla\log\pi\) 不相关,期望贡献为 0),但方差更小。所以 REINFORCE 实现里用的是 \(G_t\)(步骤 2),这不是定理写错了,而是同一个无偏梯度的低方差写法。初学若误用整条回报,能跑但更抖。

3. 高方差是头号敌人,基线是第一剂解药

REINFORCE 能用,但方差极大:回报 \(G_t\) 受整条轨迹的随机性影响,同一个策略两次采样可能差很远,梯度估计像在噪声里找方向。更糟的是一个"尺度问题":如果所有回报都是正的(比如奖励恒非负),那么每个动作的概率都被往上抬,只是被抬的多少不同——这显然浪费,我们真正想要的是"比平均好的抬、比平均差的压"。

解药叫基线(baseline):从回报里减去一个只依赖状态、不依赖动作的量 \(b(s)\): \[ \nabla_\theta J=\mathbb{E}\!\left[\nabla_\theta\log\pi_\theta(a\mid s)\,\big(G_t-b(s_t)\big)\right]. \] 最常用的基线就是状态价值 \(b(s)=V^\pi(s)\)("这个状态平均能拿多少分")。减掉它之后,括号里变成"这次比平均好还是差",有正有负,更新方向立刻变得有意义。

基线降方差:减去 V 后优势跨越 0 两侧,分布更集中 裸回报 G 全都被抬高 → 信号弱 高方差(宽) 0 2 4 6 8 所有更新都「抬高」,共同正偏移淹没信号 优势 A = G − V (V ≈ 4.8,把整簇平移到 0 附近) 低方差(窄) 0(平均水平) −3 0 3 比平均差 → 压低 比平均好 → 抬高 减 V 方差 ↓ 约 58% 分布更集中 · 跨越 0 两侧 · 更新更稳
基线降方差:原始回报全为正(左,所有更新都'抬高',信号被共同正偏移淹没);减去 V 后优势有正有负(右,比平均好的抬、差的压),分布更集中、更新更稳。

例题:证明加基线不改变梯度期望

要证 \(\mathbb{E}_{a\sim\pi_\theta}\big[\nabla_\theta\log\pi_\theta(a\mid s)\,b(s)\big]=0\)。

\[ \sum_{a}\pi_\theta(a\mid s)\,\nabla_\theta\log\pi_\theta(a\mid s)\,b(s) = b(s)\sum_a \pi_\theta(a\mid s)\,\frac{\nabla_\theta\pi_\theta(a\mid s)}{\pi_\theta(a\mid s)} \] \[ = b(s)\sum_a \nabla_\theta\pi_\theta(a\mid s) = b(s)\,\nabla_\theta\underbrace{\sum_a \pi_\theta(a\mid s)}_{=\,1} = b(s)\,\nabla_\theta 1 = 0 . \]

关键是两步:(1) \(b(s)\) 与 \(a\) 无关,作为常数提到求和外(这一步靠的是"常数提取",不是别的);(2) 真正承重的一步——梯度与求和可交换,把 \(\sum_a\nabla_\theta\pi=\nabla_\theta\sum_a\pi\),而 \(\sum_a\pi\equiv 1\),其梯度恒为 0。所以减基线不引入偏差,却能把方差里那一大坨"共同的正偏移"消掉。

关键结论。 基线是 RL 里"免费的午餐":不改期望、只降方差。这正是上一课贝尔曼估计出的 \(V(s)\) 的高光用途——它天生就是"该状态的平均回报",拿来当基线再合适不过。

4. 优势函数与 Actor–Critic

把"回报减基线"取它的期望版本,就得到优势函数(advantage): \[ A^\pi(s,a)=Q^\pi(s,a)-V^\pi(s). \] 它回答一个极其具体的问题:"在状态 \(s\) 下选 \(a\),比这个状态的平均水平好多少?" \(A>0\) 就该抬概率,\(A<0\) 就该压。理想的策略梯度其实是 \[ \nabla_\theta J=\mathbb{E}\big[\nabla_\theta\log\pi_\theta(a\mid s)\,A^\pi(s,a)\big]. \]

但 \(Q,V\) 我们并不知道,得估。于是有了 actor–critic(演员—评论家)双网络结构:

最简单的优势估计就是单步 TD 误差 \(\delta_t=r_t+\gamma V(s_{t+1})-V(s_t)\)。当 \(V=V^\pi\)(真值函数)时,\(\delta_t\) 是 \(A^\pi\) 的无偏估计(因为 \(\mathbb{E}[\delta_t\mid s_t,a_t]=Q^\pi-V^\pi=A^\pi\));实践中我们只有近似的 critic \(V_\phi\),用它替代时 \(\delta_t\) 就带偏差(偏差正比于 \(V_\phi-V^\pi\)),但换来的是很低的方差。这个"低方差、有偏"正是下面 GAE 在 \(\lambda=0\) 一端的特征。

Actor–Critic 闭环:评价→改进的循环Actor–Critic 闭环:评价 → 改进循环环境 Env状态/回报来源Actor π_θ出动作Critic V_φ打分 / 估 Vs状态a动作r , s'回报与新状态优势 A = Q − V策略梯度更新 ∇logπ · ATD 误差 δ = r + γV(s') − V(s)Critic 回归更新(自评)状态/动作流奖励流优势→改进 Actor
Actor–Critic 闭环:环境给状态 s,actor(策略π)出动作 a 作用于环境得到 r 和 s';critic(价值 V) 评估并算出优势 A=Q−V,优势回传给 actor 做策略梯度更新,critic 自己用 TD 误差更新。

GAE:在偏差和方差之间拨一个旋钮

"用几步回报来估优势"有个两难:步数少(如单步 TD)方差小但偏差大(严重依赖不准的 \(V_\phi\));步数多(如蒙特卡洛全回报)偏差小但方差大GAE(广义优势估计,Generalized Advantage Estimation)用一个衰减因子 \(\lambda\in[0,1]\) 把各步 TD 误差指数加权平均: \[ \hat A_t^{\text{GAE}}=\sum_{l\ge 0}(\gamma\lambda)^l\,\delta_{t+l}. \] \(\lambda=0\) 退化成单步 TD(低方差高偏差),\(\lambda=1\) 退化成蒙特卡洛(高方差低偏差)。\(\lambda\) 就是那个偏差—方差旋钮,实践里常取 0.95 左右。现在点到为止,PPO 里会用到它。

5. On-policy vs Off-policy:能不能重用旧数据?

策略梯度有个昂贵的前提:梯度是对"当前策略 \(\pi_\theta\) 采的样"求期望。一旦更新了 \(\theta\),旧数据就"过期"了,严格来说得重新采样——这叫 on-policy(同策略),REINFORCE、A2C、PPO 都属此类,数据利用率低但稳定。

与之相对,off-policy(异策略)(如 Q-learning、DQN)允许用另一个策略(甚至历史回放池)采的数据来学习,数据利用率高,但训练更易不稳定。

PPO 想要"鱼和熊掌兼得":用一批旧策略 \(\pi_{\text{old}}\) 的数据做好几步更新(提高数据利用率),但通过重要性采样修正分布差异,并限制每步别走太远以保持稳定。这就引出了重要性比与裁剪。

6. PPO:别一步把策略毁掉

朴素策略梯度有个致命脆弱点:学习率稍大,一步更新就可能把策略推到一个糟糕区域,而坏策略采的数据又更糟,负反馈崩盘、再也回不来信任域(trust region)思想说:每步更新要限制在"离旧策略不太远"的范围里,小步快走才稳。这个名字借自优化——就像优化里只在当前点的一个"可信半径"内相信你的近似模型,超出半径模型就不可靠了;在这里,"模型"是用旧数据估出来的优势,"半径"就是新旧策略的偏移量。

怎么衡量"用旧数据评估新策略"?用重要性比(importance ratio) \[ r_t(\theta)=\frac{\pi_\theta(a_t\mid s_t)}{\pi_{\theta_{\text{old}}}(a_t\mid s_t)} . \] \(r>1\) 表示新策略比旧策略更想选这个动作。代理目标 \(\mathbb{E}[\,r_t(\theta)\,\hat A_t\,]\) 就是"用旧数据估的、新策略的期望优势"。但如果不加约束,模型会为了拉高一个高优势动作把 \(r\) 推到天上去——一步走太远。

PPO 的招数干净利落:裁剪代理目标(clipped surrogate objective) \[ L^{\text{CLIP}}(\theta)=\mathbb{E}_t\Big[\min\big(r_t\hat A_t,\;\operatorname{clip}(r_t,1-\epsilon,1+\epsilon)\,\hat A_t\big)\Big]. \] 逐字读这个 \(\min\)(下面以 \(|\hat A|=1\) 为例):

这恰好和下面调一调③ 的数值对上:\(A=-1\) 时 \(r=0.8\) 与 \(r=0.5\) 都给 \(-0.80\)(封底),而 \(r=1.5\) 给 \(-1.50\)(未裁、更负)。

PPO 裁剪目标随重要性比 r 的变化 0 0.8 1.0 1.2 2.0 1−ε 旧策略 1+ε r = π / π_old 目标 L^CLIP A > 0:裁平(封顶 1+ε) 裁掉的额外收益 → 无动机走太远 A < 0:裁平(封底 −0.8) 裁掉的 额外收益 信任域
PPO 裁剪目标随重要性比 r 的变化。A>0(绿):r 超过 1+ε 后目标被裁平、不再奖励更大的偏移;A<0(红):r 低于 1−ε 后被裁平。阴影区是被裁掉的部分。
等价的 KL 视角。 裁剪只是实现信任域的一种"硬"手段。另一种"软"手段是直接在目标里加 KL 惩罚:\(\mathbb{E}[r_t\hat A_t]-\beta\,D_{\mathrm{KL}}(\pi_\theta\|\pi_{\theta_{\text{old}}})\)。两者精神一致——都在说"新策略别离旧策略太远"。裁剪用一个 \(\epsilon\) 的"护栏",KL 惩罚用一个 \(\beta\) 的"弹簧"。这里 KL 写成新策略在前 \(D_{\mathrm{KL}}(\pi_\theta\|\cdot)\),因为它是用新策略采样来估计的;下一节 RLHF 会沿用同一约定直接复用这根弹簧。
易错:\(\epsilon\) 不是学习率。 \(\epsilon\)(常取 0.1–0.2)控制的是"单步策略能偏离旧策略多远",学习率 \(\eta\) 控制"每个梯度步走多大"。两者都管"步长"但层级不同:即使 \(\eta\) 很小,多跑几个 epoch 也可能让 \(r\) 累积出界,所以裁剪这道护栏始终必要。

7. ★ 直通 RLHF:奖励模型 + PPO + KL 锚

现在把这套机器接到大模型上。RLHF(基于人类反馈的强化学习)分两步:

  1. 训练奖励模型(reward model)\(R_\psi\):用人类对"哪个回答更好"的偏好数据,学一个给回答打分的函数;
  2. 用 PPO 优化语言模型策略\(\pi_\theta\),去最大化奖励——但带一个 KL 锚: \[ \max_\theta\;\mathbb{E}_{x,\,y\sim\pi_\theta}\Big[\,R_\psi(x,y)\;-\;\beta\,D_{\mathrm{KL}}\big(\pi_\theta(\cdot\mid x)\,\big\|\,\pi_{\text{ref}}(\cdot\mid x)\big)\Big]. \]

这里把语言生成看成 RL:状态 = 已生成的前缀,动作 = 下一个 token,奖励 = 整段回答的得分 \(R_\psi\)。注意奖励只在序列末尾给一个标量,怎么把它摊到中间每个 token(靠 \(\gamma\) 折扣、价值函数 bootstrap 或 GAE 把末端信号往前回传)是 RLHF 里最关键、也最容易把初学者绕晕的工程问题——见下方 skip。

那个 \(-\beta\,D_{\mathrm{KL}}(\pi_\theta\|\pi_{\text{ref}})\) 是灵魂所在——\(\pi_{\text{ref}}\) 是微调前的参考模型,KL 惩罚把策略拴在参考模型附近,防止它为了骗高奖励而说出语无伦次的胡话(reward hacking)或丢掉原有的语言能力。

ML 呼应第 5 课的 KL 埋点

还记得第 5 课讲信息论时埋下的 \(D_{\mathrm{KL}}(p\|q)\)——它度量"用 \(q\) 近似 \(p\) 多吃亏",非负、不对称。在 RLHF 里它摇身一变成了"别跑偏"的正则项:\(\beta\) 大则策略保守、贴着参考模型;\(\beta\) 小则更敢追高奖励但更冒险。这与第 6 节 PPO 的 KL 视角是同一根弹簧——两处都把新策略 \(\pi_\theta\) 写在前(KL 不对称,方向取决于对谁采样,这里都是对新策略采样估计),只不过第 6 节锚的是上一步的旧策略,这里锚的是参考模型

RLHF 流水线:奖励模型打分减 KL 锚定,PPO 更新策略提示 x 进入策略 π_θ 生成 y,奖励模型 R_ψ 打分,减去 β·KL(π_θ‖π_ref) 组成目标,PPO 回环更新策略;底部预告 DPO 与 GRPO。RLHF 流水线提示 x策略 π_θ(待训练 LLM)奖励模型 R_ψ(打分器)参考模型 π_ref(冻结)组合目标 maximizeR_ψ(x,y) − β·KL(π_θ‖π_ref)生成 y打分 R(x,y)−β·D_KL(π_θ‖π_ref)锚定参考模型→防胡话 / 防 reward hackingPPO 更新预告(后继者):DPO — 去掉 R_ψ 与 PPO 循环,直接用偏好对优化策略GRPO — 去掉 critic,用组内相对优势
RLHF 流水线:策略 π_θ 生成回答 y,奖励模型 R_ψ 打分,减去 β·KL(π_θ‖π_ref) 作为最终目标,PPO 据此更新 π_θ;KL 锚把策略拴在参考模型附近,防止 reward hacking。

两个"省事"的后继者(点到,留作预告)

例题:对二动作 softmax 策略手算 \(\nabla_\theta\log\pi\)

设两个动作的 logits 为 \(\theta=(\theta_0,\theta_1)\),\(\pi(a)=\mathrm{softmax}(\theta)_a\)。对实际选到的动作 \(a\): \[ \log\pi(a)=\theta_a-\log\!\big(e^{\theta_0}+e^{\theta_1}\big). \] 对 \(\theta_j\) 求偏导: \[ \frac{\partial\log\pi(a)}{\partial\theta_j}=\mathbb{1}[j=a]-\pi(j). \] 即梯度向量 \(\nabla_\theta\log\pi(a)=\mathbf{e}_a-\pi\)。取 \(\theta=(0.5,-0.5)\),则 \(\pi=(0.731,0.269)\),若选到 \(a=0\): \[ \nabla_\theta\log\pi(0)=(1-0.731,\;-0.269)=(0.269,\,-0.269). \] 直觉:抬高被选动作的 logit、压低其余动作的 logit,压低多少正比于它们当前的概率。我用有限差分验证过,两者完全吻合(见下方代码)。

调一调,观察现象

下面三个小实验都用 numpy 现场写的最小环境,几秒跑完。建议每个都先结果再运行。

调一调 ①:有无基线的方差对比

改什么:在伯努利老虎机上,固定策略,分别用"裸回报"和"回报减基线 \(V\)"估策略梯度,比较方差。
预期看到:两者梯度均值几乎相同(基线不改期望),但带基线的方差明显更小(这里约降到 42%)。
为什么:基线把所有动作共有的"正偏移"消掉,只留下"比平均好/差"的部分。

import numpy as np
rng = np.random.default_rng(0)
p = np.array([0.25, 0.75])          # 动作1更好
pi = np.array([0.5, 0.5])           # 固定均匀策略
b = (pi*p).sum()                    # 基线 = V = 期望奖励
def grad_logpi(a, pi):
    g = -pi.copy(); g[a] += 1.0; return g
g_nob, g_b = [], []
for _ in range(2000):
    a = rng.choice(2, p=pi)
    r = float(rng.random() < p[a])
    gl = grad_logpi(a, pi)
    g_nob.append(gl*r)              # 无基线
    g_b.append(gl*(r-b))            # 有基线
g_nob, g_b = np.array(g_nob), np.array(g_b)
print("mean no-baseline :", g_nob.mean(0))
print("mean baseline    :", g_b.mean(0))
print("var  no-baseline :", g_nob.var(0).sum())
print("var  baseline    :", g_b.var(0).sum())
print("variance ratio   :", g_b.var(0).sum()/g_nob.var(0).sum())
# 预期: 两个 mean 几乎相等, ratio < 1 (约 0.42)

调一调 ②:跑一个最小 REINFORCE,看基线如何稳住训练

改什么:放开策略让它更新,看 \(\pi(\text{好动作})\) 是否爬向 1。再把 use_baseline 切成 False。注意这里特意把奖励整体抬高了一个常数偏移 OFFSET=5——让所有回报都远离 0,正是基线最该出手的场景。
预期看到:好动作概率从 0.5 稳步升到 ~0.9+;去掉基线后,收敛后的曲线明显更抖(这里跨 50 个种子,收敛段的步间抖动方差约差几百倍)。如果你只跑一条种子、且不加偏移,这个差别很可能肉眼看不出来——降方差的效果要么靠"奖励远离 0"放大,要么靠"多种子平均"才稳定显现。
为什么:基线把回报里那一大坨"共同正偏移"(这里是 5)减掉,梯度信号才不被它淹没;不减的话,每个动作都被那个大正数往上猛推,方向信号被噪声盖住。

import numpy as np
def softmax(z): z=z-z.max(); e=np.exp(z); return e/e.sum()
def run(seed, use_baseline, OFFSET=5.0, eta=0.1, steps=400):
    rng = np.random.default_rng(seed)
    p = np.array([0.1, 0.9]); theta = np.array([0.0, 0.0])
    traj = []
    for it in range(steps):
        pi = softmax(theta)
        a = rng.choice(2, p=pi)
        r = float(rng.random() < p[a]) + OFFSET      # 奖励整体抬高
        base = (pi*p).sum() + OFFSET if use_baseline else 0.0
        adv = r - base
        g = -pi.copy(); g[a] += 1.0                  # grad log pi(a)
        theta += eta * g * adv
        traj.append(softmax(theta)[1])
    return np.array(traj)
# 单条种子先看学没学会
tr = run(seed=1, use_baseline=True)
print("final pi(good) =", round(tr[-1], 3))
# 跨 50 种子比较收敛段(后200步)的步间抖动方差
import numpy as np
def jitter(use_b):
    return np.mean([np.diff(run(s, use_b)[200:]).var() for s in range(50)])
print("jitter var  baseline :", jitter(True))
print("jitter var  no-base  :", jitter(False))
print("ratio (b/nob)        :", jitter(True)/jitter(False))
# 预期: pi(good) 升向 0.9+; ratio 远小于 1 (带基线抖动小得多)

调一调 ③:PPO 裁剪到底裁掉了什么

改什么:扫一遍重要性比 \(r\),分别在 \(A=+1\) 与 \(A=-1\) 下打印裁剪目标,改 eps 看护栏宽窄。
预期看到:\(A=+1\) 时目标在 \(r>1+\epsilon\) 后封顶不再增(\(r\) 偏大一侧被裁);\(A=-1\) 时在 \(r<1-\epsilon\) 后封底不再降(\(r\) 偏一侧被裁),而 \(r\) 偏大一侧不裁、惩罚继续加深;\(\epsilon\) 越大护栏越宽。
为什么:裁剪只取消"把 \(r\) 推向对自己有利方向"的额外收益(好动作往大推、坏动作往小推),从而限制单步偏移;坏动作被推向大 \(r\) 本就该重罚,不裁。

import numpy as np
def clip_obj(r, A, eps=0.2):
    return min(r*A, np.clip(r, 1-eps, 1+eps)*A)
for r in [0.5, 0.8, 1.0, 1.2, 1.5]:
    print(f"r={r:>3}:  A=+1 -> {clip_obj(r, 1.0):+.2f}   "
          f"A=-1 -> {clip_obj(r, -1.0):+.2f}")
# 预期(eps=0.2):
#   A=+1 时 r=1.2 与 r=1.5 都给 +1.20 (封顶), r 偏小一侧不裁
#   A=-1 时 r=0.8 与 r=0.5 都给 -0.80 (封底), 而 r=1.5 给 -1.50 (未裁、更负)

动手练习

  1. 基线最优值(推导):已知减任意 \(b(s)\) 不改期望。证明使梯度估计方差最小的基线是 \(b^*(s)=\dfrac{\mathbb{E}[(\nabla_\theta\log\pi)^2 G]}{\mathbb{E}[(\nabla_\theta\log\pi)^2]}\)(对方差关于 \(b\) 求导置零)。说明为什么实践里仍常用 \(V(s)\) 近似它。
  2. reward-to-go(代码):把调一调②改成一个两状态、最长 5 步的小 MDP,分别用"整条回报 \(G(\tau)\)"和"未来回报 \(G_t\)"更新,打印两种梯度估计的方差。骨架:
    import numpy as np
    # 两状态 MDP: s0--a->s1, 终点给奖励; 自己设转移与奖励
    # 采样若干轨迹, 对每条算 G(tau) 与各步 G_t
    # 比较 sum_t gradlogpi*G(tau)  vs  sum_t gradlogpi*G_t 的方差
    # 预期: 用 G_t 的方差更小
  3. 单步 TD 优势(代码):在调一调②的老虎机上把基线从"真实 \(V\)"换成一个在线估计的 \(\hat V\)(用 \(\hat V\leftarrow\hat V+\alpha(r-\hat V)\) 滑动平均),观察它是否收敛到 \(\sum_a\pi(a)p_a\)(注意若加了偏移则再加上偏移)并依然降方差。
  4. KL 惩罚 vs 裁剪(思考+代码):把调一调③的裁剪目标换成 \(r\hat A-\beta\,(r-1-\log r)\),扫 \(\beta\) 看它如何同样地"抑制 \(r\) 偏离 1"。这里的 \(r-1-\log r\) 是用单个样本的重要性比 \(r\) 去估计 \(D_{\mathrm{KL}}(\pi_\theta\|\pi_{\text{old}})\) 的常用估计量(在 \(r=1\) 处取最小值 0、两侧上翘,恒非负),所以它扮演的正是把 \(r\) 拉回 1 的那根"弹簧",与正文里分布层面的 KL 锚是一回事。定性比较"弹簧"(KL)和"护栏"(裁剪)两种形状的差异。
  5. RLHF 目标实验(代码):构造一个 5-动作离散"句子"分布,给定一个玩具奖励向量与参考分布 \(\pi_{\text{ref}}\),数值优化 \(\sum_a\pi(a)R(a)-\beta D_{\mathrm{KL}}(\pi\|\pi_{\text{ref}})\)。改 \(\beta\):\(\beta\to0\) 时 \(\pi\) 退化成"全压在最高奖励动作",\(\beta\) 大时 \(\pi\to\pi_{\text{ref}}\)。验证这个趋势。

掌握自检

模块 2 到此完成!你已经把概率、信息论、强化学习的地基打牢。下一站:进模块 3(Python/PyTorch 工程)把这些算法真正写成能跑的代码,或者直接进入深度学习。这两条路最终会在 RLHF 这里重逢。