· 强化学习

强化学习笔记:从策略梯度到 PPO

把「让模型表现更好」这个模糊目标,一步步变成一行可以求导、可以写代码的公式:贝尔曼方程 → 轨迹概率 → 策略梯度 → GAE → PPO-clip,附 RLHF 实现细节。

这份笔记在讲什么

一句话:把"让模型表现更好"这个模糊目标,一步步变成一行可以求导、可以写代码的公式。

整条链条只有五步,后一步总是解决前一步留下的麻烦:

步骤遇到的问题解法
1. 定义"好"(P2)一条轨迹的好坏不能直接当目标(轨迹无穷多)目标 = 回报的期望
2. 求导(P3) 里含有环境,环境未知,梯度算不出来期望的导数 → 导数的期望(log 技巧),环境项自动消失
3. 估期望(P3–P4)期望是天文数字级的求和蒙特卡洛:采样几条轨迹取平均
4. 降方差(P4–P6)同一条 用到底,把无关的历史奖励也算进来 → 噪声换成 ;再减基线 ;再换 TD 误差;再 GAE 加权
5. 稳更新(P7–P8)策略每步都变,一步迈大了就崩PPO-clip:比值超出信任域就切断梯度

符号表

符号含义备注
时刻的状态、动作大模型里 = 已生成的上文, = 下一个 token
策略网络,给定状态输出动作分布就是那个要训练的 LLM
环境的状态转移概率未知,也不需要知道
即时奖励原稿两个符号混用
折扣因子0.9~0.99;压住远期奖励,保证级数收敛
从 起的折扣回报一个采样出来的标量
状态价值 = 从 出发的期望回报用来当基线 / 当 Critic
优势 = 这个动作比平均水平好多少决定"提高还是降低"该动作概率
新旧策略的概率比值PPO 的核心变量

P1(01.jpg)贝尔曼方程 · 状态价值

标题:贝尔曼方程  右上批注:折扣

核心:当前价值 = 即时奖励 + 未来价值

递归:〇 → 〇 → 〇 → 〇 →(依次对应 2026、2027、2028、2029)

在 RL 中,价值函数:

↑ 箭头批注:当前价值 =

读懂这段

为什么需要 。 未来的钱不值钱: 时,第 10 步的 1 块钱折算到今天只有 。数学上它还兼一个更硬的作用——保证 在无限长时仍然收敛。

贝尔曼方程真正省掉的是什么。 直接算 要枚举从 出发的所有未来路径,指数爆炸。贝尔曼把它压成一句:我只算一步,剩下的交给"下一状态的价值"。表格型 RL 能收敛、DQN 能只学一步差分,全靠这个递归。

一个必须点破的符号冲突。 原稿状态图里把单步奖励写成 、,容易和折扣因子 混淆。看式子 就知道:前一项是这一步拿到的奖励 ,后一项才带折扣。读的时候把 一律当作 ( 的即时奖励)。原书里这个量写作 ,符号更不易混。

状态转移示意(S4 出发)

  • ,转移概率
  • ,转移概率

逐状态展开

⇓(把括号里的部分收成 、)

读懂这段

这就是"按定义展开 → 认出 → 合并成求和号"三步:

  1. 先把 按所有可能的下一步展开,每条分支乘自己的概率 ;
  2. 括号里剩下的部分,正好就是 和 的定义式 —— 于是可以直接替换;
  3. 替换完得到一个"只含自己和自己邻居"的方程,写成求和号就是通用的贝尔曼方程。

这一步是全篇的地基:后面所有"优势"、"TD 误差"都是在算这个方程两边的差。


P2(02.jpg)轨迹的概率 · 策略参数化

起点公式:

期望的分支展开(树状图)

分出若干分支 , 再分出 ……

右侧列表:

概率回报
……
  • 每条轨迹有一个概率
  • 每条轨迹有一个

⇓

轨迹定义

单轨迹一般去掉奖励 ,因为 、 一旦确定, 也就定了……只有 、 是不确定的。

这条轨迹的概率如下:

底部从左到右的标注:策略 → 转移概率 → 策略 → 初始分布

右侧批注:

  • 转移概率 、 不知道(环境)
  • 是知道的,是个神经网络

读懂这段

