☰
基于Q-learning与ε-greedy的随机迷宫求解:Matlab实现与调参实战
2026/10/17 3:46:46 网站建设 项目流程

前阵子做仿真练习时,我琢磨着用Q-learning算法搭配ε-greedy策略,让一个智能体在随机生成的方形迷宫里自己学会从起点走到终点。本以为这是个“老掉牙”的入门案例,真正动手之后才发现,光是把迷宫生成、奖励设计、参数收敛这三件事理顺,就够我折腾好几天。这篇内容把我从零到一实现“基于Q-learning算法和ε-greedy策略解决随机生成的方形迷宫问题”的完整过程、Matlab代码、实验数据和踩坑记录都整理了出来,适合刚接触强化学习、想在一个具体例子上把算法跑通一遍的同学。

1. 问题定义:随机方形迷宫里到底要学什么

1.1 迷宫环境的状态、动作、奖励三要素

对于Q-learning来说,不管迷宫长什么样,第一步永远是把它抽象成“状态、动作、奖励”三个要素。

状态就是智能体当前所在的格子坐标,我用 (N \times N) 的二维矩阵表示整个迷宫,1代表墙,0代表可通行。起点固定在左上角(1,1),终点固定在右下角(N,N)。每个回合,智能体从这个起点出发,按照某个动作规则一步步移动,直到抵达终点或者耗尽了本回合的最大步数。

动作集合简单,上、下、左、右四个方向。在Matlab里,我维护一张 (N \times N \times 4) 的三维Q表,四个通道分别对应四个动作,Q(s,a)就是“我在这个格子,选择这个动作,未来能拿到的期望回报”。这就是Q-learning的记忆载体,训练过程中不断调整这张表里的数字,直到它能在每个格子告诉我们哪个动作更值钱。

奖励设计是这部分最容易出问题的地方。我一开始的想法特别单纯:到达终点给+100,其他情况一律0。结果跑出来的行为相当糟糕——智能体经常在迷宫里无意义地绕圈,明明已经快到终点了又绕回远路去。原因也简单:每一步都没有任何代价,路径长短无所谓,只要最后能撞到那一下+100就算成功。后来我调整成了“每走一步扣0.1,撞墙扣1,到达终点+100”,才逼着它学出短路径。

1.2 为什么我的第一版方案失败了

第一次实现时我犯了一个很隐蔽的错误:智能体撞墙时,我让它原地不动,但奖励依然给0。当时觉得“反正位置没变,也不算大事”,结果训练曲线越画越奇怪,Q表在墙边上是一片“暧昧”的值。

问题在于Q-learning的本质是对动作价值做估计。如果一个动作执行后没有任何正负反馈,智能体就不会认为它有多糟。表现为Q(当前格子, 撞墙方向)会慢慢趋近于0,而走到空地的动作因为带了-0.1的步数惩罚,Q值反而可能更低。这就彻底颠倒了优先级,智能体会觉得“撞墙也不是不能接受”,于是反复在墙边磨蹭。

另一个失败点是没设最大步数。一开始我天真地让每个回合一直跑到终点为止,想着“总归会跑到的”。但实际上在30×30这种稍大的迷宫里,纯随机探索很可能一路走到“死胡同区域”并在里面打转,如果不在步数上设置上限,一个回合可能跑出几万步,大部分时间都在无意义地浪费算力。后来我统一把最大步数设成 (N \times N \times 4),每个回合如果没到终点就强制结束,重新开始下一回合并逐步积累经验,训练才稳定下来。

2. Q-learning更新公式和ε-greedy策略,到底在做什么

2.1 状态-动作值函数更新公式的拆解

Q-learning最核心的就是下面这个更新公式:

