把游戏写成五元组
强化学习的论文开头几乎都有一句「我们把问题建模为一个 MDP,定义为五元组……」。这一章把那五样东西一个个摊开。摊开之后你会发现,最重要的信息不是它们是什么,而是其中有一样你永远拿不到。
五样东西
MDP = ( S , A , P , R , γ )
| 符号 | 叫什么 | 是什么 | 谁给的 |
|---|---|---|---|
| S | 状态集 | 所有可能的处境 | 环境设计者 |
| A | 动作集 | 所有可以做的选择 | 环境设计者 |
| P | 转移概率 | 在 s 做 a,落到 s' 的概率 | 世界本身 |
| R | 奖励函数 | 这一步给多少分 | 你 |
| γ | 折扣因子 | 未来的分打几折 | 你 |
看第四列。五样里有两样是你写的——而这本书认为,那两样才是真正的工作量所在。
把 P 摊开看一眼
下面这台把上一章那个格子世界的转移概率表完整摊开了。点任意一格,选一个动作,它会告诉你「做完之后会落到哪儿、各有多大概率、各拿多少分」。
三个值得停一下的细节
① 80/10/10:这个世界不完全听话
你选 ↑,只有 80% 真的往上走,左右各 10%。这个设定不是为了刁难你,它模拟的是所有真实系统都有的东西:执行误差。机器人的电机有滞后,游戏有帧率抖动,网络有丢包。
它带来一个重要后果:你不能只记一条最优路线。因为你随时会被推到路线之外,所以你必须对每一个可能到达的状态都有对策。这就是为什么策略是一个函数 π(s),而不是一串动作序列。
顺带说,这也是「策略」和「计划」的分野:计划是一串动作,策略是一套应对。在有噪声的世界里,只有后者活得下来。
② 撞墙留在原地——而这居然是有用的
demo 里选一个靠墙的格子,往墙的方向走,你会看到概率里有一项是「回到自己」。
看起来是个无聊的边界处理,但它有个漂亮的用途:靠墙走可以「对冲」滑动。想往上走又怕被滑到右边掉进 −1?那就选一个撞墙的方向——被滑到墙上的那 10% 会原地不动,等于白付 0.04 但至少没出事。
第 7 章那个最优策略里就有这一手。而它不是任何人设计的,是从这张表里算出来的。
③ 奖励挂在「转移」上,不是挂在「状态」上
这是个容易含糊过去、但会实实在在影响数值的地方。这本书统一采用这个口径:
r(s, a, s') = 每步代价 + 终点奖励(s')
也就是「走一步就付 −0.04,落到终点格再另外结算 +1 或 −1」。
换个口径也行(比如把每步代价记在「到达」上),但所有价值会整体差一个常数。选这一版是因为它能和两本教科书同时对上:AIMA 那个 4×3 世界(每步 −0.04),和 Sutton & Barto 那个悬崖世界(每步 −1,掉崖额外 −99,合起来正好 −100)。第 7 章和第 13 章会逐位核对。
写价值迭代的时候,很多人(包括我写这本书的第一版)会顺手把终止状态的价值初始化成它的奖励,比如 V[goal] = +1。
这会把那个 +1 算两遍:一遍在「走进终点」那一步的奖励里,一遍在终点自己的价值里。我第一次跑出来的价值是 1.84、1.89、1.94,而教科书上是 0.81、0.87、0.92——整整高出一大截。
正确的想法很简单:终止状态没有未来,所以它的价值就是 0。那 +1 不在它身上,在「走进它」那一步的奖励里。
这类 bug 有个共同特征:程序不会报错,曲线照样收敛,只是答案是错的。这就是为什么第 7 章要拿教科书的数字当锚——没有外部基准,你根本不知道自己算错了。
马尔可夫性:那个「M」
MDP 的第一个字母是 Markov。它断言的是这么一件事:
下一步会发生什么,只取决于当前状态和当前动作,与你是怎么走到这儿的无关。
P(s' | s , a , 之前所有历史) = P(s' | s , a)
换句话说:当前状态已经包含了做决策所需的全部信息,历史可以扔掉。
这不是对世界的一个描述,而是对「状态」这个词的一个要求。世界不会自动满足它,是你在设计状态的时候必须让它满足。
违反它的样子很具体:
- 状态只有「当前这一帧」→ 看不出球在往哪飞 → 补:堆四帧(DQN 的做法)
- 状态只有「当前关节角度」→ 看不出腿在往哪甩 → 补:把角速度加进状态
- 扑克里状态只有「我的牌」→ 看不见对手押了多少 → 补:把下注历史编码进状态
当你补不上的时候(比如对手的底牌你真的看不见),这个问题就叫 POMDP(部分可观测 MDP)。它难得多,标准应对是用一个循环网络(LSTM/GRU)把历史压成一个隐状态,人为造一个「够用的状态」出来。
一个常见的误读:马尔可夫性意味着 agent 不能记住任何东西。
不是。它说的是状态里必须包含你需要记住的东西。如果你需要记住「三步之前有没有拿钥匙」,那就把「有没有钥匙」放进状态——放进去之后,这个 MDP 依然是马尔可夫的。
所以马尔可夫性不是一条限制,是一条设计纪律:它逼你把「做决策需要什么」显式地想清楚,然后全部写进状态里。
然后是这一卷真正的转折
上面那张转移概率表,我们叫它模型。有了它,接下来三章的算法都能跑——它们统称动态规划,能算出精确的最优解,而且不需要试错一次。
问题是:
你能拿到 Atari 的转移概率表吗?「在这个画面按左,下一帧变成那个画面的概率是多少」——这张表不存在,游戏的作者也没有它。
QWOP 呢?那是一个物理引擎。理论上确定性的,实际上你不可能把它写成一张 P 表。
你唯一能做的是玩一下试试。那正是卷 III 的全部内容。
那为什么还要花三章讲动态规划?三个理由,都很实在:
- 它给出参照答案。第 12 章你会看到 Q-learning 从零学出来的策略,和这一卷算出来的最优解完全一致——如果不知道正确答案长什么样,你怎么判断它学对了?
- 贝尔曼方程在这里最清楚。它是整门学科的心脏,而在有模型的情况下,它是一个可以直接执行的赋值语句。后面所有算法都是它的近似版本。
- 模型有时候真的有。棋类游戏的规则你完全知道;机器人有物理仿真器。这一支叫基于模型的强化学习,AlphaZero 和 MuZero 都在这一支上,而且它的样本效率高出一大截。
Gymnasium 的 toy_text 那几个环境把 P 表直接暴露出来了,可以拿来验证你自己写的动态规划:
import gymnasium as gym
env = gym.make("FrozenLake-v1", is_slippery=True)
# env.unwrapped.P[state][action] -> [(prob, next_state, reward, done), ...]
print(env.unwrapped.P[0][1])
# [(0.333.., 0, 0.0, False), (0.333.., 4, 0.0, False), (0.333.., 1, 0.0, False)]
注意 is_slippery=True 那一项——那就是本章的 80/10/10,只是 FrozenLake 更狠,是 1/3 均分。把它设成 False,环境就变成确定性的,题目会简单一个量级。
拿这个练手很合适:先自己写一版价值迭代,跑 FrozenLake,再对着别人的实现核对。这一步做过之后,后面所有算法你都会有底。
那个学走路的小人,它的 MDP 是这样的:
- S:十几个连续实数(角度、角速度、躯干倾角……)。无穷多个状态——第 14 章会因为这个把整套方法换掉。
- A:四个关节的力矩,也是连续的。
- P:物理引擎。它存在,但你写不出来,也没法枚举。
- R:作者写的那一行。这本书的全部主题。
- γ:作者设的一个数,通常 0.99。下一章你会看到它有多大威力。
五样里,你实际写的是 R 和 γ,选的是 S 和 A 的表示,而 P 是你完全碰不到的。
这一章的一句话
MDP 就五样东西,其中两样是你写的、一样是你永远拿不到的。而拿不到的那一样,正是接下来两卷的分界线。
下一章:那个 γ。它常被当成一个「为了让数学收敛」的技术性参数,但你会看到,把它从 1.00 拨到 0.90,agent 会当场换一条路走。