这一步只做了一件事:把"轨迹的概率"拆成"谁的锅"。 用链式法则展开联合概率,再用马尔可夫性把 砍成 ——历史不必记。

拆完你会发现概率被精确地分成两类因子:

  • 、 —— 环境的,我们既不知道也没法改;
  • —— 模型的,唯一能被 影响的。

这个二分是全篇的转折点:下一节对 求导时,环境那半边整体消失,只剩下策略项。这就是 PPO/GRPO 这些算法能"无模型(model-free)"训练的数学原因——我们从来不需要知道环境长什么样。

用 LLM 的视角重写

这条轨迹, 是提示词:

← 找到一组神经网络参数 ,让这个连乘最大化

(原书把同一个式子写成倒序 ,并以训练数据 abcd 为例——完全等价。)

⇓

← 批注:交叉熵损失函数

读懂这段

同样的式子换成 LLM 就是监督微调(SFT):最大化"标准答案每个 token 的条件概率"的连乘,等价于最小化逐 token 的负对数似然,也就是交叉熵。

注意此时 是给定的标准答案;而 RL 里 是模型自己采样出来的——同一个式子,采样者换成自己,就从 SFT 变成了 on-policy RL。后面 PPO 的 ratio 项,本质是在问"比起旧模型,新模型给出这串 token 的概率变了多少"。

目标函数的引出

强化学习的目的:找到一个 ,使得从环境获得的回报越大越好。

(一条完整轨迹获得所有奖励的回报)——不能作为目标,因为有无数条轨迹

每条轨迹都有一个确定的 。

所以目标函数应该是:

:采样策略 ,得到一条轨迹

右下批注:强化学习的目的是让 的期望值越大越好,通过调整 实现(求导)。

读懂这段

单条轨迹的 有极强随机性:同样的策略,这次运气好 ,下次运气差 。拿它当目标去爬坡,等于跟着噪声走。 所以要取期望——优化的是"平均表现",不是"这一次表现"。

顺带解释了笔记后面那句吐槽:"强化学习的山,形状一直在变。"(见 P3 末)


P3(03.jpg)目标函数求导 → 策略梯度

如何实现 对 求导?把上式中期望 展开:

(右侧批注:每条轨迹的回报 ;每条轨迹的概率 ;对每一个 遍历求和)

左侧圈注:这个公式直接写代码

底部批注:这个求导过程叫 策略梯度法,图灵奖;期望的导数变成导数的期望。

读懂这段

这一页是整份笔记的技术核心,值得逐行拆。

① 为什么 的导数没了。 是采样出来的数,不含 ,所以 。这一项消失不是技巧,是事实:我们改不了奖励,只能改"走出这条轨迹的概率"。

② 关键的一步换形:。(也就是 score function / log-derivative trick)为什么要多此一举?因为两种写法的可采样性完全不同:

  • :要求你对每条轨迹都算梯度再加权,而 本身不构成一个概率分布,没法用采样估计。
  • :这正好是" 在分布 下的期望",采样平均就能估。

③ 环境项就在这一步蒸发。 把 P2 的分解代进 :

于是梯度里只剩下 。注意它不再含转移概率——所以 DQN 之后的策略梯度方法天然 model-free。

④ 直观含义。 这个式子只说了一句话:这条轨迹回报高,就把这条轨迹上每个动作的概率往上推;回报低,就往下压。梯度方向由 的符号决定,大小由 的大小决定, 只负责"往哪个方向推"。

右侧: 的分解

蒙特卡洛近似

在期望无法计算的情况下(就还是天文数字),采用蒙特卡洛方法近似估算这个期望。

(旁边画了一个不规则图形,标 100 与 75)← 蒙特卡洛方法求面积(扔豆子,阴影部分占 75%)

蒙特卡洛方法,样本数为 :

每个 都能算出来,但没用,因为我们要算的是 。↑ 批注:常数,对 求导为 0

解决方法:期望的导数变为导数的期望 → 策略梯度法

使用蒙特卡洛方法近似策略梯度的期望,样本数为 :

样本数为 ():

读懂这段

