news 2026/10/17 4:25:05

用DQN强化学习做栅格地图路径规划:Matlab实现与调参实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用DQN强化学习做栅格地图路径规划:Matlab实现与调参实战

提到二维栅格地图路径规划,绝大多数人脑子里先蹦出来的是A*——这个结论在我做仿真之前也一样。后来我逐渐发现,传统规划算法能在完全已知、静态的栅格环境里给出漂亮的最优路径,可一旦环境信息不完整,地图还频繁变化,重规划的时间和算力成本会很快把优势吃掉。于是我开始尝试用Deep Q Learning(DQN)来求解二维栅格地图路径规划问题,并在Matlab里完整实现了环境搭建、网络训练和路径可视化。整个过程让我对强化学习有了更实际的理解,也踩了不少坑。这篇博文就是一次比较完整的复盘,适合正在用强化学习做路径规划的学生、研究者,以及想从传统算法切换到深度学习思路的工程人员。

1. 为什么栅格路径规划迟迟没被深度学习接管

在讨论DQN之前,有必要先把传统路径规划方法为什么“够用但不完全够用”这件事说清楚。二维栅格地图是路径规划里最经典的测试床:地图被离散成一个个格子,每个格子要么是自由空间,要么是障碍物。机器人的任务就是找一条从起点到终点、避开障碍物的路径。这个问题看起来很直观,但在算法层面却有完全不同的解决路径。

1.1 传统规划算法的强项和软肋

经典做法里,Dijkstra算法按照边的权重均匀向外扩展,可靠但效率一般;A在Dijkstra的基础上加入了启发函数,只要启发函数满足一致性,就能在静态地图上找到最优路径,而且速度非常快。RRT/RRT则更适合连续高维空间,在栅格地图上往往不如A直接。我自己做测试时,10x10的栅格地图用A往往瞬间出结果,路径长度也漂亮,这也是很多人不愿意换成深度学习方法的原因。

但传统算法有一个基本假设:环境必须是已知的、静态的。A需要在规划之前拿到完整的障碍物分布,一旦地图发生变化,之前算出来的路径就可能作废,必须重新规划。在稍大一点的地图里,频繁重规划会带来明显的计算开销。更麻烦的是,当机器人只有局部传感器时,它根本拿不到完整地图,A的全局搜索优势就发挥不出来。这些场景让我意识到,路径规划也许可以从“搜索一条路径”变成“学习一种避开障碍走向目标的策略”。

1.2 DQN迁移到路径规划的逻辑

强化学习恰好提供了这个视角。路径规划被建模成一个序贯决策问题:智能体在每个时刻观察环境状态s,选择一个动作a,环境返回奖励r和下一个状态s',智能体的目标是最大化累计折扣奖励。Q-learning通过维护一张Q表来记录“在状态s下执行动作a的价值”。二维栅格地图的状态数量会随着地图尺寸平方增长,地图稍大,Q表就爆炸了,而且很多相似状态无法共享经验。

DQN做的事情很直接:用深度神经网络去拟合Q函数Q(s,a),输入是状态描述,输出是每个动作的Q值。这样网络自动完成了“观察障碍物→推断当前位置风险→评估哪个方向更可能到达目标”的特征提取过程。加上经验回放和目标网络,训练过程能稳定不少。这听起来像是把路径规划问题变成了一个带感知的决策问题,很适合在部分可观测或动态环境中做扩展。不过,它在样本效率上远不如A*,因为需要大量试探才能学到稳定策略。这也是很多人对DQN路径规划的第一印象:能跑通,但训练慢,调参麻烦。接下来我会从状态、动作、奖励这三个最基础的设计点说起,因为这三个点直接决定你的网络能不能学出来。

2. 用DQN之前必须先想清楚的三件事:状态、动作与奖励

我在最开始写DQN时,犯过一个典型错误:以为只要有地图和坐标,网络就能自己搞定一切。结果训练了半天,智能体只会原地打转。原因很简单,状态给的信息不够,动作定义不合理,奖励又把智能体带偏了。这三件事是DQN落地之前就得想清楚的,后面所有网络结构、超参数调整都是为它们服务。

