ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MDP策略迭代MATLAB实战:MDPtoolbox源码解析与Grid World案例

MDP策略迭代MATLAB实战:MDPtoolbox源码解析与Grid World案例

简介:面向机器学习与强化学习初学者的马尔科夫决策过程(MDP)策略迭代MATLAB实现资源包,聚焦动态规划中的经典求解算法,帮助理解策略评估与策略改进的交替迭代过程,适用于离散状态和动作空间的决策问题建模与仿真。压缩包共93个文件,包含33个m源码、20个html文档、18个dat数据文件、8个txt说明及pdf/tex文档等,整体2.32MB;其中策略迭代、策略评估等主程序配合MDPtoolbox,可直接运行并验证最优策略求解流程。目前已有312人学习下载。资源内附HTML格式的函数说明、示例与README,以及若干文本说明,便于对照查阅策略迭代的理论推导、代码调用方式和结果可视化方法;dat文件可作为测试数据快速上手。通过学习这套MATLAB实现,既能掌握MDP建模与策略迭代的核心步骤,也能迁移到强化学习的策略优化场景,适合课程设计、算法复现与入门实践。

1. MDP策略迭代的MATLAB工程:MDPtoolbox 源码比调包结果更有价值

第一次跑策略迭代的机器学习初学者,十有八九会把它当成黑匣子:输入转移矩阵、奖励矩阵,丢给一个求解函数,输出一组最优策略,中间值函数怎么迭代、策略怎么贪心改进,全被吞掉了。MDP.zip 这份 MATLAB 实践工程恰好把黑匣子拆开了——MDPtoolbox 自带 mdp_policy_iteration.m 主算法、mdp_check.m 合法性检查、mdp_eval_policy.m 策略评估、mdp_value_iteration.m 值迭代等一整套源码,外加 HTML 文档和 examples 示例,既能看穿策略评估与策略改进的闭环逻辑,也能直接改参数套用到自己的状态转移问题。对正在学强化学习、准备期末考试或者做课程设计需要一份能改能跑的 MATLAB 参考实现的人,这份资源正合适。

2. 拆包 MDPtoolbox:文件结构、建模参数与合法性检查

下载下来先别急着跑示例,把压缩包里几十个文件按功能理一遍,能省掉后面一半的翻车时间。MDPtoolbox 是函数式设计,每个 .m 文件对应一个独立算法模块,单独调用就能工作,这在 MATLAB 工程里是非常友好的结构。

2.1 压缩包里到底装了什么

整个包的核心可以分成四类,按用途看更清楚:

分类文件作用
核心求解算法mdp_policy_iteration.m、mdp_policy_iteration_modified.m、mdp_value_iteration.m、mdp_value_iterationGS.m、mdp_finite_horizon.m、mdp_LP.m、mdp_relative_value_iteration.m策略迭代、改进版策略迭代、值迭代、Gauss-Seidel 值迭代、有限时域值迭代、线性规划求解法、平均奖励准则下的相对值迭代
计算算子mdp_eval_policy.m、mdp_bellman_operator.m、mdp_bellman_operator_policy.m、mdp_computePR.m、mdp_span.m给定策略求值函数、Bellman 算子、按策略改进的 Bellman 算子、从转移矩阵和奖励构造期望奖励、值函数跨度计算
检查与辅助mdp_check.m、mdp_check_square_stochastic.m、mdp_rand.m、mdp_verbose.m、mdp_silent.m模型参数合法性检测、随机矩阵专项检测、随机 MDP 生成器、日志开关
文档与示例DOCUMENTATION.html、mdp_policy_iteration.html 等各函数 HTML 页、README、examples 目录、race 示例目录函数用法说明、完整示例场景

压缩包里还有一个 www.pudn.com.txt,那只是下载站的说明文件,不影响代码使用。README 和各函数的 HTML 文档反而是最值得先读的,里面标了每个函数的输入输出格式和参数默认值。