坑在这里: 本身可以蒙特卡洛估(采样取平均就行),但我们要的是 ,不能对"样本平均"直接求导——因为 已经采出来了,是常数,求导为 0。所以顺序必须是:先解析求导(P3 那一长串),再对结果做蒙特卡洛。这就是"期望的导数 → 导数的期望"这句批注的分量。

别被 迷惑:那行是"最大化 乘以回报"的写法,用梯度上升时才等价于上面的梯度式( 只作用在 上, 在 处取极值)。手写省略了梯度符号,读代码时以 版为准。

为什么够用。 一次采样一条轨迹、在同一条轨迹上取 个时间步,本身就是 个样本;而且 on-policy 每次更新完就重采,偏差靠迭代摊平。代价是方差大——这就是 P4 要解决的问题。

右侧方框:RL 与 DL 的差别

强化学习,策略每次采样轨迹不一样,目标函数也在每次变化, 一变,目标函数也变了。 爬山,每走一步,山的形状都在变化,还要找到山最高点。 而深度学习,山的形状不变,只要找到山的最低点。 噪声问题,后续一直要解决!

读懂这段

这句比喻对应一个具体差别:监督学习的损失面由固定的数据集定义,是静态的;RL 的损失面由当前策略自己的采样分布定义—— 一动,采样分布就变,损失面跟着变形。

后果有两个,后面每页都在应付其中一个:

  • 方差:同一条轨迹的 抖动很大 → 基线、TD、GAE(P4–P6);
  • 分布漂移:数据是旧策略采的,参数却已经更新了,用旧数据算新梯度会失真 → PPO 的 ratio 和 clip(P7)。

右下:单步采样示例

,一条样本轨迹,一步结束了。

↑ ← 最大化(采用 )

优化 以后,从 到 概率变大。


P4(04.jpg)REINFORCE 改进与基线

采样一条轨迹的梯度是

梯度公式推导可得:

上面式中 是目前为止获得的所有奖励总和。

一条轨迹上,无论哪个时刻 ,式中都是 。

始终使用固定不变的 来增加(减少)采取动作 的概率。

某个动作的好坏根据行动之后获得奖励总和的平均水平定的,与行动之前获得奖励无关。

行动 的权重 ,包含了时刻 之前奖励,这部分奖励是噪声数据。为去除噪声,对 做以下修改:

换 ,采样同一条轨迹, 算出的策略梯度方差、均值误差比较小。

换成 ,称为 REINFORCE 改进。

REINFORCE 这个名字其实是个首字母缩写:REward Increment = Nonnegative Factor × Offset Reinforcement × Characteristic Eligibility(奖励增量 = 非负因子 × 偏移强化 × 特征资格)。记住结论就行:有权重、能增能减。

读懂这段

问题:功劳摊错了人。 用整条轨迹的 给每一步加权,意味着"第 1 步的动作"和"第 8 步的动作"拿同一个分数。可第 1 步根本不可能影响它之前的奖励 。这部分是纯噪声,只会把梯度搅浑。

改法:因果性(causality)。 第 步的动作只能影响 之后的奖励,所以权重换成 ,把历史奖励切掉。

数学上这是精确的,不是近似。 因为 ——历史奖励与当前动作的梯度期望正交,删掉它不改变期望,只降低方差。这是"降方差但不引入偏差"的第一次操作,后面 baseline 用的是同一个恒等式。

为什么要引入基线

玩倒立摆时,不管怎么推,获得的奖励都是正奖励。

不管怎么玩,都会提升某个状态采取某个动作的概率,即使奖励很少也如此。

应该在获得奖励多时,给予正奖励;获得奖励少时,给一个负的奖励 —— 引入基线。

右侧竖线批注:

  • :输入为 ,输出为基线的函数
  • 这里最常用状态价值函数

读懂这段

如果奖励恒正,梯度就永远在"加概率"。 假设所有 ,那么每次更新都在把"这一轮实际做过的动作"的概率往上推——包括那些做得烂的动作,只是推得少一点。结果:好动作和坏动作的相对差异被淹没在共同的正偏移里,学得极慢。

把 减掉一个基准 ,就变成"高于平均 → 推;低于平均 → 压",正负分开。

为什么减掉它不影响正确性。 只需要一个恒等式:

所以对任意只依赖 的 ,减掉它期望不变、方差变小。这是白拿的收益——唯一要求是 不能依赖 。

