news 2026/10/20 4:01:43

基于Q-learning与ε-greedy的随机迷宫生成与Matlab实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Q-learning与ε-greedy的随机迷宫生成与Matlab实现

最近几年但凡做强化学习入门,不管是课程设计还是自己练手,十个人里有八个都会碰一次"用Q-learning走迷宫"。这个题目我也带人做过好几轮,说实话,它确实是强化学习里最容易讲清楚、最容易出效果、也最容易埋坑的一个Demo。标题里"随机生成的方形迷宫""ε-greedy策略""Matlab实现"这三个关键词放在一起,基本就锁定了这是一个从零搭建、训完还要能跑出路径的完整小项目。

这篇东西我按自己实际做过的流程来写,不绕弯子。先讲清楚为什么这套组合能解决迷宫问题,再把随机迷宫的生成、状态建模、Q表更新、参数调节全部拆开,最后把我在Matlab里踩过的坑和排查思路列成速查表。无论你是刚接触强化学习,还是已经跑通了一点基础代码但路径总是不理想,照着这个思路改参数、改奖励、改迷宫生成方式,基本都能得到一个能看的结果。

1. 为什么偏偏是Q-learning和ε-greedy

1.1 迷宫问题的本质是序列决策问题

方形迷宫不管怎么随机生成,最后都可以抽象成一张网格图。Agent在格子上移动,每一步只能往上下左右走,遇到墙就不能走。目标是从起点走到终点。这件事的本质就是:在未知或部分未知的环境里,做出一个连续的动作序列,让累积奖励最大。

用强化学习的语言翻译一遍:状态就是当前所在的格子,动作就是上下左右四个方向,奖励规则由你自定义,策略就是把每个状态映射到动作的规则。这样一个网格迷宫天然就是离散状态、离散动作、有限步数的序列决策问题。而这恰好是Q-learning最舒服的适用场景,因为它本来就基于表格存储Q值,状态和动作都少时,一张Q表就能覆盖所有情况。

1.2 Q-learning是tabular方法里最稳的选择

迷宫场景下你可以选很多算法,比如Sarsa、DQN、策略梯度,但大多数时候我会先给Q-learning。原因很简单:状态数可控,一个N×N的迷宫也就N²个状态,每个状态4个动作,Q表大小是N²×4。这个规模在Matlab里直接用矩阵存就行,不需要神经网络,不需要GPU,不需要复杂的回放缓冲区。训练速度快,代码逻辑直白,出了问题也容易排查。

而且Q-learning是异策略算法,它的更新用的是目标策略下的最大Q值。也就是说,它在探索过程中学到的东西,理论上可以收敛到最优策略,不受当前探索动作的干扰。迷宫这种确定性环境里,Q-learning的收敛性表现很好,实测下来比Sarsa更激进一点,路径往往能更快逼近最短路线。

1.3 ε-greedy解决的是探索与利用的平衡

如果一直选Q值最大的动作,Agent会很快陷入局部最优,比如在某个死胡同附近反复走同一条路,永远不会发现另一条更短的通道。如果一直随机选动作,Agent永远在乱逛,Q表得不到有效更新。

ε-greedy的思路很朴素:以ε的概率随机选择动作,以1-ε的概率选择当前Q值最大的动作。训练前期ε设得高一点,让Agent多探索;训练后期ε降下来,让Agent更多利用已经学到的经验。配合一个衰减系数,就能实现从"乱试"到"稳走"的自然过渡。标题里特意点出ε-greedy,说明这个项目不是单纯跑通Q-learning,而是希望你把探索策略也当做一个可调的核心变量来对待,这点我在后面参数部分会重点展开。

2. 随机方形迷宫的生成与建模细节

2.1 先说清楚"随机生成"的坑

很多初学者一上来就生成一个随机0/1矩阵,0是通路、1是墙,结果训练了半天发现Agent永远到不了终点。原因不是Q-learning没学好,而是迷宫本身就不连通,起点和终点之间压根没有通路。

