如果你刚开始接触强化学习,大概率会在各种教材和课程里撞见同一个经典练习:在一个网格世界里,给定某个策略,先把它的价值函数算出来,再根据价值函数改进这个策略,循环往复直到收敛。标题里的“(5 动作)”往往是最容易被忽略的地方——大家习惯了上下左右四个方向,多出来的那个“停留”看起来像是凑数。我最初也是这么想的,直到自己动手把策略评估与策略改进完整跑了一遍,才意识到恰恰是这第五个动作,让整个实验更贴近真实问题:现实系统里,“什么都不做”往往也是一个必须纳入比较的合法选项。
这篇文章我就用一个 5×5 网格世界,把策略评估和策略改进从头到尾实现一遍,给出能直接运行的 Python 代码、迭代过程中价值表的变化,以及几个我自己反复踩过的坑。适合刚接触强化学习的同学,也适合想快速复现一个最小 Demo、把贝尔曼方程真正落到代码里的开发者。
1. 为什么是5个动作:网格世界的问题设定与建模细节
1.1 网格世界在强化学习里的定位
网格世界是强化学习里最“顺手”的实验场,因为它是一个完整的马尔可夫决策过程(MDP):状态、动作、转移概率、奖励、折扣因子,该有的全都有,而且状态空间是有限的、可视的。你可以直接把状态价值表打出来看,用眼睛判断算法对不对,这在 CartPole 那种连续控制任务里很难做到。
我这次用的设定是 5×5 网格,坐标从 (0,0) 到 (4,4),终点放在右下角 (4,4)。一旦进入终点,回合立即结束。除了终点之外,每个格子都是普通状态,可以执行动作。这样一个小网格,状态只有 25 个,动作只有 5 个,价值表也不过是 5×5 的矩阵,打印出来非常直观。
小网格还有一个好处:你可以手推几个格子的价值,跟代码运行结果对照。比如终点旁边一格的价值应该是多少,我后面会专门算一下。这种“纸上能算、代码能跑、眼睛能看”的特点,让它成为理解策略评估和策略改进的最佳起点。
1.2 动作空间、状态空间与奖励约定
动作空间就是标题里说的“5 动作”,具体定义如下表:
| 动作索引 | 动作 | 效果 |
|---|---|---|
| 0 | 上 | 行坐标减 1,越界则留在原地 |
| 1 | 下 | 行坐标加 1,越界则留在原地 |
| 2 | 左 | 列坐标减 1,越界则留在原地 |
| 3 | 右 | 列坐标加 1,越界则留在原地 |
| 4 | 停留 | 原地不动 |
转移规则我特意做了简化:动作是确定性的,也就是说你选了“右”,只要不越界,就一定会向右移动一格;如果越界,就留在原地。这样便于建立直觉。真实环境里通常会用带噪声的转移,我后面会讲到修改方式。
奖励约定是:每走一步(包括撞墙留在原地),获得 -0.1;如果这一步进入了终点,获得 +1,并且回合终止。折扣因子 γ 取 0.9。之所以给每步一个小的负奖励,是为了让策略有“少走冤枉路”的压力,但惩罚又不能太重,否则到达终点的 +1 会被完全淹没。这个尺度我一开始用的是 -1,结果价值表整张都是负数,策略迭代了好几轮才缓过来,后面会展开说。
1.3 多出来的“停留”动作到底改变了什么
先说结论:即使在这个确定性环境里,“停留”几乎不会被最优策略选中,但保留它依然重要。
原因有三个。第一,它把策略空间从 4 个选择扩展到 5 个,等于强迫算法在改进阶段明确回答“不动到底值不值”,而不是默认“宁可乱动也不停留”。第二,在带噪声的转移下,“停留”往往是一个规避风险的选项——你原地不动,至少不会因为随机滑步而掉进坑里。第三,从建模角度看,很多真实系统确实允许“什么都不做”作为动作,比如机器人原地等待、交易系统空仓,把它纳入动作空间是更自然的做法。
我在跑实验时还发现一个细节:当某个状态的所有动作价值相等时,argmax会固定选择索引最小的那个动作。在这个 5 动作定义下,索引 0 是“上”。这会让打印出来的策略看起来偏向某个方向,但其实只是并列最优时的一个任选。严谨一点的做法是把所有最优动作等概率分配,不过对教学实验影响不大。
2. 策略评估:用迭代把“这个策略有多好”算成一张价值表
2.1 状态价值函数与贝尔曼期望方程
策略评估要回答的问题是:给定一个策略 π,如果我从状态 s 出发,之后每一步都按 π 选动作,长期下来我能拿到的期望累计回报是多少?这个值就是状态价值函数 Vπ(s)。
它的计算依赖贝尔曼期望方程:
Vπ(s) = Σ π(a|s) Σ P(s'|s,a) [R + γ Vπ(s')]
翻译成大白话:站在格子 s 上,我先看策略 π 让我以多大概率选每个动作 a,再看环境会把我送到哪个 s'、给我多少奖励 R,最后把“这一步的奖励 + 打折扣的下一状态价值”加权平均起来。如果策略是确定性的,π(a|s) 就是一个 one-hot 向量,公式会更简单。
为什么用迭代而不是直接解这个方程?因为这是一个含 25 个未知数的线性方程组,理论上能直接解,但强化学习里更通用的做法是迭代。从一个全零的价值表开始,反复套用上面的公式更新,数学上对应的是压缩映射,最终会收敛到唯一的 Vπ。这个过程不需要环境是真实世界,只要我们有模型(也就是知道 P 和 R),就能做“规划式”的价值计算。
2.2 迭代评估的实现细节与收敛判断
我先把环境交互和策略评估的代码写出来。这里最容易踩坑的是“同步更新”和“原地更新”的区别,我直接用了同步更新:每一轮迭代都基于旧的 V 表计算新的 V 表,全部算完后再整体替换。
import numpy as np GRID_SIZE = 5 TERMINAL = (4, 4) ACTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1), (0, 0)] # 上、下、左、右、停留 ACTION_NAMES = ['上', '下', '左', '右', '停'] GAMMA = 0.9 THETA = 1e-4 def step(state, action): """执行动作,返回 (next_state, reward, done)""" if state == TERMINAL: return state, 0.0, True r, c = state dr, dc = ACTIONS[action] nr, nc = r + dr, c + dc # 越界则留在原地 if 0 <= nr < GRID_SIZE and 0 <= nc < GRID_SIZE: r, c = nr, nc if (r, c) == TERMINAL: return (r, c), 1.0, True return (r, c), -0.1, False def policy_evaluation(policy): """给定策略(每个状态上的动作概率分布),迭代计算状态价值表""" V = np.zeros((GRID_SIZE, GRID_SIZE)) while True: delta = 0.0 new_V = V.copy() for r in range(GRID_SIZE): for c in range(GRID_SIZE): if (r, c) == TERMINAL: continue v = 0.0 for a, p_a in enumerate(policy[r, c]): (nr, nc), reward, done = step((r, c), a) v += p_a * (reward + GAMMA * V[nr, nc]) new_V[r, c] = v delta = max(delta, abs(v - V[r, c])) V = new_V if delta < THETA: break return V注意policy的形状是 (5, 5, 5),policy[r, c]是一个长度为 5 的动作概率向量。比如均匀随机策略里每个动作概率都是 0.2;确定性策略里某个动作概率是 1.0,其余是 0.0。
收敛判断用的是所有状态价值变化量的最大值delta,当它小于阈值THETA时停止。这个阈值我习惯取 1e-4,教学完全够用;如果你想要更高精度可以取 1e-8,但迭代次数会明显变多。
2.3 评估一个“很差的策略”:从全零 V 表开始
为了能直观看到策略评估的效果,我构造了一个故意很差的策略:所有非终止状态都选择“左”。这个策略永远到不了终点,因为在最左列的格子会撞墙留在原地,然后无限循环,所以整张价值表应该都是明显的负数。
我跑下来的结果是,价值表大部分格子都在 -0.9 到 -1.0 之间。原因也很简单:一旦把自己锁死在某个格子附近,每步 -0.1 的惩罚会无限累积,考虑折扣后极限就是 -0.1 / (1 - 0.9) = -1.0。如果一个格子向左走几步后才开始撞墙,它的价值会稍微好一点点,但也就是 -0.9 上下。
这个结果很有教学意义:一个差策略的价值表是“平的、负的、毫无方向感”的,你看不出哪里更好。这时你就能理解,为什么策略改进必须从动作层面入手——价值表本身不会告诉你“下一步该往哪走”。
3. 策略改进:贪心选择动作,让旧策略一步步变聪明
3.1 为什么要从 V(s) 过渡到 Q(s,a)
状态价值 V(s) 告诉我们“站在这个格子有多好”,但策略改进需要回答的是“站在这个格子,我下一步该选哪个动作”。光有 V 是不够的,因为 V 是策略 π 在所有动作上加权平均的结果,单个动作的好坏被平均掉了。
所以我们必须计算动作价值函数 Q(s,a):在状态 s 先执行动作 a,之后继续按旧策略 π 走,得到的期望回报。计算公式是:
Q(s,a) = Σ P(s'|s,a) [R + γ Vπ(s')]
在这个确定性环境里,P 只有一个可能的结果,所以 Q 就是简单的“这一步的奖励 + γ × 下一步状态的价值”。有了 Q,贪心改进就非常直接了:每个状态选 Q 最大的那个动作,作为新策略。
3.2 策略改进定理与策略迭代的闭环
这里有一个关键定理支撑,叫策略改进定理:如果你在每个状态都按贪心方式选择动作,得到的新策略 π' 在每个状态的价值都不会比旧策略 π 差。换句话说,基于价值表的贪心改进不会让你越改越差,这是一个单调改进的过程。
配合这个定理的算法流程就是著名的策略迭代:
- 初始化一个策略,比如全“左”的差策略;
- 对当前策略做策略评估,得到 Vπ;
- 基于 Vπ 计算 Q,贪心改进得到新策略;
- 如果新策略和旧策略相同,停止;否则回到第 2 步。
这个流程最大的魅力是收敛速度极快。在我这个小网格上,通常 2 到 3 轮迭代就稳定了。因为每一轮改进都同时改变了很多状态的决策,价值表会在下一轮快速反映这些变化。
3.3 完整代码:改进后的策略长什么样
下面补上策略改进和完整循环的代码。
def policy_improvement(V, old_policy): """根据价值表做贪心改进,返回新策略以及是否稳定""" policy = np.zeros((GRID_SIZE, GRID_SIZE, len(ACTIONS))) stable = True for r in range(GRID_SIZE): for c in range(GRID_SIZE): if (r, c) == TERMINAL: policy[r, c, len(ACTIONS) - 1] = 1.0 # 终点保持“停留” continue q_values = [] for a in range(len(ACTIONS)): (nr, nc), reward, done = step((r, c), a) q_values.append(reward + GAMMA * V[nr, nc]) best_a = int(np.argmax(q_values)) old_best = int(np.argmax(old_policy[r, c])) if old_best != best_a: stable = False new_p = np.zeros(len(ACTIONS)) new_p[best_a] = 1.0 policy[r, c] = new_p return policy, stable def policy_iteration(): # 初始策略:所有格子都选“左”,足够差,也足够直观 policy = np.zeros((GRID_SIZE, GRID_SIZE, len(ACTIONS))) policy[:, :, 2] = 1.0 policy[TERMINAL[0], TERMINAL[1], len(ACTIONS) - 1] = 1.0 for it in range(1, 20): V = policy_evaluation(policy) new_policy, stable = policy_improvement(V, policy) print(f"第 {it} 轮:价值表均值 {V.mean():.4f},策略是否稳定:{stable}") if stable: print_policy(new_policy) print_v(V) return new_policy, V policy = new_policy return policy, V def print_policy(policy): print("策略矩阵(每个格子显示最优动作):") for r in range(GRID_SIZE): row = [] for c in range(GRID_SIZE): if (r, c) == TERMINAL: row.append("★") else: row.append(ACTION_NAMES[int(np.argmax(policy[r, c]))]) print(" ".join(row)) def print_v(V): print("价值函数(保留3位小数):") for r in range(GRID_SIZE): print(" ".join(f"{V[r, c]:6.3f}" for c in range(GRID_SIZE))) policy_iteration()我实际跑出来的结果里,第 1 轮改进后的策略就已经全部指向终点方向了:起步区域向上或向右,中段向右或向下,靠近终点时直接朝终点移动。由于这个环境里存在并列最优,argmax会倾向选索引小的动作,所以你看到的矩阵可能并不是唯一正确答案,但每个格子的箭头都朝向终点方向。
改进后的价值表也有规律:离终点越近,价值越高。按前文奖励约定,距离终点一步的格子价值是 1.0,两步的格子价值是 -0.1 + 0.9×1.0 = 0.8,三步是 -0.1 + 0.9×0.8 = 0.62。起点 (0,0) 距离终点八步,价值大约在 -0.04 附近——跟初始全“左”策略的 -0.9 比起来,简直是天壤之别。这才是策略改进真正带来的收益。
4. 收敛、边界条件与调参:网格实验里最容易踩的几个坑
4.1 终止状态的价值处理
我见过太多人在这一步翻车:迭代时没有跳过终止状态,导致终点价值被反复更新,整张价值表一直在漂移。正确的做法是让终止状态的 V 值恒为 0,不参与更新。
我代码里是用if (r, c) == TERMINAL: continue跳过的,并在策略矩阵里把终点的动作固定为“停留”。如果你忘了这一步,终点价值会被来自其他状态的转移不断“刷新”,整个表的收敛速度会变慢,甚至出现振荡。这个小细节也对应一个现实认知:回合结束后的状态没有未来价值,它是一个吸收态。
4.2 撞墙语义、折扣因子和奖励尺度的影响
撞墙的处理方式直接影响边界格子的价值。我这里用的是“越界则留在原地”,这是最常见也最简单的建模。但有些实现会把越界解释为“撞墙弹回原方向反方向一格”,甚至给撞墙额外惩罚。这几种方式都会改变边界格子的 Q 值,进而改变最优策略。如果你复现别人的实验发现策略形状不一样,先检查撞墙语义,这个比调参更能解释差异。
折扣因子和奖励尺度也要组合着看。γ 越接近 1,算法越“看得远”,远期奖励对当前决策影响更大,但收敛也越慢;γ 太小则目光短浅,在网格上可能选不出绕过障碍的路径。每步惩罚 -0.1 和 -1 的区别更是关键:惩罚太重时,哪怕终点有 +1,起点附近的价值也会被压成比较大的负数,策略会额外偏好“快速结束”,绕路行为的代价很高;惩罚太轻时,策略又可能无所谓绕路。我建议新手先固定 γ=0.9,把步惩罚调成 -0.1,跑通后再对比改参数的效果。
4.3 同步更新细节与阈值选择
我在policy_evaluation里特意用了new_V = V.copy(),然后遍历所有状态算新值,最后整体替换。这是同步更新。如果你直接写V[r,c] = ...,那就是原地更新,虽然通常也能收敛,但它用了一部分本轮的新值和一部分旧值,顺序还会影响结果。对于教学和调试,同步更新更容易复现,我推荐这么做。
阈值的选取也要提一句。THETA = 1e-4在 25 个状态的小网格上很快,但如果你把网格扩大或者 γ 调大,想更快看到效果,可以用 1e-3 先粗跑一遍,确认策略形状对了再提高精度。另一个容易犯的错是把策略稳定的判断和价值收敛的判断混为一谈。策略稳定意味着新旧策略每个状态的动作完全一致;价值收敛则说明 V 表变化足够小。即使价值收敛了,策略也可能因为并列最优而需要一轮额外迭代,所以我在主循环里先评估,再改进,检查的是新旧策略是否一致。
最后,关于并列最优的处理:如果 Q 值相同,argmax会固定选第一个动作索引。我测试时看到过某些格子策略显示“上”,但其实“右”同样是最优。这不是 bug,只是贪心策略的“任选性”。你想验证这一点,可以把动作顺序改成“右、下、左、上、停”,打印结果会不一样,但每格的价值应该几乎相同。
5. 从网格世界走向真实问题:我的实操体会与扩展方向
5.1 策略迭代与值迭代,“模型已知”的边界在哪里
做完策略迭代后,你其实已经掌握了强化学习里最核心的规划框架:模型已知,就用贝尔曼方程迭代。策略迭代是“评估 + 改进”分两阶段;值迭代则是把贪心改进直接揉进更新里,每轮都用 V(s) = max_a Q(s,a) 更新,少一次完整的评估。在小网格上两者差异不大,但思维上值迭代更像“每一轮都在向最优价值逼近”。
需要区分的是,这整个讨论都假设我们有环境模型,也就是知道转移规则和奖励。如果模型未知,你就要进入无模型学习的世界,类似 Q-learning 那种用采样来估计 Q 值的方法。网格世界的价值在于,让你先看清“已知模型时解法长什么样”,再去理解无模型方法到底是在弥补什么。
5.2 几个值得动手做的变体实验
如果你也想加深理解,我建议别满足于跑通这个 Demo,试着改这几个方向:
- 把转移改成带噪声的版本:80% 概率按指定方向走,20% 概率随机向左右偏。这时候“停留”动作的价值会立刻变得有趣,某些原本贪心选择移动的状态可能会改为停留。
- 加入障碍物或悬崖格:比如在网格里挖一个奖励为 -2 的坑,掉进去就回到起点。这会改变最优路径的形状,让你看到价值表和策略矩阵的对应关系。
- 初始策略换成均匀随机:全“左”策略虽然直观,但随机策略评估出的价值表更有“平均感”,改进过程也更平滑。
- 把贪心改成软策略:用 softmax 或 ε-greedy 代替 argmax,可以看到“探索”如何影响策略改进。
我实际把转移改成噪声版时,最明显的变化是边界格子的“停留”动作会被选中,这在确定性环境里几乎不会发生。那一刻我才真正理解了为什么标题要强调“5 动作”,而不是简单的“4 方向移动”。动作空间里的每个选项都不是摆设,它们会在环境不确定性增加时体现出价值。
说回这次实验本身,我最大的体会是:网格世界虽然简单,但它把强化学习最核心的“评估—改进”循环摊开在了桌面上。你亲手打印出那张价值表,看着它从全零变成有方向感的曲面,再看到策略矩阵从“全部向左”变成“齐指终点”,这种反馈是纯理论学习给不了的。如果你第一次跑的时候发现收敛很慢,先别急着怀疑代码,检查一下终止状态是否被更新、撞墙语义是什么、奖励尺度是否合理——这三个地方出问题的概率远比你想象的高。
下次如果你要跟人讲策略评估和策略改进,我建议直接打开一个网格世界,把全“左”策略的价值表打出来,再迭代两轮给他看。比任何公式都直观。