为什么最优基线是 。 让方差最小的 是 在给定状态下的条件期望,即 。于是 有了名字:优势(advantage)——这个动作比该状态的平均水平好多少。

趋近于 时,三种方法均值相等。

只采样一条轨迹时,第三种比第二种方法更接近均值,第二种比第一种更接近均值……

↓ 批注: 的期望值(输入为 ,输出为基线的函数)

  • :价值函数神经网络的所有参数
  • :将价值函数模型化的神经网络

左下圈注:训到最后: = 的均值

右下框:

训到 ,就是我们想要的结果,导数为 0,可能到了极值点。

读懂这段

真实环境里 不可得,于是用神经网络 去拟合它——这就是 Critic 的诞生。到这一步,算法已经有两个网络了:Actor 出动作,Critic 打分。P5 正式给这个组合命名。

"训到 "该理解为收敛信号,而不是硬目标。 理想情况下优势在期望意义上为 0(有正有负、平均抵消),而不是每个样本都恰好等于 0;后者只会在策略完全无法改进时出现。


P5(05.jpg)TD 方法 · Actor-Critic

蒙特卡洛采样:采样出一条完整轨迹,然后更新策略神经网络 。

时序差分方法(TD 方法):使用 1 个时间步(或 个)后的结果就能更新。

训练价值函数 时,蒙特卡洛方法使用的是整回报 ;TD 方法使用的是 。

时刻单步 TD 误差:

蒙特卡洛

训练使得 越来越接近

即:二者均方误差损失越小越好,

批注:(高方差,低偏差)——因为 是一整条采样轨迹加出来的,随机性全在里面。

TD 方法

训练使得 越来越接近

即:二者均方误差损失越小越好,

批注:(低方差,高偏差)易于收敛,一步就可以更新。

读懂这段

方法目标值方差偏差何时能更新
MC(真实采到的整段回报)高( 步噪声累加)低(无偏)必须等轨迹结束
TD(自己估自己)低(只用一步)高( 不准就跟着错)每步都能更新

TD 目标里用了 本身,这叫 bootstrap:拿当前估计去当下一时刻的标签。代价是偏差,收益是能在线学习、方差小得多。 的绝对值恰好度量"这一步比预期好多少",所以它天然就是优势的估计。

注意不要混淆两种"TD 误差":训练 时它是 Critic 的回归残差;拿来当 时它是 Actor 的权重。同一个量,两个用途。

(括号整体标注:单步 TD 误差  右侧:TD = time difference)

训练目标:单步 TD 误差趋于 0。

因为当目标函数到达极大值点时,对 求得的梯度一定为 0,单步 TD 误差也为 0。

以上 TD 算法就是 Actor-Critic 架构。

  • Actor(演员):策略 网络
  • Critic(评论家):价值函数 网络

框注: 和 是两个神经网络,要同时训练这两个神经网络。

Actor-Critic 的意思:使用 来评估基于策略 采取动作的好坏。

读懂这段

一句话概括分工:Actor 负责"演",Critic 负责"评";Critic 评得越准,Actor 的梯度噪声越小。 两者互相喂养——Critic 的标签来自 Actor 采的轨迹,Actor 的方向来自 Critic 的优势估计,所以必须同时训练(实际实现里往往分两个优化器和不同学习率)。

相比 REINFORCE,这里的优势不再等整条轨迹回传,一步就能算——on-policy 采样的利用率高了一个量级。

一步 TD 误差

右上气泡:贝尔曼期望方程

两步 TD 误差

三步 / 多步 TD 误差

读懂这段

的推导值得盯一眼:中间那个 是怎么"凑"出来的。原式里本来没有它,但加减一个 之后,式子自动裂成"两个相邻的单步误差",一个 加一个 。

由此递推: 步优势 = 前 个 的折扣和。 是纯 TD(偏差大)、 是纯 MC(方差大),中间任意 都在两者之间插值。于是自然的想法是:别选 了,把所有 加权平均起来——这正是 P6 的 GAE。

右下框:

也叫优势 1 步 TD 目标 相对基线 的优势 对多步 TD 误差累加权平均 —— 广义优势估计