所以随机生成迷宫的核心不是"随机",而是"随机但有解"。我常用两种做法。第一种是随机铺墙再用广度优先搜索检查连通性,不连通就撤销这面墙。第二种是用DFS回溯直接生成一个完美迷宫,天然保证任意两点之间都有唯一通路。前者实现简单,适合快速实验;后者生成的迷宫更有"迷宫感",通道曲折、死胡同多,也更考验强化学习算法。

两种方法我都用过,给个对比:

生成方式实现难度迷宫特点适合场景
随机铺墙+BFS校验低通道比较松散,死胡同较少快速验证算法,调参数阶段
DFS回溯完美迷宫中通道曲折,死胡同多,路径唯一展示效果,测试算法收敛能力

2.2 随机铺墙加连通性校验的实现

用Matlab写第一种方法比较快。先生成全0矩阵,起点和终点固定为通路,然后随机选一些格子放墙,每放一堵就BFS检查一次起点能否到达终点,如果不能就撤销。这个思路本质上是"构造后验证",简单粗暴但可靠。

function maze = genMazeWithCheck(n, wallRatio) maze = zeros(n, n); % 随机铺墙 total = n * n; numWalls = floor(total * wallRatio); idx = randperm(total, numWalls); for k = 1:numWalls [r, c] = ind2sub([n, n], idx(k)); if (r == 1 && c == 1) || (r == n && c == n) continue; end maze(r, c) = 1; if ~isConnected(maze, n) maze(r, c) = 0; % 破坏连通性就撤销 end end end function ok = isConnected(maze, n) visited = false(n, n); queue = [1, 1]; visited(1, 1) = true; head = 1; while head <= size(queue, 1) r = queue(head, 1); c = queue(head, 2); head = head + 1; dirs = [-1 0; 1 0; 0 -1; 0 1]; for k = 1:4 nr = r + dirs(k, 1); nc = c + dirs(k, 2); if nr >= 1 && nr <= n && nc >= 1 && nc <= n ... && ~visited(nr, nc) && maze(nr, nc) == 0 visited(nr, nc) = true; queue(end + 1, :) = [nr, nc]; end end end ok = visited(n, n); end

wallRatio我一般取0.2到0.35,太大容易生成过程反复撤销,效率变低;太小迷宫几乎全是通道,看起来不怎么像迷宫。

2.3 DFS回溯生成完美迷宫

第二个方法更有意思。用一个奇数尺寸的矩阵表示迷宫,比如(2N+1)×(2N+1),外围全是墙,内部格子之间用墙隔开。从起点格子开始,随机挑一个未访问的相邻格子,打通它们之间的墙,然后递归继续。这样生成的迷宫没有环,任意两个格子之间只有一条通路。

Matlab里用迭代栈写DFS会比递归稳妥,避免递归层数太深导致栈溢出。核心体会是:不要试图在原始的N×N网格上直接打通墙,因为那样很难处理"两个格子之间隔着墙"的表达。改成奇数尺寸矩阵,让每个通路格子周围都自然围着一圈墙,打通时只需要把中间的墙置0,逻辑非常干净。

function maze = genPerfectMaze(N) % 尺寸为 2N+1 maze = ones(2*N + 1, 2*N + 1); % 内部格子全部置为通路 maze(2:2:end-1, 2:2:end-1) = 0; visited = false(N, N); stack = [1, 1]; visited(1, 1) = true; dirs = [-1 0; 1 0; 0 -1; 0 1]; while ~isempty(stack) r = stack(end, 1); c = stack(end, 2); candidates = []; for k = 1:4 nr = r + dirs(k, 1); nc = c + dirs(k, 2); if nr >= 1 && nr <= N && nc >= 1 && nc <= N && ~visited(nr, nc) candidates(end + 1, :) = [nr, nc, k]; end end if isempty(candidates) stack(end, :) = []; else pickIdx = randi(size(candidates, 1)); nr = candidates(pickIdx, 1); nc = candidates(pickIdx, 2); k = candidates(pickIdx, 3); % 打通对应墙 wallR = 2*r - 1 + dirs(k, 1); wallC = 2*c - 1 + dirs(k, 2); maze(wallR, wallC) = 0; visited(nr, nc) = true; stack(end + 1, :) = [nr, nc]; end end % 开入口和出口 maze(2, 1) = 0; maze(2*N, 2*N + 1) = 0; end

