如果你搜过“Q-learning 迷宫 Matlab”这类关键词,那你大概率跟我一样,是被机器人路径规划这个方向拉进来的。前段时间我做一个移动机器人导航的模拟项目,地图是一张栅格图,机器人只能通过传感器感知周围几个格子的障碍情况。第一反应是用A*,写完了也跑通了,但很快发现一个尴尬的问题:一旦把机器人放到一张从未探索过的地图上,A*就完全没法用——它需要一个全局已知的地图才能规划,而我们实际能拿到手的往往只有机器人当前位置附近的局部环境信息。后来我把目光转向强化学习,花了两周把Q-learning从原理到Matlab实现完整过了一遍,迷宫路径规划算是彻底跑通了。这篇文章就把整个过程整理出来,包括状态和奖励的建模细节、可复现的Matlab代码、调参实验记录,以及我实际踩过的几个坑。适合刚接触强化学习、或者做路径规划想换个思路的读者参考。
1. 为什么迷宫路径规划用Q-learning而不是直接套A*
1.1 传统路径规划的前提:必须有一张全局地图
在深入代码之前,先把一个关键问题说清楚。BFS、DFS、Dijkstra、A这几种经典算法,本质上都是在“已知完整地图”的前提下做搜索。它们的工作流程是:先把地图转成图结构或栅格结构,然后从起点开始,按照某种规则去扩展节点,直到找到终点。A之所以比Dijkstra快,是因为它用启发式函数(比如曼哈顿距离、欧氏距离)引导搜索方向,让扩展节点往终点方向偏,而不是四处发散。但这个过程的隐含前提是:我需要知道地图上每一个格子是空地还是障碍。
在仿真迷宫这种全局可见的场景里,A几乎是秒杀级别的,一个百格的迷宫毫秒级就能出最短路径。但如果把场景换成真实机器人,事情就没那么简单了。机器人携带的激光雷达或视觉传感器只能感知有限范围,地图的其他部分对它来说是未知的。这时候如果直接用A,面对地图上的未知区域,启发式搜索就无从谈起,因为搜索过程要求每个节点的“可通行状态”都是已知的,未知区域既不能当障碍处理,也不能当空地处理,算法没有定义这种情况。
1.2 试错学习解决“地图未知”的思路
Q-learning的思路和A*完全不同,它不做全局搜索,而是让智能体在环境中不断试错,通过动作后的奖励反馈来学习“在某个状态下做某个动作好不好”。你不需要告诉它地图长什么样,只需要让它在每个格子尝试上下左右移动,然后根据到达新位置后的奖励信号去更新一个Q值表。这个Q值表记录了每个格子位置下执行四个方向动作的“长期价值”,训练完成后,机器人每走到一个格子,查一下哪个动作的Q值最大,就往那个方向走,路径自然就规划出来了。
打个比方:A*像是一个拿着完整地图的导游,出发前会把整个行程规划好;Q-learning更像是一个游客在游乐园里瞎逛,每个路口试一下,记住哪些方向能走近道、哪些方向会撞墙,逛的次数多了,自然就知道怎么走最快。导游的优势是第一次就能找到最优路线,游客的优势是哪怕没有地图,多逛几遍也能找到窍门。
1.3 无模型特性与适用边界的思考
Q-learning属于无模型的强化学习方法,意思是它不需要知道环境的状态转移概率。具体到迷宫问题里,我不需要事先计算“在这个格子往右走有多大概率会到另一个格子”,这个转移关系是在每次实际移动时被体验到的,相当于隐式建模。正是因为这种无模型特性,Q-learning天生适合局部可观测、动态变化的场景——环境变化了也没关系,Q表在线更新,多跑几轮就能适应新情况。
但这不意味着Q-learning要取代A*。在完全已知的静态地图上,A又快又准,Q-learning还要训练几百上千个回合才能逼近最优策略,效率差得远。我的理解是这两个方法解决的其实是不同的问题:A解决“已知地图下的最短路径”,Q-learning解决“未知或动态环境下如何通过经验学习出可行路径”。做迷宫路径规划研究时,两者都值得掌握,但如果你是奔着“让机器人能在陌生环境里自己找到路”这个目标去的,那Q-learning是更接近问题本质的切入点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迷宫问题的数学建模与奖励函数的关键设计
2.1 状态、动作、转移:把迷宫翻译成强化学习语言
任何强化学习问题都要明确四件事:状态、动作、奖励、转移。放到栅格迷宫里,定义起来很直观。
状态就是机器人当前所在的格子坐标。在Matlab里我习惯用一个线性索引来表示,即把二维坐标(r,c)映射到一维编号 sidx = sub2ind([rows, cols], r, c),这样Q表可以直接设计成一个二维矩阵,行数是所有格子的数量,列数是动作数量,取值就表示“在这个格子执行这个动作的价值”。障碍格子虽然在环境里存在,但智能体永远不会从障碍格子出发,所以Q表里那些行实际不会被用到,不需要刻意删除。
动作是机器人下一步尝试移动的方向。最基础的是四方向动作集合:上、下、左、右。如果你想模拟斜向移动,也可以扩展成八方向,但相应的动作维度和撞墙边界判断都会更复杂,入门阶段建议先做四方向。
转移在这类栅格问题里是确定性的:执行一个动作,要么移动到相邻格子,要么撞墙停在原地。这里有一个建模细节很容易被忽略:撞墙之后状态还算不算“执行动作后的新状态”?我采用的是撞墙则状态保持不变、返回负奖励的做法。这种处理方式模拟了物理世界中机器人被障碍挡住无法前移的情况,逻辑上更真实,实现上也简单——撞墙时直接把下一状态设为当前状态即可。
2.2 奖励函数设计的三种流派
奖励函数的设计是整个迷宫路径规划里最影响训练效果的部分,没有之一。常见的方案有三类:
第一种是稀疏奖励:到达终点给+1或+100,其他所有动作奖励为0。这种方案最贴合强化学习的原始定义,但在迷宫这种探索空间较大的场景里收敛极慢,因为智能体刚开始完全乱走,绝大多数回合都碰不到终点,奖励信号太稀薄,Q表很难被有效更新。我曾经在一个20×20的迷宫里试过纯稀疏奖励,训练5000个回合仍然经常原地打转。
第二种是密集负奖励:每走一步给-1,到达终点给+100。这样智能体为了最大化累计奖励,自然会倾向走更少的步数,也就是更短的路径。这是我在迷宫场景下比较推荐的方案。
第三种是势场奖励:根据智能体与终点的距离变化动态给奖励,比如离终点更近一步加0.1,离终点更远一步扣0.1。这种方案收敛非常快,但存在一个风险:势场函数本身可能引入局部极小值,导致智能体学会的路径偏离全局最优。迷宫问题里障碍分布复杂,势场奖励容易和障碍的边缘产生“陷阱”,初期调参会花不少时间。
我最终选择的奖励组合是:到达终点+100、撞墙-10、普通移动-0.1。每一步的负奖励很小,不会让智能体为了避免走太多步而放弃探索;撞墙惩罚设得相对重一些,能让智能体尽快学会绕开障碍;终点奖励足够大,能压过整条路径上的累计负奖励。以文中这个迷宫为例,从起点到终点最优路径大约是12步,总奖励约为100 - 12×0.1 = 98.8,正反馈信号非常明显。
2.3 终点状态的Q更新:一个容易被忽略的细节
在写Q表更新公式时,有一个细节新手很容易写错。标准的Q-learning更新公式是:
Q(s,a) ← Q(s,a) + α × [r + γ × max(Q(s',a')) - Q(s,a)]
这个公式里的 max(Q(s',a')) 指的是下一状态中所有动作的最大Q值,它代表“从下一状态开始继续走,还能获得的未来价值”。但要注意:如果到达的就是终点,那么游戏已经结束,不存在未来价值,这一项应该直接置为0,而不是去查终点状态的Q表。如果不特殊处理,终点Q表的初始默认值会被代入公式,污染整条路径的Q值传播。我习惯在代码里加一个逻辑判断,当下一状态是终点时,更新公式里去掉 γ × max(Q(ns,:)) 这一项,代码会清晰很多。
这个细节虽然小,但在实际训练中影响很大。错误版本的代码在几百个回合内往往也能跑出路径,但最终策略不稳定,偶尔会绕远路,排查很久才发现是终点项没有处理干净。
3. Matlab实现:从迷宫地图到Q表收敛的完整过程
3.1 环境定义与参数初始化
Matlab实现的好处是矩阵处理和可视化都极其方便,不需要额外装工具箱,纯基础函数就能跑通。我用的迷宫是一个10×10的栅格,矩阵中0表示可通行,1表示障碍,最外围一圈全是墙。
matlab复制maze = [
1 1 1 1 1 1 1 1 1 1;
1 0 0 1 0 0 0 0 0 1;
1 1 0 1 0 1 1 1 0 1;
1 0 0 0 0 1 0 0 0 1;
1 0 1 1 0 1 0 1 0 1;
1 0 0 0 0 0 0 1 0 1;
1 1 1 0 1 1 0 1 0 1;
1 0 0 0 0 0 0 0 0 1;
1 0 1 1 0 1 1 1 0 1;
1 1 1 1 1 1 1 1 1 1
];
[rows, cols] = size(maze);
startRC = [2, 2]; % 起点坐标
goalRC = [8, 8]; % 终点坐标
startIdx = sub2ind([rows, cols], startRC(1), startRC(2));
goalIdx = sub2ind([rows, cols], goalRC(1), goalRC(2));
在写循环之前,我强烈建议先把“转移表”和“奖励表”预计算出来,而不是在训练循环里每次临时判断撞墙和奖励。原因有两点:一是训练循环要跑上千个回合,每回合几十步,循环里反复做 sub2ind、边界判断、障碍判断会拖慢整体速度;二是查表方式让主循环的逻辑更清晰,把“环境规则”和“学习过程”彻底解耦。预计算的核心代码如下:
matlab复制% 动作:1上 2下 3左 4右
dr = [-1, 1, 0, 0];
dc = [0, 0, -1, 1];
nA = 4;
nStates = rows * cols;
% 预计算每个状态下执行每个动作的下一状态和即时奖励
nextState = zeros(nStates, nA);
rewardMat = zeros(nStates, nA);
for r = 1:rows
for c = 1:cols
if maze(r, c) == 1
continue;
end
sidx = sub2ind([rows, cols], r, c);
for a = 1:nA
nr = r + dr(a);
nc = c + dc(a);
if nr < 1 || nr > rows || nc < 1 || nc > cols || maze(nr, nc) == 1
nextState(sidx, a) = sidx; % 撞墙,状态不变
rewardMat(sidx, a) = -10;
else
nidx = sub2ind([rows, cols], nr, nc);
nextState(sidx, a) = nidx;
if nidx == goalIdx
rewardMat(sidx, a) = 100; % 到达终点
else
rewardMat(sidx, a) = -0.1; % 正常移动
end
end
end
end
end
两个表都是 nStates × 4 的矩阵,行对应状态编号,列对应四个动作。训练循环里只需要查 nextState(s,a) 和 rewardMat(s,a),不再需要碰迷宫矩阵本身,干净利落。
超参数我设置如下:学习率alpha=0.1,折扣因子gamma=0.9,初始探索率epsilon=0.9,最低探索率0.01,每回合衰减系数0.995,训练回合数2000,单个回合最大步数200。
3.2 训练主循环:一回合就是一次完整的试错
每次训练回合从起点出发,智能体根据策略选择动作、执行动作、获得奖励、更新Q表,直到到达终点或者步数耗尽,然后进入下一个回合。动作选择使用的是经典ε-greedy策略:以概率ε随机选动作,以概率1-ε选择当前Q值最大的动作。
matlab复制alpha = 0.1;
gamma = 0.9;
epsilon = 0.9;
epsilonMin = 0.01;
epsilonDecay = 0.995;
episodes = 2000;
maxSteps = 200;
Q = zeros(nStates, nA);
stepCount = zeros(episodes, 1); % 每回合的步数
totalRewards = zeros(episodes, 1); % 每回合的总奖励
for ep = 1:episodes
s = startIdx;
totalReward = 0;
if rand < epsilon
a = randi(nA);
else
[~, a] = max(Q(s, :));
end
ns = nextState(s, a);
rt = rewardMat(s, a);
totalReward = totalReward + rt;
stepCount(ep) = 1;
if ns == goalIdx
Q(s, a) = Q(s, a) + alpha * (rt - Q(s, a));
else
Q(s, a) = Q(s, a) + alpha * (rt + gamma * max(Q(ns, :)) - Q(s, a));
end
s = ns;
while s ~= goalIdx && stepCount(ep) < maxSteps
if rand < epsilon
a = randi(nA);
else
[~, a] = max(Q(s, :));
end
ns = nextState(s, a);
rt = rewardMat(s, a);
totalReward = totalReward + rt;
stepCount(ep) = stepCount(ep) + 1;
if ns == goalIdx
Q(s, a) = Q(s, a) + alpha * (rt - Q(s, a));
else
Q(s, a) = Q(s, a) + alpha * (rt + gamma * max(Q(ns, :)) - Q(s, a));
end
s = ns;
end
epsilon = max(epsilonMin, epsilon * epsilonDecay);
totalRewards(ep) = totalReward;
end
这里我把第一步的代码写在循环外,后续步骤走在while循环里,逻辑上稍微有点冗余,但好处是判断结构清晰。你也可以把首个状态统一进while循环,本质上没区别。注意在每一步更新完Q表后,要把下一步状态赋给当前状态,游戏中实际执行的路径就由这些s的转移串起来。
有个细节建议保留:每回合结束后记录 stepCount 和 totalRewards。训练完画一条步数随回合变化的曲线,一眼就能看出训练是否收敛——如果曲线从几百步快速下降到稳定值,说明Q表越学越“聪明”;如果始终在一个值附近震荡,说明参数设置有问题,值得往下接着调。
3.3 训练过程的动态可视化
Matlab做可视化的体验是其他语言比不了的。训练过程中我习惯每隔50个回合,用 imagesc 把迷宫画出来,然后把当前回合智能体实际走过的轨迹用白色线条标出来,这样能看到策略的演化过程。早期回合的轨迹乱七八糟,到处都是撞墙痕迹;后期回合的轨迹逐渐稳定成一条从起点到终点的通路。
最终策略的展示可以用贪心策略——完全按Q值最大的动作走,从起点跟到终点:
matlab复制figure;
imagesc(maze);
colormap(gray);
hold on;
% 用贪心策略走一遍路径
s = startIdx;
path = s;
maxIter = maxSteps;
while s ~= goalIdx && numel(path) < maxIter
[~, a] = max(Q(s, :));
s = nextState(s, a);
path(end + 1) = s;
end
[rPath, cPath] = ind2sub([rows, cols], path);
plot(cPath, rPath, 'r-o', 'LineWidth', 2);
plot(startRC(2), startRC(1), 'go', 'MarkerSize', 12, 'LineWidth', 2);
plot(goalRC(2), goalRC(1), 'bo', 'MarkerSize', 12, 'LineWidth', 2);
还有一个很直观的观察方式:把Q表可视化。每个格子的四个方向Q值可以画成小箭头,箭头长度表示动作价值大小。训练完成后,从起点看,指向终点的方向的箭头通常是最长的,整体看起来会形成一条明确的“流”,顺着箭头流就能到终点。这种图发到论文或者报告里非常加分。
3.4 收敛判据与结果评估
训练完不能直接说“成功了”,需要有可量化的指标。我通常看三个东西:回合步数曲线、回合总奖励曲线、贪心策略的成功率。
回合步数曲线是最直观的:横轴是回合编号,纵轴是该回合从起点到终点消耗的步数。训练初期因为探索乱走,步数会很高甚至达到最大步数上限;随着Q表更新,步数应该快速下降并趋于一个稳定值。稳定值对应一条固定路径的长度,如果稳定后的步数明显大于A*计算的最短路径长度,就说明Q学习收敛到了次优策略,要考虑是不是γ太低或者ε衰减太快。
回合总奖励曲线和步数曲线本质上是互补的,因为每步-0.1、撞墙-10的奖励设计使得总奖励和步数及撞墙次数强相关。成功率则是连续取训练完后Q表的贪心策略跑50次,检查每次是否都能让机器人从起点走到终点。正常情况下,只要训练收敛,这50次应该是100%成功的。如果成功率不稳定,最可能的原因是Q表震荡得厉害,学习率α高或者ε没有完全衰减到很低。
我训练这个10×10迷宫的经验是:大约500~800回合后步数曲线就稳定了,最终的路径长度落在12格左右,训练时间在普通笔记本上不超过1分钟。如果你发现训练几千回合仍然波动很大,别急着怀疑代码,先检查奖励设计和参数设置,这是后面要重点说的内容。
4. 实测调参:Alpha、Gamma和Epsilon的联动效应
4.1 学习率的收敛速度与震荡平衡
学习率α决定了每次Q值更新时,新信息覆盖旧信息的程度。α太大,Q值跟着最新的一个奖励剧烈波动,容易出现震荡甚至发散;α太小,每一次试错产生的影响微乎其微,收敛非常慢。
我在这个10×10迷宫上做了几组对比实验,都在2000回合、其余参数相同的条件下运行:
| 学习率α | 收敛所需回合(约) | 收敛后稳定性 | 最终路径质量 |
|---|---|---|---|
| 0.05 | 900 | 很稳定 | 最优路径 |
| 0.1 | 550 | 稳定 | 最优路径 |
| 0.5 | 300 | 后期有小震荡 | 多数情况最优 |
| 0.9 | 200 | 明显震荡 | 偶尔绕路 |
从表格看,α=0.1是入门时一个很稳妥的选择。对迷宫这种奖励信号相对清晰的环境,0.1已经能保证收敛速度不错。如果后面做更复杂的连续状态问题,通常会把α再调小一点,配合经验回放等手段。注意,实际项目里我从来不会单独调α,因为它和γ、ε之间存在联动关系,调完α至少要在同样的条件下重新跑一遍,用步数曲线确认真的收敛了。
4.2 折扣因子的“远视程度”
折扣因子γ决定了智能体对长远回报的重视程度。γ=0时只考虑即时奖励,相当于只按“当前这一步收益最大”做决策,完全忽略后续影响;γ越接近1,智能体越会牺牲眼前的小利益去换取长远的高价值。
在迷宫场景里,如果γ偏低,比如0.5,智能体可能会学出一条“每步负奖励较少但总步数很长”的路径,因为它不擅长把远处终点的+100价值传回来。我试过把γ从0.9降到0.7,路径就从12格变成18格左右,而且是在反复训练之后稳定下来的次优结果。γ=0.9或0.99对10×10这种小迷宫差别不大,但如果是50×50的大迷宫,建议直接设0.99,让奖励信号能有效回溯到较远的起点附近。需要留意的是γ接近1时,如果奖励设计本身有问题(比如每步负惩罚很大),Q值可能变成很大的负数,反而让策略倾向留在原地规避惩罚。
4.3 探索率衰减曲线:大胆探索到稳健利用
ε-greedy策略的本质是在“探索新路径”和“利用已有经验”之间做平衡。初始ε=0.9意味着90%的步数在随机走,这在前几十上百回合非常必要,因为Q表还是一片空白,不随机走就完全学不到东西。但如果整个训练期都保持0.9,即使已经学到了一条好路径,智能体仍然频繁随机撞墙,步数曲线就会持续震荡。
我的做法是给ε加一个指数衰减:epsilon = max(epsilonMin, epsilon × 0.995)。初始0.9,经过约450回合会降到0.1左右,训练后期基本稳定在0.01,此时动作选择几乎完全趋向贪心。你也可以用分段衰减或线性衰减,核心思想是一样的——前期多探索,后期多用。
一个常见的坑是出发时ε已经太小,导致探索不足。判断方法很简单:如果训练过程中的步数曲线下降得很快,但最终路径是绕远路,而且回合间波动极小,那大概率是探索不够,Q表从来没发现过那条更短的走廊。解决方法就是把初始ε调高、把衰减系数调慢,或者前100个回合直接强制随机探索,保证所有动作都被充分尝试。
4.4 奖励惩罚值的微调心得
奖励设计是这整套代码里最值得反复打磨的部分。终点奖励从+50调到+500对最终策略没有本质影响,因为正负方向一致,Q值会被等比缩放,argmax不受影响。真正影响策略的是“普通移动惩罚”和“撞墙惩罚”之间的相对比例。
我再强调一下:如果每步惩罚是-1,撞墙惩罚也是-1,智能体学不出来绕开障碍的倾向,因为撞墙和移动的代价一样,它没有足够动力去回避墙;但如果每步惩罚是-0.1、撞墙是-10,智能体很快就能意识到“撞墙是非常糟糕的选择”,从而学会绕路。同理,如果每步惩罚设到-5,智能体会过度追求少走步数,反而可能卡在某个看似能缩短路程的障碍前反复尝试。我最终的组合-0.1/-10/100是多次实验后的折中结果,在速度和路径质量之间取得了平衡。
5. 踩坑实录:训练不收敛的完整排查链路
5.1 反复失败:奖励过于稀疏引发的“开环现象”
我第一次用纯稀疏奖励跑这个迷宫时,出现了非常典型的现象:前几百个回合的步数曲线一直贴着最大步数上限,说明智能体每次都没能到达终点。原因不复杂:在格子只有终点才给出+1反馈的情况下,随机移动要恰好碰到终点的概率实在太小,即使碰到了也只能更新终点附近那一小片格子的Q值,其他地方仍然一片空白。
排查这个问题的思路是:先用一条显然可行的路径来测试环境,看奖励信号能不能正确传导。我当时直接在起点附近连续手动构造几个动作,跟踪Q表的变化,然后意识到稀疏奖励导致Q值传播覆盖范围非常有限,必须给每个普通动作一个微小的负奖励,让Q值能沿着路径逐格往回传播。改完密集负奖励之后,训练曲线才算真正降下来。
5.2 “撞墙原地不动”引发的步数爆炸
最初版本里撞墙处理就是把状态设为原位置,但当时每步惩罚是-0.1,撞墙惩罚也是-0.1。结果智能体学会了一种很奇怪的行为:在死胡同尽头反复撞墙,每撞一次虽然拿不到好处,但也没吃到特别大的亏,于是随机探索阶段经常一个回合跑满200步,大部分时间在对着一堵墙原地敲。
解决方案分两步:一是放大撞墙惩罚到-10,让撞墙行为明显“不划算”;二是给每个回合设置最大步数上限,防止无意义循环把时间拖得很长。养成一个习惯:任何“状态不变”的环境动作,都必须配合足够大的负奖励或者步数终止条件,否则强化学习很容易钻这种“原地循环也不被惩罚”的空子。
5.3 收敛到了次优路径:根因在探索不足
有一段时间我的Q-learning稳定收敛了,步数曲线也很平滑,但最终给出的路径比A*算出的最短路径多了6格,属于妥妥的次优解。一开始我以为是γ太低,把γ调到0.99变化不大,后来检查ε曲线才找到真正原因:ε衰减太快。初始0.5、衰减系数0.99,这意味着很早就进入高利用率低探索状态,Q表里有一部分格子的某些方向从始至终没有被尝试过,那些格子的Q值始终是初始化值0,路径自然就固定在了首次发现的可行方案附近。
解决方法是改用乐观初始化。把Q表初值直接设为0,是很多教材的默认做法,但0并不是最好的选择。如果把Q表初值设为一个较大的正数,比如50,动作选择会更倾向于尝试尚未走过的方向——因为未探索方向的价值仍是50,高于已探索方向被更新后的值,这被称为乐观初始化的探索策略。改成乐观初始化后,路径质量明显改善,最终稳定在了最优长度。这个技巧在迷宫这类奖励信号相对简单的任务里效果特别好,且完全不需要额外调参。
5.4 训练慢的优化手段:预计算转移表与关闭绘图
如果你的迷宫做得很大,比如50×50甚至100×100,Matlab训练速度会是明显的瓶颈。最耗时的环节不是Q更新本身,而是循环内部的sub2ind、ind2sub以及边界障碍判断。我的建议是把环境相关的一切能预计算的都提前算好,训练循环里只做最朴素的查表和Q值更新,代码运行速度能有几倍到十几倍的提升。
另一个往往被忽略的优化是:动态绘图非常耗时。imagesc、plot这些绘图函数在循环里频繁调用,GPU资源倒是其次,主要是Matlab的图像渲染管线开销大。我一般把绘图频率调到每50回合或者每100回合一次,或者干脆关掉实时可视化,训练完成后再统一画曲线和最终路径。如果你在调参期间需要观察过程,可以临时把绘图间隔调小,但不要每回合都画,否则你会把大量时间浪费在等渲染上。
6. 从单一迷宫走向扩展:这个模型的边界与下一步方向
6.1 换起点不用重新训练:Q表策略的迁移能力
一个很有价值且常被忽视的特点是:Q-learning学到的不是“从起点A到终点B的一条路径”,而是整个状态空间上的动作价值分布。这意味着只要迷宫环境不变,把起点换到任意一个可通行格子上,用训练好的Q表做贪心策略,机器人就能直接从新起点导航到终点,不需要重新训练。这个特性和A很不一样——A每次换起点都要重新跑一遍搜索,而Q-learning只需要查表。
我在测试时随机换过几个起点,只要这些起点和终点在同一个连通区域内,策略都能成功导航。当然,如果新起点距离训练时经常经过的区域很偏,Q值可能不够准确,这时候只需要在新起点附近再随机跑一些回合做增量更新,Q表很快就能补上这部分知识。这种迁移能力在真实机器人重新定位、或者任务起点动态变化的场景里非常实用。
6.2 动态障碍物环境下的在线更新
如果迷宫里的障碍物位置会变化,Q-learning的优势也会显现出来。比如在训练完成后,人为改变某个格子的状态,把原来可通行的地方变成墙壁,或者相反。由于Q表会在线更新,智能体在新一轮训练中遇到障碍物后会收到撞墙惩罚,相关格子的Q值会逐渐下调,动作策略也就会跟着调整,最终绕开新出现的障碍。
这里有一个实际的工程经验:动态障碍环境里不会保留过高的ε,而是把ε维持在一个较小的常数值,比如0.05~0.1,保证Q表持续有机会采样到变化后的环境状态。直接用ε=0.01的收敛状态去应付动态障碍,一旦环境变化,智能体可能很长时间探测不到变化,因为没有随机动作去“碰”那些改变了的格子。
6.3 Q表的容量瓶颈:从Q-Learning走向深度Q网络
前面所有实现都有一个隐含依赖:Q表的大小等于状态数乘以动作数。10×10的迷宫只有100个状态、4个动作,Q表只有400个数字,遍历更新完全没有压力。但如果地图变成100×100,Q表瞬间涨到40000项,虽然还能勉强撑住,但处于边缘;如果是连续坐标、连续动作的机器人运动规划问题,Q表直接就不存在了——状态空间是无穷多个,无法枚举。
面对这个问题,主流的扩展方向是用深度神经网络替代Q表,也就是DQN(深度Q网络):输入是状态,输出是各个动作的价值预测,参数由神经网络拟合。更进一步还有Double DQN处理过估计问题、Dueling DQN将状态价值和动作优势分离等变体。我在跑通迷宫Q-learning之后尝试过用Matlab的深度学习工具箱实现一个简单DQN版本,代码结构本质上还是这套环境建模和训练循环,只是把Q(s,a)的查表操作替换成了网络前向传播和反向传播。
6.4 多机器人协同与奖励塑形的方向
另一个扩展方向是让多个机器人共享同一个Q-learning框架:每个机器人独立维护自己的Q表,或者共享一张Q表。多机器人避碰问题需要在奖励设计上做文章,比如两个机器人距离过近时给负惩罚,引导它们学习分散路线。这类问题比单机器人迷宫路径规划复杂一个量级,但基本组件仍然是状态、动作、奖励这三个要素,这也是为什么把一个简单的迷宫Q-learning吃透,比直接上手复杂框架要重要得多——底层思维完全是相通的。
最后说点实际体会。我前前后后调过的参数组合至少有几十组,最大的感受是:Q-learning迷宫路径规划的核心不在算法公式有多复杂,而在奖励函数的设计和对参数联动关系的理解。如果你照着上面的代码跑通了,但发现路径不理想,我建议的排查顺序永远是先看奖励设计,再调整ε衰减,最后才动α和γ。这个顺序能帮你省下大量瞎试参数的时间。这套代码里预计算转移表的思路,后来也被我带到了其他项目里,无论是模拟器还是实物验证,提前把环境规则算清楚这件事永远不会过时。