P6(06.jpg)GAE 与策略梯度法汇总

GAE:将不同步数的优势指数加权平均

右上:代码实现三步

  1. 先向前计算每一步的 1 步 TD 误差:
  2. 把数据逆序:
  3. 遍历逆序数组:
    • …

中间大公式(代码中用的公式):

,是 GAE 引入的一个超参数。

  • : → 批注: 时刻单步 TD 误差(方差小,偏差大)
  • : → 批注:基线版 REINFORCE(方差大,偏差小)

读懂这段

是一个"偏差-方差旋钮"。 只看一步,方差最小、偏差最大; 看到轨迹尽头,退化成 MC,无偏但噪声大。实践中取 附近——远端 被 指数衰减,几乎不贡献方差,却保留了一部分无偏性。

为什么写成递推而不直接算 。 直接算是 ;递推从尾巴往前扫一遍就够,。这就是"逆序数组"那三步的全部意义——同一个数学对象,换个计算顺序。写代码时只需记住一行:

adv = 0.0
for t in reversed(range(T)):
    delta = r[t] + gamma * v[t+1] - v[t]
    adv = delta + gamma * lam * adv     # 累积量
    advantages[t] = adv

GAE 的 Actor-Critic 策略梯度定理

批注: 中 A 表示 Advantage(优势)

策略梯度法汇总

#名称
1最简单的策略梯度法
2REINFORCE
3带基线的 REINFORCE
4Actor-Critic
5广义优势估计

批注:它也叫优势函数(Advantage Function),在状态 采取动作 时,策略的优势是多少。

批注:广义优势估计是对单步 TD 误差的进一步改良。

读懂这段

这五行的结构完全一样,差别只在那一个标量 ——"用什么东西给这一步的动作打分"。把这张表记住,Policy Gradient 家族就没有秘密了:

打分依据偏差方差需要什么
整条轨迹回报无偏最大等轨迹结束
从 起的回报无偏大等轨迹结束
减基线无偏中需要
单步 TD 误差有偏小需要 ,在线
GAE可调可调需要 , 控制

注意 、… 都带着 的折扣和,而 P7 的 PPO 只是在这个梯度的基础上再加一层"别走太远"的闸门——数学主干到 P6 就结束了。


P7(07.jpg)PPO-clip 的六种情况

梯度上升算法:

找到一个算法,使得训练出来的新策略一定比旧策略好:

带裁剪的 PPO 目标函数

标注:旧策略产生的轨迹 / 旧策略采取动作 的优势估计 / 将比值裁剪到

针对同一个状态 ,新策略采取动作 的概率,和旧策略采取动作 的概率的比值。

读懂这段

为什么要 这个比值。 数据是旧策略采的,但参数更新后策略已经是新的。用旧数据估新策略的表现,必须做重要性加权(importance sampling), 就是那个权重。 表示这个动作的概率没变, 表示翻倍。

为什么要 clip。 重要性加权的方差随 爆炸;而且策略一步走太远,旧数据就彻底失真, 的估计从"可信"变成"胡说"。clip 的作用是:只要 还在 里,就正常更新;一旦越界,目标函数被削成常数,梯度变 0——这一步对参数失效,等下一轮重新采样。

这就是"信任域"的软实现:TRPO 用 KL 硬约束 + 二阶优化(贵、难调),PPO 用一次 min + clip(便宜、稳),这也是它能成为工业默认的原因。

的作用容易被忽略。 它保证目标是下界:既防止 朝"有利方向"过度膨胀(被 clip 截住),又不会因为 clip 反而给出错误激励;对不利方向( 变小)则照常惩罚。没有 、只用 clip 的话,目标会出现"越差越激励"的漏洞。

六种情况的逐条判定

情况 1:,
不裁剪,梯度有效,正常增大这个动作的概率。

情况 2:,
不裁剪,梯度有效,正常降低这个动作的概率。

情况 3:,
,取 ,不裁剪,梯度有效。

情况 4:,
,取 ,触发裁剪,梯度 = 0,不能继续压低该动作的概率。

情况 5:,
,取 ,触发裁剪,梯度 = 0,不能无限放大好动作概率。

情况 6:,
,取 ,不裁剪,梯度有效,继续狠狠降低这个动作概率。

