ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MDPtoolbox策略迭代实战:MATLAB强化学习入门与避坑指南

MDPtoolbox策略迭代实战:MATLAB强化学习入门与避坑指南

简介:这份资源面向机器学习与强化学习入门者及需要动手实现MDP求解的开发者,聚焦马尔科夫决策过程中策略迭代算法的MATLAB实践。包内以MDPtoolbox工具包为核心,涵盖策略评估、策略改进、值迭代、贝尔曼算子、有限时域与线性规划求解等模块,并配有HTML文档、示例脚本与说明文件,便于理解算法原理并直接运行验证。资源共93个文件,以m脚本、html文档、dat数据、txt说明为主,另含少量图片与排版文件,压缩包约2.32MB,目录结构清晰,按算法与文档分类组织。已有312人学习下载。通过阅读代码与文档,读者可掌握策略迭代的完整流程,理解状态值函数计算与策略收敛判定,并将其迁移到离散状态动作空间的强化学习问题中,适合作为课程实验或自学练手项目。

1. 拆开 MDP.zip:一个能跑策略迭代的 MATLAB 工具箱长什么样

如果你正在做强化学习入门作业,或者要给一个离散状态、离散动作的决策问题找求解器,大概率会撞上「策略迭代」这个词。网上一搜,理论推导铺天盖地,真能直接跑起来的代码却不多。MDP.zip 就是冲着这个缺口来的——它把 MDPtoolbox 这套 MATLAB 函数库连同文档、示例一起打包,核心文件mdp_policy_iteration.m实现了策略评估与策略改进的交替迭代,配套还有值迭代、线性规划、有限时域等求解入口。拿到手的人通常有两类诉求:一类是机器学习课程里要交策略迭代的实现,另一类是想把库存、维护、路径规划这类问题建模成 MDP 后快速验证。这个包不依赖额外工具箱,纯 MATLAB 脚本加函数,解压即用,适合想跳过推导直接看数值结果的人。

2. 策略迭代在 MDPtoolbox 里怎么落地:从五元组到收敛策略

2.1 先搞清楚这个包对 MDP 的数学约定

MDPtoolbox 对问题的描述非常克制,它只认一个五元组:状态数 S、动作数 A、转移概率 P、即时奖励 R、折扣因子 gamma。这里最容易翻车的是 P 的维度约定。常见做法是把 P 组织成 S×A 的元胞数组,每个元胞P{s,a}是一个 S×S 的矩阵,第 i 行第 j 列表示「在状态 s 执行动作 a 后跳到状态 j」的概率。每一行必须满足概率和为 1,否则mdp_check会直接报错。奖励 R 同样是 S×A 的矩阵,R(s,a)表示在状态 s 采取动作 a 的即时收益,注意它不随下一状态变化,这是这个工具箱的简化假设,如果你的问题奖励依赖转移结果,需要自己先做期望折算。

策略迭代的数学骨架是两步循环。策略评估阶段,固定当前策略,反复用贝尔曼期望方程更新值函数,直到相邻两次的值函数差小于阈值;策略改进阶段,对每个状态遍历所有动作,用一步前瞻算出动作值,取最大者对应的动作作为新策略。如果新策略和旧策略完全一致,说明已经收敛到最优策略。MDPtoolbox 把这两步分别封装在mdp_eval_policy和mdp_policy_iteration内部,你调用时只需要给五元组和参数。

2.2 最小可运行示例:手写一个两状态 MDP

下面这段代码构造一个最简单的两状态、两动作 MDP,状态 1 是「正常」,状态 2 是「故障」,动作 1 是「维护」,动作 2 是「不维护」。转移概率和奖励都是人为设定,目的是让策略迭代跑出非平凡结果。

% 两状态两动作 MDP 最小示例 S = 2; A = 2; P = cell(S, A); R = zeros(S, A); % 状态1:正常 P{1,1} = [0.9 0.1; 0 0]; % 维护:大概率保持正常 P{1,2} = [0.6 0.4; 0 0]; % 不维护:更容易滑向故障 R(1,1) = -1; % 维护有成本 R(1,2) = 2; % 不维护省成本 % 状态2:故障 P{2,1} = [0.7 0.3; 0 0]; % 维护:大概率修复 P{2,2} = [0.1 0.9; 0 0]; % 不维护:大概率继续故障 R(2,1) = -3; % 修复成本高 R(2,2) = -5; % 不维护持续损失 % 补齐每行概率和为1(这里第二行全零,需按实际语义补) P{1,1}(2,:) = [0 1]; P{1,2}(2,:) = [0 1]; P{2,1}(2,:) = [0 1]; P{2,2}(2,:) = [0 1]; gamma = 0.95; mdp_check(P, R); % 先做合法性检查 [V, policy, iter] = mdp_policy_iteration(P, R, gamma); disp('最优值函数:'); disp(V); disp('最优策略:'); disp(policy); disp(['迭代轮数:', num2str(iter)]);