2.1 状态表示:别把二维栅格一股脑扔进网络

状态是神经网络的输入,它决定网络能依靠哪些信息做出决策。第一种思路是把整张地图的障碍物分布、当前智能体位置、目标位置拼成多通道图像,然后用卷积网络处理。这种方案信息最完整,但地图一换尺寸,网络输入维度就变了,而且全局地图在真实机器人上往往拿不到。第二种思路是只使用当前智能体周围的局部窗口,例如以当前位置为中心的5x5或7x7邻域,再拼上目标相对位置。这样做更贴近真实机器人靠传感器感知局部环境的情况,而且输入维度固定,网络规模可以控制。

我在Matlab里默认用7x7局部窗口,加上当前智能体与目标的归一化坐标差,一共51维输入。局部窗口本身是一个二维矩阵,为了边界统一,我会把地图边界外的格子都当作障碍物处理。这个处理很重要,否则智能体在边界附近时窗口会出现“空洞”,网络会学到不一致的模式。如果你用全局地图,可以用多层通道:障碍物通道、目标位置通道、当前位置通道,这样网络更容易分辨不同语义信息。我的建议是:先做局部窗口方案,状态维度小,训练快,调试简单;想进一步提升性能再切换全局卷积方案。

2.2 动作空间:上下左右还是八方向

动作空间决定智能体能往哪些方向移动。二维栅格地图上最自然的选择是四方向:上、下、左、右。四方向的好处是动作代价一致,奖励设计简单,碰撞处理直观。八方向允许斜向移动,路径通常会更快,但斜向移动和直向移动的代价不同,奖励需要重新设计;而且斜向移动容易在障碍物边缘“钻墙角”,状态空间复杂很多。

对于新手,我强烈建议先用四方向跑通。以10x10地图为例,四方向的输出层就是4个Q值,即使随机策略也能在几百回合内看到起色。动作的执行必须配合边界和障碍物判断:如果目标格子是障碍或者超出地图范围,智能体位置保持不变,同时给予一个负奖励。这样做的目的是让智能体学会“撞墙是不好的”,而不是让动作变得非法。不要一开始就做动作掩码,先把奖励惩罚机制跑通,后面再考虑更精细的处理。

2.3 奖励函数:稀疏与密集之间的平衡

奖励函数是DQN里最影响收敛速度的设计。最稀疏的形式是:到达终点给+10,碰撞障碍给-1,其他每一步给-0.05。这样智能体只能在成功到达终点时获得大正奖励,学习信号少,收敛慢,但学到的东西往往更稳健。我一开始追求收敛速度,加了密集奖励:每一步根据与终点距离的变化,靠近就给正奖励,远离就给负奖励。结果智能体出现了一种很搞笑的行为——在终点附近来回横跳,因为这种动作可以反复获得“靠近”奖励,虽然永远不会到终点。

之后的改进方案是保留每步-0.05的小惩罚,但不再每一步给距离奖励,而是在成功到达终点时给+10,同时记录当前回合步数,如果步数超过上限仍然结束这一回合但不给正奖励。这样做虽然训练慢一点,但策略比较干净。如果你确实需要密集引导,可以把距离奖励做成“势能函数”的形式,系数取非常小,比如0.1,而且必须保证到达终点后立即结束,不留给智能体刷奖励的机会。奖励函数的本质是在告诉智能体“什么行为是有价值倾向的”,但它也要够简单,不要让智能体找到规则漏洞去刷分。

2.4 网络输出层与Q值的含义

网络结构并不复杂。输入状态是51维向量,我用三个全连接层:128、128、4。最后一层是动作数量,输出每个动作的Q值。Q值的含义是:从当前状态出发,执行这个动作之后,按照当前策略继续走的累计折扣回报期望。网络训练的损失函数就是让预测Q值逼近目标值,目标值由真实奖励和下一状态的最大Q值计算。

在Matlab里构建这样的网络很简单:

