卷 V · 打分CH 21深度 21/24

分数半天不动的时候

上一章的教训是「别乱加奖励」。那把奖励写得干干净净——只在真正成功的时候给分——不就没事了?这一章告诉你另一边的悬崖有多深。

稀疏奖励势函数塑形好奇心与课程

最干净的那种奖励

稀疏奖励:赢了 +1,其余全是 0。

它有一个不可替代的优点:它不可能被钻空子。因为它就是你真正要的东西,一字不差,没有任何中间指标可以刷。

那就用它好了。问题是它多半学不动,而且「学不动」的程度可能超出你的预期。

一个空房间

20 行 20 列,起点在左下角,终点在右上角。走到终点 +1,其余每一步都是 0。每局最多 180 步(最短路径 38 步,时间是充裕的)。

▶ 动手 · 看第一行那个「学会了几次」

四次独立实验,每次六百局。红线一直贴着天花板。

结果

稀疏版:四次实验里学会的次数是 0。第一次撞到终点通常要几十到几百局,六百局跑完,成功次数平均只有个位数,末段的平均步数还顶在 180——也就是基本每局都超时

塑形版:基本第一局就走到了,二十到三百局之间学会,六百局里成功五百多次,末段 50 步左右(最短 38 步)。

差距不是「快一点」,是「能」和「不能」

为什么会这么惨

原因说穿了很朴素,但值得盯着看一会儿:

◆ 在第一次撞到终点之前,学习是完全没有发生

