· 强化学习
强化学习笔记:从策略梯度到 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 出发)
- ,转移概率
- ,转移概率
逐状态展开
⇓(把括号里的部分收成 、)
读懂这段
这就是"按定义展开 → 认出 → 合并成求和号"三步:
- 先把 按所有可能的下一步展开,每条分支乘自己的概率 ;
- 括号里剩下的部分,正好就是 和 的定义式 —— 于是可以直接替换;
- 替换完得到一个"只含自己和自己邻居"的方程,写成求和号就是通用的贝尔曼方程。
这一步是全篇的地基:后面所有"优势"、"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 步 TD 误差:
- 把数据逆序:
- 遍历逆序数组:
- …
中间大公式(代码中用的公式):
,是 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 | 最简单的策略梯度法 | |
| 2 | REINFORCE | |
| 3 | 带基线的 REINFORCE | |
| 4 | Actor-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 在本轮内保持不变)
附:五个容易踩的坑
- 不是折扣因子(P1 原稿写法),是单步奖励。
- 不是为了好看,是为了把"不可采样的求和"变成"可采样的期望"。
- baseline 不改变期望,只降方差;前提是 不能依赖 。
- 的 GAE 就等于带基线的 REINFORCE,不是新东西。
- PPO 有两处 off-by-one:logits 丢最后一个;
start = len(query)−1。
附:全篇公式速查
| 主题 | 公式 |
|---|---|
| 贝尔曼期望方程 | |
| 回报 | |
| 目标函数 | |
| 策略梯度定理 | |
| REINFORCE | |
| 带基线 | |
| 单步 TD 误差 | |
| k 步优势 | |
| GAE | |
| PPO-clip | |
| 概率比值 | |
| LLM 即时奖励 | |
| 终止奖励 |