qinghuan
← 回到知识探索

AI 直觉 / 动手实验

奖励减去同批平均值,为什么学习信号会少一截?

从两次抛硬币的完整枚举,分清常数基线、同批均值和留一法;停止梯度也不能代替统计独立。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先写出应该得到的真实梯度
  4. 为什么固定基线可以被减掉
  5. 同批均值里,有自己的一份
  6. 留出自己,基线就来自其他样本
  7. 无偏与低波动,也需要分别检查
  8. 枚举所有批次并保存实际输出
  9. 在训练代码里,先查四个具体位置
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
  • 知道概率期望是按概率加权的平均
  • 知道梯度决定参数更新方向
  • 先读 MDP、Bellman 与策略梯度:先理解奖励乘对数概率梯度,再检查基线怎样改变这个估计式。
遇到不熟悉的词,先看这 2 个解释
基线
用奖励减去一个参照水平,再将差额用于策略更新。
留一法
计算某个样本的参照水平时,排除它自己,只用同组其他样本。
打开完整术语小抄 ↗

“奖励减去一个基线,通常不改变平均学习方向。”看到这句话,很容易把这一批奖励的平均值直接减掉。但这份平均里含有样本自己的奖励,事情就有了变化。

本篇用一个只选 0 或 1 的策略算清楚:每组独立抽两次时,减同批均值使期望梯度从 0.25 变成 0.125;留出当前样本再算基线,可以在给定条件下恢复到 0.25。这里讨论的是一个明确的估计式,不是给所有 PPO、GRPO 或 RLHF 实现下结论。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行源码
  • README.md:运行步骤与练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和运行验证记录
  • SHA256SUMS.txt:文件摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py

解压后,在含 demo.py 的目录打开终端。命令分别回车执行。同一固定问题、独立 Bernoulli 动作、固定奖励下的策略梯度精确枚举;未运行 PPO、GRPO 或 RLHF 训练。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

同批平均值里留着自己,会少掉多少信号?

正在准备实验控件…

先保留 p=0.5、每批 2 个样本,比较期望和方差。再把独立抽样切成复制,检查留一法的结论还是否成立。

直接求导得到的真实梯度
0.250000
同批居中后的梯度期望
0.125000
留一法梯度期望
0.250000
均值是否正确,与波动是否更小,分别比较
基线梯度期望梯度方差
不减基线0.2500000.031250
减同批均值0.1250000.015625
留一法0.2500000.062500
固定基线 0.50.2500000.000000

独立模式中,同批居中期望保留 1/2;留一法恢复真实期望,但并不保证方差更小。

查看 4 组动作及其概率
动作组概率普通估计同批居中留一法
0, 00.2500000.0000000.0000000.000000
1, 00.2500000.2500000.2500000.500000
0, 10.2500000.2500000.2500000.500000
1, 10.2500000.5000000.0000000.000000

一步二元策略,奖励等于动作,梯度针对策略 logit;给定策略后独立采样或复制一次结果,按真实批次概率加权。浏览器使用双精度数,下载程序用 Fraction 精确核对;不包含标准差归一化、裁剪、KL 项或真实模型训练。

先写出应该得到的真实梯度

策略以概率 p 选动作 1,以 1−p 选动作 0;奖励 R(a)=a。期望奖励 J=p。我们用参数 theta 控制 p=sigmoid(theta),所以 dJ/dtheta=p(1−p)。

默认 p=0.5,真实梯度就是 0.25。对于抽中的动作 a,d log pi(a)/dtheta=a−p:选 1 时是 +0.5,选 0 时是 −0.5。下面把这个量简称“对数概率梯度”。

每组 N 次采样都来自同一个固定策略、同一个问题,并且相互独立。奖励不直接依赖 theta;计算估计式时只让对数概率部分提供策略梯度。没有标准差归一化、概率比裁剪或 KL 项。

为什么固定基线可以被减掉

普通估计器对 N 个样本取平均:g=(1/N)Σ R_i×s_i,其中 s_i 是第 i 个样本的对数概率梯度。减固定基线 b 后变为 (1/N)Σ(R_i−b)×s_i。

对一个固定问题,E[s_i]=0。因此不依赖该次动作的固定 b 对期望贡献为 b×E[s_i]=0。它可以改变波动,却不改变梯度期望。

随机基线也要核对与当前样本的关系。如果在固定问题和策略后,基线与当前样本条件独立,并满足所需期望存在,这个抵消仍成立。不能把“数值已经算完”误当成“与样本独立”。

