资讯动态

强化学习入门:5×5网格世界中的策略评估与策略改进实战

发布时间:2026/10/20 3:05:01 来源:尧图企业网站定制
如果你刚开始接触强化学习大概率会在各种教材和课程里撞见同一个经典练习在一个网格世界里给定某个策略先把它的价值函数算出来再根据价值函数改进这个策略循环往复直到收敛。标题里的“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 方向移动”。动作空间里的每个选项都不是摆设它们会在环境不确定性增加时体现出价值。说回这次实验本身我最大的体会是网格世界虽然简单但它把强化学习最核心的“评估—改进”循环摊开在了桌面上。你亲手打印出那张价值表看着它从全零变成有方向感的曲面再看到策略矩阵从“全部向左”变成“齐指终点”这种反馈是纯理论学习给不了的。如果你第一次跑的时候发现收敛很慢先别急着怀疑代码检查一下终止状态是否被更新、撞墙语义是什么、奖励尺度是否合理——这三个地方出问题的概率远比你想象的高。下次如果你要跟人讲策略评估和策略改进我建议直接打开一个网格世界把全“左”策略的价值表打出来再迭代两轮给他看。比任何公式都直观。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