这段代码里mdp_check是必须走的一步,它会检查 P 的每个元胞是否为方阵、每行是否非负且和为 1、R 的维度是否匹配。mdp_policy_iteration的第三个参数 gamma 控制未来奖励的折现程度,越接近 1 越看重长期收益。返回值policy是一个 S×1 向量,每个元素是该状态下的最优动作索引。iter告诉你策略改进了多少轮才稳定,这个数字在调试时很有用——如果它异常大,通常意味着 gamma 太接近 1 或者奖励尺度差异过大。

2.3 参数怎么调:gamma、阈值和最大迭代次数

mdp_policy_iteration的完整签名允许你传入评估阈值和最大迭代次数。默认情况下,策略评估内部用mdp_eval_policy反复迭代直到值函数变化小于 1e-10 或者达到 1000 次。如果你的状态空间很大,这个内层循环会成为瓶颈。常见做法是把评估阈值放宽到 1e-6,牺牲一点精度换速度,对最终策略通常没有影响,因为策略改进只关心动作值的相对大小。

gamma 的选择直接决定策略形态。gamma 接近 0 时,智能体几乎只看眼前奖励,策略会偏向即时收益高的动作;gamma 接近 1 时,长期回报被放大,策略更愿意为未来收益付出当前成本。在维护类问题里,gamma 设 0.9 到 0.99 之间比较常见。如果你发现策略在几个动作之间反复横跳、迭代不收敛,先检查 gamma 是否等于 1——等于 1 时无限时域问题可能没有有限值函数,工具箱会报错或给出无意义结果。

提示:mdp_check报错时不要急着改代码,先把 P 的每个元胞打印出来,逐行看概率和是否为 1。浮点误差累积到 1e-15 级别通常能通过,但 0.999 这种明显偏差一定会被拦下。

3. 把 MDPtoolbox 接进自己的问题:建模、验证与批量实验

3.1 从实际问题到 P 和 R 的转换套路

拿到一个现实问题,第一步不是写代码,而是画状态转移图。以设备维护为例,状态可以按健康度离散成「良好、一般、故障」三档,动作分成「不维护、小修、大修」。对每个状态动作对,问自己两个问题:执行这个动作后,下一状态的概率分布是什么?这个动作的即时成本或收益是多少?把答案填进 P 和 R 的对应位置。如果某个转移在物理上不可能,概率填 0,但注意每行仍然要归一化。

奖励的符号约定容易搞混。MDPtoolbox 默认最大化奖励,所以成本要写成负数。如果你的问题是最小化成本,把所有 R 取负号再求解,最后把策略解读回成本语境即可。奖励的绝对尺度不影响最优策略,但影响值函数的数值范围和收敛速度。常见做法是把奖励归一化到 [-1, 1] 区间,避免值函数爆炸。

3.2 用 mdp_eval_policy 单独验证一个给定策略

策略迭代返回的是最优策略,但有时候你需要评估一个手工设定的策略,比如「永远维护」或「故障才修」。这时候直接调mdp_eval_policy:

% 评估固定策略:状态1选动作1,状态2选动作1 fixed_policy = [1; 1]; V_fixed = mdp_eval_policy(P, R, gamma, fixed_policy); disp('固定策略的值函数:'); disp(V_fixed); % 与最优策略对比 [V_opt, policy_opt] = mdp_policy_iteration(P, R, gamma); disp('最优策略值函数:'); disp(V_opt); disp(['值函数提升:', num2str(V_opt - V_fixed)]);

mdp_eval_policy的第四个参数是策略向量,长度必须等于状态数。它内部同样用迭代法解线性方程组,返回每个状态在该策略下的期望累积奖励。把固定策略的值函数和最优值函数相减,差值就是「不采用最优策略的代价」,这个数字在向业务方解释为什么要做优化时比抽象的策略向量更有说服力。

3.3 批量跑不同 gamma 和奖励配置

做实验时经常需要扫参数。下面这段脚本把 gamma 从 0.8 到 0.99 扫一遍,记录每个 gamma 下的最优策略和迭代轮数,方便观察策略相变点。

gammas = 0.80:0.01:0.99; results = zeros(length(gammas), 3); for k = 1:length(gammas) [V, pol, iter] = mdp_policy_iteration(P, R, gammas(k)); results(k, :) = [gammas(k), iter, sum(pol == 1)]; end disp('gamma 迭代轮数 选动作1的状态数'); disp(results);

这段代码的输出能帮你判断 gamma 在哪个区间内策略发生翻转。如果某个 gamma 附近迭代轮数突然飙升,说明值函数在该参数下接近奇异,数值稳定性变差,实际部署时应该避开这个区间。sum(pol == 1)统计选择动作 1 的状态个数,是一个粗糙但直观的策略激进程度指标。

