☰
MATLAB实现DQN深度强化学习路径规划:从原理到工程实战
2026/10/20 3:02:41 网站建设 项目流程

做路径规划这行的老哥们,这两年多少都听到过“强化学习”“深度强化学习”这些词。今天要聊的这个项目,就是用 DQN(Deep Q Network)这种深度强化学习算法,去解一个二维栅格地图上的路径规划问题,而且整套东西是用 MATLAB 完整实现的,属于典型的“算法原理 + 工程落地”的练手项目。

这个项目解决的问题很直白:传统路径规划算法(A*、Dijkstra、RRT 这些)依赖已知的全局地图,做的是贪心搜索或随机采样。而 DQN 的思路完全换了个方向——让一个智能体自己在地图上“走”,通过与环境的反复交互,自己学会怎么绕开障碍物、怎么走最近的路到达终点。听起来挺玄乎,但真正动手做的时候会发现,地图怎么建、状态怎么编码、奖励怎么给、网络怎么搭、训练怎么调,每一步都有坑等着你,任何一个环节糊弄过去,训练出来的策略基本就是原地打转或者疯狂撞墙。

适合谁来参考?正在做相关课题研究或者课程设计的学生、想从零上手深度强化学习但不想一上来就啃 Python 框架的同学、以及手里只有 MATLAB 环境、想快速验证强化学习路径规划思路的工程师。我会按照我实际踩坑的顺序,把整个链路讲清楚:从环境建模开始,到网络搭建、训练主循环、参数调优,最后到问题排查。下面直接进入正题。

1. 先想明白一件事:路径规划为什么要请出 DQN

1.1 传统方法的痛点在哪里

做路径规划的人,第一反应肯定是 A* 或者 Dijkstra。它们在小规模、静态、完全已知的栅格地图上表现极好,能给出确定的最优解。但问题在于,这两位的隐含假设是“地图是已知的、不变的”。一旦地图中途发生变化——比如新增了障碍物、目标点移动了、或者地图本身压根就没有完整建模,传统方法要么需要全部重算,要么在未知区域直接抓瞎。

DQN 切入的角度完全不同。它把路径规划看成“序贯决策问题”:智能体站在某个栅格上,观察周围环境,选择一个动作(往上走、往下走、往左还是往右),每走一步获得一个即时奖励,一路走到目标点为止。整个过程不需要提前知道全局路径,不需要穷举搜索,只需要通过海量的试错(训练)把“状态到动作”的映射关系学出来。

我在实际项目里最深的一个体会是:DQN 真正有价值的场景不是替代 A*,而是那些“环境不完整、目标会变、或者要求训练出一个能直接复用到相似地图上的通用策略”的场景。当你需要在 100 张相似地图上反复规划路径,每张图都去跑一遍 A* 可能不算慢,但如果你想让一个策略拿到新地图上直接就能用,A* 做不到,DQN 可以。

1.2 DQN 的适用范围要泼盆冷水

说实话,如果地图就 10×10,固定不变,找一条静态最短路径,A* 零点几毫秒就出结果了,还保证最优。这时候用 DQN 纯属给自己找麻烦。DQN 训练要跑几万步,还要调网络、调奖励,最后出来的路径还不一定是最优的。

所以动手之前先给自己列个判断清单:

  • 环境模型不精确、想在线自适应——用 DQN 方向是对的;
  • 状态空间大但可以离散化(栅格地图天然满足)——适合;
  • 需要训练一个能泛化到类似地图上的策略——适合;
  • 地图极小、固定不变、必须要最优解——老老实实用 A* 或 Dijkstra;
  • 训练时间只有一下午、机器配置很一般——谨慎考虑,DQN 收敛没那么快。

我自己做模拟项目 X 的时候,最开始拿了一个 20×20 的小栅格图,障碍物随机生成,目标点固定。为了对比,A* 和 DQN 都跑了一遍。A* 是秒出,DQN 训练了差不多 2 万步才稳定出一条像样的路径。所以你要清楚:DQN 买的是“泛化能力”和“决策速度”,代价是“训练成本”。

2. 核心原理拆解:Q 表是怎么一步步变成神经网络的

2.1 先回顾一下 Q-learning 的底子