layers = [ featureInputLayer(51, 'Normalization', 'none') fullyConnectedLayer(128) reluLayer fullyConnectedLayer(128) reluLayer fullyConnectedLayer(4) ]; net = dlnetwork(layers);

如果你的状态是地图图像,可以把featureInputLayer换成imageInputLayer,后面接卷积层。但我要提醒一点:神经网络不是越大越好。对于10x10栅格、51维输入,128宽度的全连接层已经足够。过大的网络反而更容易过拟合训练地图,并且增加每个回合的训练耗时。先把小网络跑通,再考虑加深。

3. Matlab版实现:环境构建、网络定义与训练主循环

这一部分是整篇博文最需要动手的部分。我不打算贴一整份完整代码,因为那样会淹没在细节里。我更想把实现拆成三个模块:环境交互、网络维护、训练更新。理解了这三个模块之间的关系,你就能根据自己的地图尺寸改出可用版本。

3.1 环境交互模块:用一个类把栅格地图封装起来

环境模块负责维护地图、智能体位置、起点、终点,对外提供两个关键方法:reset和step。reset把智能体放回起点,并返回初始状态;step接收动作,计算下一状态、奖励和是否结束。把环境写成独立类,不仅代码清晰,也方便后续改地图尺寸或奖励规则。

Matlab里可以用handle类实现:

classdef GridEnv < handle properties map agentPos startPos goalPos actions = [-1 0; 1 0; 0 -1; 0 1]; % 上下左右 maxSteps = 200; stepCount = 0; end methods function env = GridEnv(map, startPos, goalPos) env.map = map; env.startPos = startPos; env.goalPos = goalPos; env.agentPos = startPos; end function obs = reset(env) env.agentPos = env.startPos; env.stepCount = 0; obs = env.getState(); end function [obs, reward, done] = step(env, action) env.stepCount = env.stepCount + 1; delta = env.actions(action, :); newPos = env.agentPos + delta; % 检查越界或障碍 if newPos(1) < 1 || newPos(1) > size(env.map,1) || ... newPos(2) < 1 || newPos(2) > size(env.map,2) || ... env.map(newPos(1), newPos(2)) == 1 reward = -1; done = false; else env.agentPos = newPos; if isequal(env.agentPos, env.goalPos) reward = 10; done = true; else reward = -0.05; done = false; end end if env.stepCount >= env.maxSteps done = true; end obs = env.getState(); end end end

getState方法负责生成局部窗口和目标相对向量。生成局部窗口时,我会遍历当前点周围7x7范围,遇到越界位置补1,遇到障碍物也置1,剩余位置用0表示自由空间。这个状态向量可以迭代拼接成一维向量。这里注意,坐标顺序不要弄反,行索引对应y方向,列索引对应x方向。我在调试时曾经因为行列顺序不一致,导致智能体总是在错误的方向上撞墙。

3.2 网络构建与目标网络初始化

DQN训练需要两个网络:当前网络用于选择动作和更新参数,目标网络用于计算目标Q值。目标网络在一开始是当前网络参数的拷贝,之后每隔一定步数同步一次。这样做的目的是让目标值相对稳定,避免训练过程中“目标跟着预测一起跑”导致发散。

在Matlab中,构建当前网络后,用dlnetwork初始化,然后通过net.Learnables赋值给目标网络。如果你用的是自定义训练循环,必须注意targetNet不需要梯度更新,所以在计算目标Q值时,要专门用targetNet前向传播。我习惯在每200步同步一次目标网络,步数太短目标波动大,太长则目标过时。你也可以尝试软更新,每次用一小部分当前网络参数去“混合”目标网络参数,效果通常更平滑。

3.3 经验回放与梯度更新

