从 GridWorld 理解强化学习:状态、价值函数和动态规划
一直想要系统学习RL的相关知识,脑子里想着能不能完全依靠 Codex 进行学习,于是就有了下面的教程。
完整内容:https://github.com/lanheixingkong/rl-course
第一课从多臂老虎机开始理解强化学习:探索与利用我们用多臂老虎机理解了“探索 vs 利用”。Bandit 很适合入门,因为它把问题压缩到最小:每一步只需要从几个动作里选一个。
但真实强化学习问题通常不只是“哪个动作整体最好”。更常见的是:
我现在处在什么状态?
在这个状态下,哪个动作长期更好?
这一课用一个小网格世界 GridWorld,学习 RL 里非常核心的三个概念:
-
• state: 状态,agent 当前在哪里; -
• policy: 策略,在每个状态下该做什么; -
• value function: 价值函数,从某个状态开始长期能拿多少回报。
1. 问题:机器人在网格里找路
地图是一个 3x4 的网格:
(0,0) (0,1) (0,2) (0,3)=goal +1
(1,0) WALL (1,2) (1,3)=pit -1
(2,0) (2,1) (2,2) (2,3)
机器人可以做四个动作:
|
|
|
|---|---|
U |
|
R |
|
D |
|
L |
|
奖励规则:
-
• 每走一步,默认得到 -0.04,表示走路有成本; -
• 到达右上角终点 (0,3),得到+1; -
• 到达坑 (1,3),得到-1; -
• 撞墙或出界,就留在原地。
这里要先分清:
人类想要的任务目标:到达终点
agent 实际优化的目标:累计奖励最大
agent 不会天然理解“终点”这个词。它只会根据奖励数字做选择。我们把到达终点设置成 +1,把掉坑设置成 -1,把每走一步设置成 -0.04,就是在告诉 agent:
到达终点是好事;
掉坑是坏事;
拖太久也不好。
所以“走到终点”不是 agent 直接理解的目标,而是我们通过 reward design 诱导出来的行为。
为什么 RL 要这样做?因为 agent 和环境的交互接口通常很简单:
agent 选择 action
环境返回 next_state 和 reward
agent 学习让长期 reward 更大的 action
算法能直接比较的是数字,而不是人类语言里的意图。goal 这个词对人有意义,但对算法来说,真正有意义的是:
到达 goal 会得到 +1
在这一课里,agent 优化的是折扣累计奖励:
return = r1 + gamma * r2 + gamma^2 * r3 + ...
Values 表里的数值,就是从每个格子出发,按照当前策略走下去时预计能拿到的累计奖励。
和第一课不同,这一课的 agent 知道地图规则。所以它不需要先在线试错,而是可以直接利用环境规则进行推算。这类问题叫 planning。
planning 对应的是“环境模型已知”。所谓环境模型,就是 agent 在某个状态选择某个动作后,环境会返回什么 next_state 和什么 reward。
在这一课里,GridWorld.step(state, action) 就是环境模型。因为地图、墙、终点、陷阱和奖励都已经写在代码里,所以算法不需要真的让机器人走很多次。它可以直接问:
如果在这里向上,会到哪里?得到多少 reward?
如果在这里向右,会到哪里?得到多少 reward?
如果在这里向下,会到哪里?得到多少 reward?
如果在这里向左,会到哪里?得到多少 reward?
然后根据这些规则计算最优策略。
相对应地,也确实存在“环境未知,需要从数据中学习”的问题。但这里要更精确一点:环境未知不一定都是“先试错,再推算最优策略”。常见有三类:
第一类是 model-free RL。agent 不显式学习环境模型,而是直接从经验中学习“哪个状态下哪个动作更好”。下一课的 Q-learning 就属于这一类。
第二类是 model-based RL。agent 先用数据学习一个近似环境模型,再用这个学到的模型进行 planning。
第三类是 offline RL。agent 不在线试错,而是从已有历史交互数据中学习策略。这在真实试错成本高或有风险的场景里很重要。
可以用下面的表对比:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
所以第二课先学 planning,不是因为现实问题都能提前知道环境规则,而是因为 planning 可以把 state、action、reward、value、policy 这些概念拆得很清楚。等这些概念稳定之后,再进入环境未知的 Q-learning 会更自然。
2. 运行实验
在项目根目录运行:
python lessons/02_gridworld_dp/gridworld_dp.py
你会看到:
GridWorld config:
gamma : 0.95
step_reward : -0.04
goal_reward : 1.0
pit_reward : -1.0
Policy iteration converged in 4 iterations.
Values:
0.82 0.91 1.00 1.00
0.74 WALL 0.91 -1.00
0.67 0.74 0.82 0.74
Policy:
R R R +1
U WALL U -1
U R U L
如果你想看 evaluate_policy() 内部到底怎么更新 value,可以运行:
python lessons/02_gridworld_dp/gridworld_dp.py --show-evaluation --debug-sweeps 2
--show-evaluation 表示打印 policy evaluation 的过程。--debug-sweeps 2 表示每次 evaluation 只展示前 2 轮 sweep,避免输出太长。
你会看到类似片段:
Policy iteration 1: evaluate current policy
evaluation sweep 1 in policy iteration 1
state action next reward old V new V
(0, 0) R (0, 1) -0.04 0.00 -0.04
(0, 1) R (0, 2) -0.04 0.00 -0.04
(0, 2) R (0, 3) 1.00 0.00 1.00
...
max value change delta = 1.000000
这段输出的读法:
|
|
|
|---|---|
state |
|
action |
|
next |
|
reward |
|
old V |
values[state]
|
new V |
reward + gamma * values[next_state] 算出来的新价值
|
delta |
|
先看第一轮就够了。它能帮你把这行代码看成具体数字:
values[state] = reward + gamma * values[next_state]
3. 先看输出,不急着看公式
Values 表示每个格子的长期价值。
Values:
0.82 0.91 1.00 1.00
0.74 WALL 0.91 -1.00
0.67 0.74 0.82 0.74
你可以把每个数字读成:
如果从这个格子开始,长期大概能拿到多少回报。
越靠近右上角终点,价值通常越高。坑是坏终点,所以是负数。
Policy 表示每个格子应该往哪里走。
Policy:
R R R +1
U WALL U -1
U R U L
例如左下角是 U,表示在左下角应该向上走。中下方 (2,1) 是 R,表示应该向右走。
这张 policy 表就是算法算出来的导航规则。
4. 为什么这比 Bandit 更像完整 RL
Bandit 没有状态。它只问:
哪个动作整体最好?
GridWorld 有状态。它问:
在当前状态下,哪个动作长期最好?
同一个动作在不同状态下意义不同:
-
• 在 (0,2)做R很好,因为右边是终点; -
• 在 (1,2)做R很糟,因为右边是坑; -
• 在 (2,3)做L可能比U更合理,因为可以避开坑。
所以从这一课开始,RL 不再只是估计 Q(a),而是要理解:
V(s): 某个状态的价值
policy(s): 某个状态下该采取的动作
5. 动态规划在这里做什么
这一课用的是 policy iteration。它由两个步骤反复交替:
policy evaluation : 固定当前策略,计算每个状态的价值
policy improvement : 根据价值函数,给每个状态选择更好的动作
直到策略不再变化,就停止。
直觉上就是:
-
1. 先问:如果按当前导航规则走,每个格子值多少钱? -
2. 再问:如果我知道每个格子的价值,当前格子是不是有更好的走法? -
3. 重复,直到导航规则稳定。
6. 按源码执行顺序读代码
先不要从公式开始,也不要从零散代码片段开始。
源码文件是:
lessons/02_gridworld_dp/gridworld_dp.py
这个程序可以分成三层:
|
|
|
|
|---|---|---|
main()
parse_args()、print_values()、print_policy()
|
|
|
GridWorld
step()
|
|
|
evaluate_policy()
improve_policy()、policy_iteration()
|
|
|
所以后面不是随机摘代码,也不是说其他代码没用。我们只是在先抓住 RL 核心链路:
main()
-> 创建 GridWorld 环境
-> policy_iteration()
-> evaluate_policy() 固定策略,计算 values
-> improve_policy() 根据 values 改进 policy
-> 如果 policy 不变,停止
-> print_values() / print_policy()
你可以重点读源码中的这几段:
第 24-55 行:GridWorld 和 step()
第 58-97 行:evaluate_policy()
第 100-123 行:improve_policy()
第 126-162 行:policy_iteration()
其他代码主要是参数解析、打印输出和错误提示。它们有用,但不是理解本课 RL 原理的主线。
7. 代码里的状态和动作
代码:
Action = str
State = tuple[int, int]
ACTIONS = ["U", "R", "D", "L"]
State 是 (row, col),例如 (2,0) 表示左下角。
Action 是 "U"、"R"、"D"、"L" 中的一个。
动作如何改变状态,由 DELTAS 定义:
DELTAS = {
"U": (-1, 0),
"R": (0, 1),
"D": (1, 0),
"L": (0, -1),
}
例如 "U" 让行号减 1,所以是向上。
8. 环境模型:step()
这节课里,agent 知道环境模型。代码里的环境模型就是:
def step(self, state: State, action: Action) -> tuple[State, float]:
它回答:
如果我在 state 做 action,会到哪里?会拿到多少 reward?
这就是 model-based planning 里的 model。
核心逻辑:
dr, dc = DELTAS[action]
next_state = (state[0] + dr, state[1] + dc)
如果撞墙或出界:
next_state = state
奖励:
reward = self.terminal_rewards.get(next_state, self.step_reward)
如果下一格是终点或坑,就拿终点奖励;否则拿普通步进奖励。
到这里为止,我们只是在定义“世界如何回应 agent 的动作”。还没有开始算最优策略。
9. policy_iteration() 是总流程
在源码里,先看函数一开始:
policy = {
state: "R"
for state in env.states()
if not env.is_terminal(state)
}
seen_policies = set()
这段代码做了两件事。
第一,创建一个初始策略 policy。
policy 是一个字典,意思是:
每个非终止状态 -> 当前打算采取的动作
这段字典推导式等价于:
policy = {}
for state in env.states():
if not env.is_terminal(state):
policy[state] = "R"
也就是说,程序一开始先随便给每个普通格子一个动作:全部向右走 "R"。
为什么可以随便开始?因为 policy iteration 本来就是:
先给一个不一定好的策略
再评估它
再改进它
反复进行,直到策略稳定
所以初始策略不需要一开始就是最优策略。它只是算法的起点。
这里跳过了终点和坑:
if not env.is_terminal(state)
因为终点和坑是 terminal state,到了那里 episode 已经结束,不需要再规定“下一步往哪里走”。
第二,创建 seen_policies:
seen_policies = set()
它是一个集合,用来记录算法以前见过哪些策略。
正常情况下,policy iteration 会收敛到稳定策略。但如果奖励设计有问题,例如 step_reward 太大且为正数,agent 可能觉得一直绕圈比到达终点更划算,策略可能反复循环。seen_policies 的作用就是检测这种情况,避免程序一直跑下去。
然后再看外层流程:
while True:
values = evaluate_policy(env, policy, gamma)
policy, stable = improve_policy(env, values, policy, gamma)
if stable:
return policy, values, iterations
这段代码说明 policy iteration 只做两件事:
第一步:evaluate_policy 先评价当前 policy 有多好
第二步:improve_policy 再根据评价结果改进 policy
为什么先讲这段?因为它是总目录。下面所有代码片段都属于这两个步骤之一。
10. 价值函数:V(s)
代码里:
values = {state: 0.0 for state in env.states()}
values[state] 就是状态价值,公式里写成:
V(s)
它的含义是:
从状态 s 出发,未来长期能拿到多少回报。
这不是当前一步奖励,而是包含未来的长期价值。
为什么要单独讲这一行?因为后面输出里的 Values 表,就是这个 values 字典算出来的。
它对应关系是:
代码里的 values[state] <-> 公式里的 V(s) <-> 输出里的 Values 表
11. policy evaluation:固定策略,计算价值
在 evaluate_policy() 里:
action = policy[state]
next_state, reward = env.step(state, action)
values[state] = reward + gamma * values[next_state]
这三行可以翻译成:
当前状态价值 = 当前这一步奖励 + 下一状态价值的折扣
为什么只重点讲这三行?因为它们正好把 RL 的四个核心量连起来:
state -> action -> next_state + reward -> 更新 values[state]
其他行也有用:
|
|
|
|---|---|
while True |
|
for state in env.states() |
|
old_value = values[state] |
|
if env.is_terminal(state) |
|
delta = ... |
|
if delta < theta |
|
所以不是其他代码没用,而是这三行最直接对应 Bellman 更新。
公式写成:
V(s) <- r + gamma * V(s')
变量对照:
|
|
|
|
|---|---|---|
s |
state |
|
a |
action |
|
s' |
next_state |
|
r |
reward |
|
gamma |
gamma |
|
V(s) |
values[state] |
|
V(s') |
values[next_state] |
|
完整 RL 书里常见的 Bellman 公式更复杂:
V(s) = sum_a pi(a|s) * sum_s' P(s'|s,a) * [r + gamma * V(s')]
本课代码没有写这个复杂形式,因为环境是确定性的:一个状态加一个动作,只会得到一个确定的下一状态。
所以当前可以先理解简化版:
V(s) <- r + gamma * V(s')
为什么这种“自洽”可以评估价值?
关键在于:V(s) 的定义本来就是“从状态 s 出发,按照当前 policy 走下去,未来能拿到的折扣累计奖励”。
也就是:
V(s) = 从 s 开始的长期回报
而长期回报可以拆成两部分:
长期回报 = 眼前这一步 reward + 从下一状态开始的长期回报
因为下一状态的长期回报要打折,所以就是:
V(s) = r + gamma * V(s')
这不是人为发明出来的技巧,而是价值定义的递归展开。
例如从 (0,1) 出发,当前 policy 选择向右:
(0,1) --R--> (0,2),reward = -0.04
那么 (0,1) 的价值应该等于:
V(0,1) = -0.04 + 0.95 * V(0,2)
如果 (0,2) 的价值是 1.00,那么:
V(0,1) = -0.04 + 0.95 * 1.00 = 0.91
这正好对应输出里的:
Values 第一行: 0.82 0.91 1.00 1.00
所以 policy evaluation 其实是在找一组数字,让每个状态都满足这种关系:
当前状态价值 = 当前一步奖励 + 折扣后的下一状态价值
如果一组 values 满足所有状态的这个关系,那它就不是随便猜的数字,而是当前 policy 下每个状态的长期价值。
为什么可以反复更新逼近这组数字?因为 gamma < 1。
gamma < 1 的意思是:越远的未来影响越小。每次更新虽然会引用未来价值,但未来价值被乘上 gamma,影响会逐步变弱。所以反复更新后,value 会逐渐稳定到同一组答案。
为什么要循环多轮?因为 values[state] 依赖 values[next_state]。一开始所有价值都是 0.0,第一次更新只能得到很粗的估计。多轮更新以后,远处终点或陷阱的影响会一格一格传回来。
有时你会看到:
evaluation converged after 118 sweeps
这不是 agent 真实走了 118 步,也不是试错了 118 次。它的意思是:在当前固定 policy 下,程序把所有状态的 value 反复重新计算了 118 轮。
本质上,evaluate_policy() 在解一个自洽问题:
每个状态的价值,要和它下一步到达状态的价值互相一致。
比如第一轮 policy iteration 的初始策略是“所有格子都向右走”。对于 (1,0),右边是墙,所以向右会撞墙,仍然留在 (1,0):
state = (1,0)
action = R
next_state = (1,0)
reward = -0.04
于是更新公式变成:
V(1,0) <- -0.04 + 0.95 * V(1,0)
右边又出现了 V(1,0) 自己。这种情况叫“自环”:当前状态的价值依赖当前状态自己。
如果直接解方程:
V(1,0) = -0.04 + 0.95 * V(1,0)
可以得到:
V(1,0) = -0.8
但代码没有直接用代数解方程,而是用迭代逐步逼近:
第 1 轮:V = -0.04
第 2 轮:V = -0.04 + 0.95 * (-0.04) = -0.078
第 3 轮:V = -0.04 + 0.95 * (-0.078) = -0.1141
...
最后慢慢接近 -0.8
因为 gamma = 0.95 很接近 1,每一轮还会保留很多未来价值,所以收敛会比较慢。theta = 1e-4 又要求变化小到 0.0001 以下才停止,所以第一轮 policy evaluation 可能需要很多 sweep。
所以,evaluate_policy() 的本质不是“找更好的策略”,而是:
在当前 policy 不变的前提下,
反复更新每个状态的 value,
直到这些 value 互相一致、基本不再变化。
为什么不直接走迷宫试错?
因为这一课的前提是:环境模型已知。
代码里已经有:
next_state, reward = env.step(state, action)
这意味着程序可以直接问环境模型:
如果我在 state 做 action,会到哪里?会得到多少 reward?
既然规则已经知道,就没有必要通过真实走很多次来估计结果。
如果用“走迷宫试错”的方式评估当前 policy,大概会变成:
从某个 state 出发
按照当前 policy 一直走
记录一路拿到的 reward
重复很多次
用平均结果估计这个 state 的 value
这种方法当然也可以,它接近后面会学的 Monte Carlo / model-free learning。但在本课这个场景里,它有几个问题:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
所以本课选择反复计算 value,不是因为 RL 永远都这样做,而是因为:
已知模型时,用模型直接算,比靠试错估计更干净、更稳定、更省样本。
等到第三课 Q-learning,我们会去掉这个假设。那时 agent 不再提前知道完整环境模型,只能拿到自己每一步实际经历的:
state, action, reward, next_state
那就必须通过“走出来的数据”学习,而不能像本课这样直接规划。
12. policy improvement
有了状态价值后,下一步是改进策略。
代码:
for action in ACTIONS:
next_state, reward = env.step(state, action)
action_values[action] = reward + gamma * values[next_state]
best_action = max(action_values, key=action_values.get)
new_policy[state] = best_action
含义:
在当前状态,分别试算 U/R/D/L 四个动作的长期价值;
选择价值最高的动作作为新策略。
注意,这里不是实际让机器人走四次,而是在已知环境模型里计算四种可能结果。
这里和 evaluate_policy() 的区别非常重要:
|
|
|
|
|---|---|---|
evaluate_policy() |
|
|
improve_policy() |
|
|
所以完整逻辑是:
1. GridWorld.step() 提供环境模型:s + a -> s' + r
2. evaluate_policy() 用当前 policy 更新 V(s)
3. improve_policy() 用 V(s) 重新选择 action
4. policy_iteration() 让 2 和 3 交替,直到 policy 不变
13. 改 gamma:短视和长期主义
运行:
python lessons/02_gridworld_dp/gridworld_dp.py --gamma 0.5
默认 gamma=0.95 时,左下角价值是 0.67。改成 gamma=0.5 后,左下角价值变成 -0.01。
原因是:左下角离终点远,终点奖励属于较远的未来。如果 gamma 小,未来奖励会被严重打折。
所以 gamma 可以理解为:
我有多重视未来?
14. 改 step_reward:走路成本
运行:
python lessons/02_gridworld_dp/gridworld_dp.py --step-reward -0.2
python lessons/02_gridworld_dp/gridworld_dp.py --step-reward -1.0
step_reward 越负,走路成本越高。你会看到远离终点的格子价值明显下降。
当 step_reward=-1.0 时,有些状态甚至会倾向于更快结束,而不是绕远路。这不是算法坏了,而是 reward design 改变了问题目标:
如果每多走一步都非常痛苦,快速结束可能比长距离绕路更划算。
这说明 reward design 很重要。你给什么奖励,agent 就会围绕什么目标优化。
15. 改 pit_reward:惩罚大小
运行:
python lessons/02_gridworld_dp/gridworld_dp.py --pit-reward -5.0
在当前地图里,最优策略本来就会避开坑,所以 policy 可能不变。但坑本身的价值会从 -1.00 变成 -5.00。
这说明分析 RL 结果时不能只看策略是否变化,也要看价值函数如何变化。
16. 两个容易误解的实验现象
改参数后策略为什么经常一样
这不说明策略永远不受参数影响。
在当前这个 3x4 确定性小地图里,最优路线比较稳定。很多参数变化会先改变 Values,但不一定改变 Policy。
例如 gamma=0.95 时,左下角价值约为 0.67;gamma=0.5 时,左下角价值约为 -0.01。路线可能仍然是朝终点走,但这个状态的长期价值已经明显下降。
在更复杂的地图里,策略可能会变化。例如存在长路和短路、危险区域、随机滑动、多个终点时,gamma、步进成本和惩罚大小都可能改变最终路线。
所以正确理解是:
参数一定会影响价值判断;
是否改变策略,取决于环境结构和参数变化幅度。
step_reward 能不能是正数
不是绝对不能。
但在“尽快到达终点”的任务里,正的 step reward 很危险。因为 agent 实际优化的是累计奖励,不是“到达终点”这句话。正步进奖励可能让 agent 觉得一直走路比结束更好。
例如默认 gamma=0.95,如果 step_reward=0.2,那么无限走路的折扣收益大约是:
0.2 / (1 - 0.95) = 4.0
而终点奖励只有 1.0。这时一直走路比到终点更划算,任务目标就被 reward design 改坏了。
小的正数不一定会出问题。例如 step_reward=0.01 时,无限走路的折扣收益大约是 0.2,仍然低于终点奖励。
所以规则不是“必须为负数”,而是:
如果希望 agent 完成任务,reward 设计不能让不完成任务更划算。
当前代码会检测重复策略。如果正步进奖励导致 policy iteration 在循环策略之间来回切换,程序会停止并提示你检查 reward design。
所以这个案例的目标更准确地说是:
通过奖励设计,让到达终点成为累计奖励最高的行为。
如果 reward design 让“永远走路”更高分,agent 就会优化这个错误目标。
17. 本课你应该带走什么
第一,GridWorld 引入了状态。RL 不再只是“哪个动作平均最好”,而是“在当前状态下哪个动作长期最好”。
第二,V(s) 是状态价值,不是一步奖励。它包含未来。
第三,如果环境模型已知,可以用 planning 方法直接算策略。policy iteration 就是一个典型方法。
第四,Bellman 更新不要先背公式,先看代码:
values[state] = reward + gamma * values[next_state]
这就是:
当前价值 = 当前奖励 + 折扣后的下一状态价值
第五,reward 和 gamma 的设置会改变价值判断,有时也会改变策略。
18. 进入下一课前的问题
你应该能回答:
-
1. Bandit 为什么没有状态,而 GridWorld 有状态? -
2. Values表里的数字是什么意思? -
3. Policy表里的箭头是什么意思? -
4. gamma小了为什么远处状态价值下降? -
5. policy evaluation 和 policy improvement 分别做什么? -
6. values[state] = reward + gamma * values[next_state]为什么是 Bellman 更新?
下一课会去掉一个关键假设:agent 不再提前知道环境模型。那时我们会进入 Q-learning。
如果觉得内容不错,欢迎你点一下「在看」,或是将文章分享给其他有需要的人^^
相关好文推荐:

0条留言