同批均值里,有自己的一份

把 b 换成本批均值 R_bar=(R_1+…+R_N)/N。对样本 i 而言,R_bar 包含 R_i/N;这一项与自己的 s_i 有关,不能当作独立常数提出期望。

展开 E[R_bar×s_i]:自己的那项贡献 (1/N)E[R_i×s_i];其他样本在上述独立条件下贡献为零。于是减均值后的期望恰好是原梯度的 (N−1)/N。

N=2 时保留一半,N=4 时保留四分之三。N=1 时奖励减自己等于零,信号完全消失。给 R_bar 做 detach 只会阻断自动微分路径,不会删除这份统计依赖。

留出自己,基线就来自其他样本

留一法为样本 i 使用 b_i=(Σ_j R_j−R_i)/(N−1),要求 N≥2。自己的奖励被明确去掉,基线只由其他独立抽样的奖励构成。

在本文条件下,E[b_i×s_i]=0,留一法估计器恢复真实梯度期望。还有一个可以逐组检查的代数关系:R_i−b_i=N/(N−1)×(R_i−R_bar)。因此本例的留一法等于把未归一化的同批居中奖励乘回这个因子。

这个关系不自动覆盖再除标准差、裁剪、不同回答长度加权或跨题混组。固定 N 时的常数缩放可以与某些简单更新的学习率一起讨论,但不能据此假定任意优化器和整个训练过程都等价。

无偏与低波动,也需要分别检查

p=0.5、N=2 时,四组独立动作是 [0,0]、[0,1]、[1,0]、[1,1]。普通估计器平均为 0.25,同批均值为 0.125,留一法为 0.25。代码还会用 p=0.25、0.75 和其他 N 检查公式。

但这个小例子里,普通估计器方差是 0.03125,留一法方差反而是 0.0625;使用固定基线 0.5 时方差为零。留一法在这里无偏,不代表它总是所有基线中波动最小的。

再把第二个样本改成第一个样本的复制品,只会出现 [0,0] 或 [1,1]。留一法的每个奖励差都为零,期望也变成零。独立抽两次允许偶然得到相同结果;复制一次结果则是另一种联合分布。

枚举所有批次并保存实际输出

保存为 demo.py,运行 python demo.py。代码使用 Python 3.14 标准库 Fraction,按每个批次的真实概率精确加权,不把不等概率的批次简单平均。

34 个精确检查涵盖三个动作概率、三种组大小、逐组缩放关系、N=1 拒绝、复制反例和方差比较。程序没有训练语言模型,也没有调用 RLHF 框架。