经验回放是DQN稳定性的关键。每个回合里的转移数据(s, a, r, s', done)会被存进一个固定容量的循环队列。训练时随机抽取一小批经验来更新网络,这样能打破相邻样本之间的时间相关性,避免网络被同一个回合内的连续状态带偏。

核心更新逻辑可以这样描述:从经验池采样一批数据,用当前网络计算这批状态的预测Q值,再取出其中对应所选动作的Q值;用目标网络计算下一状态的最大Q值,按公式算出目标值r + gamma * max(Q_target(s')) * (1 - done);接着计算均方误差损失,向当前网络反向传播梯度,用Adam优化器更新。

在Matlab里,如果要自己实现,可以这样写示意:

qValues = extractdata(forward(net, dlX)); % 当前状态所有动作Q值 q_sa = qValues(sub2ind([batchSize, numActions], (1:batchSize)', actions + 1)); maxQNext = max(extractdata(forward(targetNet, dlXNext)), [], 2); y = rewards + gamma * maxQNext .* (1 - done); loss = mse(dlarray(q_sa, 'CB'), dlarray(y, 'CB')); gradients = dlgradient(loss, net.Learnables); net = adamupdate(net, gradients, iteration);

这段代码省略了dlarray维度格式和一些类型转换,但核心逻辑是完整的。实际编码时也可以考虑用Reinforcement Learning Toolbox里的rlQAgent,它帮你封装好了这些细节;但如果你希望彻底理解DQN原理,还是建议自己实现一遍。我在初学时就是先自己写,再回头看工具箱文档,很多以前不理解的参数瞬间就通了。

3.4 训练流程:回合、步数和epsilon衰减

DQN训练最外层的结构就是多回合循环。每回合重置环境,智能体在当前状态下根据epsilon-greedy策略选动作:以epsilon概率随机探索,否则选择当前网络输出最大的动作。执行动作后存经验,更新网络,直到回合结束。每完成一个回合,统计该回合总奖励和步数,同时按计划衰减epsilon。

一个典型的训练流程如下:

  1. 重置环境,获取初始状态。
  2. 按epsilon概率随机动作,否则用当前网络得到最优动作。
  3. 执行动作,得到下一个状态、奖励、是否结束。
  4. 将经验存入回放池。
  5. 当回放池样本足够,随机采样batch更新当前网络。
  6. 每隔固定步数同步目标网络权重。
  7. 回合结束,记录总奖励和步数,衰减epsilon。

这里的细节比较多,我建议把训练过程封装成一个函数,每一步都打印关键信息。否则一旦网络不收敛,你很难判断是奖励问题、状态问题还是更新逻辑问题。打印内容至少包括当前episode、epsilon、平均奖励、平均步数和近100回合的成功率。

4. 训练曲线、调参表和踩过的坑

代码跑起来之后,真正折磨人的是训练过程。DQN的训练曲线不像监督学习那样平滑,它可能前期长期在低位徘徊,突然出现一段跳跃,然后又震荡回去。如果你没有心理预期,很容易以为程序写错了。下面我直接说一些可复现的调参结论和我在实战里踩过的大坑。

4.1 训练曲线通常长什么样

在一个随机生成障碍物的10x10栅格地图上,如果状态、动作、奖励都按上面说的设计,你会发现前几十个回合里奖励基本都是负数,因为智能体完全在乱撞。随着epsilon从1.0逐渐衰减,网络开始记住一些成功到达终点的经验,奖励曲线会出现明显上升。但这个过程不是线性的,而是会有多次回退,有时连续几十回合成功率降为零,然后又突然恢复。这大概率是目标网络同步导致的目标值变化,不一定是代码写错了。

我习惯每隔100个回合记录一次“去探索成功率”,也就是把epsilon强制设为0.1以下,测试固定策略下成功到达终点的比例。这个指标比总奖励更能反映策略质量。对于10x10地图,我的经验是:学习率5e-4,gamma 0.95,batch size 64,经验池容量2万,目标网络每300步同步一次,大约在300到500回合之后成功率能稳定到80%以上。如果你发现1000回合仍然完全没起色,那一定是状态、奖励或更新逻辑的问题,而不是再调大网络就能解决的。

4.2 推荐超参数速查表

以下是我在Matlab里反复调试后觉得比较稳妥的起点值,你可以把它当作基准,再根据自己的地图尺寸修改:

参数推荐值说明
学习率5e-4 到 1e-3太高会震荡,太低收敛慢
折扣因子γ0.9 到 0.99越大越看重长期收益
初始epsilon1.0前中期充分探索
最终epsilon0.05 到 0.1后期以利用为主
epsilon衰减每100回合乘以0.85,或线性衰减衰减太快会困在局部最优
经验池容量1e4 到 1e5容量过小容易忘记学过的经验
batch size32 到 64过大会增加单次更新耗时
目标网络同步频率200 到 500步不同步或太频繁都会不稳定
网络宽度128 或 256地图更大时可以加宽

我特别想强调epsilon衰减:如果衰减太快,智能体还没有充分探索就过早开始利用当前策略,很容易学到一个“只会直走撞墙”的局部策略。如果衰减太慢,后期仍然是大量随机动作,网络学不到精确Q值。一个折中方案是分段衰减,前200回合epsilon保持0.5以上,之后慢慢降到0.1。

4.3 三个典型问题与解决思路

第一个问题是“终点附近徘徊”。现象是:智能体能走到终点附近,但就是不进入终点,或者反复横跳。原因大概率是奖励函数给了过多的“靠近终点”奖励,智能体找到了刷奖励的循环。解决方法是回收密度奖励,回到稀疏奖励,同时给每步负惩罚。如果你确实需要引导,可以把“第一次接近终点”当作一次特殊事件给予额外奖励,之后不再重复给。

第二个问题是“Q值高估导致策略漂移”。DQN天然存在Max操作带来的高估问题,目标网络只是缓解,并没有消除。训练后期你会看到目标Q值和当前Q值差距越来越大,成功路径反而变差。这时候可以考虑Double DQN:用当前网络选择最优动作,再用目标网络计算该动作的Q值。这是一个很小的改动,但对稳定性提升非常明显。

第三个问题是“固定地图过拟合”。如果你只在同一张栅格地图上训练,智能体可能学会“背地图”,而不是学会“避开障碍”。把地图随机化,每次回合开始时重新生成障碍布局,能迫使网络学习到更泛化的避障策略。当然,随机地图也会让训练难度增大。我的建议是先固定地图跑通,再切换成随机地图测试泛化性能。

4.4 评估路径规划质量的几个指标

训练结束后,别只看网络参数,要用测试指标来说话。我常用这几个维度:

  • 成功率:连续测试100回合,统计成功到达终点的比例。
  • 平均步数:成功路径的长度是否偏离最优解太远。
  • 路径重复率:观察轨迹是否在某个区域反复绕圈。
  • 泛化成功率:换一张训练时没见过的新地图,看策略是否仍然有效。

如果泛化成功率很低,说明网络只是在记忆训练地图。可以把训练地图从单张改为多张,或者临时在环境中加入一些随机障碍。评估时要把epsilon强制调低,比如0.02,这样才能看出“学到的最优策略”而不是“随机碰运气”。

5. DQN路径规划的使用边界与后续改进

跑通DQN路径规划之后,我并没有觉得它能替代A*。相反,我越来越清楚它适合什么、不适合什么。这个认知可能比代码本身更重要。

5.1 什么时候该用DQN,什么时候不该用

如果环境完全已知、静态、需要严格最优路径,比如仓储机器人在固定地图上每天走同一路线,直接用A*或者JPS这类搜索算法就好,又快又稳。DQN训练半天,还可能得到一条多绕几步的路径。

但如果环境存在不确定性,机器人只能通过传感器获得局部地图,或者障碍物会动态变化,DQN的价值就体现出来了。它学习到的是一种“决策策略”:看到前方障碍就转向,看到目标就靠近。这种策略天然具有应对环境变化的能力。另一个常见用途是作为混合规划器:先用A*生成全局参考路径,再用DQN在局部做避障微调,两个方法各管一段。我在做后续仿真时,这种混合架构的成功率和稳定性比单独用DQN好很多。

5.2 从DQN到更高效的变体

如果你已经不满足于基础DQN,有两条升级路线值得一试。一条是在DQN内部做改进,例如Double DQN解决Q值高估、Dueling DQN把状态价值和动作优势分开建模、Prioritized Experience Replay让重要样本以更高概率被采样,这些改进可以叠加。另一条是直接从动作空间入手,如果机器人可以在连续方向上移动,DQN的离散动作就不够用了,此时应该考虑DDPG、TD3或SAC这类连续控制算法。

坦白说,基础DQN在二维栅格地图上的优势更多是“框架简单、可视化直观”,真要追求路径质量,上面的这些变体每一项都能带来实打实的提升。尤其是Double DQN和优先经验回放,几乎是必修课。Matlab里实现这些变体时,主要改动都在损失函数计算和采样逻辑上,基础架构不需要推翻。

5.3 在Matlab里把Demo推向实用

如果要让这个Demo更有实用价值,我建议做三件事。第一,把地图尺寸放大,同时改用卷积网络处理局部窗口图像,而不是手工拼接的一维向量,这样能明显提升泛化能力。第二,在环境模型中加入动态障碍物,例如某些格子的障碍状态每隔固定步数发生变化,状态里加入障碍物速度或时间步编号,让网络学习时间维度上的避障。第三,训练结束后把网络参数导出,写成独立的评估函数,这样可以直接在Simulink中调用,或者生成C代码部署到嵌入式设备。这部分工作更多是工程层面的打磨,但会让你的研究从“小Demo”走向“可交付的方案”。

我在实际仿真里还有一个体会:不要在第一次训练时就追求完美地图和复杂奖励。先用5x5地图、一个障碍物,把“智能体能学会到达终点”这件事验证通过,再逐步增加障碍数量、地图尺寸和随机性。每一个阶段只引入一个变量,否则一旦训练失败,你根本不知道问题出在哪一步。

最后再分享一个小技巧:把训练过程中的状态、动作和Q值输出到日志文件里,尤其是失败回合的完整轨迹。很多看似玄学的问题,其实都能从轨迹里发现端倪——比如智能体在某个固定地方反复撞墙,大概率是局部窗口填充逻辑有问题;比如明明下一步就是终点却不进去,大概率是奖励函数或者目标网络更新出了问题。DQN路径规划本质上还是一个调试问题,耐心把数据流看明白,比盲目调超参数有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 4:25:03

MFC顶层父窗口创建:解决多窗口遮挡的窗口框架搭建指南

做Windows客户端开发的人&#xff0c;大概率都遇到过这种场景&#xff1a;两个功能窗口同时弹出来&#xff0c;新窗口永远盖在旧窗口上面&#xff0c;用户在任务栏里来回切换&#xff0c;界面像拼图一样被遮挡得乱七八糟。这个问题在MFC里尤其常见&#xff0c;因为很多人的第一…

作者头像 李华
网站建设 2026/10/17 4:24:57

VSCode插件自动消失?六大根因与排查解决指南

1. 先把现象搞清楚&#xff1a;你的"自动卸载"到底是哪一类我在日常开发和给同事排障的过程中&#xff0c;遇到过很多次"插件又没了"的反馈。说实话&#xff0c;"VSCode 自动卸载插件"这个说法&#xff0c;本身涵盖了好几种现象&#xff0c;而它…

作者头像 李华
网站建设 2026/10/17 4:24:17

ELK容器化部署避坑指南:从Docker Compose到日志语义建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 4:24:11

OpenClaw本地部署实战:搭建可调工具的AI代理环境全指南

老实说&#xff0c;OpenClaw 加本地大模型&#xff0c;再配上一堆自定义 skill&#xff0c;这套组合本身并不算复杂&#xff0c;真正折腾人的是"本地"两个字。我前前后后花了两周才把一套能稳定跑起来的环境搭好&#xff0c;中途踩了不少文档里根本不会写的坑。这篇文…

作者头像 李华
网站建设 2026/10/17 4:23:47

深入理解MMDetection配置系统:从跑通到精通的第二篇

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华