注意:批量实验时每次调用mdp_policy_iteration都会重新初始化,不要试图在循环外复用中间变量,工具箱内部没有暴露热启动接口。

4. 避坑与排查:策略迭代跑不通时先看这几处

4.1 现象:mdp_check 报「transition probabilities do not sum to 1」

原因几乎总是 P 的某个元胞某一行概率和不为 1。常见来源是手工填表时漏了归一化,或者从数据估计转移概率时用了频数而不是频率。解决方法是写一个循环逐行检查并打印偏差:

for s = 1:S for a = 1:A row_sums = sum(P{s,a}, 2); bad = find(abs(row_sums - 1) > 1e-10); if ~isempty(bad) fprintf('P{%d,%d} 第 %d 行概率和为 %.6f\n', s, a, bad, row_sums(bad)); end end end

定位到具体行后,要么重新归一化,要么检查数据源是否把「转移到自身」的概率漏掉了。

4.2 现象:策略迭代不收敛,iter 一直增长到上限

先看 gamma 是否等于 1。无限时域 MDP 在 gamma=1 且存在吸收态时可能没有有限最优值,工具箱会陷入无意义的迭代。把 gamma 降到 0.99 或更低通常能解决。如果 gamma 正常但仍不收敛,检查奖励矩阵是否存在极端值,比如某个 R(s,a) 是 1e6 而其他都是个位数,这种尺度差异会让值函数迭代在数值上失稳。解决办法是把奖励整体缩放,或者对奖励做截断。

4.3 现象:返回的策略看起来「不合理」,但值函数收敛了

这通常不是代码 bug,而是建模问题。策略是值函数的贪心结果,如果奖励设计没有反映真实偏好,最优策略自然不符合直觉。排查方法是把每个状态动作对的 R 和 P 打印出来,手工算一步动作值,看是否和工具箱返回的策略一致。另一个常见原因是状态离散太粗,把本质不同的情况合并成了一个状态,导致策略在该状态下只能折中。

4.4 现象:状态数稍大就内存不足

P 是 S×A 个 S×S 矩阵,内存占用是 O(S²A)。S=1000、A=10 时,光 P 就接近 80MB(双精度),加上中间计算很容易吃满内存。常见做法是改用稀疏矩阵存储 P,或者换用mdp_value_iteration这类不需要显式构造策略评估线性系统的求解器。如果问题本身有稀疏结构,比如每个状态只转移到少数几个邻居,稀疏存储能把内存降一个数量级。

4.5 现象:从旧版本 MATLAB 迁移后函数报未定义

MDPtoolbox 的部分函数名和 MATLAB 内置函数或其它工具箱冲突,比如mdp_span在某些路径下会被遮蔽。解决方法是把工具箱目录用addpath加到搜索路径最前面,或者用which mdp_policy_iteration确认实际调用的是哪个文件。如果返回空,说明路径没加对;如果返回的是别的目录,说明有同名文件抢先。

5. 进阶技巧:用有限时域和策略修改版处理非标准问题

标准策略迭代假设无限时域、折扣奖励、平稳策略。但实际作业里经常遇到有限步数的问题,比如「未来 10 个周期内怎么决策」。这时候用mdp_finite_horizon,它按时间反向递推,返回每个时刻的最优策略。调用方式和无限时域类似,但需要额外指定时域长度:

horizon = 10; [V, policy] = mdp_finite_horizon(P, R, gamma, horizon); % policy 是 S×horizon 矩阵,第 t 列是第 t 步的最优动作

policy的每一列对应一个决策时刻,越靠后的列越接近无限时域的解。如果你发现前几步的策略和无限时域最优策略差异很大,说明时域截断效应显著,需要加长 horizon 或者改用无限时域模型。

另一个容易被忽略的文件是mdp_policy_iteration_modified.m。它实现的是修改版策略迭代,在策略评估阶段不追求完全收敛,只做有限次迭代就进入策略改进。这种「不精确策略评估」在状态空间大时能显著加速,代价是可能多几轮外层迭代。我一般会在标准版跑得太慢时切到修改版,把内层评估次数设成 5 到 10,观察最终策略是否一致。如果一致,就用修改版;如果不一致,说明内层评估精度对策略有实质影响,得老老实实跑标准版。

验证策略是否真正最优,除了看迭代是否收敛,还可以用mdp_bellman_operator手工算一步贝尔曼残差。对返回的策略,计算每个状态的「当前动作值」和「最大动作值」之差,如果所有差值都小于 1e-8,说明策略满足最优性方程。这个检查比单纯看迭代轮数更可靠,尤其是在奖励尺度差异大、收敛判据可能误判的情况下。

从那以后我每次拿到一个新的 MDP 模型,都强制先跑一遍mdp_check,再手工算两个状态的动作值对答案,确认建模无误后才批量实验。这个习惯帮我省下了大量在错误模型上调参的时间。希望帮到你。

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

RELATED READING

延伸阅读

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