所有 Q 值初始化为 0。所有奖励是 0。于是 TD 误差 r + γ·max Q(s') − Q(s,a) 恒等于 0 + 0 − 0 = 0

Q 表纹丝不动。四个动作精确地一样好,ε-greedy 退化成完全随机。

它不是学得慢。它是在做纯粹的随机游走,而且这个随机游走对未来毫无帮助——因为没有任何东西被记下来。

而随机游走撞到一个特定格子的概率,随着房间变大是指数级下降的。

更糟的是,就算撞到了一次,事情也才刚开始

回想第 7 章那个水波:价值要从终点一格一格往回传。用 TD(0) 的话,理想情况下一局才传一格。起点离终点 38 步,就需要几十局把信号传回来——而在这期间,它还得反复走到终点才能维持这个信号,否则一次侥幸的成功会被后面的失败稀释掉。

这就是为什么 DQN 在《蒙特祖玛的复仇》上得 0 分——那个游戏要先下梯子、跳过骷髅、拿到钥匙,才能开第一扇门。中间一百多步,一分都没有。靠随机探索撞对这个序列的概率,接近于零。

四条出路

① 势函数塑形——上面那条绿线

第 19 章介绍过,这里是它真正发挥作用的地方:

F(s, s') = γ · Φ(s') - Φ(s)          Φ(s) = -距离终点的曼哈顿距离 × 0.03

每走一步,如果离终点近了,就给一点点正奖励;远了就给一点点负的。

而它不会被「来回走」刷分:往前走一格赚 0.03,退回来正好亏 0.03,精确抵消。这是上一章那张表里「赛车来回开」那个漏洞的解药。

Ng 等人 1999 年的定理保证了:不管 Φ 怎么选,最优策略都不变。这是目前唯一有理论保证的塑形方式。

⚠ 但它有个前提,而且这个前提常常不成立

要写出 Φ,你得知道「离目标有多远」

在这个格子世界里,曼哈顿距离随手就能算。但是:

  • 《蒙特祖玛的复仇》里,「离通关有多远」怎么算?
  • 一段代码「离编译通过有多远」怎么算?
  • 一个回答「离让用户满意有多远」怎么算?

如果你能写出一个好的 Φ,说明你对这个问题已经理解得相当深了——而那种时候,你往往也不那么需要强化学习了。

这是个诚实的困境,不是可以绕过去的技术细节。所以下面三条出路才重要。

② 好奇心:把「没见过」本身当成奖励

思路:既然外部没给奖励,那就自己造一个内部奖励——鼓励它去没去过的地方。

r_total = r_外部 + β · r_好奇心

怎么衡量「没见过」?主流做法是预测误差:训练一个模型去预测「做了这个动作之后会看到什么」,预测得越差,说明这个状态越新奇,就给越多奖励

这一招在蒙特祖玛上真的管用,让分数从 0 变成了几千。

⚠ 「电视噪声问题」——好奇心自己也会被黑

好奇心奖励有一个非常有名的失败模式,而且它本身就是一个奖励黑客案例:

如果环境里有一台播放随机雪花的电视,agent 会站在电视前面一动不动地盯着看——因为雪花永远无法预测,预测误差永远很高,好奇心奖励永远拿满

这和上一章那个转圈的赛艇是同一件事:你写下的(预测误差高)和你想要的(探索新区域)之间有差距,它精确地找到了那个差距。

后来的方法(比如 RND,随机网络蒸馏)绕开了这个:不预测环境,而是预测一个固定的随机网络的输出。因为目标网络是确定性的,随机噪声不会带来持续的预测误差——见过的状态就是见过了。

这个故事的意义在于:给奖励打补丁的时候,那个补丁本身也会被黑。这一层递归没有尽头。

③ 课程学习:先给它一个够得着的目标

最朴素,也常常最有效:把难题拆成一串由易到难的题。

在上面那个房间里,可以这么做:先把终点放在离起点 3 格的地方(随机游走很快能撞到),学会之后挪到 6 格、12 格、最后 38 格。每一步都建立在上一步学到的东西上。

更聪明的一个变体叫 HER(事后经验回放),思路相当妙:

「这局你没走到终点,走到了 (7,9)?那就假装 (7,9) 本来就是目标——恭喜你,成功了。」

于是每一次失败都变成了一次针对另一个目标的成功。在机械臂抓取这类任务上效果非常显著,因为它把 100% 的失败数据变成了 100% 的有效数据。

④ 模仿学习:直接给它看几段演示

最省事的一条:录几段人类操作,先用监督学习把策略预训练一遍,再用强化学习微调。

AlphaGo 就是这么起步的——先学人类棋谱,再自我对弈。而 ChatGPT 也是:先 SFT(监督微调),再 RLHF。

这条路的实用性被严重低估。如果你能搞到哪怕几十条演示,它通常比调三天超参有用得多。

那到底该稀疏还是稠密

这一卷的三章合起来,其实是一个两难:

稀疏奖励稠密(塑形过的)奖励
能被钻空子吗不能能,而且很容易
学得动吗常常学不动学得动
要多少领域知识几乎不要要不少
失败的样子分数一直是 0分数很高,行为是错的

看最后一行。这是这一卷最想让你记住的对照:

◆ 两种失败,一种会喊,一种不会

稀疏奖励失败得很响。分数一直是 0,你一眼就知道出事了。烦,但至少你知道。

塑形过的奖励失败得很安静。分数在涨,曲线在收敛,一切正常——只有行为是错的。

所以实践中的路线是这样的:

用塑形过的奖励去训练(因为稀疏的学不动),用稀疏的那个去验收(因为塑形过的会骗你)。

而且验收指标绝对不能进入训练。它一旦进去,也会被优化,也就不再是一把干净的尺子。

⌗ 换成真机:先量一下你的问题有多稀疏

在写任何塑形之前,先做一个五分钟的测量:用完全随机的策略跑一千局,看它能撞到奖励几次。

import gymnasium as gym
import numpy as np

env = gym.make(ENV_ID)
hits = 0
for ep in range(1000):
    s, _ = env.reset()
    total = 0
    while True:
        s, r, term, trunc, _ = env.step(env.action_space.sample())  # 纯随机
        total += r
        if term or trunc: break
    if total > 0: hits += 1

print(f"随机策略 1000 局里拿到正奖励 {hits} 次")

这个数字直接决定了你的路线:

  • > 50 次:够稠密,直接上 PPO,别费劲塑形。
  • 1 – 50 次:能学,但会慢。可以考虑塑形或者加长训练。
  • 0 次别浪费 GPU 时间了。先解决探索问题——课程、演示、好奇心,随便哪个都行,但必须先解决。

这五分钟能省下你几天的算力。而它是我见过最常被跳过的一步——大部分人会直接开训,然后花两天调超参,最后才发现 agent 从头到尾就没碰到过奖励。

↩ 回到那个视频

你看的那个学走路的视频,作者几乎不可能用纯稀疏奖励(「走到 100 米 +1」)。用了的话,屏幕上会是几百万帧原地抽搐,然后什么也不会发生。

他用的一定是稠密的:每一帧都按「这一帧往前挪了多少」给分。这样从第一帧起就有梯度可用。

而这个选择,把他直接送进了上一章那个坑——「每一帧往前挪多少」的最优解,可能是一头栽出去。

这一卷的三章其实是一条闭环:稀疏学不动 → 那就塑形 → 塑形会被钻空子 → 那就去看录像、改奖励 → 改完可能又太稀疏……

这个循环,就是强化学习工程师的日常工作。它不是调算法,是反复地重新定义「好」。

这一章的一句话

稀疏奖励不是学得慢,是在第一次撞到奖励之前根本没有可学的东西。用塑形过的奖励训练,用稀疏的那个验收——而验收的那把尺子,绝对不能进入训练。

卷 V 到此结束。最后一卷落地:先看 ChatGPT 是怎么被打分教出来的(你会发现这一卷讲的每一个坑,在那里都原样重现了一遍),然后是一台在你浏览器里当场训练的两足小人,最后是一张排障表。