2.2 MDP 建模必须交代的四件事:S、A、P、R

策略迭代解决的是离散状态、离散动作的马尔科夫决策过程,建模时必须先交代四件事:状态空间 S、动作空间 A、转移概率 P、奖励 R,折扣因子 γ 也属于调度参数。P 和 R 是整个算法唯一依赖的数据输入,所以构造它们的准确程度直接决定求解结果。

在 MDPtoolbox 里,P 用三维数组表示,维度是 S×S×A:P(s, s', a) 表示在状态 s 执行动作 a 后转移到状态 s' 的概率。对每个动作 a,P(:, :, a) 必须是随机矩阵,也就是每一行的概率之和等于 1。R 是二维数组,维度是 S×A,R(s, a) 表示在状态 s 执行动作 a 能拿到的即时奖励。

这里有个常见的认知落差:很多人觉得机器学习的数据处理是指清洗表格,但在 MDP 问题里,构造 P 和 R 就是你的数据预处理。后面所有迭代计算都是在做线性代数运算,如果 P 的行和不是 1,或者 R 的维度跟 P 不对齐,后面跑出来的策略全是错的,而且 MDPtoolbox 大概率会直接报错,让你连后悔的机会都没有。

2.3 用 mdp_check 校验矩阵合法性:求解前必须走的一步

mdp_check.m 存在的意义,就是替你把 P 和 R 的维度、随机性、一致性全部查一遍。它的返回值是错误消息列表,如果列表为空,说明模型参数合法,可以进入求解环节。

% 构造一个 2状态×2动作 的转移矩阵 P = zeros(2, 2, 2); % S=2, A=2,第3维是动作 P(:, :, 1) = [0.7 0.3; % 动作1:状态1->状态2的概率行和为1 0.2 0.8]; P(:, :, 2) = [0.9 0.1; % 动作2 0.4 0.6]; R = [5 10; % 状态1的动作奖励 -1 2]; % 状态2的动作奖励 % 调用 mdp_check 做合法性检测 error_msg = mdp_check(P, R); if isempty(error_msg) disp('P 和 R 校验通过,可以进入策略迭代'); else disp(error_msg); end

这段代码的逻辑很直白:先构造一个两状态两动作的玩具问题,P 的每个动作切片都是一行一个概率分布,R 对应每个状态执行每个动作的回报。mdp_check 内部会逐一切片检查三件事:矩阵维度是否为 S×S×A、每个动作切片是否为随机矩阵、R 的维度是否为 S×A。检查通过才继续往下走。

提示:如果你是自己手算概率填矩阵,填完后第一件事永远是 mdp_check,不要直接丢给 mdp_policy_iteration。这一步能挡住 90% 的维度错误和归一化错误,是这份工具包里最值得养成习惯的调用。

3. 策略迭代核心实现:mdp_policy_iteration.m 的输入输出与循环逻辑

策略迭代不是一堆玄学,它本质上是在做两步交替:先固定策略算值函数,再根据值函数改进策略,直到策略不再变化。MDPtoolbox 把这两步封装进 mdp_policy_iteration.m,源码可以直接打开看,逻辑比想象中简洁。

3.1 策略评估与策略改进的理论闭环

给定策略 π,策略评估要解的是一个线性方程组:

Vπ = (I - γPπ)^-1 * Rπ

其中 Pπ 是根据策略 π 从 P 中抽出来的 S×S 转移矩阵,Rπ 是对应的期望奖励向量。这个式子的意义是:在策略 π 下,每个状态的值函数等于未来所有折扣奖励的期望和,一步到位求解,不需要逐步迭代,这也是策略迭代比值迭代收敛快的原因。

策略改进则是利用刚算出的 Vπ,对每个状态 s 做一次贪心选择:

π_new(s) = argmax_a [ R(s,a) + γ * Σ_s' P(s'|s,a) * Vπ(s') ]

这里面的核心逻辑是:值函数代表长期回报,Bellman 算子把单步奖励和未来回报加起来,取最大动作作为新策略。理论已经证明,只要新策略和旧策略不一样,值函数就严格提升;当策略不再变化时,它一定满足 Bellman 最优性方程,也就是全局最优策略。

3.2 mdp_policy_iteration.m 的标准调用方式与核心循环

mdp_policy_iteration 的标准调用格式是:

[V, policy, iter, cpu_time] = mdp_policy_iteration(P, R, discount, max_iter, eval_type);

参数含义如下:

参数说明
PS×S×A 的转移概率数组,必须通过 mdp_check
RS×A 的奖励矩阵
discount折扣因子 γ,必须小于 1 才能保证收敛
max_iter最大迭代次数,默认 1000,防止死循环
eval_type策略评估方式,0 表示直接解线性方程组,1 表示用迭代法求值函数
V输出的收敛值函数,S×1 向量
policy输出的最优策略,S×1 向量,每个元素是状态对应的最优动作编号
iter实际迭代轮数
cpu_time求解耗时

下面这段是 mdp_policy_iteration.m 核心循环的骨架,跟我手头的源码逻辑一致,去掉了子函数细节,方便看清整体结构:

% mdp_policy_iteration 核心循环骨架 V = zeros(S, 1); % 值函数初始化为 0 policy = ones(S, 1); % 策略初始化为全动作1 iter = 0; while true iter = iter + 1; policy_old = policy; % 保存旧策略,用于收敛判断 % 策略评估:从当前策略抽出 P_pi 和 R_pi,解线性方程组 P_pi = build_P_from_policy(P, policy); % 省略子函数实现 R_pi = compute_R_from_policy(R, policy); % 省略子函数实现 V = (speye(S) - discount * P_pi) \ R_pi; % 策略改进:对每个状态做 Bellman 贪心 for s = 1:S Q_s = zeros(A, 1); for a = 1:A % 动作 a 的价值 = 即时奖励 + 折扣 * 后继状态值函数期望 Q_s(a) = R(s, a) + discount * squeeze(P(:, s, a))' * V; end [~, best] = max(Q_s); policy(s) = best; end % 收敛判断:策略不再变化则停止 if isequal(policy_old, policy) break; end if iter >= max_iter warning('达到最大迭代次数,策略可能未收敛'); break; end end

这段代码里最有嚼头的是两个细节。第一个是策略评估用的(speye(S) - discount * P_pi) \ R_pi,MATLAB 的\直接走 LU 分解,比写逆矩阵再加乘快得多,而且数值更稳,这是我会特别留意的实现习惯。第二个是 Q_s 的计算用了squeeze(P(:, s, a)),它把三维数组中某个动作下的第 s 行抽成向量,转置后跟 V 做内积,正好是状态转移的期望值。新手最容易在这个维度顺序上犯迷糊,建议自己打开 mdp_policy_iteration.m 对照着读一遍。

3.3 变体与选型:策略迭代、值迭代、LP、相对值迭代怎么挑

MDPtoolbox 提供了这么多求解函数,不是炫技,它们分别对应不同问题规模。策略迭代收敛快但每步要解线性方程组,状态数上到几千以后矩阵求解开销很大;值迭代实现简单,每一步只做 Bellman 算子更新,适合大规模问题,但收敛速率慢;Gauss-Seidel 值迭代在更新时立即用新值函数,通常比普通值迭代快;线性规划 mdp_LP.m 在理论上能给出精确解,但状态动作空间膨胀后不适合;还有一个容易忽略的 mdp_relative_value_iteration.m,它是专门处理 γ=1 平均奖励准则的工具。

嵌入式系统里跑 MDP 时,我一般会先用策略迭代在小规模算例上验证模型,确认没问题后换成值迭代跑大规模场景。mdp_policy_iteration_modified.m 是策略迭代的折中变体,它在策略评估阶段不做完整求解,只迭代有限步就进入策略改进,适合状态空间中等偏大的情况。选型逻辑就这么一句话:小规模求精确解用策略迭代,大规模求可行解用值迭代,γ=1 用相对值迭代。

4. 跑通一个 Grid World:从状态转移矩阵构建到最优策略

理论说再多,不如跑通一个完整算例。Grid World 是强化学习入门最经典的实验环境,也是机器学习课程设计里最常出现的场景:智能体在网格里移动,每走一步扣一点奖励,目标是尽快到达终点。我用 4×4 网格给你完整过一遍。

4.1 定义 4×4 网格环境:状态编号与动作约定

先定环境规则:16 个格子,编号 1 到 16,第一行是 1 2 3 4,第二行 5 6 7 8,以此类推。动作编号约定为 1=上、2=右、3=下、4=左。状态 1 和状态 16 是终止状态,到达后不再获得负奖励,每走一步的即时奖励是 -1,碰到边界执行动作会停留在原地。

这个环境的特殊之处在于终止状态要设计成吸收态,也就是到达后不管执行什么动作都留在原地且奖励为 0。这样才能让值函数满足 V(终止) = 0,策略迭代才能正确计算出从任意状态出发的最优路径。

4.2 在 MATLAB 里构建转移概率矩阵 P 和奖励矩阵 R

构建 P 的代码分两步走,先初始化再逐状态填充:

% Grid World 4x4:16个状态,4个动作 S = 16; A = 4; % 1=上, 2=右, 3=下, 4=左 P = zeros(S, S, A); % 转移概率三维数组 % 遍历每个状态,计算每个动作的确定性转移 for s = 1:S row = ceil(s / 4); % 行编号,行优先布局 col = mod(s - 1, 4) + 1; % 列编号 for a = 1:A s_next = s; % 默认停在原地,边界情况 switch a case 1 % 上 if row > 1 s_next = s - 4; end case 2 % 右 if col < 4 s_next = s + 1; end case 3 % 下 if row < 4 s_next = s + 4; end case 4 % 左 if col > 1 s_next = s - 1; end end P(s, s_next, a) = 1; % 确定性转移,概率为1 end end % 终止状态设为吸收态:任何动作都留在原地 % 由于上面边界逻辑已经保证停在原地,这里只需把奖励置0

逻辑说明:对每个状态和动作,先猜目标状态是当前状态,再根据边界条件判断是否真的移动。这样处理的好处是自动覆盖所有边界情况,不用逐条写 16 个状态的特判。P(s, s_next, a) = 1 表示从 s 执行动作 a 后确定性地到 s_next,没有随机性。

R 矩阵更简单:

R = -1 * ones(S, A); % 每走一步扣1分 R(1, :) = 0; % 终止状态1不扣分 R(16, :) = 0; % 终止状态16不扣分 gamma = 0.9; % 折扣因子

奖励矩阵的维度是 S×A,每个状态四个动作有相同的即时奖励。终止状态的奖励设为 0,配合 P 里的自环转移,值函数会自动收敛到 0。这两段代码合在一起就是完整的 MDP 模型,也是后续所有算法共用的输入。

4.3 调用策略迭代并解读输出

模型构造完后,调用就一行:

% 调用策略迭代求解 [V, policy, iter, cpu_time] = mdp_policy_iteration(P, R, gamma); % 输出整理成网格形状方便观察 V_mat = reshape(V, 4, 4)'; policy_mat = reshape(policy, 4, 4)'; disp('每个状态的值函数(行优先,终端状态为0):'); disp(V_mat); disp('最优策略(1上 2右 3下 4左,0表示终止状态):'); disp(policy_mat);

跑完以后你会看到值函数从终止状态向外逐层变负,离终点越远负得越多,这是每步 -1 奖励的必然结果。策略矩阵里,靠近状态 1 的格子会倾向向左或向上,靠近状态 16 的格子会倾向向右或向下。需要注意这个环境存在并列最优路径,比如状态 4 到状态 1 的左移和到状态 16 的下移距离一样,策略迭代只会输出其中一个编号,不代表另一个动作是错的。

这里有个判断方法:把输出策略代回 Bellman 最优性方程,如果每个状态的 Q 最大值对应的动作就是 policy 输出的动作,说明结果自洽。这个验证动作我在下一章会展开写。

4.4 看一眼值函数热力图:用可视化确认收敛合理性

数值结果不够直观,可以用 MATLAB 内置的 imagesc 画值函数热力图:

% 值函数热力图:颜色越暖代表值越大 figure; imagesc(V_mat); colorbar; title('Grid World 值函数热力图');

正常收敛的热力图应该是终止状态附近颜色明显偏暖,离终止状态越远越冷,整体过渡平滑,不应该出现个别状态跟周围差一大截的异常点。如果出现这种异常点,回查 P 里对应位置的概率填充,十有八九是某个动作的转移写错了。

5. 常见问题与避坑:MDPtoolbox 的五个翻车现场

MDPtoolbox 整体封装得很干净,但真正用起来,踩坑点集中在矩阵维度、归一化和收敛参数这三件事上。下面这五条是我实际跑过的问题,按「现象→原因→解决」写给你。

5.1 mdp_check 报错:transition probabilities must be stochastic

现象:调用 mdp_check(P, R) 返回的错误消息里写着转移概率不是随机矩阵,具体提示某个动作切片的行和不为 1,或者是负概率。

原因:绝大多数情况是手填概率时某一行忘记归一化,或者用浮点累加算出行和时出现了 0.99999 这种精度偏差。比如 0.1 + 0.2 + 0.7 在 double 里并不严格等于 1,严格校验会认为这一行不合法。

解决:对每个动作切片做一次显式归一化,P_slice = P(:, :, a); P(:, :, a) = P_slice ./ sum(P_slice, 2); 然后再过一次 mdp_check。如果确定自己没填错,就是浮点误差,可以检查 mdp_check 源码里的阈值设置。还有一条更省心的路子:用 mdp_rand.m 生成随机 MDP 做测试,能绕开手工构造带来的低级误差。

5.2 奖励矩阵维度对不上:index exceeds array bounds

现象:调用 mdp_policy_iteration 时直接报索引越界,或者 mdp_check 提示 R 的维度不匹配。

原因:P 的维度是 S×S×A,R 的维度是 S×A,但很多人习惯性地以为奖励矩阵也应该是 S×S×A,也就是每个转移对都配一个奖励,于是写着写着就把 R 填成了 S×S×A 或者干脆 S×1。

解决:翻回第二章的维度表格核对。mdp_policy_iteration 在构建 R_pi 时只需要按状态取动作对应的即时奖励,所以 R 一定是 S×A。如果你需要更精细的转移对奖励建模,可以用 mdp_computePR.m 从联合概率和即时奖励里生成期望奖励矩阵,然后把计算结果转成 S×A 再喂给策略迭代。

5.3 折扣因子取 1 导致策略不收敛

现象:gamma 设成 1,策略迭代跑了几千次还在振荡,状态 1 的动作一会是左一会是右,永远停不下来。

原因:γ=1 对应平均奖励准则,值函数会随着时间步无限增长,Bellman 方程里的 (I - γPπ) 矩阵变得奇异或接近奇异,线性方程组解不出来。

解决:换用 mdp_relative_value_iteration.m。它专门处理平均奖励准则,通过相对值函数的跨度做收敛判断。如果只是想让策略收敛,把 gamma 调成 0.99 就行,输出策略在绝大多数情况下差别很小。这个坑几乎每个做 MDP 的人都会踩一次,我现在看到 gamma=1 的第一反应就是直接跳转到相对值迭代。

5.4 策略迭代和值迭代的结果对不上

现象:同一个 P、R、gamma,mdp_policy_iteration 和 mdp_value_iteration 输出的 V 不是完全相等,个别状态的最优动作编号也不一样。

原因:多数情况是收敛条件不同。mdp_value_iteration 默认用 epsilon 做早停,值函数迭代到一定程度就停;mdp_policy_iteration 的策略改进是严格的贪心选择,只要策略稳定才算收敛。两类停止准则对动作价值差异不大的状态很敏感,几个状态的 Q 值相差 0.01 时,策略编号就可能跳变。

解决:把两者的参数对齐。mdp_value_iteration 的 epsilon 设小一个量级,比如 1e-12,max_iter 调大;再看输出的 iter 是否接近。如果策略编号仍然不同,用 Bellman 最优性方程手算验证哪个策略的目标函数值更大。这个坑不是工具包的问题,是数值精度和停止准则的差异,理解这一点就能避免无谓的怀疑。

5.5 HTML 文档打不开或者样式错乱

现象:解压后在资源管理器里双击 DOCUMENTATION.html 和 mdp_policy_iteration.html,页面要么空白,要么排版全乱。

原因:HTML 文档之间是相对链接引用的,个别浏览器对本地文件跨目录加载有限制,或者压缩包解压时把 html 文件和 image 目录拆散了。包里 logo_inra.gif、arrow.gif 这些图片是文档配图,缺失会直接导致排版错乱。

解决:下载后先完整解压,保持 html 文件和图片在同一目录层级不动,用 Chrome 或 Firefox 打开。如果还是空白,直接打开对应 .m 文件看注释,MDPtoolbox 每个函数头部都写着调用格式和参数说明,比 HTML 文档更直观。README 是纯文本,用记事本打开即可。

6. 策略收敛之后:用 Bellman 方程验证最优性,再往 Q-Learning 走一步

拿到 V 和 policy 只是第一步,我习惯再多做两步验证和延伸,确保结果不是碰运气算出来的。第一步验证是 Bellman 最优性方程回代:对每个状态 s,用输出的 V 计算所有动作的 Q 值,看 policy 给出的动作是否真的是 argmax。

% 从策略迭代结果反推 Q 表并验证 Q = zeros(S, A); for s = 1:S for a = 1:A % Q值 = 即时奖励 + 折扣 * 后继状态值函数期望 Q(s, a) = R(s, a) + gamma * squeeze(P(:, s, a))' * V; end end % 检查每个状态的最优动作是否与 policy 一致 [~, opt_actions] = max(Q, [], 2); mismatch = sum(opt_actions ~= policy); if mismatch == 0 disp('验证通过:策略满足 Bellman 最优性方程'); else disp(['有 ', num2str(mismatch), ' 个状态的动作不是最优,需要回查模型']); end

这段代码把值函数重新展开成 Q 表,核心逻辑是:如果策略迭代真的收敛到最优,那么从 V 推出来的 Q 最优动作应该跟 policy 完全一致。这个验证能同时揪出两类问题:P 或 R 构造错误导致的值函数不合理,以及收敛阈值设置不当导致的策略振荡。

第二步延伸是从 V 出发理解 Q 表的结构,然后往无模型算法走。MDPtoolbox 的策略迭代是 model-based 方法,它直接利用已知的 P 和 R 求解;到 Q-Learning、DQN 这一步,环境模型未知,只能靠采样估计 Q 值。但两者共用同一个 Bellman 方程作为理论底座,所以你在 MDPtoolbox 里调试好的值函数和策略,完全可以作为 Q-Learning 的初始化参照,或者作为验证智能体学习效果的上限基线。

我的个人习惯是:拿到任何 MDP 相关的 MATLAB 工程,第一件事永远是先过 mdp_check,再跑一遍策略迭代,然后用上面的 Q 表验证收尾。这套流程帮我挡掉过至少三次矩阵维度错误和一次浮点归一化问题,从那以后每次跑强化学习实验,我都强制自己走完这三步,再往上叠加值迭代或者 Q-Learning。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进