展开收起代码 · 小实验python · 79 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Exact policy-gradient expectations for a one-step Bernoulli policy."""
from fractions import Fraction as F
from itertools import product


def estimate(actions, p, method):
    n = len(actions)
    if not n or (method == "leave_one_out" and n < 2):
        raise ValueError("leave-one-out needs at least two samples")
    rewards = list(actions)  # Fixed reward: R(a) = a.
    mean = F(sum(rewards), n)
    total = F(0)
    for action, reward in zip(actions, rewards):
        if method == "none":
            baseline = F(0)
        elif method == "constant":
            baseline = F(1, 2)
        elif method == "batch_mean":
            baseline = mean
        elif method == "leave_one_out":
            baseline = F(sum(rewards) - reward, n - 1)
        else:
            raise ValueError("unknown baseline")
        score = action - p  # d log pi(a) / d logit(theta).
        total += (reward - baseline) * score
    return total / n


def moments(n, p, method):
    mean = second = F(0)
    for actions in product((0, 1), repeat=n):
        count = sum(actions)
        probability = p ** count * (1 - p) ** (n - count)
        value = estimate(actions, p, method)
        mean += probability * value
        second += probability * value * value
    return mean, second - mean * mean


def main():
    p = F(1, 2)
    exact = p * (1 - p)
    checks = {}
    for probability in (F(1, 4), F(1, 2), F(3, 4)):
        truth = probability * (1 - probability)
        for n in (1, 2, 4):
            checks[f"p={probability},N={n},plain"] = moments(n, probability, "none")[0] == truth
            checks[f"p={probability},N={n},centered"] = moments(n, probability, "batch_mean")[0] == F(n - 1, n) * truth
            if n >= 2:
                checks[f"p={probability},N={n},loo"] = moments(n, probability, "leave_one_out")[0] == truth
                checks[f"p={probability},N={n},sample_identity"] = all(
                    estimate(a, probability, "leave_one_out") == F(n, n - 1) * estimate(a, probability, "batch_mean")
                    for a in product((0, 1), repeat=n))
    try:
        estimate((1,), p, "leave_one_out")
    except ValueError:
        checks["N1_rejected"] = True
    else:
        checks["N1_rejected"] = False
    copied = ((1 - p) * estimate((0, 0), p, "leave_one_out")
              + p * estimate((1, 1), p, "leave_one_out"))
    checks["correlated_copy_breaks_unbiasedness"] = copied == 0 and copied != exact
    checks["constant_baseline_zero_variance_in_this_example"] = moments(2, p, "constant")[1] == 0
    checks["loo_can_increase_variance"] = moments(2, p, "leave_one_out")[1] > moments(2, p, "none")[1]
    assert all(checks.values()), checks
    print("policy: P(action=1)=0.5; reward=action; derivative is wrt logit")
    print(f"exact_gradient={float(exact):.6f}")
    for n in (1, 2, 4):
        plain = moments(n, p, "none")[0]
        centered = moments(n, p, "batch_mean")[0]
        loo = "undefined" if n == 1 else f"{float(moments(n, p, 'leave_one_out')[0]):.6f}"
        print(f"N={n} batches={2**n} plain={float(plain):.6f} batch_mean={float(centered):.6f} leave_one_out={loo}")
    print(f"N=2 variance: plain={float(moments(2, p, 'none')[1]):.6f} leave_one_out={float(moments(2, p, 'leave_one_out')[1]):.6f} constant_0.5={float(moments(2, p, 'constant')[1]):.6f}")
    print(f"copied_samples_N=2: leave_one_out={float(copied):.6f}")
    print(f"PASS {len(checks)} exact checks; independent one-step model, no RLHF training")


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。同一固定问题、独立 Bernoulli 动作、固定奖励下的策略梯度精确枚举;未运行 PPO、GRPO 或 RLHF 训练。

运行后对照结果
policy: P(action=1)=0.5; reward=action; derivative is wrt logit
exact_gradient=0.250000
N=1 batches=2 plain=0.250000 batch_mean=0.000000 leave_one_out=undefined
N=2 batches=4 plain=0.250000 batch_mean=0.125000 leave_one_out=0.250000
N=4 batches=16 plain=0.250000 batch_mean=0.187500 leave_one_out=0.250000
N=2 variance: plain=0.031250 leave_one_out=0.062500 constant_0.5=0.000000
copied_samples_N=2: leave_one_out=0.000000
PASS 34 exact checks; independent one-step model, no RLHF training

在训练代码里,先查四个具体位置

第一,均值沿哪个维度计算:同一问题的多份回答,还是整批不同问题混在一起。第二,当前样本是否进入自己的基线。第三,采样在给定问题和策略后是否独立。第四,基线之后还有没有标准差、裁剪或其他加权。

这些条件决定这里的公式能否直接使用。没有价值网络,不意味着基线可以随意构造;停止梯度,也不保证估计式没有统计偏差。

本篇把奖励设为固定函数,策略也只有一步。真实多步轨迹、策略更新后的旧样本复用以及依赖参数的奖励,需要单独推导或核对实现。本实验支持局部机制,不能代替模型训练效果评测。

记住这三点

  • 同批均值含有样本自己的奖励,本文期望梯度会缩成 (N−1)/N。
  • 留一法要求 N≥2,并依赖固定问题下的采样独立与奖励条件。
  • detach 处理微分路径;统计独立、无偏和方差是另外需要核对的事情。

别把例子用过头

  • 把当前批次奖励均值当作与每个动作无关的常数。
  • N=1 仍除以 N−1。
  • 把复制的回答当成重新独立采样。
  • 把本篇未标准化的估计式结论直接套到带裁剪或标准差归一化的目标。

自己试一小步

若两种动作的奖励改为 R(0)=2、R(1)=5,p=0.25、N=3,三种估计器的期望会怎样?

  1. 先对期望奖励直接求 theta 的导数。
  2. 枚举全部 8 个批次,写清各批次概率。
  3. 分别实现普通、同批均值和留一法,比较期望与方差。

做到这些,就算完成

  • 奖励不再等于动作,别漏改 reward 的定义。
  • 概率为 0.25 时,8 个批次并非等概率。
  • 用实测结果说明哪个结论保持、哪个方差排序可能改变。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。