简介:基于Q-Learning的路径规划MATLAB仿真系统,面向算法初学者与进阶学习者,可在任意障碍物环境中实现自主路径规划,并支持自由设定起点与目标点。系统以MATLAB GUI为载体,包含完整模型文件、界面文件与说明文档,既能用于强化学习算法入门,也可作为改进Q-Learning、开展学术研究的基础平台。压缩包共36个文件,以24个m脚本为核心,涵盖主程序、动作选择、传感器模拟、轨迹绘制与数据统计等模块;辅以fig界面文件、mat数据文件、txt说明文档以及eps/tif示意图,便于对照理解与二次开发。包体仅222KB,结构紧凑,已有266人学习下载。无论是初步接触强化学习,还是希望深入GUI编程与算法优化,这套仿真系统都能提供直观、可运行的实践参考。
1. 基于Q-Learning的路径规划MATLAB仿真系统:自测到底在测什么
把一辆AGV或者动态避障小车的模型扔进MATLAB,用Q-Learning在栅格地图上跑出路径,这件事本身不难——难的是你的仿真结果怎么让人相信。我们常说路径规划算法跑通了,但“跑通”和“自测可信”是两回事:很多人训练完画出一条路径就算交差,但换了起始点、换了地图就找不到路了,甚至训练曲线根本没收敛。Q-Learning的MATLAB仿真系统自测,核心不是证明算法“能走”,而是证明奖励函数写对了、Q表真的在学习、以及新环境下策略是否还有泛化能力。这篇笔记我会把地图建模、Q表更新、回放测试和调参踩坑一次讲透,适合正在做课设、毕设或者算法原型验证的同学直接按步骤复现。
2. 为什么用栅格地图搭Q-Learning路径规划:状态、动作与奖励的设计顺序
2.1 状态空间用线性索引而不是坐标对,训练速度快一个量级
Q-Learning路径规划在MATLAB里的常见做法,是把环境离散成栅格地图。一张二维地图用map(row, col)表示,0是可通行,1是障碍。状态空间就是所有“无障碍”的栅格索引,动作空间是上下左右四个方向。这里有个容易被忽略的效率问题:如果状态用[row, col]二元组表示,存Q表时要么用cell数组,要么自己弄索引映射,MATLAB的矩阵计算优势就发挥不出来;更常见的做法是用线性索引,一列state_idx就代表一个格子,Q表直接建成一个n_states × n_actions的矩阵,更新时一行代码搞定。
一开始我也是用坐标对存状态,直到地图从8×8换到30×30,训练时间肉眼可见地变慢。改成线性索引后,行坐标用[row, col] = ind2sub([rows, cols], state_idx)随时换算,动作选择直接用Q(state_idx, :)取一整行,训练循环里省掉所有查表开销。
grid_rows = 10; grid_cols = 10; map = zeros(grid_rows, grid_cols); map(4, 2:8) = 1; % 放一面横墙 map(7, 4:9) = 1; % 再放一面横墙 start_idx = 2; % 起点:第1行第2列 goal_idx = sub2ind([grid_rows, grid_cols], 10, 9); % 终点 all_free_idx = find(map == 0); n_actions = 4; % [上, 下, 左, 右] Q = zeros(numel(map), n_actions);这里的Q表初始化成全零矩阵,行长是numel(map)而不是numel(all_free_idx)。障碍格子的状态虽然永远不会被访问,但保留它们的行会让索引换算不用做偏移,防止动作越界时状态错位。换取查表速度,浪费一点内存完全值得。地图里放两面墙是我常用的测试地形,它能强制算法绕路——如果一条没有任何障碍的“直路地图”跑通,说明不了任何问题。
2.2 动作集选四方向还是八方向:不要一开始就上八方向
四方向动作是上下左右,八方向加了对角线。从路径视觉效果上,八方向走出来的折线更自然,尤其在无人机三维路径规划仿真里,斜着飞比横平竖直更接近实际。但是八方向在栅格地图上埋了一个大坑:对角线移动会“穿墙”。假设当前格子在墙的左下角,向右上角移动时,如果右上角和当前格子之间有墙,那么对角线动作应该被禁止。这意味着每走一步都要额外查两个正交方向的障碍情况,或者在地图设计时保证三角形空洞不存在——但后者几乎做不到。
做AGV路径规划或者动态避障小车路径规划的课设时,我建议先用四方向调通训练逻辑,再考虑扩展八方向。四方向策略成熟、边界条件少,而且Q表只有四列,训练收敛后能直接用[~, best_action] = max(Q(state_idx, :))拿策略,调试期盯一个状态的行为非常直观。后面真有需求了,再把n_actions从4改成8,奖励和状态转移逻辑不动,只把valid_actions函数改一版就行。
function valid_actions = get_valid_actions(row, col, rows, cols) valid_actions = false(1, 4); if row > 1, valid_actions(1) = true; end % 上 if row < rows, valid_actions(2) = true; end % 下 if col > 1, valid_actions(3) = true; end % 左 if col < cols, valid_actions(4) = true; end % 右 endget_valid_actions只负责边界检查,不查障碍。障碍检测放在环境交互函数里:移动到障碍格则保留原地状态并给负奖励。这样的分层好处是,训练主循环里不用一边写Q更新、一边写地图边界判断,逻辑清爽,出错也好定位。
2.3 奖励函数是路径规划仿真里唯一真正影响成败的模块
Q-Learning路径规划看起来是算法问题,实际是奖励函数问题。很多人第一次跑通,奖励只写了“到达终点+10,碰撞-1”,结果训练完路径绕了一个大圈——因为每一步的代价是0,Agent不在乎多走多少步。反过来,如果你每走一步都扣0.5,那Agent会倾向满地图乱窜寻找更快到达终点的路,在早期探索阶段特别容易崩溃。
我常用的奖励设计分三档:终点奖励+100、碰撞惩罚-10、每一步-0.1。步数惩罚必须存在,这是让路径短且直的引擎;但它的绝对值不能太大,否则会压过碰撞惩罚,让Agent觉得“撞一下多走几步也值”。如果地图特别大或者特别绕,我会把步数惩罚降到-0.05,碰撞惩罚提到-20,保证安全约束严格优先。奖励函数改完一定要重新训练——Q表是“惯出来的”,换个奖励结构,旧Q表直接作废。
在训练初期,还要考虑一个实际问题:epsilon-greedy探索时,Agent经常会在障碍边缘反复试探。碰撞惩罚太轻时,它会在墙边蹭很多步才离开,训练曲线看着一直在涨,但实际没学到有效信息。把碰撞惩罚调重,再配合合理的epsilon衰减,探索阶段才会自然收敛。
2.4 学习率、折扣因子和探索率的初始值怎么定
Q-Learning有四个关键参数:学习率alpha、折扣因子gamma、探索率epsilon、以及epsilon的衰减方式。路径规划场景我的经验值:alpha = 0.1、gamma = 0.9、epsilon = 0.9,随后线性衰减到0.05。alpha不建议大于0.3,否则Q值震荡很大;gamma超过0.95时,在临近终点的区域会出现“进退两难”问题——Agent在终点附近反复兜圈,因为错误的下一步带来的收益折现后仍然很大,这个坑后面专门展开。
epsilon的衰减是新手最容易忽略的。固定epsilon = 0.2训练,意味着20%的步数纯随机走,路径必然不稳定;而衰减到0.05以后,基本靠贪心策略做微调。我一般把训练次数设在500个episode,epsilon从0.9线性衰减到0.1,衰减步长等于训练轮数。这样前期充分探索、后期稳定收敛。如果地图复杂,建议把训练轮数提高到2000,衰减步长同步拉长,否则探索不充分。
训练循环里我只在rand() < epsilon时随机选动作,否则按max(Q(state_idx, :))取最优动作。每一步都用标准Q表更新公式:
Q(s, a) = Q(s, a) + alpha * (reward + gamma * max(Q(s_next, :)) - Q(s, a));这里max(Q(s_next, :))是下一状态的最大Q值,哪怕下一步移动撞墙留在原地,都要照常计算——撞墙后Agent还在原状态,Q值更新必须继续,否则状态价值不更新。
3. 用MATLAB实现Q-Learning训练主循环:从零到收敛的最小可复现代码
3.1 训练脚本的结构与每行代码的含义
训练主循环一般写成函数,输入是地图、起终点和超参数,输出是Q表、每轮累计奖励、最终路径。脚本拆成三层:最外层是episode循环,中间是单次路径探索,内层是“走一步 → 更新Q表”。每次episode从起点出发,走到终点或者超过最大步数max_steps就结束。max_steps我通常设成rows*cols*3,过小会让Agent在复杂地图里“学不起”,过大会让无意义探索拖慢训练。
function [Q, reward_curve] = train_q_learning(map, start_idx, goal_idx, opts) rows = size(map, 1); cols = size(map, 2); Q = zeros(numel(map), 4); alpha = 0.1; gamma = 0.9; epsilon_start = 0.9; epsilon_end = 0.05; n_episodes = opts.n_episodes; max_steps = rows * cols * 3 | opts.max_steps; reward_curve = zeros(n_episodes, 1); epsilon = epsilon_start; delta_epsilon = (epsilon_start - epsilon_end) / n_episodes; for ep = 1:n_episodes state = start_idx; total_reward = 0; for step = 1:max_steps [row, col] = ind2sub([rows, cols], state); valid = get_valid_actions(row, col, rows, cols); if rand() < epsilon candidates = find(valid); % 只从合法动作里随机选 action = candidates(randi(numel(candidates))); else Q_values = Q(state, :); Q_values(~valid) = -inf; % 非法动作直接屏蔽 [~, action] = max(Q_values); end [next_state, reward, done] = step_environment(...) Q(state, action) = Q(state, action) + alpha * (reward + gamma * max(Q(next_state, :)) - Q(state, action)); state = next_state; total_reward = total_reward + reward; if done, break; end end reward_curve(ep) = total_reward; epsilon = max(epsilon - delta_epsilon, epsilon_end); end end这个脚本里有几个细节值得说明。第一,~valid的位置会将地图边界外动作的Q值压成-inf,避免max选到一个明明不可执行的“上移”动作;第二,随机探索时只在合法动作里选,而不是四个动作里随便选一个再修正,这一步能减少大量无效学习;第三,alpha和gamma写死在函数里,方便后面用参数扫描脚本统一测试;第四,reward_curve记录了每一轮的累计奖励,这是判断Q表有没有收敛的第一证据。
3.2 环境交互函数:碰撞、到达终点和越界的统一处理
step_environment是连接训练主循环和地图的桥梁。它接收当前状态和动作,返回新状态、奖励和是否结束。这个函数最需要小心的是“越界”和“撞墙”到底算不算同一件事——我的处理是都算无效移动,Agent留在原地,奖励给-10。这样动作试探会自发避开障碍,而不是靠硬编码禁止动作。
function [next_state, reward, done] = step_environment(state, action, rows, cols, map, goal_idx) [row, col] = ind2sub([rows, cols], state); next_row = row + (action == 1) - (action == 2); % 上+1,下-1 next_col = col + (action == 3) - (action == 4); % 左-1,右+1 if next_row < 1 || next_row > rows || next_col < 1 || next_col > cols next_state = state; reward = -10; done = false; return; end next_state = sub2ind([rows, cols], next_row, next_col); if map(next_state) == 1 next_state = state; reward = -10; done = false; return; end if next_state == goal_idx reward = 100; done = true; return; end reward = -0.1; done = false; end值得注意,调用sub2ind前先做边界判断,是为了避免sub2ind在next_row = 0时产生“回绕索引”。MATLAB的sub2ind不支持越界坐标,直接传一个0进去不会报错,但会返回一个错误的位置索引,这个错极难排查。我自己就翻过车,训练曲线一路下降,最后发现是碰撞判断生效了,但next_state算错到了地图另一侧。先判定边界、再算索引,这个顺序绝对不能反。
3.3 训练次数和最大步数怎么配:500轮地图和2000轮地图的差别
训练轮数直接决定Q表的可信度。8×8、10×10这种小型地图,500轮一般够用;20×20以上或者障碍密集的地图,500轮大概率只学到局部策略,原地绕圈、走到死胡同里出不来都是常见现象。我常用“最小训练轮数 = 地图格数 × 3”来粗算:100格的地图至少300轮,400格的地图至少1200轮。这只是一个启动值,真正的判定标准是奖励曲线是否在结束前趋于平稳。
max_steps的设置会影响训练的“耐心”。如果max_steps太小,Agent探索到一半被迫重置,永远学不到远距离多步转移的奖励;太大会让无效探索拉长,增加无意义的Q值回退。我通常设成rows*cols*3,在10×10地图上就是300步。这个值还需要跟步数惩罚配合调整——如果步数惩罚是-0.1,300步的单轮累计步数惩罚是-30,与+100的终点奖励不在一个数量级,Agent仍有足够动力寻找终点。
写训练脚本时,顺带把随机种子固定下来:rng(42)。Q-Learning是随机算法,不固定种子,你没法判断“今天效果好”是调参起作用了,还是仅仅随机种子好——自测时甚至会得到完全相反的结论。固定种子是仿真系统可复现性的最低要求。
4. 自测的核心方法:训练完先别画路径,跑一遍白盒指标再说
4.1 自测脚本:成功率、平均步数和奖励曲线的三件套验证
训练完成后第一件事不是画路径,而是写一个独立的测试脚本,用训练阶段的Q表、但不启用探索,让Agent从固定起点出发,反复测试。用纯贪心策略,即每一步取max(Q(state, :)),统计三样东西:到达终点的成功率、平均步数、以及是否出现死循环(超过max_steps还没到终点)。只有每次测试都稳定到达终点,并且步数没有明显波动,Q表才算基本学明白了。
function [success, steps] = test_policy(Q, map, start_idx, goal_idx, n_tests) rows = size(map, 1); cols = size(map, 2); max_steps = rows * cols * 2; success = 0; steps = zeros(n_tests, 1); for t = 1:n_tests state = start_idx; for s = 1:max_steps [row, col] = ind2sub([rows, cols], state); valid = get_valid_actions(row, col, rows, cols); Q_values = Q(state, :); Q_values(~valid) = -inf; [~, action] = max(Q_values); [state, ~, done] = step_environment(state, action, rows, cols, map, goal_idx); if done success = success + 1; steps(t) = s; break; end end if steps(t) == 0, steps(t) = max_steps; end end success_rate = success / n_tests; end这里有个细节:test_policy里没有epsilon,是完全确定性的策略,所以n_tests重复跑100次其实结果都是一样的。但把n_tests参数留着有两个好处:一是在测试随机策略时可以用;二是如果地图里存在多路径,多次测试可以验证策略稳定性。我一般在自测时跑10次,只看成功率是1.0还是0.0,不取平均值。另外,记录steps时,如果Agent在最大步数内没到达终点但也没有死循环,这个状态值得特别留意——说明策略让Agent陷入了某种周期运动,这往往是四个动作里有“来回震荡”的情况。
4.2 三张可视化图判断训练是否真收敛:不要在奖励曲线上自欺欺人
数值指标之外,我建议至少画三张图。第一张是训练曲线:横轴是episode,纵轴是每轮总奖励。一个健康的曲线是前100轮快速上升、中段波动减小、尾部趋于平缓。如果到训练结束曲线还在剧烈波动,先别调参,把训练轮数翻倍再试。
第二张是Q表最大值热力图。把每个状态的最大Q值用reshape(max(Q, [], 2), rows, cols)映射回地图,再用imagesc画出来。理想情况下,终点的Q值最高,邻近终点的区域次高,并且呈现“从起点到终点逐渐升高”的梯度。如果热力图高亮区域杂乱无章,说明某些状态没被充分访问,增加探索率或训练轮数。
第三张是路径回放图。把最终的路径点画在地图上,检查两点:一是路径有没有穿过障碍格(踩坑里讲过的显示颠倒问题很容易造成误判);二是路径上有没有明显的来回往返段。有来回升降说明Q表在该区域没有学好,把该区域单独拿出来加训,比整体重训效率高得多。
figure; subplot(1,3,1); plot(reward_curve); title('Episode Reward'); subplot(1,3,2); imagesc(reshape(max(Q, [], 2), rows, cols)); colorbar; title('Max Q Value'); set(gca, 'YDir', 'normal'); % 防止上下颠倒 subplot(1,3,3); imagesc(map); colormap(gray); hold on; path = trace_path(Q, map, start_idx, goal_idx); plot(path(:,2), path(:,1), 'r-o', 'LineWidth', 1.5); title('Recovered Path');set(gca, 'YDir', 'normal')是一个血泪经验——MATLAB的imagesc默认把第一行画在底部,而我们的地图矩阵第一行是地图顶部,直接画会出现上下颠倒,看起来路径“穿墙”,动不动就让人怀疑算法错了。
4.3 换终点、换地形做“转移测试”,但别期望表格法Q-Learning泛化
路径规划自测最容易忽视的一步,是更换地图或者更换终结点来测试泛化能力。很多同学跑完一张地图就宣布系统完成,但换了终点,训练从不收敛;换了地图,Q表直接失效。注意,这个失效不一定是bug——表格版Q-Learning本身的泛化能力就是弱的,它没有对状态做任何泛化,新地图的任何格子都可能是一个从未见过的输入。但是,这不代表自测应该跳过这个环节,反而要主动试,给系统加上“重训练”的开关,让新地图可以重新初始化Q表开始学习。
泛化测试我有两个固定动作:第一个是把终点点位移两格,重新训练100轮,看能否快速收敛——如果收敛很慢,说明奖励函数设计不太合理;第二个是换一张完全不同形状的地图(比如S形走廊),从零训练,看是否能在合理轮数内收敛。S形走廊是测试Q-Learning路径规划的好场景,它需要长距离探索,中间没有奖励引导,全靠末期终点奖励反向传播。如果这样的一张图能收敛,那么这套仿真系统的奖励设计和参数就在可接受范围。
5. 路径规划MATLAB仿真最容易踩的五个坑:现象、原因、解决
5.1 路径绕远路:奖励函数没写“步数代价”
现象:训练完成后路径虽然能到达终点,但明显绕了一个大圈,甚至走出一些无意义的往返路线。原因:步数惩罚缺失或过小,Agent发现原地绕圈和直接走过去的Q值没差别,于是策略随机。解决:给每步一个固定的负奖励,比如-0.1。负奖励同时会抑制震荡行为——来回走两步的代价是-0.2,而直接拐弯可能只需-0.1。这里最值得注意的不是惩罚本身,而是“奖励结构不能只在终点有信号,中间每一步都要有代价结构”。
5.2 训练曲线震荡不收敛:探索率没有衰减
现象:训练曲线从头到尾都在上下波动,到结束也没稳定;回放路径每跑一次都不一样。原因:epsilon固定在一个较高值,Agent一直以一定概率随机乱走,Q表被迫更新一些无意义的动作价值。解决:将epsilon从0.9线性衰减到0.05。衰减步长和训练轮数对应,训练轮数本身也要结合地图大小。如果你的地图是30×30,只训练300轮,那么衰减再完美也来不及探索充分,训练轮数先加到一个较大值再说。
5.3 终点旁边打转:折扣因子过高导致“未来收益”过度膨胀
现象:路径已经走到了终点附近,但在终点外围来回移动,差一两格就是进不去。原因:gamma设成0.99甚至更高时,终点附近状态的Q值被远期终点奖励过度抬高,Agent在每个中间状态都倾向于去一个Q值更高的邻居,而那个邻居又指向另一个状态,形成局部震荡。解决:把gamma降到0.85~0.9,让Q值更看重眼前几步的收益。值得警惕的是,这个问题在训练初期不明显,往往在路径快收敛时才暴露,排查时先看Q值热力图:如果终点附近没有一个平滑的高亮斜坡,而是出现斑驳,优先怀疑gamma。
5.4 用imagesc画地图路径像“穿墙”:Y轴方向设置颠倒
现象:路径明明在空地上,画出来却从障碍物上穿过去。原因:MATLAB的imagesc默认坐标系统的原点在左上角,但Y轴正向向下;而矩阵的第一行是“地图顶部”,如果路径点的行坐标直接对应矩阵的行下标,画出来会像做了垂直翻转。解决:在imagesc后加一行set(gca, 'YDir', 'normal')。这个问题属于低级错误,但造成的影响很迷惑——算法调试半天才发现是显示问题,所以放在这里提醒。
5.5 Q值全零状态下,Agent一出门就撞墙:探索阶段被“合法动作筛选”误杀
现象:训练初期Agent行为像无头苍蝇,连第一步都频繁撞墙,奖励一直是最低的。原因:探索阶段随机选动作,如果地图边界形状复杂,有效的试探动作比例低,早期Q值更新过于稀疏;当Q表全零时,max(Q)在多个动作之间随便选,可能选到撞墙动作。解决:两种做法我都用过——一是初始化Q值时给所有状态同一个小的正值,这样max不会再“全零无差别”;二是确保探索时只在valid_actions中选,避免把大量时间花在撞墙上。如果发现训练早期奖励长期很低,优先检查是不是探索动作里包含大量撞墙。
6. 参数调优与进阶验证:把仿真系统做到能说服答辩老师
到这里,训练和自测已经能跑通。最后一步是让成果从“能跑”变成“能信”。一个进阶做法是参数扫描:把alpha、gamma、步数惩罚三个参数各取几个档位,形成网格组合,每组固定随机种子跑一次训练,然后用test_policy统计成功率。这样你能拿到一张“参数-成功率”表格,直接展示在答辩PPT里,比口头说“我调参调了很久”有说服力得多。我用过的参数网格大致是:alpha在0.05/0.1/0.2,gamma在0.85/0.9/0.95,步数惩罚在-0.05/-0.1/-0.2,共27组,每组500轮。地图10×10时总时长约十几分钟,可接受。扫描结果通常会发现gamma=0.95的组合在路径长度上更短,但成功率不稳定,这是一种经典权衡:越看重长期收益,策略越“激进”,对探索充分性要求越高。
还有一个值得试的进阶玩法:做一套对比验证。用同样的地图分别跑Q-Learning和Dijkstra,把最短路径距离算出来,再用Q-Learning的训练结果路径长度去对比。Q-Learning因为有步数惩罚和随机性,路径长度一般不会严格等于最短路径,但两者差距应该在合理范围。这一步的价值是给“仿真系统”提供外部基准——不仅证明Q-Learning自己会走,还证明它走得接近最优。如果对比发现路径长度比Dijkstra长很多,说明奖励函数里的步数惩罚权重过低,把惩罚加大再试。
三维或者更复杂的场景,常见扩展方式是修改状态表达。无人机三维路径规划仿真里,网格变成三维矩阵map(x, y, z),状态索引用sub2ind([nx, ny, nz], x, y, z),动作从上到下、左右、前后共六个方向,Q表变成numel(map) × 6。理论上Q-Learning算法核心一行不用改,只要step_environment里多了z轴移动和越界判断。实际跑起来,三维地图训练收敛难度比二维高很多,这时不要死磕”完整三维成功率“,先切二维切片训练,再逐层扩展,是更务实的路径。OOP架构的多算法图像处理系统其实不冲突——你完全可以把Q-Learning训练器封装成一个MATLAB class,直接替换内部策略模块,对上层仿真结果完全透明。代码组织上,用函数比用脚本好,脚本适合跑一次性实验,函数才适合做成可复用系统。这一点做到了,不管做课设还是做工程验证都会顺手很多。
做这套自测系统时,我自己最深的教训是:永远不要用训练阶段的数据来证明模型有效。我之前有一次把训练末尾的路径截图当“成果图”,后来发现那张路径其实是靠epsilon还没衰减完碰运气走出来的,换成纯测试策略直接找不到终点。从那以后我定了一条规矩:所有对外展示的路径图、成功率数据,一律来自独立的测试脚本和固定的随机种子。只有这样的自测结果,才是能说服别人、也说服未来自己的结论。希望这套流程能帮你在MATLAB环境里少走弯路,把Q-Learning路径规划做到真正可信,有需要欢迎继续交流细节。
本文还有配套的精品资源,点击获取