分数半天不动的时候
上一章的教训是「别乱加奖励」。那把奖励写得干干净净——只在真正成功的时候给分——不就没事了?这一章告诉你另一边的悬崖有多深。
最干净的那种奖励
稀疏奖励:赢了 +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 是怎么被打分教出来的(你会发现这一卷讲的每一个坑,在那里都原样重现了一遍),然后是一台在你浏览器里当场训练的两足小人,最后是一张排障表。