这个生成的迷宫因为是奇数尺寸,Q-learning的状态空间不是简单N×N了,而是(2N+1)²,训练规模略大,但效果很好。如果只是交作业或快速复现,第一种就够了;如果是想真正展示算法能力,第二种更出彩。

2.4 状态、动作和奖励的建模要点

状态我一般直接用线性索引,也就是把二维坐标(r, c)映射成标量state = (r-1)*n + c。这样Q表就是一个n²×4的矩阵,Matlab索引和更新都方便。动作定义成1到4,对应上、下、左、右。从状态反推坐标用ind2sub,更新后用sub2ind转回索引,不会出错。

奖励设计是决定路径质量的关键。不要只给终点正奖励,那样Agent虽然能到终点,但可能绕很远的路。我常用的奖励方案是:

  • 到达终点:+10
  • 撞墙:-1,留在原地
  • 每走一步:-0.1,用来惩罚绕路

那个每步-0.1的作用很多人忽略,但它非常关键。没有它,Agent对路径长短无感,只要最终能到达终点就满意,走500步和走50步没区别。有了它,Q-learning才会自己去找更短的路径。这个细节,基本能决定你的Agent走出来的是一条正常路线还是一条龟速瞎逛路线。

3. Q-learning核心逻辑与Matlab代码实现

3.1 Q表初始化与五个关键参数

Q表直接zeros初始化就行,不需要随机。有一点要注意:如果Q表全零,而奖励全为正,Agent会倾向于"先试所有动作再收敛";如果奖励有负值,全零初始化本身就带着乐观偏差,会鼓励探索。迷宫问题里我倾向于全零初始化,配合ε-greedy探索,效果很好。

训练前先把参数列清楚,我惯用的一组初始值:

参数含义推荐值调节直觉
alpha学习率0.1~0.3太大震荡,太小龟速
gamma折扣因子0.9~0.99越大越看重远期奖励
epsilon初始探索率0.8~1.0前期必须充分乱走
epsMin最小探索率0.01~0.1防止后期永远在乱走
decayε衰减系数0.99~0.995衰减越快,后期越保守

gamma这个值你细品一下:它决定了未来奖励折算到当前的价值。迷宫问题里最终奖励只有终点那一次+10,如果gamma太小,比如0.5,终点奖励传递不了几步就衰减没了,Agent根本学不到"往终点方向走"这件事。我一般不低于0.9,N越大越要靠近0.99,因为路径更长,奖励往回传递的步数也更多。

3.2 训练主循环与Q值更新

Q-learning的更新公式是:

