
简介用MATLAB实现深度Q网络DQN解决最短路径问题的完整代码包面向有一定MATLAB基础、希望系统掌握强化学习落地方法的算法学习者与研究者。资源以网格世界为仿真环境完整呈现DQN求解最短路径任务的核心流程涵盖状态空间与动作空间定义、奖励函数设计、Q网络结构搭建、经验回放缓冲区、目标网络定期同步以及ε-greedy探索策略等关键环节并对每个模块做了清晰划分便于读者按图索骥。压缩包共20个文件包括15个M脚本、4个MAT数据文件和1张奖励变化图整体仅872KB轻量精简适合快速下载与二次开发。目前已有2649人学习下载。文件目录采取基础函数、环境仿真、DQN主程序三大模块组织基础函数提供状态动作处理、策略绘制、Sigmoid激活等通用能力环境模块构建网格场景与转移逻辑主程序则完成训练循环、目标Q值计算、权重更新和结果可视化等任务同时附带训练好的权重、奖励记录与仿真时间数据可直接运行验证。借助这些代码读者可以直观观察智能体从随机探索到收敛最短路径的全过程并通过调整学习率、折扣因子、探索概率等参数理解DQN的调参逻辑非常适合课程设计、算法对比以及强化学习进阶实践。1. 用MATLAB把DQN落到最短路径上为什么这个组合值得试最短路径问题在教科书里是Dijkstra和A*的天下但一旦地图变大、拓扑变化频繁或者你手里的路网根本不是静态图传统方法就得重新算一遍。DQNDeep Q-Network的思路是把“找路”变成“学一个策略”让智能体在环境里试错最终看到任何起点都能自己走出一条接近最短的路。用MATLAB做这件事最大的好处是省去搭环境、写可视化的时间——你可以在同一个平台里完成建图、训练、验证不需要Python和MATLAB来回倒腾。这个方向适合两类人一类是刚接触强化学习、想用最短路径当练手任务的研究生另一类是做路径规划但不想改C/Python底座的工程师。MATLAB的Deep Learning Toolbox自带深度网络定义和训练流程配合自定义环境能让你把精力放在reward设计和训练策略上而不是纠结框架怎么写。下面这套方案我亲测过地图从5×5到20×20都能收敛关键在环境怎么抽象、reward怎么给、网络怎么配。2. 把最短路径建模成MDP状态、动作和奖励的设计决定了收敛速度DQN解决的是马尔可夫决策过程MDP所以第一步不是写网络而是把“找最短路径”翻译成状态、动作、奖励三件套。这个翻译质量直接决定训练能不能收敛也是最容易翻车的地方。2.1 状态表示只给相对位置不给全局坐标常见做法是让智能体知道“自己在哪、目标在哪、周围有没有墙”。但如果你把整个地图的坐标都塞进状态网络要学的映射关系就太复杂了在小地图上还能跑地图一大就学不动。我用的是“相对坐标 局部视野”的组合。假设地图是一个网格智能体位置是(x_agent, y_agent)目标是(x_target, y_target)那么状态向量定义为% 状态 [目标相对x, 目标相对y, 机器人当前x, 机器人当前y, 是否达到目标] function state getState(agentPos, targetPos, gridSize) state zeros(1, 5); state(1) (targetPos(1) - agentPos(1)) / gridSize(1); % 归一化相对距离 state(2) (targetPos(2) - agentPos(2)) / gridSize(2); state(3) agentPos(1) / gridSize(1); state(4) agentPos(2) / gridSize(2); state(5) 0; % 当前步未到达目标 endstate(1)和state(2)是智能体和目标之间的归一化差值state(3)和state(4)是自身位置的归一化坐标。归一化是关键不归一化的话x和y的量纲一致但数值范围大神经网络容易在初始阶段被大数值输入带偏。为什么不直接把所有墙的位置也放进状态因为墙的信息可以编码到动作执行逻辑里——走到墙边上就留在原地并把这一步算作一次无效动作。这样网络不需要学习墙的分布只需要学习“往哪个方向走能更快接近目标”。遇到迷宫类地图再考虑加局部视野普通栅格地图没必要。2.2 动作设计四方向还是八方向动作空间一般选4上下左右或8加对角线。4方向是最稳妥的起点动作空间小收敛快路径虽然是曼哈顿风格的但长度在网格地图上就是最优的。8方向路径更自然但动作空间翻倍训练时间明显变长而且对角线穿越墙角的问题需要额外处理——很多初学的人在这里踩坑。% 动作映射1上 2下 3左 4右 function nextPos step(action, currentPos, gridSize, obstacles) nextPos currentPos; switch action case 1, nextPos(1) currentPos(1) - 1; case 2, nextPos(1) currentPos(1) 1; case 3, nextPos(2) currentPos(2) - 1; case 4, nextPos(2) currentPos(2) 1; end % 边界和障碍物检测 if nextPos(1) 1 || nextPos(1) gridSize(1) || ... nextPos(2) 1 || nextPos(2) gridSize(2) || ... ismember(nextPos, obstacles, rows) nextPos currentPos; % 无效动作位置不变 end end这一段逻辑很简单但有个隐藏细节无效动作和有效动作在DQN里必须同时进入训练数据否则网络会永远倾向选无效动作——因为它不知道那个动作“不能走”。所以step返回的位置要和状态更新逻辑严格对应不能把无效动作跳过。2.3 奖励设计稀疏大奖励和密集小惩罚的组合奖励设计是这个任务的核心。只给“到达终点1”其他情况给0理论上DQN能学但在地图稍大时会非常慢。我一般用这套组合% 奖励计算 % reward 到达目标 50 % 每一步 -0.05时间惩罚 % 无效动作 -0.1撞墙或边界 % 距离缩短时额外 0.1势能奖励 function [reward, done] getReward(newPos, targetPos, obstacleHit) done false; if isequal(newPos, targetPos) reward 50; done true; return; end reward -0.05; if obstacleHit reward reward - 0.1; end % 势能奖励这一步比上一步更接近目标给一点正向反馈 if distance(newPos, targetPos) distance(prevPos, targetPos) reward reward 0.1; end end每一步的-0.05时间惩罚是“最短路径”的关键。没有这个惩罚智能体学会走到终点之后就不管绕不绕路了——它只关心能不能到达。加了时间惩罚后走的路越长累积负奖励越多网络会自动偏向更短的路径。势能奖励的0.1属于reward shaping目的是在稀疏奖励环境中给智能体“梯度”信号。但要注意这个奖励不能给太大否则智能体会为了短期的“更接近目标”而忽略全局最优在死胡同里来回进出。经验值是时间惩罚的2倍左右我试过5倍效果反而不稳定。2.4 环境接口让MATLAB的rl函数能用你的环境如果你用Reinforcement Learning Toolbox需要把上面这些逻辑封装成环境对象。常见做法是用rlFunctionEnv或自定义类。我一般直接用深度网络和自定义训练循环在这个项目里更可控% 使用 rlFunctionEnv 的简单封装MATLAB R2020a obsInfo rlNumericSpec([1 5]); % 状态维度5 actInfo rlFiniteSetSpec([1 2 3 4]); % 4个离散动作 env rlFunctionEnv(obsInfo, actInfo, myStepFunction, myResetFunction);这个封装的好处是后续可以配合rlTrainingOptions直接训练坏处是调试时看不到中间量。新手建议先把step和reset函数写成普通脚本跑通一轮再封装进rl环境。如果一步到位封装出了问题只能看到训练曲线看不到具体状态变化排查效率极低。3. 搭建DQN网络结构的细节MATLAB里用dlarray自定义训练DQN的精髓在于用深度网络逼近Q值函数而不是查表。在MATLAB里可以走两条路用Reinforcement Learning Toolbox内置的rlDQNAgent或者用Deep Learning Toolbox的dlnetwork自定义。前者配置快后者灵活。对于最短路径这个任务内置Agent已经足够但你要理解它背后的默认参数才能调好。3.1 选择网络结构不是越深越好最短路径问题的状态空间并不复杂一个三层的全连接网络就够了。我用的是[5, 64, 64, 4]的结构——输入5维状态两个64节点的隐藏层输出4个动作的Q值。% 定义DQN网络结构 net [ featureInputLayer(5, Normalization, none) fullyConnectedLayer(64) reluLayer fullyConnectedLayer(64) reluLayer fullyConnectedLayer(4)];Normalization设成none是因为输入已经在getState里做过归一化不需要网络再做一次。隐藏层用relu而不是tanh因为relu在深层的梯度传播更稳定收敛更快。为什么不加卷积层卷积适合处理图像类状态我们这里是向量状态全连接层已经能表达。很多人一上来就套CNN结构结果参数爆炸、训练极慢完全没必要。如果地图特别大、状态包含局部视野图像再考虑用卷积——那种情况一般配合rlVectorImageObservation使用。网格最短路径问题全连接足够了。3.2 经验池和软更新两个影响稳定性的组件DQN的两个关键组件是经验回放池和目标网络。经验池打破样本相关性目标网络解决自举导致的不稳定问题。这两块代码不复杂但很多人在这里翻车。% 经验池存储结构 bufferSize 10000; stateBuffer zeros(5, bufferSize); actionBuffer zeros(1, bufferSize); rewardBuffer zeros(1, bufferSize); nextStateBuffer zeros(5, bufferSize); doneBuffer zeros(1, bufferSize); bufferIndex 1; bufferCount 0; % 存入一条经验 function storeExperience(state, action, reward, nextState, done) stateBuffer(:, bufferIndex) state; actionBuffer(bufferIndex) action; rewardBuffer(bufferIndex) reward; nextStateBuffer(:, bufferIndex) nextState; doneBuffer(bufferIndex) done; bufferIndex mod(bufferIndex, bufferSize) 1; bufferCount min(bufferCount 1, bufferSize); end经验池大小我推荐10000——足以覆盖多个episode的经验又不至于让旧经验在池子里待太久导致策略变化后旧经验拖慢学习。注意MATLAB的循环里写这些函数会慢建议采样时用向量化操作。目标网络更新有两种方式硬更新每N步直接把主网络参数复制过去和软更新每一步按比例τ融合。软更新更平滑是我常用的方式% 软更新目标网络 tau 0.005; targetNet.Learnables 0.995 * targetNet.Learnables 0.005 * net.Learnables;这个更新方式本质上是滑动平均让目标Q值的更新变得平缓避免因为主网络Q值突变导致学习震荡。τ太小比如0.001会让目标网络跟不上主网络训练变慢τ太大比如0.1又失去了目标网络的稳定性意义。0.005是我试过的比较合适的值。3.3 训练循环主体完整的一次DQN迭代把上面的组件串起来核心训练循环长这样。这是一段必须理解的代码直接决定了训练能不能收敛。gamma 0.95; % 折扣因子 epsilon 0.9; % 初始探索率 epsilonMin 0.05; % 最低探索率 epsilonDecay 0.995; % 每episode衰减 batchSize 32; optimizer adamupdate(optimizer, net.Learnables); for episode 1:maxEpisodes state resetEnvironment(); done false; while ~done % epsilon-greedy选择动作 if rand epsilon action randi(4); else qValues predict(net, dlarray(state, CB)); [~, action] max(extractdata(qValues)); end % 执行动作获取转移 [nextState, reward, done] simulateStep(state, action); % 存储经验 storeExperience(state, action, reward, nextState, done); % 从经验池采样并更新 if bufferCount batchSize idx randperm(bufferCount, batchSize); sBatch stateBuffer(:, idx); aBatch actionBuffer(idx); rBatch rewardBuffer(idx); nsBatch nextStateBuffer(:, idx); dBatch doneBuffer(idx); % 计算目标Q值 qNext predict(targetNet, dlarray(nsBatch, CB)); qTarget rBatch gamma * max(qNext) .* (1 - dBatch); % 预测当前Q值 qPred predict(net, dlarray(sBatch, CB)); % 只对选中的动作计算loss loss mse(qPred(idxTensor), qTarget); % 反向传播 gradients dlgradient(loss, net.Learnables); [net, optimizer] adamupdate(net, gradients, optimizer); % 软更新目标网络 targetNet.Learnables (1 - tau) * targetNet.Learnables tau * net.Learnables; end state nextState; end % epsilon衰减 epsilon max(epsilonMin, epsilon * epsilonDecay); end这段代码有几个关键点。predict(net, dlarray(state, CB))中的CB表示列向量和batch维度MATLAB的dlarray格式是CB——C是通道维度B是batch维度这是用dlnetwork做推理时的固定写法写错了会报维度错误。目标Q值的计算公式r gamma * max(qNext) * (1 - done)里的(1 - dBatch)很重要。终止状态没有下一时刻Q值如果不乘这个掩码终止状态的Q值会被高估导致网络学到错误的目标训练无法收敛。这是一行代码的问题但影响很大。mse(qPred(idxTensor), qTarget)的idxTensor需要构造——只取选中的动作对应的Q值。完整写法是qPred(sub2ind(size(qPred), aBatch, 1:batchSize))也就是对batch里每条样本只算出那个动作的预测值来和target比。如果直接把整个Q输出和target比网络会同时更新没选的动作训练效率低且不稳定。3.4 采样和训练参数的匹配关系上面的代码里batchSize选择32经验池10000这两个参数需要配合。经验池大了采样的多样性好但batchSize太小的话梯度估计噪声大batchSize太大训练速度快但容易过拟合近期经验。我一般在5×5小地图上跑500个episodebatchSize32够用20×20地图需要涨到2000个episodebatchSize可以保持32但需要把经验池提到20000。核心原则是地图变大时优先增大经验池而不是batchSize。经验池不够大时会明显看到训练曲线后期震荡因为更新输入的经验太相似了。另外要注意adamupdate的用法。MATLAB中的adamupdate需要维护两个状态变量optimizer包含动量项必须在循环前初始化optimizer adamupdate(optimizer, net.Learnables, gradients);如果写成了net adamupdate(...)MATLAB会报错或静默失败。我自己就吃过这个亏——训练曲线一直不降结果是optimizer状态根本没传递进去。4. 训练过程中的关键参数调优学习率、折扣因子和ε衰减4.1 一张参数表解决“默认值能不能用”的问题训练DQN最怕的不是网络结构不对而是超参数不匹配。每个超参数负责一个维度不能单独看。下表是我在这类任务上常用的起点值和调优方向参数起点值作用调优方向学习率α0.001每一步更新的步长loss震荡调小收敛太慢调大折扣因子γ0.95对未来奖励的重视程度地图大/目标远适当调高到0.99探索率ε起始0.9初始随机探索比例策略先随机后确定大点没关系ε最低值0.05保持最低探索太高则路径长且不稳定ε衰减率0.995/episode从探索到利用的速度地图大衰减慢一点0.99经验池大小10000样本多样性上限地图大调到20000batchSize32每次更新的样本数8-64之间试软更新τ0.005目标网络平滑度震荡时调小到0.001这张表的取值不是随便写的。比如γ0.95意味着一百步以后的奖励折算到现在只剩0.5%如果目标在100步以外网络基本学不到“到达终点”的奖励信号。所以地图大时调高γ到0.99相当于给远期奖励更多权重。4.2 学习率与loss曲线的对应关系学习率是最容易发现问题的参数。我通常的做法是先跑到loss不降然后看loss是“锯齿状震荡”还是“平滑下降”。锯齿状通常意味着学习率偏大梯度在最优值附近来回跨越平滑下降但极慢说明学习率偏小。但loss曲线在DQN里有个陷阱loss下降不代表策略变好。因为Q值是自举更新的loss降的是“预测Q值和目标Q值的差距”如果目标Q值本身是错的loss再低也没用。所以要结合“每个episode的总奖励”曲线判断。看总奖励曲线的趋势比看loss可靠得多。4.3 epsilon衰减在最短路径任务里的特殊作用epsilon是探索和利用的开关。在最短路径任务里“利用”意味着走当前认为的最短路径“探索”意味着尝试绕路。如果epsilon降得太快智能体可能只找到一条局部最短的路没有尝试过其他可能性降得太慢智能体学会了路径还一直在绕。我的经验值5×5地图500个episode足够epsilon从0.9按0.995衰减到0.05正好在400个episode左右用完。20×20地图需要2000个episode衰减率要降到0.99才能保持探索节奏。判断epsilon衰减是否合适的标准是——看后期episode里智能体是否还在“犯错”如果一条路径走了几步发现是死路还继续走说明探索率太高如果地形一变化就完全不会走了说明探索率太低。5. DQN最短路径的常见排查与避坑记录5.1 坑1训练时loss不降反升且伴随NaN现象训练到几十个episodeloss突然变成NaN之后训练彻底崩溃所有输出都是NaN。原因绝大多数是梯度爆炸。MATLAB的dlarray默认单精度如果网络权重更新幅度过大很快就溢出成NaN。另一个常见诱因是reward设计里出现了Inf——比如距离函数忘记了对数保护除零导致Inf。解决先检查reward计算里有没有0/0或log(0)然后降低学习率到0.0005重试。如果还出现NaN在计算loss后加一步gradients clipGradient(gradients, -1, 1)做梯度裁剪。5.2 坑2智能体学会撞墙——无效动作惩罚失效现象训练完成后智能体在模拟环境中不断撞墙原地打转但总奖励却不低。原因我在reward设计里把撞墙惩罚设在step函数外部而step函数返回的位置没更新时智能体以为走到了新位置但奖励和状态不匹配。比如状态更新时用了currentPos没碰撞的旧位置但reward算的是nextPos碰撞后的原地位置两者对不上网络学到的是“撞墙也能拿到正面的势能奖励”。解决把撞墙检测和位置更新、奖励计算放在同一个函数里确保位置没变时势能奖励为0且有明确负惩罚。调试时可以打印每一步的状态和奖励验证“位置更新”和“奖励计算”是否一致。5.3 坑3到达目标但路径明显绕路——时间惩罚力度不够现象智能体总能到终点但路径长度比Dijkstra算出的最优路径长30%以上且训练后期路径长度不再下降。原因时间惩罚-0.05相对于到达奖励50太小智能体只要到达终点就赚够分数“少绕路”这一点点损失根本不在乎。这就是典型的稀疏奖励掩盖了密集惩罚。解决把时间惩罚调大到-0.2同时把到达奖励降到20。保持比值关系让“少走一步”的收益足够明显。调完后路径长度会在训练后期显著下降。5.4 坑4训练曲线震荡但平均奖励在缓慢上升——soft更新τ过大现象每个episode的总奖励曲线振幅很大一会儿接近最优一会儿又跌回去平均趋势上升缓慢。原因目标网络软更新τ设成0.05目标Q值变化太快主网络追着变化的目标跑每一步梯度方向都不稳定形成震荡。解决把τ降到0.001~0.005之间目标网络变得相对稳定震荡幅度明显减小。注意τ不是越小越好太小会拖慢学习速度建议在0.001到0.01范围内网格搜索。5.5 坑5MATLAB Replay Buffer内存爆炸现象运行到几百个episode后训练速度越来越慢系统内存占用飙升。原因我用的经验池存储方式是每次storeExperience都对整个buffer数组做拼接操作相当于O(N²)内存拷贝。MATLAB的数组操作如果频繁resize会触发大量内存分配。解决把经验池预分配成固定大小的数组用索引环形覆盖我第3章代码里的bufferIndex mod(bufferIndex, bufferSize) 1就是干这个的。训练结束后再保存Buffer不要每步都写硬盘。如果数据量还得涨考虑用tall数组或者定期离线训练。6. 验证路径最优性把学到的策略和Dijkstra对比训练完成后真正证明“这个模型不是运气好”的方法是对比验证。我会把学到的策略在固定起终点上跑10次记录路径长度然后和Dijkstra算出的正解对比误差在10%以内算合格。% 用训练好的网络做一次路径规划 state getState(startPos, targetPos, gridSize); path startPos; while ~isequal(path(end,:), targetPos) length(path) 500 qValues predict(net, dlarray(state, CB)); [~, action] max(extractdata(qValues)); nextPos step(action, path(end,:), gridSize, obstacles); path [path; nextPos]; state getState(nextPos, targetPos, gridSize); end % path就是模型输出的路径对比Dijkstra结果注意这里有个实践细节模型输出的路径可能包含重复点撞墙卡住时位置不变。对比路径长度前先过滤掉连续重复的行否则一个死循环就能把长度拉得很长误判为模型很差。我还习惯做一步可选验证随机换起点终点观察策略是否稳定。DQN本质是从状态到动作的映射换起点不需要重新训练把新的起点状态喂进去就行。如果在没见过的起终点上也能走出较短路径说明学到的是“位置和目标的相对关系”而不是死记硬背了一条特定路线。这一步通过的话就可以放心把方案落地到实际项目里了。养成一个习惯训练过程中每隔50个episode保存一次网络参数这个“后悔药”虽然占点硬盘但能让你在训练发散回滚时不用从头再来。我用MATLAB的save(dqn_checkpoint.mat, net)保存优不优雅无所谓关键时刻能救命。以上这套流程跑通之后你会觉得DQN做最短路径没有那么玄学——它就是一个会试错的函数逼近器给它环境、给它反馈它就能帮你找路。希望帮到你。本文还有配套的精品资源点击获取