☰
MATLAB实现Q-Learning迷宫路径规划:原理与实战
2026/9/28 6:09:40 网站建设 项目流程

简介:面向强化学习初学者的MATLAB Q-Learning迷宫路径规划资源包,围绕Q-Learning算法基础与迷宫环境建模展开,详细涵盖状态空间定义、动作选择策略、Q表初始化及迭代更新机制。借助迷宫逃脱这一直观场景,完整演示基于价值迭代的路径规划流程,适合算法入门、课程设计及路径规划相关研究参考。资源包共包含593个文件,以481个m源码为主体,配套mat数据文件、fig图表、c/cpp扩展源码、pdf说明文档等,压缩包整体7.32MB,目录结构便于检索。目前已有315人学习下载。资源包内不仅提供可直接运行的编码案例,还包含实验配置参数与大量仿真结果图,便于读者对照源码理解智能体试错寻优过程;在此基础上可继续开展算法优化、与其他技术融合或多智能体扩展等进阶工作,是理论到实践的良好过渡。

1. 在MATLAB里用Q-Learning做迷宫规划:它到底解决什么问题

你大概率在课上或某个技术论坛里见过这样的演示:一个小方块在10×10的栅格迷宫里乱撞,一开始每一步的方向都是蒙的,跑两三百个回合之后,它居然能从起点一路绕开所有障碍走到出口。这背后就是标题里那套组合——matlab实现强化学习Q-Learning算法迷宫路径规划。说得再具体一点:用MATLAB搭一个栅格迷宫环境,用Q-Learning这张Q表去记录“在哪个格子选哪个方向值多少钱”,最终让智能体学会迷宫逃脱。

为什么这套方案在课程实验和毕设里出镜率这么高?因为它不依赖神经网络,不需要装深度学习框架,一个稀疏矩阵就能装下智能体的全部“经验”。你甚至不需要懂深度强化学习算法的网络结构、经验回放池那些概念,只要理解状态、动作、奖励、Q值更新这四件事,就能跑出一个能看、能演示、能讲清楚原理的完整案例。适合谁?适合正在做强化学习课程作业的本科生、准备毕设演示的研究生,以及想快速验证Q-Learning收敛过程的工程师。

我会按自己做这个实验的顺序来讲:先建模,再搭环境,再写训练循环,最后讲参数和踩坑。每一步都会给出能直接复现的MATLAB代码。

2. 从MDP到Q表更新:先吃透三行公式再写代码

2.1 迷宫问题的MDP四要素:状态、动作、奖励与转移

Q-Learning处理的不是迷宫地图本身,而是迷宫背后那个叫马尔可夫决策过程(MDP)的数学模型。任何强化学习任务,第一步都是把问题翻译成MDP的四要素。迷宫逃脱刚好是最直观的例子。

状态集S是智能体所在的所有格子,一个格子就是一个状态。在这个方案里我用“线性索引”表示状态,也就是把二维坐标压成一维编号;动作集A是四个方向:上、下、左、右。奖励函数R告诉智能体每个行为是好是坏:到达终点给正奖励,撞墙给负奖励,普通移动给一个小成本。转移P指的是“在状态s执行动作a后,会落到哪个状态”,对迷宫这种确定性环境来说,转移就是查一次坐标表,没有随机性。

把这四样理清楚,Q-Learning才能开始工作。Q表的结构非常直接:行是状态数,列是动作数,Q(s,a)就表示“在这个格子里,选择这个方向往后的累计回报期望”。迷宫越小,Q表越稀疏,训练越快。

2.2 一条更新公式看懂Q-Learning:从Bellman方程到迭代实现

Q-Learning的核心是一条更新公式,所有代码最终都落在这三行计算上:

