最近有朋友问我能不能用Matlab写一个Q-learning算法跑通一个随机生成的方形迷宫。说实话用强化学习解迷宫不算新题但正因为环形相对完整它反而是理解Q-learning整个机制最适合的练手项目。把这个问题从头到尾做一遍你对Q表、ε-greedy、折扣因子这些概念的理解会比死磕十篇理论文章都管用。这篇就按我实际调试的思路完整走一遍从迷宫建模到Matlab代码实现再到训练参数怎么调、踩过哪些坑尽量讲透。1. 项目整体思路与问题拆解拿Q-learning解迷宫核心思路不是让智能体记住某一条特定路径而是让它学会“在任何位置应该朝哪个方向走才是最优的”。这是一种基于价值迭代的强化学习思路。对迷宫这个问题来说状态就是智能体当前所在的格子动作就是向上、下、左、右移动一格奖励则由迷宫和任务设计给出。算法需要维护一张Q表用来记录每个状态下执行每个动作的长期收益期望再根据这张表不断更新和改善策略。为什么迷宫适合用来入门Q-learning因为它的状态空间天然离散方形网格本身就是一个清晰的马尔可夫决策过程建模对象每个格子之间的转移关系明确没有连续动作和多智能体博弈这些干扰因素。同时迷宫的可视化非常直观训练效果好不好看一眼路径就知道了这种即时反馈对理解算法参数的影响帮助很大。再加上Q-learning属于表格型方法所有价值信息都存在一张二维表里程序结构相对直白Matlab的矩阵操作和绘图能力还能天然加速这类数组运算。选择Matlab来实现并不像很多人想的那样只是因为“课设要求”。Matlab处理这种表格更新和矩阵索引非常高效代码里几乎不需要关心内存管理和迭代器细节其次是imagesc、plot这类绘图函数能快速把迷宫、Q值热力图、训练曲线呈现出来。对于研究算法效果、做参数实验来说这种“改一个参数立刻能看到行为变化”的体验是纯命令行环境给不了的。2. 核心模块与关键设计2.1 方形迷宫的建模方式迷宫用一个n×n的二维矩阵maze表示0代表可通行的空地1代表墙壁。边界通常全部置为墙壁这样智能体就不会走出地图范围。起点设置在左上角终点设置在右下角实际编码时用行列索引记录比如start [2, 2]、goal [n-1, n-1]。为了让问题具备“随机”属性迷宫内部的墙壁通过随机数生成设置一个障碍密度参数obstacleRatio通常取0.2到0.3之间比较合适。太高会导致路径过于曲折太低则迷宫太简单训练出来的策略没区分度。2.2 状态与动作空间的定义状态空间是整个迷宫的所有可通行格子的集合数量最多为n×n。在Q表里为了索引方便把二维坐标(row, col)线性映射为一维状态编号用Matlab自带的sub2ind函数即可完成转换。动作空间定义为一个有限集合我通常用1、2、3、4分别表示上、下、左、右四个移动方向。执行某个动作后先计算目标格子的坐标再检查目标格子的maze值如果越界或者是墙则判定为撞墙智能体原地不动。2.3 奖励函数的设计奖励函数直接决定智能体的行为倾向这块是新手最容易忽略的。终点奖励设为10撞墙惩罚设为-1每一步移动惩罚设为-0.05。为什么步进惩罚要设成一个很小的负数而不是0如果每走一步都是0奖励智能体到达终点后虽然能拿到10但它在迷宫中绕多少路、走多久才到终点在这个训练框架下是等价的也就是路径不唯一很容易出现绕远路甚至原地打转的情况。加一个小的负向步进惩罚等于引入了“时间成本”智能体才会暗中追求最短路径。2.4 ε-greedy策略与探索机制ε-greedy是强化学习里最简单的探索策略每次选择动作时以ε的概率随机选一个动作进行探索以1-ε的概率选择当前Q值最大的动作进行利用。训练初期需要大量探索来发现未知区域所以ε初始值要高通常设为1也就是最开始完全随机走随着训练进行智能体对环境的认识逐渐清晰需要减少随机探索更多利用已经学到的经验此时ε按照一定衰减率逐渐下降最终维持在一个较低值比如0.05或0.1保留少量探索以防漏掉更优路径。3. Matlab代码实现与训练流程3.1 随机生成迷宫并保证可解随机生成迷宫并不难难在保证生成的迷宫一定存在从起点到终点的通路。如果起点被墙壁包围或者终点孤立训练永远无法收敛而这类问题在随机生成时非常容易出现。我最初的做法是简单随机撒墙就遇到了一次“理论可达但路径极长”的迷宫训练5000轮依然没有收敛。后续在生成后加了一个广度优先搜索检查函数如果起点和终点之间没有通路就重新生成这样虽然多花一点时间但能保证训练任务本身是良定的。function maze generateMaze(n, obstacleRatio) maze zeros(n, n); maze(1,:) 1; maze(n,:) 1; maze(:,1) 1; maze(:,n) 1; for i 2:n-1 for j 2:n-1 if rand obstacleRatio maze(i,j) 1; end end end maze(2,2) 0; maze(n-1,n-1) 0; end至于连通性检查可以写一个简单的BFS函数这里只提思路从起点开始做宽度优先遍历检查能否在4邻域内到达终点如果无法到达就重新调用生成函数直到图连通为止。别小看这一步它能避免大量无效训练时间。3.2 Q表初始化与动作选择函数Q表的大小是(n*n, 4)初始值全部设为0即可。在Q-learning中初始Q值本身不影响最终收敛结果只会影响前期的探索倾向。如果不让初始Q值为0而设成一个较大的正值智能体会倾向于不探索因为“现在这个状态的所有动作看起来收益都很好”这会让训练变得特别慢。Q zeros(n*n, 4);动作选择函数是训练循环里被调用最频繁的一段逻辑我一般单独抽成一个函数function action chooseAction(stateIdx, Q, epsilon, numActions) if rand epsilon action randi(numActions); else [~, action] max(Q(stateIdx, :)); end end3.3 环境交互与状态转移智能体根据当前状态和动作得到下一个状态和奖励是训练循环的核心环节。需要注意的一点是如果执行动作后撞墙我选择让智能体保持原地也就是nextState等于当前状态同时给予撞墙惩罚。这样Q表会学到“在这个状态下执行这个动作会导致负收益”从而逐渐降低该动作的Q值达到回避撞墙的效果。function [nextStateIdx, reward, done] step(maze, stateIdx, action, n, start, goal) [row, col] ind2sub([n, n], stateIdx); switch action case 1 newRow row - 1; newCol col; case 2 newRow row 1; newCol col; case 3 newRow row; newCol col - 1; case 4 newRow row; newCol col 1; end if newRow 1 || newRow n || newCol 1 || newCol n || maze(newRow, newCol) 1 nextStateIdx stateIdx; reward -1; else nextStateIdx sub2ind([n, n], newRow, newCol); if newRow goal(1) newCol goal(2) reward 10; else reward -0.05; end end done (nextStateIdx sub2ind([n, n], goal(1), goal(2))); end撞墙时返回原状态从马尔可夫决策过程的角度来看依然是一个有效的状态转移转移概率为1转移到自身并返回负奖励。从实验效果来看这种处理方式比把撞墙当成终止状态更合理因为终止会让智能体学会“原地撞墙来结束回合”完全偏离目标。3.4 Q值更新与训练主循环Q-learning的更新公式是Q(s, a) ← Q(s, a) α [ r γ · max a Q(s, a) - Q(s, a) ]其中α是学习率γ是折扣因子。简单解释一下这行公式在做什么它先用当前的即时奖励r加上下一个状态能拿到的最大未来收益乘上折扣因子得到一个“目标Q值”然后用目标值和当前Q值的差乘以学习率作为本次更新的修正量。如果当前Q值已经很好差值为0就无需更新这就是收敛的含义。alpha 0.1; gamma 0.9; epsilon 1.0; epsilonDecay 0.995; epsilonMin 0.05; numEpisodes 800; maxSteps 200; for ep 1:numEpisodes stateIdx sub2ind([n, n], 2, 2); for stepCount 1:maxSteps action chooseAction(stateIdx, Q, epsilon, 4); [nextStateIdx, reward, done] step(maze, stateIdx, action, n, start, goal); bestNextQ max(Q(nextStateIdx, :)); Q(stateIdx, action) Q(stateIdx, action) alpha * (reward gamma * bestNextQ - Q(stateIdx, action)); stateIdx nextStateIdx; if done break; end end epsilon max(epsilonMin, epsilon * epsilonDecay); end这段代码就是整个算法的骨架核心全部浓缩在这个双层循环里。外层控制回合数内层控制每一回合的最大步数。步数上限的设置很关键如果迷宫本身比较大但maxSteps设得太小智能体连慢慢摸到终点的机会都不够如果设得太大每一回合训练耗时明显增加。我的经验是对15×15的迷宫maxSteps设在200左右足够训练初期可能经常触发上限但随着Q表更新到达终点的步数会逐渐下降。3.5 提取训练结果并可视化训练结束后用学到的Q表做一次纯贪婪策略推演也就是每一步都选择Q值最大的动作从起点一路走到终点记录路径。可视化这块Matlab提供了很自然的解决方案imagesc显示迷宫和路径颜色区分墙壁与通路非常直观。此外还可以绘制每个回合的总步数和总奖励曲线观察训练过程中智能体的行为有没有逐渐改善。如果训练顺利步数曲线应该呈下降趋势奖励曲线应该呈上升趋势最终收敛到一个稳定区间。path []; stateIdx sub2ind([n, n], 2, 2); while stateIdx ~ sub2ind([n, n], n-1, n-1) [~, action] max(Q(stateIdx, :)); [nextStateIdx, ~, ~] step(maze, stateIdx, action, n, start, goal); [row, col] ind2sub([n, n], stateIdx); path [path; row, col]; stateIdx nextStateIdx; end这里有个容易踩的细节路径提取的循环条件必须保证最终能到达终点。如果Q表训练不充分贪婪推演很可能陷入循环。所以建议在循环里加一个迭代计数上限比如如果步数超过500还没有到达终点就直接中断说明训练质量不达标回头调整参数。4. 训练参数对收敛效果的影响4.1 学习率α更新步伐的大小学习率决定每次Q值修正的幅度。α太大Q值更新幅度大前期收敛看似快但后期容易在两个最优值之间来回震荡无法稳定α太小学习过程非常慢需要大量回合才能看到效果。我实测下来0.1是个不容易出错的起点。如果你追求收敛速度可以试试0.2到0.3但要同时增加训练轮数来稳定。这里还有一个常用的技巧先让α保持较大值快速学习训练中期改为较小的α来精细收敛也就是学习率衰减。虽然Q-learning理论上保证在特定条件下收敛实际工程中很少有人一开始就把α设得很小。4.2 折扣因子γ远见程度的度量γ越接近1智能体越看重未来收益γ越小智能体越短视。在迷宫场景中终点奖励只有在真正到达终点的那一刻才会出现如果γ太小比如0.5智能体可能学不会“为了远处的奖励而忍受沿途的步进惩罚”。因为每走一步的-0.05惩罚经过不断折扣积累会把远处的10稀释得所剩无几。我一般取0.9到0.95对于15×15以内的迷宫效果就足够好。如果迷宫更大建议取0.98甚至0.99否则奖励传递链太长远处的终点奖励根本“传不过来”。4.3 ε衰减率探索的节奏控制ε衰减率是最影响训练观感的参数。如果衰减太快比如每回合乘0.9那么几十回合之后ε已经接近0智能体过早进入利用阶段但此时Q表还没学完整很多区域从未探索过智能体会一直卡在一条已知但很可能不是最优的路径上这就叫“局部最优陷阱”。如果衰减太慢智能体迟迟无法稳定利用已学经验训练曲线会一直震荡行为了无章法。经验区间是每回合乘0.99到0.995这样到200回合时ε仍在0.13左右到300回合时降至0.05左右刚好匹配学习曲线的不同阶段。4.4 回合数与最大步数设置的估算训练回合数没有一个通用的万能值它取决于迷宫尺寸、障碍密度和奖励设计的综合影响。15×15的迷宫、障碍密度0.2我通常跑800到1000个回合就能得到一条稳定的短路径如果把迷宫扩大到25×25可能需要3000回合以上。判断是否收敛的最直接方式是看最近100个回合的平均步数曲线如果曲线已经平缓且没有明显的下降趋势说明训练已达上限。这时候加再多的回合也只是浪费时间。5. 常见问题与排查技巧5.1 智能体始终到达不了终点这个现象最常出现在训练初期也就是前几十个回合。因为Q表全零智能体只能随机乱走到达终点的概率本身就很小。但如果训练了几百个回合依然到不了终点问题就出在环境或参数上。优先检查三件事第一迷宫是否真的存在通路用BFS函数验证第二奖励设计是否合理终点奖励是否足够大能覆盖探索路径所需要的步进惩罚总和第三maxSteps是否太小导致智能体即使找到了路也走不完。还有一个隐蔽问题终点格子本身必须是可通行的有些随机生成代码会把终点也覆盖成墙壁这个问题我排查的时候花了不少时间。5.2 训练后期路径仍然震荡不稳定如果训练已经进行了大量回合智能体的路径仍然每回合都变化多半是ε没有衰减到位智能体还在频繁随机探索。把ε衰减率调小一点或者加大回合数让ε降到更低区间。此外学习率α过大也会导致路径震荡因为Q值每次更新幅度太大好不容易逼近的稳定值又被下一次更新推远了。5.3 Q表更新了但路径提取陷入死循环这是训练结束后的常见问题根源通常是Q表在某些状态下存在“错误的Q值优势”。比如某个非终点的状态的某个动作Q值被高估贪婪策略就偏向那个动作结果陷入A→B→A的循环。遇到这种情况可以先检查Q表对应位置的值看看目标动作的Q值是不是真的比其他动作高如果确实高说明更新还不充分增加训练回合数或者降低学习率重新训练。如果时间紧也可以用一种偷懒的办法在路径提取循环里加步数上限超限就直接把当前状态对应Q值清零强制换一条路但这种方法治标不治本适合临时调试点不适合项目最终交付。5.4 不同随机种子下训练效果差异巨大这是Q-learning在表格型方法中的典型问题同一个算法同一个参数换一个随机种子可能第一次训练200轮就收敛第二次800轮还在乱撞。原因很简单随机种子决定初始的Q值更新顺序和探索轨迹不同种子下智能体发现终点的早晚差异很大。应对方法是做多组随机种子的重复实验报告平均性能而非单次结果。就我个人习惯而言每个参数组合至少跑10个种子然后取平均步数和方差这样的实验结论才经得起推敲。5.5 训练时间过长如何加速如果你发现训练耗时难以接受可以尝试两个优化方向。第一减少不必要的图形刷新训练过程中不要每回合都重绘迷宫可以每50回合或100回合再刷新一次绘图开销在实际训练耗时里占比很高。第二用更大的训练步长配合适当的α加速早期的Q值传播。另外如果迷宫尺寸超过30×30表格型方法的性能瓶颈就会非常明显这时可以考虑把问题压缩或者换用函数逼近方法不过在迷宫这个场景下Q-learning的简洁性优势依然大于它的性能局限性。实际调试过程中的几点体会整套项目写完我最大的感受是迷宫虽然简单但它把所有强化学习基础概念逼着你亲手实现一遍。不写代码你永远不会意识到奖励函数的设计和ε衰减的节奏对结果影响有多大。如果你也在做类似的Q-learning项目我建议先从一个5×5的小迷宫开始把训练目标——比如100回合内到达终点——作为里程碑跑通之后再慢慢加大迷宫尺寸。训练过程中多保存几个不同阶段的Q表快照对比它们的差异你会发现智能体的“理解”是逐步建立起来的这个观察过程本身比最后的路径结果更有价值。