[ Q(s,a) \leftarrow Q(s,a) + \alpha \left[ r + \gamma \cdot \max_{a'} Q(s',a') - Q(s,a) \right] ]

我第一次看这个公式时感觉很抽象,后来在Matlab里一行一行写出来,才发现它其实是特别朴素的“用新信息修正旧估计”。

  • (\alpha) 是学习率,决定一次更新吸收多少新信息。设为0.1,代表每次只往新信息方向挪一小步;设为0.9,基本就是“我用新值覆盖旧值”了。
  • (\gamma) 是折扣因子,代表智能体有多重视未来收益。(\gamma) 越接近1,它越愿意为了将来的大奖励先绕远路;(\gamma) 太小,它就会变得短视,只看眼前几步。
  • (\max_{a'} Q(s',a')) 是下一步所有动作中Q值最大的那个,它回答了一个关键问题:执行完当前动作后,下一个状态本身值多少钱。

这段公式真正的威力在于奖励的“倒向传播”。+100的终点奖励是在最后一步才发生的,如果更新公式里没有max那项,奖励最多只能影响终点前一步的格子。有了它,每个回合结束后,奖励会像墨水滴进水里一样,沿着走过的路径一点点往回扩散,前面的格子也能慢慢知道自己通往一个“有价值”的方向。

2.2 ε-greedy的探索-利用平衡

如果每一步都选Q值最大的动作,这是纯贪心利用,智能体只会沿着已知路径走,永远发现不了可能有更好的路。但如果每一步都完全随机,那它又像无头苍蝇,无法把经验固化到Q表里。ε-greedy的策略就是:以 (1-\epsilon) 的概率选Q值最大的动作,以 (\epsilon) 的概率随机选一个动作。

训练初期我把ε设为1.0,让智能体完全乱逛,先把迷宫的环境摸清楚。每跑完一个回合,ε就按0.995的比例衰减,最低保底0.01。这样到训练后期,它几乎总是在沿着Q值最高的路径走,只有极少时候抽风去试探一下别的地方。

这个衰减节奏非常关键。我试过把衰减系数从0.995改成0.98,看起来差别很小,但跑800个回合之后,前者的ε还在0.018左右,后者已经衰减到接近0。也就是说,后者在中后期几乎完全没有探索能力了,一旦早期找到的不是最优路径,后面根本没有机会纠正。

2.3 奖励函数设计的三种常见思路

奖励函数基本决定了智能体最后学会的行为模式。我在这个项目里试过三种配置,对比效果挺明显。

第一种是稀疏奖励:终点+100,其他全0。收敛慢,路径质量差,回合步数常常逼近上限。

第二种是步数惩罚:每走一步-0.1,终点+100,撞墙-1。这是最稳妥的默认方案,也是我最终采用的配置。

第三种是距离奖励:在步数惩罚基础上,如果这一步让智能体离终点更近,再额外给一个正奖励。听上去很合理,但实践下来有个坑——距离奖励会诱导智能体“朝终点方向硬挤”,哪怕中间隔着一堵墙,它也会不断尝试那个方向,反而增加了撞墙次数。尤其在墙密度大的迷宫里,这种奖励设计会拖慢收敛速度。

我最终选了第二种,因为它在效果好和实现简单之间实现了平衡。

3. 迷宫生成和Matlab环境建模的细节

3.1 从随机撒墙到连通性保证

随机生成迷宫最直观的做法是:生成一个全0的 (N \times N) 矩阵,然后按概率 wallProbability 把某些格子设为1。但直接这样做有一个致命问题——终点可能被墙围死,生成了一个无解迷宫,那Q-learning再怎么训练也学不出稳定策略。

我的方案分两步。第一步按概率随机撒墙,第二步做连通性检查:从起点做BFS,如果终点不可达,就随机移除一些墙,直到至少存在一条通路。这块是纯环境层面的保证,但它决定了整个训练能否正常进行。

下面是迷宫生成函数的Matlab实现:

function maze = generateMaze(n, wallProb) % 生成 n×n 方形迷宫,wallProb 为墙壁概率 rng(42); % 固定随机种子,便于结果复现 maze = rand(n) < wallProb; maze(1,1) = 0; % 保证起点可行 maze(n,n) = 0; % 保证终点可行 % BFS检查可达性,若终点不可达则逐步移除墙壁 while ~isReachable(maze, [1,1], [n,n]) [rows, cols] = find(maze == 1); idx = randi(numel(rows)); maze(rows(idx), cols(idx)) = 0; end end

isReachable函数用队列实现BFS,从起点出发扩展上下左右四个方向,遇到墙不入队,直到队列为空。如果终点没有被访问到,说明当前迷宫没有通路,需要继续移除墙壁。我在实际测试中发现,墙密度在0.3左右时,这个循环通常只需要跑一两次就能找到通路;墙密度到0.5以上时,循环次数明显变多,生成迷宫本身的耗时也开始值得注意。

3.2 Q表的数据结构和动作编码

Matlab里Q表我用 zeros(N,N,4) 表示,四个通道对应四个动作。这里有个特别容易踩的坑:Matlab的数组下标是1-based的,“向上走”对应的是行坐标减1,“向下走”对应行坐标加1。动作编码我固定为:

  • 1=上,行坐标-1
  • 2=下,行坐标+1
  • 3=左,列坐标-1
  • 4=右,列坐标+1

对应的动作增量矩阵是:

actions = [-1, 0; 1, 0; 0, -1; 0, 1]; % 上 下 左 右

越界判断要格外小心。在Matlab里,如果newRow算出来是0,直接访问 maze(newRow,newCol) 会直接报错,所以必须先判断越界再判断墙。我一开始顺序写反了,程序经常在边界处崩掉,后来养成了“先判越界,再判墙体”的习惯,就再没出过这个错。

4. 核心训练代码逐段讲解

4.1 训练主循环的结构

训练主循环的逻辑很清晰:外层循环跑episodes个回合,内层循环从起点尝试走到终点或达到最大步数。每一步按ε-greedy选动作,执行动作后得到新状态和奖励,更新Q表,最后把状态切换到新状态。

我直接贴核心代码:

% 参数配置 n = 15; alpha = 0.1; gamma = 0.95; epsilon = 1.0; epsilonMin = 0.01; epsilonDecay = 0.995; maxSteps = n * n * 4; episodes = 800; maze = generateMaze(n, 0.3); start = [1, 1]; goal = [n, n]; Q = zeros(n, n, 4) + rand(n, n, 4) * 0.01; % 加一点噪声,避免初始Q全0 actions = [-1, 0; 1, 0; 0, -1; 0, 1]; stepCount = zeros(episodes, 1); totalReward = zeros(episodes, 1); for ep = 1:episodes row = start(1); col = start(2); step = 0; rewardSum = 0; while step < maxSteps && ~(row == goal(1) && col == goal(2)) step = step + 1; % ε-greedy选择动作 if rand() < epsilon act = randi(4); else [~, act] = max(Q(row, col, :)); end newRow = row + actions(act, 1); newCol = col + actions(act, 2); % 先判越界,再判墙体 if newRow < 1 || newRow > n || newCol < 1 || newCol > n || maze(newRow, newCol) == 1 reward = -1; newRow = row; newCol = col; elseif newRow == goal(1) && newCol == goal(2) reward = 100; else reward = -0.1; end % Q-learning更新公式 [~, nextBest] = max(Q(newRow, newCol, :)); Q(row, col, act) = Q(row, col, act) + alpha * (reward + gamma * Q(newRow, newCol, nextBest) - Q(row, col, act)); row = newRow; col = newCol; rewardSum = rewardSum + reward; end stepCount(ep) = step; totalReward(ep) = rewardSum; epsilon = max(epsilonMin, epsilon * epsilonDecay); end

有几个细节我重点说一下。

第一,Q表的初始化我用了zeros + rand*0.01,含义是给所有状态-动作对加一点随机扰动。由于 max 操作在值全相等的数组上会固定返回第一个索引,而动作1在迷宫环境里未必是聪明的选择,这个小扰动可以让第一回合的探索更均匀,实测对收敛速度有一定帮助。

第二,更新公式里用到的[~, nextBest] = max(Q(newRow,newCol,:)),这是为了算 (\max_{a'} Q(s',a')) 的值。这里有一个容易忽略的点:如果下一个状态也就是终点,那它的Q值本来就应该保持0,更新后也不会有什么问题。

第三,每个回合结束时epsilon同步衰减,放在回合外、内层循环外,避免一个回合中多次衰减造成混乱。

4.2 撞墙、出界和坐标变换的处理

撞墙的处理方式这里再强调一次。代码里撞墙后位置不变,奖励-1,但更新的还是“原状态、原动作”。也就是说,Q表在明确记录“在某格子选某个方向是不划算的”。

同时,所有墙和越界判断必须放在动作执行之后、Q值更新之前。顺序不能反过来,否则不仅会因为1-based索引报错,还会让Q表学到错误的状态转移关系。

还有一个小技巧:在训练循环前我会先打印一次迷宫可达性检查的结果,确认最短路径长度。如果起点到终点的BFS路径长度超过了 (N \times N \times 2),说明这个迷宫生成得过于曲折,我会重新生成一个,保证实验的可比性。毕竟Q-learning在这种极端扭曲的地形上收敛极慢,用来调参并不合适。

4.3 基于Q表的路径回溯和可视化

训练结束之后,提取路径非常简单:从起点开始,每一步都看当前状态四个动作的Q值,选最大的那个走。但要小心“贪心回溯”也可能走进循环——原因在于训练回合有限,某些格的Q值没有得到充分更新,max操作给出的路径可能绕圈。

解决方案是加一个visited矩阵,走过就标记,如果回溯到已经访问过的格子就直接中断。实际实现如下:

% 路径回溯 route = [start]; row = start(1); col = start(2); visited = zeros(n, n); step = 0; while (row ~= goal(1) || col ~= goal(2)) && step < maxSteps step = step + 1; [~, act] = max(Q(row, col, :)); row = row + actions(act, 1); col = col + actions(act, 2); if visited(row, col) break; end visited(row, col) = 1; route = [route; row, col]; end

可视化我用imagesc画迷宫底图,墙上色为黑色,空地白色,然后把route路径用红色折线叠加上去。如果想看智能体训练过程中的移动轨迹,可以在训练主循环里加一个图形句柄的更新,每走一步就更新当前点的坐标,视觉效果非常直观,但要注意drawnow会拖慢训练速度,我一般只在调试时开。

5. 实验结果:训练曲线和路径质量

5.1 收敛过程怎么判断

判断Q-learning是否已经收敛,我习惯同时看三个指标。

第一个是每回合步数,理想情况下应该从“接近最大步数”一路下降到“接近最短路径长度”。如果步数曲线在训练后期还是大幅波动,多半是学习率太大或者ε衰减太慢。

第二个是每回合累计奖励曲线,整体应该呈上升趋势。由于撞墙惩罚和步数惩罚的存在,即使稳定后也会有小幅锯齿,这很正常。

第三个是回溯路径的长度,训练结束后用Q表贪心回溯出来的路径,应该明显短于早期乱撞时的路径。如果路径还是歪歪扭扭,说明训练回合数不足或者探索不够。

5.2 一组典型的15×15迷宫实验结果

我固定迷宫种子,跑了一组15×15、墙密度0.3的实验。前50个回合,智能体几乎都在乱逛,每回合步数动辄600以上;从100回合开始,步数迅速下降,说明Q表已经开始传递有效信息;到600回合左右,步数基本稳定在32左右。

作为对照,我用BFS算了一下这个迷宫的最短路径,结果是30步。也就是说,Q-learning最终学出来的路径和理论最优只差2步,这一步差距来自训练中的随机探索和历史路径的固化。当然,如果要追求绝对最优,BFS依然是最直接的选择。

当墙密度提高到0.5时,情况就变了。迷宫里的可达路径变少,奖励传播也会因为墙的阻断变得缓慢,Q-learning需要明显更多的回合才能稳定。我把episodes加到2000,同时把ε衰减系数改到0.998,才勉强达到类似效果。这说明对于这个算法,迷宫的地形复杂度直接影响收敛难度。

6. 调参经验和避坑指南

6.1 学习率、折扣因子、ε衰减参数怎么配

我把几组实验参数和结论整理成了表格,方便直接参考:

迷宫规模学习率α折扣γ初始εε衰减系数结论
10×100.20.951.00.98收敛快,约100回合稳定
15×150.10.951.00.995推荐,800回合稳定
30×300.050.991.00.999需1200回合以上,路径才理想

α选太大的后果是Q值来回震荡,训练曲线看起来像心电图;太小则学习太慢,大迷宫里跑几千回合也不见起色。γ在迷宫环境中影响相对小,只要大于0.9差别就不大。但如果你把奖励设计成“越靠近终点奖励越大”,γ的大小就变得很重要了,因为它决定了智能体对“距离终点还远”的状态价值的估计。

ε衰减系数是最容易被忽略的参数。0.995和0.99看着差不多,但在长回合训练下差距巨大:前者跑1000回合后ε还接近0.006,后者已经衰减到接近于零。对大迷宫来说,探索不足是比学习率不合适更隐蔽的问题,一旦中后期完全没有随机探索,智能体的路径就被锁死在某个局部最优里了。

6.2 我踩过的坑和解决办法

第一个坑是初始Q表全0。我前面说过,全0Q表会让max操作固定选第一个动作,导致初始阶段动作分布严重偏置。用Q = zeros(N,N,4) + rand(N,N,4)*0.01就能解决,效果立竿见影。

第二个坑是随机种子导致实验结果不可复现。如果不在环境生成前固定随机种子,每次跑出来的迷宫都不一样,调参时很难判断是参数变好还是迷宫变简单。我会在生成迷宫后先打印当前迷宫的基本信息,比如墙数量和BFS最短路径长度,然后固定种子保证同一迷宫在不同参数下可比。

第三个坑是奖励信号的稀疏性。在小迷宫里,终点奖励可以通过相对较少的回合扩散到整个状态空间;在大迷宫里,如果奖励扩散不到起点附近,前几十回合完全就是在瞎走,没有任何有效经验。我的做法是适当上调单回合最大步数,同时调高ε的初始值和衰减速度,让智能体前期能更大范围地探索地图。

7. 这个项目还可以怎么扩展

7.1 把随机探索换成更聪明的策略

ε-greedy简单可靠,但它探索时不考虑已有经验,所有动作被选中的概率都一样。进阶一点的替代方案是Boltzmann softmax策略:用当前Q值计算动作概率,Q值越高的动作在被随机探索时也有越大的概率被选中。公式就是温度参数τ下的softmax。

Matlab实现并不复杂,用它替换ε-greedy后,我在稀疏奖励迷宫上的收敛速度有所提升,但代价是多了一个温度参数τ要调。τ太高,行为几乎完全随机,和很大的ε差不多;τ太低,又退化成贪心策略。如果你刚接触这部分,我建议还是先用好ε-greedy,把基础思路跑通再升级。

7.2 和其他解法做对比

如果单纯为了在已知地图上求最短路径,Q-learning无论从效率还是稳定性上都无法跟BFS、A*这类算法相提并论。Q-learning真正的价值在于它不依赖全局地图,可以“边探索边学”,并且在环境发生变化时能继续训练来适应。

所以我更愿意把“Q-learning + ε-greedy解决迷宫”看作一个很适合入门的强化学习练习框架,而不是迷宫求解的最佳工具。你通过它理解了状态、动作、奖励、Q表更新、探索-利用平衡这些概念后,再去学深度强化学习,会发现很多思路是相通的。

7.3 让可视化更直观

训练过程的可视化是学习阶段特别重要的一环。除了最基础的把最终路径画出来之外,我建议可以每20个回合记录一次回溯路径,把所有路径叠加画在一张图上,颜色从蓝色渐变成红色。这样你能直观看到智能体的行为如何从乱逛变成走固定路线。

也可以把训练中每一回合的步数曲线和累计奖励曲线画成子图放在一起,实时观察收敛趋势。如果后面想更进一步,可以把Q表在栅格上的箭头方向画出来,把每个格子偏好哪个动作直接可视化。对于理解算法的行为直觉来说,这种图比任何公式都管用。

最后再分享一个我自己的体会:写这类代码时,不要只顾着盯住Q-learning公式凑代码,一定把“环境、智能体、更新公式”三个模块拆开,分别测试。环境模块错了,后面的学习全是白学。我后来把迷宫生成、撞墙判定、动作执行全部分开来调,排错效率高了很多。希望这篇内容能帮你少走一点弯路。

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

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

立即咨询