Q(s,a) ← Q(s,a) + α·[r + γ·max_{a'}Q(s',a') − Q(s,a)]

别让符号吓到。逐项拆开看:r是这次迈步拿到的即时奖励,可能是到达终点的+100,也可能是撞墙的-10。γ·max Q(s',a')是“下一步状态里,最优动作的Q值”,它代表未来收益的估计。两项相加,就是这一步“现实能拿到的总回报”的估计。括号里再减去当前Q(s,a),得到的是“我原来估值和现实之间的差距”,这叫时序差分误差,Q-Learning的本质就是不断用现实修正这个差距。

α是学习率,控制每次修正的幅度。它越大,Q表变化越猛;越小,学习越慢但更稳。路径规划场景里,我一般设0.1到0.3之间,具体到第4章再展开聊。

还有一个细节值得专门说:更新用的是max Q(s',a'),而不是“当前策略实际会执行的动作”。这意味着Q-Learning在更新时假设下一步总是选最优动作,哪怕这一步实际是随机探索走出来的。这种“用最优价值的估计来更新”的手法,就是off-policy的由来。它带来的实际效果是:智能体不用真的走出最优路径,只要在训练过程中不断摸到终点,Q值就会逐步从终点往起点传播。

2.3 为什么这里选表格Q-Learning而不是深度强化学习算法

很多人一提到强化学习就想到DQN、PPO这类深度强化学习算法,但这套迷宫方案里用表格Q-Learning是刻意为之的。迷宫的状态数是格子数,5×5迷宫25个状态,20×20迷宫400个状态,一个400×4的Q表在MATLAB里就是一个普通矩阵,显式、可打印、可直接检查。你可以随时看某个格子的Q值分布,判断算法学没学会,这是表格法的绝对优势。

换成深度强化学习算法,意味着要定义一个神经网络结构、一个经验回放池、一个目标网络,还要调网络参数和样本采样策略。在状态数只有几百个的迷宫里,这套复杂度完全不值得。调试成本会高一个量级,但收敛效果和表格法没有本质差别。所以如果你只是想验证Q-Learning原理、跑通迷宫逃脱,入门就老老实实做Q表;等你的任务变成连续状态空间,比如机械臂关节角度控制、仿真器里的端到端控制,再切到深度强化学习算法不迟。

3. 搭迷宫环境与奖励函数:矩阵地图、状态编号和动作边界

3.1 用矩阵定义迷宫:数字映射规则与合法性检查

MATLAB里最自然的迷宫表达方式就是二维矩阵。每个格子用一个数字标记含义:0表示可通行空地,1表示障碍物墙,2表示起点,3表示终点。

% 迷宫地图:0空地 1障碍 2起点 3终点 maze = [ 0 0 1 0 3; 1 0 1 0 0; 0 0 1 0 1; 0 1 0 0 0; 0 0 0 1 0 ]; start_state = find(maze == 2);

这段代码里start_state用find直接拿到起点线性索引。注意find返回的是列优先顺序的下标,也就是说第1行第1列索引是1,第2行第1列索引是2,这个顺序后续和sub2ind保持一致。

地图定义完,先做一次合法性检查再往下写,别嫌麻烦。最常见的问题有三个:一是地图里没有2或3,find会返回空矩阵,训练直接报错;二是起点或终点被墙包死,智能体永远到不了终点;三是地图里出现多个终点,导致奖励哨兵检测逻辑出错。我的习惯是用一行断言来兜底:

assert(any(maze(:) == 2), '迷宫里必须有且仅有一个起点'); assert(any(maze(:) == 3), '迷宫里必须有且仅有一个终点');

3.2 状态编号与动作集合:线性索引和动作方向表

Q表要用矩阵存取,所以状态必须转成从1到nS的连续整数。MATLAB自带的sub2ind和ind2sub就是干这个的:sub2ind把行列坐标转成线性索引,ind2sub把线性索引还原成行列坐标。

动作也约定成数字编号,这样Q表列数固定为4:1代表向上,2代表向下,3代表向左,4代表向右。为什么不用字符或字符串?矩阵索引只认数字,用数字可以直接写Q(s, a)取Q值,代码短、速度快。方向与数字的映射注释清楚就行:

% 动作编号:1=上,2=下,3=左,4=右 nA = 4;

这套编号贯穿整个训练循环,后续get_next_state函数里的switch分支就按这个编号展开。

3.3 奖励函数设计:每步成本、撞墙惩罚和终点回报

奖励函数是这套方案里最影响收敛速度的设计,它直接决定Q值的量级和学习效率。我用过很多组设置,最稳定的一组如下:

情形奖励设计理由
普通一步-1推动智能体找最短路径,避免绕路
撞墙或出界-10对无效动作给出明显负反馈
到达终点+100终局信号,量级比步数惩罚高一个数量级

这个表的核心思想是“让智能体优先找终点,其次才是走短路径”。终点奖励+100和每步-1之间差了两个数量级,即使绕了二三十步,累计惩罚也抵不过一个终点奖励,智能体最终会学会“就算绕远一点,也要找到出口”。反过来,如果终点奖励只设+1,和步数惩罚同量级,智能体很可能学成一头懒驴:原地打转甚至直接躺平,因为绕路惩罚和终点奖励抵消了。

撞墙惩罚设在-10而不是-1,是为了让智能体尽快避开无效动作。但注意,撞墙惩罚绝对值过大也有副作用,这个坑在第5章展开。

3.4 状态转移函数get_next_state:带边界检测的一次交互

训练循环里每一次“迈步”都调用同一个函数:输入当前状态、动作、地图,输出下一状态、奖励、是否结束。我把这个交互逻辑单独抽出来写,既方便训练循环保持干净,也方便单独测试某个动作的结果。

function [s_next, reward, done] = get_next_state(s, a, map, rows, cols) % 输入:当前状态编号s,动作a,地图map,迷宫行列数 % 输出:下一状态编号,奖励,是否到达终点 [r, c] = ind2sub([rows, cols], s); switch a case 1, nr = r - 1; nc = c; % 上 case 2, nr = r + 1; nc = c; % 下 case 3, nr = r; nc = c - 1; % 左 case 4, nr = r; nc = c + 1; % 右 end % 越界或撞墙:状态不变,给惩罚 if nr < 1 || nr > rows || nc < 1 || nc > cols || map(nr, nc) == 1 s_next = s; % 原地不动 reward = -10; done = false; return; end % 正常移动,计算新状态编号 s_next = sub2ind([rows, cols], nr, nc); if map(nr, nc) == 3 reward = 100; done = true; else reward = -1; done = false; end end

这段代码有几个细节值得说。越界和撞墙的处理被合并成了一个判断,因为无论出界还是撞墙,在任务语义上等价于“这个动作无效”,状态都不需要改变。用return提前结束函数,可以避免后面继续执行导致s_next被错误覆盖。终点分支单独判断,一旦踩到终点就置done为true,训练循环拿到这个标志会立刻break,不再让智能体继续走。

关于sub2ind和ind2sub的配合,需要再啰嗦一句:ind2sub输入是矩阵尺寸,不是矩阵本身,传rows和cols进去比传size(map)更直观。如果你改成size(map),要记得它返回的是[r, c]两个值,别拿一个变量去接,这个问题排错时间很长。

4. 核心训练循环:Q-Learning迭代、epsilon衰减与收敛判据

4.1 初始化Q表和超参数:一张参数表说清楚每个值的依据

训练开始前,先把Q表清零,再把超参数一次性摆出来。我的惯用初始化如下:

rng(0); % 固定随机种子,保证实验可复现 rows = size(maze, 1); cols = size(maze, 2); nS = rows * cols; % 状态总数 nA = 4; % 动作数 Q = zeros(nS, nA); % Q表初始化 alpha = 0.1; % 学习率 gamma = 0.9; % 折扣因子 epsilon = 0.3; % 初始探索率 epsilon_min = 0.05; % 探索率下限 epsilon_decay = 0.995; % 探索率衰减系数 episodes = 1000; % 训练回合数 max_steps = 500; % 单回合最大步数

每个参数的设定依据不完全相同。alpha=0.1是Q-Learning在离散小状态空间里的稳定选择,太大容易让Q值震荡发散,太小收敛速度肉眼可见地变慢。gamma=0.9意味着智能体更看重近期回报,迷宫这种任务几步就能到终点,不必用0.99那种偏向远期回报的设置。epsilon=0.3是探索和利用的折中点,初始阶段要有足够概率随机乱走才能碰到终点,但也不能高于0.5导致训练过程太飘。epsilon_min=0.05保证后期仍有少量探索,防止Q表固化。epsilon_decay=0.995的含义是每回合探索率乘一次0.995,1000回合后会从0.3衰减到约0.05,刚好落在预设下限附近。

这个量级的训练在本地MATLAB和在线网页版都能跑完,1000回合的循环在普通笔记本上也就是几秒到几十秒的事。

4.2 训练主循环代码:epsilon-greedy决策与Q值更新

超参数就位之后,训练主体就是一个双层循环:外层跑回合,内层跑单回合中的每一步。

step_hist = zeros(episodes, 1); reward_hist = zeros(episodes, 1); for ep = 1:episodes s = start_state; % 每回合从起点出发 total_reward = 0; done = false; for t = 1:max_steps % epsilon-greedy选动作 if rand < epsilon a = randi(nA); % 探索:随机选一个方向 else [~, a] = max(Q(s, :)); % 利用:选当前Q值最大的方向 end % 执行动作,获取下一状态、奖励、是否终止 [s_next, r, done] = get_next_state(s, a, maze, rows, cols); % Q值更新:Q(s,a) = Q(s,a) + alpha*(r + gamma*max(Q(s_next,:)) - Q(s,a)) Q(s, a) = Q(s, a) + alpha * (r + gamma * max(Q(s_next, :)) - Q(s, a)); total_reward = total_reward + r; s = s_next; if done break; end end step_hist(ep) = t; % 记录本回合实际走的步数 reward_hist(ep) = total_reward; % 记录本回合累计奖励 epsilon = max(epsilon_min, epsilon * epsilon_decay); end

这套循环是Q-Learning最标准的实现。每次选动作前用rand和epsilon比较,决定走探索还是利用,这保证了训练前期有足够多的随机性去“踩”终点,后期又能收敛到最优路径。执行动作后立刻更新Q表,更新公式和2.2节完全一致。

有一个容易被忽略的细节:当s_next是终点时,max(Q(s_next,:))是0,因为终点的Q表行从未被更新过。这个0在数学上是正确的——到达终点后回合结束,后续没有未来收益。所以不必额外给终点行的Q值做特殊处理,保持0反而干净。

训练结束后,Q表里存储的就是智能体学到的全部经验。下一步可以立刻用max(Q(s,:))去取每个状态的最优动作,生成策略。

4.3 收敛判据与性能评估:从每回合步数观察学习曲线

训练循环跑完,不要急着宣布成功。第一件事是画step_hist的曲线,看每回合走的步数是否在下降并趋于稳定:

figure; plot(step_hist, 'LineWidth', 1); xlabel('回合数'); ylabel('到达终点的步数'); title('每回合步数收敛曲线');

这条曲线是最直观的收敛判据。如果训练真正有效,你会看到曲线从几百步迅速下坠到一个稳定区间,比如十几步,然后保持平稳。如果曲线始终在几百步上下大幅度抖动,说明探索率衰减太快或奖励设计有问题,回到第4.1节调参。

步数曲线只能说明“能到终点”,不能说明“走到了最优路径”。我习惯再跑一个成功率评估:训练完后,用纯贪婪策略从起点走50次,统计到达终点的比例。纯贪婪的意思是每步都选max(Q(s,:)),不再加任何随机探索。如果50次全部到终点,成功率100%,才能说这个智能体真正学会了迷宫逃脱。

5. 迷宫路径规划的常见坑:Q值发散、绕路和收敛失败的排查

5.1 Q值越训越大,累计奖励冲到上千——学习率与奖励量级的搭配问题

我见过不少第一次跑通训练循环的人,兴奋地看Q表,结果发现某些格子的Q值已经涨到好几百,甚至上千,而终点奖励只有100。这种现象叫Q值发散,原因是学习率偏大加上奖励量级差距过大时,TD误差会一回合接一回合地被放大。

排查方法很简单:训练结束后打印Q表的最大值和最小值。

fprintf('Q max = %.2f, Q min = %.2f\n', max(Q(:)), min(Q(:)));

如果max(Q(:))远超终点奖励的好几倍,优先把alpha从0.3降到0.1。Q-Learning更新是迭代逼近的过程,alpha过大会让每次修正幅度过大,导致估值在真实值附近来回震荡。另外检查是否把撞墙惩罚设成了-100这种极端值,惩罚量级过大会在相邻状态间产生巨大的TD误差,同样容易让Q值爆掉。

5.2 智能体反复原地绕圈——探索率衰减策略的问题

训练完后画路径,发现智能体在某个局部回路里不停转圈,比如在2×2的免费区域里上右下左地循环,既不撞墙也到不了终点。这种翻车一般有两个原因。

第一个原因是epsilon衰减太快,智能体过早进入纯贪婪模式,而Q表还没学到足够好的状态关联,就被迫固化在当前策略里。解决方法是把epsilon_decay调到0.998以上,让探索期拉长。

第二个原因是撞墙惩罚设置过重,比如-50。智能体学会“撞墙很疼”之后,会倾向于在安全区域里绕圈,因为瞎撞的期望损失比绕圈还大。这时的表现就是路径极度保守,宁可绕远也不碰墙。解决方法是把撞墙惩罚降回-10左右,让“绕圈”的每步成本-1在长期累计下超过偶尔撞墙的代价,智能体才有动力去尝试新方向。

5.3 训练结束后成功率很低——max_steps和终点状态的处理问题

训练曲线看着在下坠,但成功率评估只有50%到70%,跑不完的回合通常卡在“超时”上。这时候先查max_steps。如果迷宫里起点到终点的最短距离是20步,你把max_steps设为30,智能体稍微绕一点路就超时,这个回合等于白训。

我的经验是max_steps取最短路径长度的5到10倍,至少保证前期随机探索阶段有足够步数去撞终点。另一个隐蔽问题在get_next_state的终点分支:如果终点判断和撞墙判断写反了,比如把map(nr,nc)==3的分支放在了越界判断里,智能体即使踩到终点也不会置done,它会继续在终点格子上做动作,把终点奖励当成普通奖励反复拿。这种问题表现就是reward_hist里出现大量重复的+100,但步数曲线永远不会降到很短。检查方式是在训练循环里加一行临时断言:

if done && map(s_next) ~= 3 error('done被置true,但当前状态不是终点'); end

5.4 每次运行结果都不一样——随机种子与初始化问题

同一份代码,同一个人,间隔一天重新跑,训练曲线长得完全不一样。这不是算法写错了,是epsilon随机探索用的随机数序列没固定。

在训练开头加一行rng(0),就能让rand、randi等随机数生成器从同一个种子出发,每次运行产生完全一样的探索序列。这行的意义不只是数据好看,它直接影响实验可复现性:别人拿你的代码跑不出你的结果,就没法相信你的收敛曲线。加了种子之后如果结果还有波动,怀疑是不是代码里在循环体内重置了rng。训练循环内不要调用rng,只在开头固定一次。

固定种子后,如果你还想评估算法稳定性,可以跑多次训练、每次用不同种子,统计平均成功率。这是更严谨的评估方式,但那是后话,先把单次实验复现做好。

5.5 迷宫尺寸一大就收敛不动——Q表稀疏与状态泛化问题

迷宫从5×5扩到20×20,状态数从25涨到400,Q表从100个值涨到1600个值。你可能会发现训练回合数增加到5000仍然不够,路径远谈不上最优。这不是代码写错了,是表格Q-Learning的天然边界:它每个状态每个动作都独立存储,状态之间没有任何泛化。智能体在某条路径上学到的东西,无法迁移到相邻格子上,一切都要靠样本去填满Q表。

这个问题的正确解法不是硬调参,而是换算法视角。要么用DQN这类深度强化学习算法,让神经网络在同一套参数下泛化不同格子的价值;要么把迷宫地图缩小,做课程学习——先训5×5,再逐步扩到10×10。对毕设级别的迷宫逃脱演示,我强烈建议控制迷宫规模在15×15以内,表格Q-Learning在这个范围里能跑得又快又稳。迷宫规模再大,就直接换深度强化学习算法路线吧。

6. 进阶:把训练过程可视化,回放策略并验证Q表质量

6.1 用imagesc画Q值热力图:验证状态价值分布

训练完成后,把Q表压缩成每个状态的最大动作价值,reshape回栅格形状,用imagesc一画,整个迷宫的学习情况就一目了然:

Q_max = max(Q, [], 2); Q_map = reshape(Q_max, rows, cols); figure; imagesc(Q_map); colorbar; axis equal; axis tight; title('Q值热力图');

热力图上,终点附近的格子应该最亮,Q值从终点向外围逐圈衰减。如果整个迷宫只有终点一格是亮的,说明Q值还没来得及往远处状态传播,训练回合数不够;如果路径沿途的格子和障碍旁边一样亮,说明智能体对撞墙和多绕几步的区分度不够,需要回头检查奖励函数。这个可视化技巧比盯着数字矩阵看直观得多,也是排查“到底学没学会”的最快路径。

6.2 用训练好的Q表做一次贪婪回放,对比最短路径

最后一个验证步骤是策略回放,用训练好的Q表从起点开始走一遍,把路径画出来:

s = start_state; path = s; for t = 1:max_steps [~, a] = max(Q(s, :)); % 纯贪婪策略 [s, ~, done] = get_next_state(s, a, maze, rows, cols); path = [path; s]; if done break; end end

回放路径如果出现了撞墙,说明Q表还在震荡期,返回去加训练回合。如果路径顺利到终点但明显绕远,说明智能体学到的是“能逃就行”而不是“最短路径”,这时候可以把终点奖励从100再调高点,或者把每步惩罚从-1调成-2,逼它少走冤枉路。更严格的做法是用BFS跑出理论最短路径长度,和回放步数对比,一致才算真正学到最优。

我每次做完迷宫实验,都会先看一眼Q值热力图,再跑一遍贪婪回放路径。热力图有没有从终点往外扩散、回放路径是不是接近最短路,这两点都对了,才敢说这个智能体真的学会了,而不是靠随机种子蒙出来的。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询