Q(s,a) ← Q(s,a) + α × [r + γ × maxₐ'Q(s',a') - Q(s,a)]

理解这个公式的诀窍是看中括号里那一坨。r是当前动作拿到的即时奖励,γ×maxₐ'Q(s',a')是下一步状态里所有动作最大Q值的折现,两者加一起代表"当前动作实际能带来的长期价值"。再减去原来的Q(s,a),就是这次更新要修正的误差。乘上α,表示只修正一点点,不让单次经验把Q值彻底改写。

主循环结构很直白:每个episode从起点出发,按ε-greedy选动作,执行动作拿到下一个状态和奖励,更新Q值,直到到达终点或者超过单局步数上限。一个episode结束之后,把ε衰减一次。

n = 9; maze = genMazeWithCheck(n, 0.3); numStates = n * n; numActions = 4; Q = zeros(numStates, numActions); alpha = 0.2; gamma = 0.95; eps = 0.9; epsMin = 0.05; decay = 0.995; episodes = 3000; maxSteps = 500; startState = 1; goalState = n * n; for ep = 1:episodes state = startState; done = false; for step = 1:maxSteps if rand < eps action = randi(numActions); else [~, action] = max(Q(state, :)); end [nextState, reward, done] = stepOnce(maze, state, action, goalState, n); Q(state, action) = Q(state, action) + alpha * ... (reward + gamma * max(Q(nextState, :)) - Q(state, action)); state = nextState; if done break; end end eps = max(epsMin, eps * decay); end

3.3 step函数:边界、撞墙和奖励

stepOnce函数负责把"动作"变成"下一个状态和奖励"。最容易写错的地方就是撞墙处理。有些写法是Agent撞墙后直接结束episode,或者给很大的负奖励,这会让Agent变得极其畏缩,停在起点不敢动。我建议撞墙后状态不变、只给一个较小的负惩罚,让Agent知道"这条路不通"但不用过度恐惧。

边界处理要看清楚:Matlab里数组索引从1开始,所以r=1时不能往上走,c=1时不能往左走,r=n时不能往下,c=n时不能往右。边界、墙、终点这三个条件都要判断,顺序别乱。

function [ns, reward, done] = stepOnce(maze, state, action, goalState, n) [r, c] = ind2sub([n, n], state); dirs = [-1 0; 1 0; 0 -1; 0 1]; % 上、下、左、右 nr = r + dirs(action, 1); nc = c + dirs(action, 2); if nr < 1 || nr > n || nc < 1 || nc > n || maze(nr, nc) == 1 ns = state; reward = -1; done = false; return; end ns = sub2ind([n, n], nr, nc); if ns == goalState reward = 10; done = true; else reward = -0.1; done = false; end end

3.4 训练完怎么把路径画出来

训练结束后,从起点开始,每一步都选Q值最大的动作,直到到达终点。这里要加一个步数上限,防止Q表没收敛时陷入死循环。路径可视化可以用plot在imagesc画的迷宫图上叠加,效果比较直观。

function path = extractPath(Q, n, startState, goalState, maxSteps) path = startState; state = startState; for step = 1:maxSteps [~, action] = max(Q(state, :)); dirs = [-1 0; 1 0; 0 -1; 0 1]; [r, c] = ind2sub([n, n], state); nr = r + dirs(action, 1); nc = c + dirs(action, 2); if nr < 1 || nr > n || nc < 1 || nc > n break; end state = sub2ind([n, n], nr, nc); path(end + 1) = state; if state == goalState break; end end end % 绘图 figure; imagesc(maze); colormap(gray); axis square; hold on; [pr, pc] = ind2sub([n, n], path); plot(pc, pr, 'r-', 'LineWidth', 2);

如果希望看训练过程,而不是只看最终路径,可以在每个episode里记录该episode是否到达终点,以及到达终点所需的步数,画两条曲线。到达终点后步数逐渐下降,基本就是收敛曲线了。

4. 参数调试经验:收敛速度与路径质量的平衡

4.1 五个参数的调节顺序

参数不要一起乱调。我习惯的调节顺序是:先定奖励,再调gamma,然后调epsilon衰减,最后微调alpha。

奖励决定学习信号的质量,这个错了后面全白搭。比如你发现Agent到了终点附近还绕来绕去,说明正奖励不够突出,或者步数惩罚太弱。把终点奖励加到20,每步惩罚从-0.1改成-0.2,路径立刻会收紧。

gamma决定了远期奖励能不能传回来。迷宫越大路径越长,gamma越要往0.99靠。一个9×9迷宫用0.95没问题,换成21×21的完美迷宫,0.95就不太够了,终点奖励传到起点附近已经衰减得没多少,Agent前期完全不知道该往哪走。

epsilon衰减最容易被忽视。我见过很多同学把epsilon固定成0.1,结果Agent从头到尾都在利用,很少探索,一旦初始路径不是最优就永远学不好。正确的做法是让epsilon从接近1开始,每轮乘一个接近1的系数,比如0.995,让探索率缓慢下降。3000个episode跑完,epsilon大约从0.9降到0.9×0.995^3000≈0.019,刚好落在0.05以下的水平。

alpha我反而放在最后调,因为它对最终性能的影响相对小。0.1和0.3在迷宫这种稳定环境下都能收敛,只是收敛速度有差异。如果你看到Q值反复横跳,曲线不光滑,把alpha调小一点就行。

4.2 训练过程中怎么判断收敛

不要只盯着一开始的步数曲线看。前几百个episode里,可能Agent大部分时间都到不了终点,步数曲线全是上限值,这很正常。关键看两个指标:一是到达终点的成功率是否接近100%,二是成功时的平均步数是否稳定在一个低位不再下降。

我有一次跑一个11×11的完美迷宫,前600个episode成功率不到三成,步数曲线也乱跳。当时差点想放弃,后来把epsilon衰减调慢到0.997,多跑了2000个episode,成功率逐渐冲到95%以上,平均步数也从两百多降到五十几。强化学习训练就像熬汤,前面看着没动静,其实Q表一直在积累信息,等到了临界点,曲线就突然往下掉。要有耐心。

4.3 我在这个项目上踩过的几个坑

第一个坑:撞墙后直接把episode结束。这样Agent会疯狂撞墙然后快速结束,Q表里撞墙的教训被不断强化,结果Agent变得畏手畏脚,宁可原地不动也不往前探索。正确处理是撞墙留在原地,只给一点惩罚,继续下一步。

第二个坑:给了太大的正奖励,比如100,又把gamma设成0.99,导致Q值爆炸式增长。这不是发散,是数值范围太大导致可视化时Q表差异不明显,路径提取时容易被微小波动干扰。正奖励不需要很大,10就够了。

第三个坑:路径提取时没有步数上限。Q表没完全收敛时,贪心策略可能在一个环里反复循环,比如从A到B到C再到B,肉眼看着路径在打转。加一个maxSteps保护,提取不到终点就提示"未收敛",不要硬画。

第四个坑:随机迷宫生成后没有保存。如果你每次运行都重新生成迷宫,训练结果不可复现,调参时没法对比。建议把迷宫的随机种子固定下来,或者把maze矩阵存成.mat文件。我通常就是rng(1)固定随机种子,这样每次跑都是同一个迷宫,方便对比参数效果。

5. 常见问题排查与扩展思路

5.1 失败模式速查表

我把实际调试中遇到的典型问题整理成一张表,方便你对照排查:

现象可能原因处理办法
Agent一直撞墙不敢走撞墙惩罚过重或撞墙后结束episode撞墙留在原地,惩罚设为-1
能到终点但路径绕远步数惩罚太弱把每步-0.1改成-0.2或-0.5
必须跑很多episode才开始收敛epsilon衰减太快decay从0.99改成0.995
最终路径一直在打转Q表未收敛或路径提取没上限增加episode,提取时加maxSteps
Q值曲线剧烈震荡alpha过大从0.3降到0.1
迷宫生成后无解随机铺墙没做连通性校验用BFS校验或换DFS生成
训练稳定但可视化闪烁每episode都重绘只绘最终路径或每隔100轮绘一次

5.2 从方形迷宫到更复杂的强化学习场景

这个项目做完之后,往上走的路其实很清晰。一个是换算法,把Q-learning换成Sarsa,对比一下两者在探索策略不同时的表现差异;再往上就是DQN,把Q表替换成神经网络,状态输入加一点迷宫的局部感知信息,就能处理更大的地图。

另一个方向是改环境。把方形迷宫换成带移动障碍的动态迷宫,或者把离散格子改成连续坐标,奖励函数设计会完全不一样。我在实际工程里接触过的路径规划问题,很多本质上都是迷宫问题的变形:机器人避障、仓储拣货路径、游戏寻路。Q-learning在这些场景里当然不是最终答案,但它的建模思路是你理解更复杂算法的一块很好的跳板。

我个人在做完这个项目后的体会是:强化学习入门的瓶颈不在算法公式有多难,而在你能不能把一个具体问题干净地建模成状态、动作、奖励三个要素。迷宫这个题目刚好能逼着你把这三件事想明白。你也别急着把代码里加一堆花哨的东西,先把Q表更新、epsilon衰减、奖励设计这三件事跑透,后面再扩展就顺很多。

如果你也正在折腾这个题目,我最后再提醒一句:多花点时间在奖励设计和迷宫生成上,这两个地方是最不显眼但影响最大的。Q-learning代码本身半小时就能写完,但一个看起来很简单的撞墙惩罚,就可能让你的Agent从"傻站在原地"变成"果断绕路"。调参的过程虽然磨人,但每一步都能看得见反馈,这大概就是这个项目最迷人的地方了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:01:15

Trae Skill实战:从零构建可复用AI编程工作流

最近一年我把 AI 编程工具从“偶尔问个问题”用到了“默认工作台”&#xff0c;试过各种提效方法&#xff0c;最后发现一个很有意思的现象&#xff1a;很多人天天把 Trae 挂在嘴边&#xff0c;却只用它的聊天窗写代码&#xff0c;从来没碰过自带的 Skill 功能。而 Skill 恰恰是…

作者头像 李华
网站建设 2026/10/20 4:01:06

EndNote中文文献双语引用模板:GB/T 7714混排与字段配置指南

简介&#xff1a;这份资源面向使用 EndNote 管理中文文献、需要中英双语引用输出的科研人员与研究生&#xff0c;重点解决中文期刊、中文书籍、中文学位论文在引用时“等”与“et al”、中英混排及双语对照难以统一的问题。网上多数教程只覆盖中文期刊一种类型&#xff0c;本包…

作者头像 李华
网站建设 2026/10/20 4:00:51

OpenCV图像处理从入门到实战:核心操作、滤波与边缘检测全解析

1. 环境准备与图像读写1.1 图像在OpenCV里到底是什么很多人一开始学OpenCV&#xff0c;上来就去背各种API函数&#xff0c;结果碰到实际问题还是不会处理。我个人的经验是&#xff0c;第一步必须先搞明白图像在OpenCV里的数据结构——它本质上就是一个多维数组&#xff0c;或者…

作者头像 李华
网站建设 2026/10/20 4:00:08

从「养虾」到Agent生产力:用TaoToken统一Key打通Harness工程落地链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 3:59:44

基于潜在时间图的递归时空图神经网络:多元时间序列预测新思路

做多元时间序列预测做到第三个年头&#xff0c;我的一个体会越来越强烈&#xff1a;预测模型的架构早就不是稀缺品&#xff0c;真正的难点在于&#xff0c;你怎么把变量和变量之间的“关系”喂给模型。交通流量、电力负荷、工业设备读数&#xff0c;这些序列单拎出来&#xff0…

作者头像 李华
网站建设 2026/10/20 3:59:29

Linux LCD驱动实战:用framebuffer正确显示BMP图像的全流程

这几天一直在调一块 Linux 驱动下的 LCD 屏幕&#xff0c;目标很简单&#xff1a;把 BMP 图像直接刷到 framebuffer 上显示。本来以为只是“读文件、写屏幕”两行代码的事&#xff0c;结果从颜色通道对不上&#xff0c;到内存映射偏移错位&#xff0c;再到肉眼可见的撕裂&#…

作者头像 李华