读懂这段

把它压成一张 2×3 表就再也不会记混( 取 0.2):

(好动作)(坏动作)
(概率已降太多)情形 3:正常往上推情形 4:截断,不许再压
落在区间内情形 1:正常往上推情形 2:正常往下压
(概率已涨太多)情形 5:截断,不许再抬情形 6:继续狠狠压

规律一句话:只有"顺着已有趋势继续加码"会被截断。已经压得很低的坏动作、已经抬得很高的好动作,都不用再使劲;反向的修正一律放行。

总结

  • Case 4(, 过小):动作很差,但新策略已压得很低,禁止继续下调概率,梯度置为 0。
  • Case 5(, 过大):动作很好,但新策略已比旧策略高太多,禁止继续上调概率,梯度置为 0。

(本页下半部分空白)


P8(08.jpg)RLHF 的奖励项 · PPO 代码细节

大模型中的即时奖励:KL 惩罚

—— 乘以 KL 散度:既叫「即时奖励」,也叫「KL 惩罚项」。

最后一个 token:奖励模型给的分

三步流程:① 采样轨迹 ② 把概率的值算出来 ③ 计算 PPO 目标函数并更新

原稿这一处要改一个字:分子写的是 ,标准写法是 (当前策略 / 冻结参考策略)。 两个分母容易混——PPO 的 ratio 是 ,KL 项是 :一个比"上一版",一个比"SFT 版"。 实现上一次迭代内 与 只差几个梯度步,所以写成 数值上差别很小,但概念上要分清。

读懂这段

LLM 的 PPO 和经典 PPO 的差别只在奖励从哪来。 经典 RL 每步都有环境给 ;语言模型只有序列末尾有一个来自奖励模型的 score,其余位置的奖励全是这个 KL 项。

KL 项在干什么。 它逐 token 约束新模型别偏离参考模型(,也就是 SFT 模型)太远:

  • 分母 固定不动,是"锚";
  • 分子是当前策略 ,衡量"这一刻模型偏离锚多远";
  • 负号 + 表示"偏离越多,惩罚越重"。

没有这一项会怎样。 模型会钻奖励模型的漏洞(reward hacking):输出一堆让 RM 打高分、但人类看毫无意义的字符串。KL 惩罚等于给"保持人话"上了个价签。

PPO.py 292 行,错位计算 —— 一定要搞明白

  • 完整轨迹输出的 logits,要去掉最后一个,因为最后一个 token 不会再预测下一个。
  • 参考模型输出的 logits,也要去掉最后一个。
  • torch.gather 算概率(每个位置预测出下个 token 的概率)。

读懂这段

这是全篇最容易写错的一行代码。根因:logits 和 label 天然错开一位。

模型输入 [这, 本, 书, 真, 好, 看],输出的 6 个位置里:

  • 位置 0(读完"这")预测的是"本";
  • 位置 5(读完"看")预测的是 <eos>;
  • 而"读完 <eos> 之后"的那一格,标准答案根本不存在。

所以永远是 logits[:, :-1] 配 labels[:, 1:],最后一个 logits 必须丢掉。忘了丢,就会把"预测 pad"的无效项混进梯度。

mask 与错位对齐(示例:prompt=这本书,response=真好看)

token 序列共 9 位:这 本 书 真 好 看 <eos> <pad> <pad>

attention_mask  [1, 1, 1, 1, 1, 1, 1, 0, 0]      # 9 位,全长
action_mask     [1, 1, 1, 1, 1, 1, 0, 0]         # 8 位,先丢掉首位
            →   [0, 0, 1, 1, 1, 1, 0, 0]         # 再清掉 prompt 段 [:start] 与 pad 段 [end:]

start = len(trajectory.query_ids) - 1        # = 2
end   = start + trajectory.response_length   # = 2 + 4 = 6

(原稿把 action_mask 画成 9 位 [0,0,1,1,1,1,1,0,0]——那是在完整序列上直接标区间;代码里的 masks 只有 8 位,区间含义相同、整体左移一位,有效位是 4 个而不是 5 个。)

Rewards   [R0, R1, R2, R3, R4, R5, R6, R7]       # 8 位,与 logp 同形
       →  [0,  0,  R2, R3, R4, R5, 0,  0]
                              ↑ end-1:奖励模型的 score 加在这一位(即"预测 <eos>"那一步)