DQN 的前身是 Q-learning,一个经典的时间差分强化学习算法。它的核心是一个 Q 表:每一行是状态,每一列是动作,表格里的值表示“在这个状态下选择这个动作,未来能拿到的累计奖励期望”。

更新公式是经典的那条:

Q(s,a) ← Q(s,a) + α [r + γ·max_a' Q(s',a') − Q(s,a)]

翻译成大白话:当前状态 s 下选了动作 a,拿到奖励 r,跳到状态 s',然后预估一下 s' 下所有动作里最好的那个未来价值,用这个“现实奖励 + 未来估值”去修正当前 Q 值。α 是学习率,γ 是折扣因子,γ 越小说明智能体越短视,越大说明越看重长远收益。

按这个逻辑,地图稍微大一点就出问题了。假设 30×30 的栅格地图,状态就是 900 个格子,动作 4 个,Q 表才 3600 项,其实还能存。但如果你想在状态里加上“智能体最近几步走没走过”“目标点的相对方位”这些信息,状态空间立刻爆炸,Q 表就存不下了。更麻烦的是,栅格地图连续变化的话,状态根本没法穷举。

2.2 从 Q 表到 DQN 的三板斧

DQN 做的事情,本质上是用一个神经网络去拟合 Q 函数:输入状态,输出每个动作对应的 Q 值。不再需要一张巨大的表格,而是靠网络参数把“状态—动作—价值”的映射关系压缩进去。

但直接拿神经网络替换 Q 表是不行的,训练根本稳不住。因为 Q-learning 本身有个毛病:训练数据是连续相关的——下一步的状态取决于上一步的动作,且更新目标里用到的“Max Q”又是网络自己算出来的,等于拿着自己的预测去更新自己,很容易震荡甚至发散。DQN 靠三个技巧把这问题压下去:

第一,经验回放(Experience Replay)。把智能体每一步的转移(s, a, r, s', done)存进一个环形缓冲区,训练时每次随机抽一小批出来。这样打破了样本之间的时间相关性,也让同一条经验能被反复利用,数据效率高不少。

第二,目标网络(Target Network)。把“算未来价值的网络”和“要更新的网络”拆成两份。行为网络每步都更新,目标网络每隔 N 步直接复制行为网络的参数。这样更新目标就相对稳定,不会每步都在变,训练就稳了。这个技巧我在实际调试中体会极深——去掉目标网络,训练曲线简直是心电图,加上了以后曲线才像条正常的学习曲线。

第三,ε-greedy 探索策略。训练初期以高概率随机选动作(探索),以低概率选当前 Q 值最大的动作(利用)。随着训练推进,ε 从 1.0 慢慢衰减到 0.05 左右,让智能体先是“瞎逛长见识”,后面“越走越精明”。ε 衰减速度如果太快,它刚探索出一点点规律就被打断,后面基本就是死守一个烂策略。

这三个技巧是 DQN 的基石,后面的代码完全围绕它们展开。

3. 栅格地图建模与 MDP 五元组设计

3.1 地图数据结构设计

二维栅格地图在 MATLAB 里最直接的形式就是逻辑矩阵或者数值矩阵:1 表示障碍物,0 表示可行走区域,再单独定义起点和终点坐标。

比如这样的数据:

% 地图尺寸 mapSize = [20, 20]; % 0为可行走,1为障碍 map = zeros(mapSize); % 随机生成障碍,注意预留起点终点区域 rng(42); % 固定随机种子方便复现 obstacleRatio = 0.25; for i = 1:mapSize(1) for j = 1:mapSize(2) if rand < obstacleRatio map(i, j) = 1; end end end startPos = [1, 1]; goalPos = [20, 20]; % 保证起点终点是可行走的 map(startPos(1), startPos(2)) = 0; map(goalPos(1), goalPos(2)) = 0;

实际做的时候我建议不要用纯随机障碍,至少加一些“墙”状障碍和“U 型障碍”,不然环境太简单,学出来就是个花架子。后面做泛化测试的时候,也可以准备多张地图切换训练,这个后面单独讲。

3.2 状态空间与动作空间的设计

这个设计直接决定训练难度,也是新手最容易随便搞的地方。我见过很多同学直接把整个地图矩阵当状态输进去——20×20 就是 400 维输入,不是不能做,但训练量大了好几倍,而且网络很容易把“地图背景”也记住,导致换张地图就废了。

更合理的做法是设计局部观测 + 目标方位的状态向量:

  • 当前位置的横纵坐标(归一化到 [0,1]);
  • 当前位置与目标点的归一化差值(dx, dy),相当于让网络知道“目标在哪个方向”;
  • 周围 8 个或者 4 个邻格是否可行走(0/1 编码),相当于让网络“看见”身边的障碍物。

把这三类信息拼起来,比如 20×20 地图上,状态向量长度大概 13 到 15 维(2 个坐标 + 2 个目标差 + 8 个邻格 + 1 个是否是终点的标识)。输入维度骤降,训练收敛速度完全不是一个量级。

动作空间就是四个离散动作:上、下、左、右。想更精细可以加四个斜向动作变成 8 方向,但要注意斜向穿越的“切角”问题——比如左上角是障碍物,能不能从右下格直接斜走到右上格?不同场景处理方式不一样。我做基础版本用的是 4 方向,简单直观,路径规划效果也足够演示。

% 动作表:上下左右 actions = [-1, 0; % 上 1, 0; % 下 0, -1; % 左 0, 1]; % 右

3.3 MDP 形式化:状态转移和奖励

整个问题可以形式化成标准的 MDP:状态就是上面定义的特征向量,动作是四个移动方向,状态转移是确定性的——执行上移就是坐标减一行,除非碰到边界或障碍物。碰到障碍物做无效移动时,一种常见做法是原地不动并给一个惩罚奖励,另一种是干脆禁止该动作并直接结束回合。我在项目里用的第一种,因为更接近真实机器人“撞到东西会停下来,而不是直接消失”的情况。

奖励函数这块是 DQN 路径规划的灵魂,我单独在下一节讲。这里先把基本框架列出来:

  • 每步走一步:给一个小惩罚(比如 -0.05 或 -1),逼迫智能体找最短路径;
  • 到达目标点:给大正奖励(比如 +10 或 +100);
  • 撞上障碍物或者踩到已经走过的格子:给负奖励(比如 -1 或 -5)。

不要想着一次性把奖励设计到位,我的经验是先跑一版最简单的,看训练曲线再迭代。后面会详细展开奖励函数的调试经过。

4. MATLAB 实现:环境类、网络与训练主循环

4.1 环境交互模块(step 函数)

把环境封装成一个函数接口,输入当前状态和动作,输出下一个状态、奖励和是否结束。这一步做得好,后面换地图换任务都方便。我在实现时用了一个简单的结构体来管理环境:

function [nextStateVec, reward, done] = stepEnv(env, state, actionIndex) % env: 包含 map, agentPos, goalPos 的结构体 % 计算目标位置 nextPos = env.agentPos + env.actions(actionIndex, :); % 越界判断 if nextPos(1) < 1 || nextPos(1) > env.mapSize(1) || ... nextPos(2) < 1 || nextPos(2) > env.mapSize(2) nextPos = env.agentPos; % 原地不动 end % 撞墙判断 if env.map(nextPos(1), nextPos(2)) == 1 reward = -1; env.agentPos = nextPos; % 原地不动或者标记惩罚 elseif isequal(nextPos, env.goalPos) reward = 10; env.agentPos = nextPos; else reward = -0.05; env.agentPos = nextPos; end % 更新状态特征向量 nextStateVec = buildStateVec(env); done = isequal(env.agentPos, env.goalPos); end

环境接口切分清楚以后,训练主体完全不用动,想换地图、换奖励、换动作集,只改环境和状态构建函数就行。

4.2 DQN 网络结构的构建

因为是拟合离散 Q 值,网络结构非常标准:输入层是状态特征维度,中间堆两到三层全连接,输出层维度等于动作数量。我用 MATLAB 的深度学习工具箱搭的网络是这样的:

% 状态特征维度,比如15 stateDim = 15; numActions = 4; dqnNetwork = [ featureInputLayer(stateDim, 'Normalization', 'none') fullyConnectedLayer(64) reluLayer fullyConnectedLayer(64) reluLayer fullyConnectedLayer(numActions) ]; dqnNet = dlnetwork(dqnNetwork); % 目标网络初始与行为网络一致 targetNet = dlnetwork(dqnNetwork);

网络规模不用太大,两层 64 神经元对 20×20 栅格地图完全够用。我试过 128 甚至 256 的隐层,训得更慢但效果并不更好,因为状态本身维度就不高。记住一个原则:网络容量跟任务复杂度匹配即可,不是越大越强。

4.3 经验回放缓冲区的实现

经验回放本质就是一个环形队列。用 MATLAB 实现时,我习惯预先分配固定大小的 cell 数组,存到上限后覆盖最旧的数据:

bufferSize = 100000; buffer = cell(bufferSize, 1); bufferIdx = 1; bufferCount = 0; % 存入经验 function addExperience(buffer, bufferIdx, bufferCount, s, a, r, sp, done) buffer{bufferIdx} = {s, a, r, sp, done}; end % 采样 function batch = sampleBatch(buffer, bufferCount, batchSize) randIdx = randi(bufferCount, batchSize, 1); batch = buffer(randIdx); end

采样注意一点:只有当 buffer 里存够了 batchSize 条数据才开始训练,否则前几百步都是白抽。这是不少人训练不动的隐藏原因——初始化就把 batchSize 设成 64,但缓冲区里只有 20 条,每次抽样全是重复数据,网络直接被带偏。

4.4 训练主循环:核心代码逻辑

训练主循环是整套代码的心脏。我按 DQN 原始论文的流程写,伪代码逻辑如下:

numEpisodes = 3000; gamma = 0.95; % 折扣因子 epsilon = 1.0; % 初始探索率 epsilonDecay = 0.995; % 每回合衰减 epsilonMin = 0.01; lr = 1e-3; % 学习率 batchSize = 64; targetUpdateFreq = 100; % 目标网络更新频率 maxStepsPerEpisode = 200; % 单回合最大步数 optimizer = adamupdate(0, dqnNet.Learnables, lr); for episode = 1:numEpisodes env = resetEnv(env, startPos); % 重置环境到起点 stateVec = buildStateVec(env); epReward = 0; for step = 1:maxStepsPerEpisode % ε-greedy 选动作 if rand < epsilon action = randi(numActions); else dlState = dlarray(single(stateVec), 'CB'); qValues = predict(dqnNet, dlState); [~, action] = max(extractdata(qValues)); end % 执行动作 [nextStateVec, reward, done] = stepEnv(env, stateVec, action); addExperience(buffer, s, a, r, sp, done); % 这里的 s, a, r, sp 需要在具体位置赋值 % 经验足够时进行梯度更新 stateVec = nextStateVec; epReward = epReward + reward; if done break; end end epsilon = max(epsilonMin, epsilon * epsilonDecay); % 周期性同步目标网络 if mod(episode, targetUpdateFreq) == 0 targetNet = dqnNet; end end

实际的梯度更新部分,需要把采样出来的 batch 转成 dlarray,然后分别算当前 Q 值和目标 Q 值,用均方误差做损失,再反向传播。这段代码略长,但核心只有三行:行 1 用行为网络算当前状态动作的 Q 值;行 2 用目标网络算下一状态的最大 Q 值,构造 TD 目标;行 3 用均方误差反向传播更新网络。

% 从batch中组装数据 sBatch = cat(2, batchS{:}); % [stateDim, batchSize] aBatch = batchA; % [1, batchSize] rBatch = cat(2, batchR{:}); % [1, batchSize] spBatch = cat(2, batchSp{:}); doneBatch = cat(2, batchDone{:}); dlS = dlarray(single(sBatch), 'CB'); dlSp = dlarray(single(spBatch), 'CB'); % 当前Q值 qPred = forward(dqnNet, dlS); % [numActions, batchSize] % 目标Q值:下个状态的最大Q qNext = forward(targetNet, dlSp); % [numActions, batchSize] [qNextMax, ~] = max(qNext, [], 1); % 组装损失 rowIndex = sub2ind(size(qPred), aBatch, 1:batchSize); qCurrent = qPred(rowIndex); qTarget = single(rBatch) + gamma * qNextMax .* (1 - single(doneBatch)); loss = mse(qCurrent, dlarray(qTarget, 'CB')); % 反向传播 grads = dlgradient(loss, dqnNet.Learnables); [dqnNet, optimizer] = adamupdate(dqnNet, grads, optimizer, stepIdx);

梯度计算要放在dlfeval里,这里为了可读性做了简写。真正跑的时候,dlgradient必须在自定义训练循环的函数内使用,这个细节别忘,不然 MATLAB 直接报错。

5. 奖励函数:整个项目里最花时间的地方

5.1 我踩过的奖励设计坑

第一版奖励我设计得特别“着急”:到达目标 +100,撞障碍物 -100,其他每一步 -1。结果训练出来一个极其激进的智能体——它确实能“到终点”,但路径全程贴着墙走,恨不得每次都蹭着障碍物边缘过去,因为只要撞到障碍物的概率不是 100%,它就宁可赌一把,赌赢了少走好几步,赌输了不过重开一局。这本质上是奖励尺度不匹配导致的:+100 和 -100 的绝对量太大,把 -1 的每步惩罚淹没了。

后来我改成了:到达目标 +10,撞障碍物 -1,每走一步 -0.05,无效动作(越界) -1。这样每步惩罚不及到达奖励的百分之一,但同时不至于让智能体为省几步去赌撞墙。训练出来的路径明显“规矩”了,贴着可行走区域走,不冒险。这个调整花了我一个周末,回头看,教训就一条:奖励的绝对量级要统一考虑,别让某一项的数值把其他项的信号完全淹没掉。

5.2 一个有用的辅助信号:距离势能

单纯靠“到达终点才有正反馈”训练非常慢,因为稀疏奖励导致智能体前期几乎不知道自己在往对的方向走。我加了距离势能作为中间引导:每走一步如果离目标点的曼哈顿距离比上一步近了,额外给 +0.3 的 shap 奖励,远了给 -0.3。

这个辅助信号要非常小心,加得太重智能体会学会“原地转圈骗奖励”——因为有些位置只要来回踱步,下一步总有一边是靠近目标的。我的经验是辅助奖励的量级必须小于每步惩罚和到达奖励之间至少一个数量级,只起方向引导作用,不能喧宾夺主。

5.3 记录奖励曲线,别凭感觉调参

做奖励调参,最好把每回合的总奖励和每回合步数都存下来画曲线。曲线能告诉你很多信息:

  • 总奖励曲线如果一路下行,说明惩罚太重、探索阶段每回合都在被扣分,要加重正向信号;
  • 总奖励如果一直横盘不动,说明奖励信号没有梯度,智能体学不出东西;
  • 步数曲线如果在掉,说明策略确实在变好,即使总奖励没怎么变也可以继续训。

我在项目里用最简单的plot(episodeRewards)画了曲线,配合每 100 回合打印一次“平均步数”,就能非常直观地判断训练是否健康。这个习惯强烈建议保留,它能帮你省下大量盲目调参的时间。

6. 常见问题与排查经验:照着这个清单查

6.1 训练曲线不收敛,奖励来回震荡

高频原因有三个:学习率太大、目标网络更新太频繁、batchSize 太小。调试顺序我建议先降学习率,从 1e-3 降到 3e-4 再试;然后把 targetUpdateFreq 调大,比如 300 步一更新;最后 batchSize 提到 128 试试。我遇到过的最乌龙情况是状态向量没归一化,坐标值从 1 到 20 直接进网络,而邻格特征都是 0/1,数量级差太多,网络训练一直在“波动”。把坐标除以地图尺寸归一化到 [0,1] 之后,问题当天就解决了。

6.2 Q 值发散,一路飙到几千上万

这是经典的“Q 值高估”问题。如果目标网络没有生效,几乎必然发生——行为网络和目标网络是同一个,每步都在更新,TD 目标跟着网络一起漂。检查点:确认目标网络是用targetNet.Learnables = dqnNet.Learnables这样完整复制,而不是赋值引用;确认训练循环里算目标 Q 值时用的是targetNet而不是dqnNet。这两个错误在代码里非常隐蔽,检查的时候要逐行看。

6.3 智能体在原地打转或者非要远路绕

典型原因之一是“走过的格子没有惩罚”,智能体会走出环形路径甚至 8 字形。解决办法可以加一步“位置去重惩罚”:如果当前位置之前已经访问过,就额外给一个 -0.1 的小惩罚。注意惩罚别太大,否则它会学出“站住不动”的消极策略。

另一个原因是 ε 衰减太快,探索不足。ε 从 1.0 衰减到 0.01,如果 500 回合就衰减完了,那智能体大概在 300 回合时还处在“半探索半利用”的混沌期就被强行切换成纯利用,学出来的策略很差。我把衰减改慢,让 ε 到大概 2000 回合左右才到 0.05,效果立刻不一样。

6.4 训练好的模型换张地图就废了

这个现象几乎是必然发生的,因为训练时地图是固定一张,网络把这张地图的布局“背”下来了。想让策略有泛化能力,有两个办法:

第一,训练时每回合随机生成一张新地图(保证起点终点可达),让智能体学会“通用避障”而不是“背地图”。代价是训练时长明显增加。

第二,如果只要求单图最优,那直接用 A* 好了——这话我再强调一遍。DQN 的应用价值只有在多图泛化或者动态环境下才体现得出来。如果非要在单图上用 DQN,那就接受它是“展示强化学习流程”而不是“生产最优路径”这个定位。

6.5 训练慢到怀疑人生

MATLAB 跑强化学习确实比不了 Python 生态的成熟框架,但代码层面的优化能省不少时间。实测下来的经验:

  • 尽量避免在训练循环内部频繁调用dlarray转换,一次性把整个 batch 转好;
  • 不要在stepEnv里写复杂可视化,训练时全程关掉绘图,只存数据,等训完了再单独回放路径;
  • 障碍物检测用矩阵索引map(pos(1), pos(2)),别用find或者循环遍历;
  • 可以把地图数据做成逻辑索引提前算好,减少重复计算。

我手头这张 20×20 地图,优化后 3000 回合大概跑十几分钟,没优化的时候同样回合数能跑一个多小时,差别全在那些不起眼的小函数上。

7. 扩展方向与个人实操心得

项目做到能成功规划一条路径,其实才刚摸到门。这套 DQN 代码稍微改改就能扩展出不少方向:

第一,Bias 网络和Double DQN的升级。上面提到的 Q 值高估问题,DDQN 用得是“行为网络选动作、目标网络估值”的方式,改造成本极低,效果立竿见影。我后面在这个项目上顺手加了 DDQN,训练曲线明显更稳,最终路径也比基础 DQN 更短。建议做完基础版后,把双网络结构升级成 DDQN,这个升级代码量不大但对最终效果影响很大。

第二,动态障碍物环境。把障碍物每过一段回合做一次移动,训练出来的策略就不会“背地图”,而是学会了“观察到障碍就绕开”的行为模式。这个方向非常适合做演示或者写进论文的实验部分。

第三,多起点多目标。把起点和终点也作为状态输入的一部分,网络学习的是“从任意位置通向任意目标的通用策略”,这其实就是往分层强化学习甚至导航大模型的方向探了探路。

第四,跟传统算法结合。比如用 DQN 生成一条初解,再用 A* 做局部精修;或者在 A* 的启发函数里引入学习到的代价估计。这种混合方案在我实际做的某跨平台系统里效果不错,比单用任何一种都稳定。

最后说说我个人的真实体会。这套 DQN 路径规划项目,最难的不是数学公式,也不是 MATLAB 语法,而是建立起“为训练过程设计正确反馈信号”的直觉。奖励函数、状态表示、探索策略、网络容量,四者互相耦合,改一个就得看另外三个的脸色。我调这个项目的过程中,有三次把训练曲线调崩了,又花两三天调回来,最后才找到了那组参数。

给刚开始做的朋友一个建议:先照着我上面给的参数组合跑通一个简单场景,拿到一条勉强能走的路径;然后再开始“作”奖励函数、换地图、加障碍。千万不要一上来就追求完美路径,那会让你陷入调参地狱。每一步都确保能看到明确的实验结果,再走下一步,这个项目就能给你带来比“一篇代码”多得多的收获。

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

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

立即咨询