读完这篇,你能做什么
用 Java 实现区分真正终止与时间截断的 GAE;在 CPU 实验中检查裁剪梯度,并提高合成批次中优选动作的概率。
学习理由、准备要求与知识点
为什么学它
策略变了,采到的动作也会变。优势估计或旧策略概率用错,程序仍可能运行,却已经在优化错误的目标。
先读这些内容
知识点清单
- 时间差分残差
- GAE反向递推
- 终止与时间截断
- 旧策略概率固定
- PPO裁剪代理目标
- 优势正负号
- RLHF参考KL
- 价值函数与策略分工
运行前先确认论文机制按文中版本说明;Java 17 完整算法,Python 3.14 与 PyTorch 2.9.1+cpu 的小型 CPU 实验。未执行 GPU、多机或大规模模型训练。
01先弄懂它是什么
希望策略更常选到效果好的动作,先要知道它比平时好多少。策略 pi(a|s) 表示在状态 s 选择动作 a 的概率,价值 V(s) 估计从这个状态开始的折扣回报;优势 A(s,a) 再比较该动作与通常水平。优势为正,希望提高它的概率;为负,希望降低。PPO 利用旧策略收集的交互记录,通过一个局部代理目标更新策略,并控制对这批记录的重复使用,而不是直接规定每个好动作加多少概率。
整理训练数据时,一条长度为 T 的交互记录,其奖励、价值和优势都可以排成 [T] 向量;同时运行 B 个环境,常用形状 [T,B]。r_t 是执行动作后得到的奖励,gamma 控制每往后一步怎样折扣,lambda 控制优势估计怎样结合短期和长期信息。这两个系数按步骤起作用,不以秒计;如果一步动作耗时变了,同样 gamma 对真实时间的折扣含义也会变。
RLHF 中几个“模型”各有不同工作。当前策略正在被更新;旧策略是采集这一批动作时用的策略,我们要保存它当时给这些动作的概率,同一批反复训练期间不能改写。参考策略则提供一个比较基准,用于惩罚当前策略偏离太远,因此不能因为它也比较旧,就和采样用的旧策略混在一起。
价值模型估计从当前状态继续下去能获得多少累计回报;奖励模型负责评分,例如判断某个回答更符合偏好,它与预测后续累计结果的价值模型也不是一回事。
文中这几个词,先认一认
- TD残差
- 先把“这一步拿到的奖励”与“下一状态的估计价值乘以折扣系数”相加,再减去原先对当前状态的价值估计,得到一次新旧估计的差额。任务真正结束还是仅因时间限制中断,会决定能否保留下一状态的估计。
- GAE
- 从当前步开始,把后续各步的 TD 残差按权重加起来:越往后一步,就再乘一次 gamma×lambda,用这个总和估计当前动作比通常水平好多少。gamma 控制未来奖励的折扣,lambda 调整近处和远处信息的份量;递推不能跨到重置后的新一局。
- 概率比
- 对于已经采样到的动作,当前策略给它的概率,除以采样时旧策略给它的概率。
02看看它怎么工作
可以把边界问题拆成两问:这一步还能借用末状态对未来回报的估计吗?还能沿数组接着读下一条轨迹吗?公式分别用 bootstrap 和 continue 控制:delta_t=r_t+gamma×bootstrap_t×V(next_t)-V(s_t),A_t=delta_t+gamma×lambda×continue_t×A_(t+1),后一个从后往前算。
任务真正结束时,两项开关都为零。如果只是计时上限到了,任务本身未自然结束,可以保留末状态的价值估计,这叫自举;但记录后面若已是重置后的新一局,就不能继续累加。只用一个 done 标记前,必须核对环境包装器怎样处理,否则学到的目标可能被改掉。
PPO 先看当前策略给已采动作的概率,比旧策略大了还是小了:ratio=exp(logpi_new-logpi_old)。它优化 min(ratio×A,clip(ratio,1-eps,1+eps)×A) 的平均值,先乘优势,再比较两项。
优势为正时,超过上界继续提高概率不再增加这项收益;优势为负时,低于下界继续降低概率也不再获益。但如果改动方向让目标变差,仍应有梯度纠正,所以不能见到 ratio 超界就把梯度全清零。
这里的裁剪改变的是训练所用的代理目标,并不是把模型所有动作的概率比强行锁在区间内,也不等于严格限制整体 KL。因为多个动作共享参数,更新还会影响未采到的动作。实际训练仍需观察近似 KL、熵、裁剪比例和价值损失,并限制更新轮数。本篇 CPU 实验只对固定合成批次中的两个动作 logits 做更新,检查优势正负与梯度方向,没有包含真实语言生成、奖励模型训练或完整在线采样。
先跑通一个最小例子
用PyTorch 2.9.1+cpu运行experiment.py。固定合成轨迹和动作批次,验证GAE、裁剪分支与真实梯度更新;不宣称复现完整PPO或RLHF。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import torch
torch.manual_seed(11)
torch.set_num_threads(1)
torch.set_default_dtype(torch.float64)
def gae(rewards, values, next_values, terminal, continuation, gamma=1., lam=1.):
advantage = torch.zeros_like(rewards)
future = torch.tensor(0.)
for t in reversed(range(len(rewards))):
delta = rewards[t] + gamma * next_values[t] * (1-terminal[t]) - values[t]
future = delta + gamma * lam * continuation[t] * future # 递推不跨越重置边界。
advantage[t] = future
return advantage
assert torch.allclose(gae(torch.tensor([1.,2.]),torch.zeros(2),torch.zeros(2),torch.tensor([0.,1.]),torch.tensor([1.,0.])),torch.tensor([3.,2.]))
assert torch.allclose(gae(torch.tensor([1.]),torch.tensor([2.]),torch.tensor([4.]),torch.tensor([0.]),torch.tensor([0.]),.9),torch.tensor([2.6]))
assert torch.allclose(gae(torch.tensor([1.]),torch.tensor([2.]),torch.tensor([4.]),torch.tensor([1.]),torch.tensor([0.]),.9),torch.tensor([-1.]))
def clipped_loss(log_ratio, advantage):
ratio = log_ratio.exp()
clipped = ratio.clamp(.8, 1.2)
return -torch.minimum(ratio * advantage, clipped * advantage).mean()
z = torch.tensor([2.]).log().requires_grad_()
clipped_loss(z, torch.tensor([1.])).backward()
assert z.grad.item() == 0. # 正优势且超过上界,直接奖励继续增大的梯度被截断。
assert abs(clipped_loss(torch.tensor([2.]).log(),torch.tensor([-1.])).item()-2.)<1e-12
logits = torch.nn.Parameter(torch.zeros(2))
actions = torch.tensor([1,1,1,0])
advantages = torch.tensor([1.,1.,1.,-1.])
old_logp = torch.log_softmax(logits,dim=0)[actions].detach() # 同一批的旧概率固定。
optimizer = torch.optim.SGD([logits], lr=.1)
for _ in range(4):
optimizer.zero_grad()
new_logp = torch.log_softmax(logits,dim=0)[actions]
loss = clipped_loss(new_logp-old_logp, advantages)
loss.backward()
optimizer.step()
probability = torch.softmax(logits,dim=0)[1].item()
assert .5 < probability < 1
assert torch.isfinite(logits).all()
print(f"GAE=[3.0,2.0] preferred_action_probability={probability:.6f}")
print("PASS 7 checks; fixed synthetic batch, not full RLHF")
核对运行结果输出GAE=[3.0,2.0],优选动作概率大于0.5,以及PASS 7 checks。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
N0["保存采样动作的旧对数概率"]
N1["区分终止和轨迹截断"]
N2["反向计算GAE与价值目标"]
N3["按正负优势计算裁剪目标"]
N4["检查KL与失败样本后再采样"]
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4按顺序看,每一步做了什么
- 保存采样动作的旧对数概率
- 区分终止和轨迹截断
- 反向计算GAE与价值目标
- 按正负优势计算裁剪目标
- 检查KL与失败样本后再采样
03对照实现代码
Stable-Baselines3 v2.7.0;PPO论文v2与GAE论文v6 · RolloutBuffer.compute_returns_and_advantage
原创单步GAE公式模型;用显式双掩码解释边界,与SB3实际接口不同。CPU实验使用PyTorch真实自动微分,但未运行SB3环境或完整RLHF。
def one_gae_step(reward, value, next_value, future, gamma, lam, bootstrap, continuation):
delta = reward + gamma * bootstrap * next_value - value # 先形成一步TD残差。
advantage = delta + gamma * lam * continuation * future # 沿同一轨迹向后累计。
target = advantage + value # 供价值网络拟合的lambda回报目标。
return advantage, target
固定版本RolloutBuffer保存rewards、values、episode_starts和log_probs,计算时从最后时间步倒序遍历。末步使用外部last_values与dones,内部步骤参考下一条episode_starts。这样做是因为buffer中存储的是每个时间步开头的标记,索引错一位会把前一局的奖励传播到后一局。
源码先算 delta,再更新 last_gae_lam,最后把 advantages+values 存成 returns。这里的 returns 是用来训练价值函数的 lambda 目标,不一定是把完整一局的实际奖励直接相加。lambda 为零时只参考一步 TD;lambda 为一、轨迹完整且终点处理正确时,优势才可写成相应折扣回报减去价值的形式。读变量名时,要一起看它具体怎样算出来。
时间到了但任务尚未自然结束时,不能只看 buffer 里一个结束标记,还要沿 on_policy_algorithm.collect_rollouts 检查处理过程。某些终止观测会先用于给奖励补上折扣价值,再由 buffer 的结束标记阻断跨局传播。本篇教学函数把 bootstrap 和 continuation 分开,是为了看清这两件事;真实 buffer 函数并不是就有两个同名输入参数。
PPO.train随后重新评价同一批动作,以保存的old_log_prob形成概率比,并使用两个乘优势目标的逐项最小值。优势标准化和价值裁剪是额外配置,不应偷偷加入推导再宣称完全相同的损失。看源码要同时看采样、buffer后处理与训练,孤立复制三行目标无法保证训练语义一致。
代码先调用谁,再调用谁
collect_rollouts采集与保存旧概率- 处理时间上限末状态价值
compute_returns_and_advantage倒序遍历- 训练时重新评价同一批动作
- 形成裁剪目标与价值损失
04动手写一个例子
带双边界掩码的GAE
输入每步奖励、当前与下一状态价值、真终止和轨迹继续标记,返回优势数组;有限数值和gamma、lambda范围需验证。
- 输入 1
奖励[1,2]、价值0、gamma=lambda=1、末步终止- 输出 1
优势[3,2]
- 输入 2
r=1、V=2、nextV=4、截断、gamma=.9- 输出 2
优势2.6
算当前这一步的优势,需要把后面若干步的 TD 残差按衰减权重加起来。若每换一个起点就重新从头加,这些后半段会重复计算,总工作量随轨迹长度的平方增长。可以改为从最后一步向前走,把后面已经算好的结果存进 future;当前步只需把它乘上衰减系数,再加自己的 delta。这样复用同一段后续计算,就能在线性时间得到相同的有限序列结果。
倒序扫描前future代表下一时间步的优势;遇到不继续的边界先屏蔽它,再加入当前delta。即使上一局与下一局在数组中相邻,也不会通过这个变量串起来。真终止却声明继续属于自相矛盾输入,程序拒绝而非猜测读者想表达什么。
空数组返回空结果,说明没有待处理轨迹;NaN、超范围折扣或运算溢出则明确失败。该算法检验的是给定价值预测和边界标记下的算术正确性,不判断价值网络是否准确。输出被后续训练使用时通常应视为固定目标,避免无意让梯度沿采样缓冲区继续传播。
为什么这样写是对的每次写入a[i]时,它等于从i开始且受continuation边界限制的折扣TD残差和。
每个时间步仅访问一次,future复用已求后缀。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static void near(double expected,double actual) {
if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
throw new AssertionError(expected+" != "+actual);
}
static void check(boolean ok) { if(!ok) throw new AssertionError(); }
static void bad(Runnable r) {
try { r.run(); } catch(IllegalArgumentException e) { return; }
throw new AssertionError("invalid input accepted");
}
static double[] gae(double[] r,double[] v,double[] next,boolean[] terminal,
boolean[] continues,double gamma,double lambda) {
if(r==null||v==null||next==null||terminal==null||continues==null
||r.length!=v.length||r.length!=next.length||r.length!=terminal.length
||r.length!=continues.length||!Double.isFinite(gamma)||!Double.isFinite(lambda)
||gamma<0||gamma>1||lambda<0||lambda>1) throw new IllegalArgumentException("input");
double[] a=new double[r.length]; double future=0;
for(int i=r.length-1;i>=0;i--) {
if(!Double.isFinite(r[i])||!Double.isFinite(v[i])||!Double.isFinite(next[i])
||(terminal[i]&&continues[i])) throw new IllegalArgumentException("transition");
double delta=r[i]+gamma*(terminal[i]?0:next[i])-v[i]; // 真终止不自举。
future=delta+gamma*lambda*(continues[i]?future:0); // 截断也停止跨段传播。
if(!Double.isFinite(future)) throw new IllegalArgumentException("overflow");
a[i]=future; // 从后向前保存当前时间步的优势。
}
return a;
}
public static void main(String[] args) {
double[] a=gae(new double[]{1,2},new double[]{0,0},new double[]{0,0},
new boolean[]{false,true},new boolean[]{true,false},1,1);
near(3,a[0]); near(2,a[1]);
near(2,gae(new double[]{1,2},new double[]{0,0},new double[]{0,0},new boolean[]{false,true},new boolean[]{true,false},.5,1)[0]);
near(2.6,gae(new double[]{1},new double[]{2},new double[]{4},new boolean[]{false},new boolean[]{false},.9,1)[0]);
near(-1,gae(new double[]{1},new double[]{2},new double[]{4},new boolean[]{true},new boolean[]{false},.9,1)[0]);
near(1,gae(new double[]{1,2},new double[]{0,0},new double[]{0,0},new boolean[]{false,true},new boolean[]{true,false},1,0)[0]);
check(gae(new double[]{},new double[]{},new double[]{},new boolean[]{},new boolean[]{},1,1).length==0);
bad(()->gae(new double[]{1},new double[]{0},new double[]{0},new boolean[]{true},new boolean[]{true},1,1));
bad(()->gae(new double[]{1},new double[]{0},new double[]{0},new boolean[]{true},new boolean[]{false},1,Double.NaN));
System.out.println("PASS 8 cases");
}
}
这些特殊情况也要试一试
- lambda为零退化成单步残差
- 截断可自举但不跨局传播
- 真正终止不使用末状态价值
- 空轨迹与非法数值分别处理
进阶选读 · 异步采样与陈旧策略诊断
场景多工作者采集速度不同,训练批次混有多个策略版本。
做法为样本记录策略版本和旧对数概率,限制陈旧度;检查重要性比与KL分布,不能只增加训练轮数掩盖分布不匹配。
复杂度T×B条样本的GAE为O(TB),神经前向与通信另外计量。
好处与代价更高采样吞吐可能增加策略陈旧,需要在效率和可解释目标之间取舍。
05项目里怎么用
开始训练前先让奖励、优势和价值目标的单位一致。若把终局奖励缩放一百倍,却保持价值损失权重和KL系数不变,整个优化平衡也变了。记录原始奖励、惩罚后奖励与标准化优势,能把奖励尺度问题与策略学习问题区分开。
用于语言模型时,动作通常就是模型生成的 token,因此要明确长短回答怎样求和或取平均。策略目标只应计算回答 token,并说清结束标记 EOS、填充 padding 和截断怎样处理;提示里的 token 不是模型主动选出的动作。保存的旧概率还要与当前计算使用相同的分词、掩码和采样策略,否则新旧概率比就不再是在比较同一件事。
生产或研究训练都应保存策略、价值网络、优化器、调度器、随机数及采样位置等状态。恢复后先对固定小批次重算损失作一致性检查。本文实验不测GPU显存、分布式同步或长序列吞吐,真正扩大规模前需要单独验证这些环节,不能由几次CPU梯度更新推导训练稳定。
容易踩的坑
- 把截断当真终止丢掉自举
- 更新时重算并覆盖旧策略概率
- 忽略负优势改变裁剪分支
- 认为裁剪严格约束全部KL
- 奖励尺度变化后不重审其他系数
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 较小lambda | 低方差、偏向短期估计 | 更依赖价值预测准确 | 早期不稳定任务 |
| 较大lambda | 利用更长回报信息 | 方差更高 | 轨迹可靠且回报可观测 |
| 多轮使用同一批 | 提高样本利用率 | 策略逐步偏离采样分布 | 配合KL监测与轮数上限 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01旧策略与参考策略有何区别?
展开参考答案与推导
旧策略定义本批采样概率比,参考策略定义长期偏离约束;生命周期和用途不同。
02为什么要两个边界掩码?
展开参考答案与推导
是否自举与是否继续下一条轨迹是两件事,时间截断可以自举但必须断开递推。
03裁剪是否保证新概率在固定区间?
展开参考答案与推导
不保证,它改变代理目标,共享参数和未采动作仍可能发生较大变化。
换个情况,还会处理吗
01超时回合价值总偏低如何查?
展开参考答案与推导
检查是否把时间截断当真正终止,以及末状态价值是否丢失或重复加入。
02平均奖励上升但KL骤增怎么办?
展开参考答案与推导
检查旧概率版本、采样掩码和更新轮数,减少步长或提前停止,再用独立任务评估。
03价值损失远大于策略损失是否直接除100?
展开参考答案与推导
先核对奖励尺度、目标和归约口径,再依据实验调整系数,不能只让日志数值接近。
写代码试一试
01实现双掩码GAE。
- 输入 / 条件
奖励[1,2],全零价值,末步终止- 输出 / 预期
[3,2]
提示 倒序存后缀
展开参考答案与推导
使用正文delta与future递推。
02验证负优势裁剪。
- 输入 / 条件
ratio=2,A=-1,eps=.2- 输出 / 预期
代理值-2
提示 先相乘再取min
展开参考答案与推导
min(-2,-1.2)=-2,仍保留纠正方向。
03构造截断对照。
- 输入 / 条件
r=1,V=2,nextV=4,gamma=.9- 输出 / 预期
截断2.6,终止-1
提示 只改变bootstrap标记
展开参考答案与推导
固定所有数值,分别断开下一局传播并比较结果。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
用 Java 实现区分真正终止与时间截断的 GAE;在 CPU 实验中检查裁剪梯度,并提高合成批次中优选动作的概率。
本篇重点加练
回合边界和裁剪目标决定学习信号,加练跨边界处理及代理目标的局限。
- 终止与时间截断
- PPO裁剪代理目标
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
长度为 4 的 rollout 同时含自然终止和时间截断,自选奖励与价值数值,按明确的边界约定逐步计算 TD 残差与 GAE。
- 提交内容
- 轨迹表与逆序计算
- 怎样算完成
- 两个边界信号分开
- 自举值来源明确
练到的知识点时间差分残差 · GAE反向递推 · 终止与时间截断
- 作业 02原理问答
固定旧策略概率为 0.2,分别选择正负优势和三个新概率,比较 PPO 代理目标;说明裁剪限制的是哪一项。
- 提交内容
- 分段目标图与解释
- 怎样算完成
- 符号和概率比一致
- 目标约束不扩大为参数保证
练到的知识点旧策略概率固定 · PPO裁剪代理目标 · 优势正负号
- 作业 03代码推演
同一观测序列可能来自两个相邻 episode 或一个连续 episode,自拟奖励与价值,比较 GAE 所需的边界信息,说明哪些字段不能省略。
- 提交内容
- 两种事件账本与目标对照
- 怎样算完成
- 环境重置边界可追踪
- 仅靠观测值不假定同一轨迹
练到的知识点终止与时间截断
综合应用
- 作业 04编程实践
编写 CPU rollout 后处理器,处理多个 episode 拼接、最后状态被截断和空轨迹;用手算案例核对 GAE 与回报目标。
- 提交内容
- 实现与边界测试
- 怎样算完成
- 轨迹不跨 episode 串联
- 状态长度契约明确
练到的知识点时间差分残差 · GAE反向递推 · 终止与时间截断
- 作业 05排错实验
在小离散策略实验中分别误更新旧策略缓存、使参考策略参与梯度、让优势连回价值网络;记录梯度与目标的改变。
- 提交内容
- 三类故障复现
- 怎样算完成
- 旧策略和参考策略身份分开
- 梯度流证据完整
练到的知识点旧策略概率固定 · RLHF参考KL · 价值函数与策略分工
- 作业 06面试场景
训练日志显示奖励上升、KL 上升、价值误差也上升。设计一次有限预算诊断,列出要保存的逐样本量及停止条件。
- 提交内容
- 诊断协议和日志 schema
- 怎样算完成
- 指标定义可复核
- 单一指标不决定因果结论
练到的知识点PPO裁剪代理目标 · RLHF参考KL · 价值函数与策略分工
- 作业 07编程实践
CPU 枚举两动作策略的新概率与正负优势,在不同裁剪阈值下记录代理目标及其梯度;检查边界点与平坦区间,声明自动求导约定。
- 提交内容
- 网格实验及分段图
- 怎样算完成
- 旧概率固定且合法
- 数值梯度与边界约定分开
练到的知识点PPO裁剪代理目标
- 作业 08排错实验
构造含截断的短 rollout,分别只修改边界 mask 和只修改裁剪阈值,观察优势与策略目标在哪一层变化,防止把所有差异归为 PPO 超参数。
- 提交内容
- 分层故障实验
- 怎样算完成
- 一次只修改一个因素
- 目标变化有中间量证据
练到的知识点终止与时间截断 · PPO裁剪代理目标
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
在 CPU 小 MDP 中比较两种 GAE 参数和两种裁剪范围,预注册样本预算与种子;报告更新稳定性和回报。
- 提交内容
- 四条件实验与报告
- 怎样算完成
- 环境交互预算公平
- 失败配置完整保留
练到的知识点GAE反向递推 · PPO裁剪代理目标 · 优势正负号
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
同一 PPO 代理目标变好,真实环境回报却变差是否可能?在小型可枚举 MDP 中研究数据分布、边界处理或更新幅度造成的反例。
- 提交内容
- 反例环境与更新记录
- 怎样算完成
- 代理目标和真实回报独立计算
- 失效原因有受控对照
练到的知识点终止与时间截断 · PPO裁剪代理目标
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
PPO原论文v2 (新标签页)↗
阅读裁剪代理目标及其与信任区域方法的区别。
- 深入参考文档英文
GAE原论文v6 (新标签页)↗
阅读TD残差加权与偏差方差取舍。
- 实践参考文档英文
SB3 v2.7.0 RolloutBuffer (新标签页)↗
核对倒序GAE、
episode_starts与returns。 - 实践参考文档英文
SB3 v2.7.0采样循环 (新标签页)↗
核对
TimeLimit.truncated的末状态自举处理。 - 实践参考文档英文
SB3 v2.7.0 PPO (新标签页)↗
核对ratio、优势标准化与两分支最小值。