批注:Rewards 中不相关的都 Mask 掉。左侧箭头标注:相乘。

Values   9 位:
  [V_这, V_这本, V_这本书, V_这本书真, V_这本书真好, V_这本书真好看, V_这本书真好看<eos>, V_…<eos><pad>, V_…]

Values[j, :-1] *= masks[j, :]        # 取前 8 位,与 8 位 mask 逐位相乘
  →   [0,   0,   V_这本书, V_这本书真, V_这本书真好, V_这本书真好看, 0,  0]

读懂这段

为什么要 -1(start = len(query_ids) - 1)。 prompt 最后一个 token 的 logits 才第一次预测 response 的第一个 token,所以 response 的"第一个可算位置"是 len(query)-1,不是 len(query)。这是 PPO 实现里第二个经典 off-by-one。

为什么要 mask 掉 prompt 段。 只对模型自己生成的 token 算策略梯度。prompt 是给定条件、不该被"优化",否则等于教模型重新生成用户的问题。

为什么 values 也要乘同一个 mask。 V(s_t) 必须和 logprob、advantage 逐位置对齐(三者的非零区间必须完全一致),否则会出现"拿 pad 位置的 value 去和真实位置的 reward 相减"的错位 bug。乘 mask 就是强制对齐,顺手把 pad 位置的 value 归零。

原书 compute_rewards 的关键 6 行(可直接对照自己的实现):

logp     = F.log_softmax(logits[:, :-1, :], dim=-1)        # 丢掉最后一个 logits
ref_logp = F.log_softmax(ref_logits[:, :-1, :], dim=-1)    # 参考模型同样处理
labels   = input_data["input_ids"][:, 1:]                  # 丢掉第一个 token
logp     = torch.gather(logp, 2, labels.unsqueeze(-1)).squeeze(-1)
rewards  = -beta * (logp - ref_logp)                       # 逐 token KL 惩罚
rewards[j, end - 1] += score_tensors[j]                    # RM 分数只加在末位
masks    = attention_mask[:, 1:].clone()                   # 8 位
masks[j, :start] = 0; masks[j, end:] = 0
rewards[j, :]        *= masks[j, :]
values[j, :-1]       *= masks[j, :]
return logp, rewards, values[:, :-1], masks

调试建议:PPO 的 bug 八成不报错,只表现为"reward 不涨"。先打印三组数的形状和 mask 后非零位置,确认 logprob / value / advantage 的非零下标完全一致,再去调超参。


附:一页纸流程

for iteration:
    1. 用当前策略 π_θ 采样 N 条轨迹,记录 (s, a, logπ_old, r, done)
    2. Critic 前向:V(s_t)                        # 注意错位:V(s_t) 对应的是 a_t
    3. 算 δ_t = r_t + γV(s_{t+1}) − V(s_t)
    4. 逆序递推 A_t = δ_t + γλ A_{t+1}            # GAE,O(T)
    5. Critic 回归:min (V(s_t) − (r_t + γV(s_{t+1})))²
    6. 算 ρ_t = exp(logπ_θ − logπ_old)
    7. Actor 上升:min(ρA, clip(ρ,1−ε,1+ε)A),mask 掉 prompt 与 pad
    8. 重复 3–7 若干 epoch(logπ_old 在本轮内保持不变)

附:五个容易踩的坑

  1. 不是折扣因子(P1 原稿写法),是单步奖励。
  2. 不是为了好看,是为了把"不可采样的求和"变成"可采样的期望"。
  3. baseline 不改变期望,只降方差;前提是 不能依赖 。
  4. 的 GAE 就等于带基线的 REINFORCE,不是新东西。
  5. PPO 有两处 off-by-one:logits 丢最后一个;start = len(query)−1。

附:全篇公式速查

主题公式
贝尔曼期望方程
回报
目标函数
策略梯度定理
REINFORCE
带基线
单步 TD 误差
k 步优势
GAE
PPO-clip
概率比值
LLM 即时奖励
终止奖励
2026-10-27 整理 · 公式以 LaTeX 重排
分享:
返回文章列表

相关文章