尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Dueling-DQN与双经验池的栅格地图路径规划MATLAB实践

基于Dueling-DQN与双经验池的栅格地图路径规划MATLAB实践 做这个课题之前我对路线规划的认知还停留在A*、RRT、人工势场法那一套。直到把Dueling-DQN和双经验池优先采样跑通在MATLAB里我才真正体会到强化学习路线规划跟传统搜索算法是完全不同的路子——前者的核心不是“怎么找路”而是“怎么让智能体学会找路”。这篇就当我的一次完整复盘从方案选型、环境搭建、网络实现到训练调参把能落地的细节和踩过的坑全写出来。这篇内容适合两类人一是想用MATLAB做强化学习课程设计、毕业设计的同学二是已经在做DQN系算法、想尝试在二维栅格环境下验证和改进算法的研究人员。我会把每一个关键步骤都讲透尤其是“双经验池”和“Dueling结构”这两块它们不是锦上添花而是能实打实提升收敛速度和最终路线的质量。1. 先搞清楚这套方案到底要解决什么问题1.1 为什么路线规划选强化学习而不是经典搜索地图已知的静态路线规划A*就能做得很好而且一定比你训出来的强化学习模型稳定。但如果地图中存在未知障碍、动态障碍或者环境经常变化经典图搜索就需要反复重新规划计算成本上来了。强化学习的路线规划思路不太一样智能体不需要每次从头搜索而是训练出一个“策略网络”输入当前位置和感知信息直接输出往哪走。这个思路放到机器人导航、仓储调度、游戏AI里都有实际意义这也是为什么近年强化学习路线规划的论文一抓一大把。但强化学习的问题也很直接——训练不稳定、收敛慢、奖励稀疏时很容易跑飞。本文用栅格地图作为验证环境本质上是一个离散状态、离散动作的问题非常适合用DQN家族算法来处理。1.2 Dueling-DQN和普通DQN的区别在哪里普通DQN的Q网络直接输出每个动作的Q值这种方式其实把一个决策信息混在一起了某个状态下到底“值多少钱”和“某个动作比别的动作好多少”这两件事被合并成了一个数值。Dueling-DQN把Q值的计算拆成两条支路一条输出状态价值V衡量“进入这个状态本身有多好”另一条输出优势函数A衡量“在当前状态下选择每个动作相对于平均水平好多少”。最后再把两条支路合并成Q值。合并公式是经典的那条Q(s, a) V(s) A(s, a) - mean_a A(s, a)。减去优势的均值是为了解决V和A的可辨识性问题防止两条支路各自漂移。实际效果就是网络对“哪些状态有价值”的表达更稳定在动作空间比较大、多个动作收益接近的场景下Dueling结构收敛速度明显快于普通DQN。路线规划恰恰就是这样一类问题——大部分空闲区域里四个动作的收益都差不多真正关键的是避开障碍、逼近终点这类状态价值差异。1.3 双经验池优先采样解决的是什么麻烦普通的DQN用经验回放把所有转移样本均匀随机采样。问题在于地图上绝大多数位置是普通格子走过去没有奖励一次训练里的有效样本可能只占一小部分。如果随机采样那些“靠近障碍物”“接近终点”的关键样本就很难被反复学习到。优先经验回放PER的思路是给每个样本算一个优先级优先级高的样本被采样到的概率更大。但直接用单一优先队列很容易走向另一个极端——高TD误差的样本总被抽中样本多样性变差训练后期容易过拟合甚至被少数异常样本带偏。双经验池的思路就是把经验按“重要程度”分到两个池子里高优先级池放TD误差大、或带奖励的样本普通池放日常样本。每次采样按固定比例从两个池子抽取既保证关键样本被高频学习又保留足够的样本随机性。这个思路很像人复习考试重点难点要多刷几遍但不能整本习题册只刷一道题否则考试换个题型就懵了。2. 环境建模与网络结构设计2.1 用MATLAB搭一个栅格地图仿真环境跑路线规划仿真环境越简单越容易验证算法本身。我用的是二维栅格地图尺寸设为12x12个格子0表示可通行1表示障碍物。这样状态空间是有限的智能体位置坐标row, col可以直接当成状态表示。% 构建12x12栅格地图, 0通路, 1障碍 map zeros(12, 12); % 手动设置几面墙 map(2, 2:4) 1; map(5:8, 3) 1; map(7, 6:9) 1; map(4:6, 9) 1; map(9, 2:5) 1; map(11, 8:11) 1; map(3, 11) 1; startPos [1, 1]; goalPos [10, 10];然后是环境的交互接口也就是step函数。每次智能体选择一个动作就更新位置返回下一状态、奖励、是否结束这三个值。越界撞墙、进了障碍格都当作“撞到障碍物”给予负奖励这一步的奖励设计直接决定智能体能不能学到“绕开障碍走过去”。% 简单示意单步环境转移逻辑 function [nextState, reward, done] envStep(state, action, map, goalPos) moves [-1 0; 1 0; 0 -1; 0 1]; % 上、下、左、右 nextPos state moves(action, :); % 撞墙或越界 if nextPos(1) 1 || nextPos(1) size(map,1) || ... nextPos(2) 1 || nextPos(2) size(map,2) || ... map(nextPos(1), nextPos(2)) 1 nextState state; reward -1; done false; return; end nextState nextPos; if isequal(nextPos, goalPos) reward 10; done true; else reward -0.02; % 每走一步给一个小惩罚鼓励最短路径 done false; end end每步-0.02这个惩罚看起来很小实际作用很大。如果没有这一步惩罚智能体学到的路径可能是“绕远路但稳到达”因为到达终点10并不会惩罚多余步数。加上这个微小惩罚之后策略才会向“少走格子”的方向倾斜。2.2 状态、动作与奖励函数怎么设计才合理状态设计上我直接用了坐标[row, col]网络输入维度是2。如果地图更大更复杂可以改成局部感知窗口比如以智能体为中心的5x5障碍物局部图再接上目标相对坐标这样泛化性会更好。但作为基础验证坐标输入足够而且收敛快适合快速迭代算法。动作空间是4方向移动上、下、左、右。4方向的优点是动作和栅格移动一一对应实现简单。想验证Dueling结构优势的话也可以换成8方向动作空间大了之后优势分支的作用会更明显。奖励函数是这次仿真收敛快慢的头号变量。我的设计是三层结构事件奖励值设计意图到达终点10强正向信号撞墙/越界-1告诉智能体这条边界不可走普通移动-0.02微惩罚抑制绕路这套奖励组合在测试中基本没有出现“原地不动”的退化情况。如果撞墙惩罚也是-1而普通移动没惩罚智能体很容易学会“撞一下墙再换个方向”因为撞墙本身不会带来额外损失反而可能试探出新路径。所以负奖励一定要保留。2.3 Dueling网络结构在MATLAB里怎么搭出来MATLAB的强化学习工具箱里有rlDuelingDQNAgent可以直接用但我更推荐自己把网络搭出来原因有两条一是自己能完全控制前向传播细节便于调试二是双经验池是自定义采样逻辑工具箱agent封装得太死反而不方便接入。自定义Dueling网络时我用了dlnetwork。先建共享特征提取层然后分裂成价值分支和优势分支前向传播函数返回Q值。具体做法是先建立一个输出维度匹配的层再在计算图中从特征向量里拆分出V和A。% 定义输入为2维状态, 输出为特征向量 featureLayers [ featureInputLayer(2, Normalization, none, Name, stateIn) fullyConnectedLayer(64, Name, fc1) reluLayer(Name, relu1) fullyConnectedLayer(64, Name, fc2) reluLayer(Name, relu2) fullyConnectedLayer(1 4, Name, fcOut) % 1个V 4个A ]; net dlnetwork(featureLayers);这里网络最后一层输出5个神经元第一个作为状态价值V后面4个作为每个动作的优势A。前向传播时手动合并成Q值function q computeQ(net, state) % state: 2x1 dlarray out forward(net, state); v out(1); a out(2:end); q v a - mean(a); end因为这个项目是我在MATLAB里自己实现训练循环的所以所有梯度更新都通过dlfeval和dlgradient完成。后面训练循环部分我再详细展开。3. 双经验池优先采样的MATLAB代码实现3.1 经验池的数据结构与写入逻辑双经验池不是两个独立的buffer这么简单关键在“如何划分”和“如何更新优先级”。我在实现时用一个结构体数组保存全部经验然后为每条经验记录一个priority字段采样时按priority选择进入哪个池子。具体结构是这样的% 经验池结构体 % exp.state - 当前状态 % exp.action - 执行动作, 1~4 % exp.reward - 即时奖励 % exp.nextState - 下一步状态 % exp.isDone - 是否终止 % exp.tdError - TD误差绝对值, 用于优先级计算每次环境交互产生一条经验先粗略估计这条经验的TD误差再决定写入哪个池子。实现上我分成两条路径如果TD误差超过阈值写入高优先池否则写入普通池。高优先池容量不够时就把池子中TD误差最低的一条丢出去。function pushExp(pool1, pool2, exp, thresh) if exp.tdError thresh if length(pool1) pool1Capacity pool1(end1) exp; else % 替换池中TD误差最小的样本 [~, idx] min([pool1.tdError]); pool1(idx) exp; end else % 普通池, 满则整体淘汰最旧样本 if length(pool2) pool2Capacity pool2(end1) exp; else pool2(1) []; pool2(end1) exp; end end end这个写入策略有个隐藏问题高优先池容易“内容固化”里面全是早期TD误差大的样本后期模型接近收敛时写入高优先池的样本变少池子里可能一直存着过期的高误差样本。所以我加了一个定期重算机制每训练200步就对全部经验重新算一次TD误差动态调整归属。3.2 优先采样比例与TD误差计算采样时我从高优先池取70%的样本从普通池取30%的样本合起来组成一个batch。这个比例不是拍脑袋定的我先试过50%分池效果跟纯随机采样差不多又试了90%高优先训练初期飞快但后期出现过拟合损失函数下到一定水平就停滞。70%是一个折中既给关键样本足够的出场机会又保证普通样本能持续修正网络的基本判断。TD误差的计算方式跟DQN一致用的是目标网络和当前网络Q值的差值function tdErr computeTdErr(net, targetNet, exp, gamma) qValue computeQ(net, dlarray(exp.state, CB)); nextQValue computeQ(targetNet, dlarray(exp.nextState, CB)); targetQ exp.reward; if ~exp.isDone targetQ targetQ gamma * max(nextQValue); end tdErr abs(targetQ - qValue(exp.action)); end把TD误差用于优先级更新这正是优先经验回放PER的本质。我稍微做了一点变形不是用严格的SumTree概率采样而是用分池加按比例采样效果上接近PER但实现简单很多也更容易在MATLAB里跑起来。3.3 采样训练循环的完整代码训练循环是整个仿真的心脏包含三层外层遍历回合中层控制单回合步数内层执行每个状态的动作选择和网络更新。我直接放出核心代码标注好每一步在干什么。% 训练主循环 numEpisodes 800; trainInterval 4; % 每4步训练一次 targetUpdateFreq 100; % 每100步同步目标网络 batchSize 128; gamma 0.95; learningRate 1e-3; epsilon 1.0; minEpsilon 0.05; epsilonDecay 0.995; thresh 0.5; % TD误差高优先级阈值 avgLoss zeros(numEpisodes, 1); avgReward zeros(numEpisodes, 1); totalSteps 0; for episode 1:numEpisodes state startPos; done false; epReward 0; stepCnt 0; lossSum 0; while ~done stepCnt 100 % 1. epsilon-greedy 选择动作 if rand() epsilon action randi(4); else qValues computeQ(net, dlarray(state, CB)); [~, action] max(qValues); end % 2. 环境交互 [nextState, reward, done] envStep(state, action, map, goalPos); epReward epReward reward; % 3. 计算TD误差并写入双经验池 exp struct(state, state, action, action, reward, reward, ... nextState, nextState, isDone, done, tdError, 0); exp.tdError computeTdErr(net, targetNet, exp, gamma); pushExp(pool1, pool2, exp, thresh); state nextState; stepCnt stepCnt 1; totalSteps totalSteps 1; % 4. 每隔若干步训练一次 if mod(totalSteps, trainInterval) 0 batch sampleDualPool(pool1, pool2, batchSize, 0.7); [loss, grad] computeDuelingGrad(net, batch, gamma); [net, ~] dlupdate(adamUpdate, net, grad, learningRate); lossSum lossSum loss; end % 5. 定期同步目标网络 if mod(totalSteps, targetUpdateFreq) 0 targetNet net; end end epsilon max(minEpsilon, epsilon * epsilonDecay); avgReward(episode) epReward; avgLoss(episode) lossSum / max(stepCnt, 1); if mod(episode, 50) 0 fprintf(Episode %d | Reward %.2f | Loss %.4f | Epsilon %.2f\n, ... episode, epReward, avgLoss(episode), epsilon); end end这里有一个很关键的细节targetNet net直接把当前网络参数复制给目标网络这种硬更新简单粗暴但注意更新太频繁会导致训练不稳定。我把它控制在100步更新一次实际测试稳定性不错。如果你想更平滑可以试试soft update即每次训练时把目标网络参数按tau的小比例向当前网络滑动% soft updatetau通常取0.005~0.01 newParams cellfun((cur, tgt) tau*cur (1-tau)*tgt, ... net.Learnables.Value, targetNet.Learnables.Value, UniformOutput, false, ... ErrorHandler, (varargin) varargin{2}); targetNet.Learnables.Value newParams;3.4 adamUpdate怎么在自定义训练循环里落地上面代码中用了dlupdate(adamUpdate, net, grad, learningRate)我用了自定义的Adam更新函数。MATLAB深度学习工具箱里没有直接给一个“输入参数就更新网络”的现成函数需要自己维护一阶矩和二阶矩。在训练循环外定义全局的矩估计变量% 训练前初始化Adam状态 m cell(size(net.Learnables.Value)); v cell(size(net.Learnables.Value)); beta1 0.9; beta2 0.999; epsAdam 1e-8;因为net.Learnables.Value在不同层、不同权重之间的存储结构是cell数组所以在Adam更新时要逐层逐参数更新。可以用一个循环遍历function net adamUpdateNet(net, grad, m, v, t, lr, beta1, beta2, epsAdam) params net.Learnables.Value; grads grad.Value; % 这里grad是dlgradient返回的梯度 for k 1:length(params) m{k} beta1 * m{k} (1 - beta1) * grads{k}; v{k} beta2 * v{k} (1 - beta2) * grads{k}.^2; mHat m{k} / (1 - beta1^t); vHat v{k} / (1 - beta2^t); params{k} params{k} - lr * mHat ./ (sqrt(vHat) epsAdam); end net.Learnables.Value params; end这一步实际运行中有个很容易踩的坑grad是在dlfeval内部计算出来的它的结构跟net.Learnables完全对齐吗不一定。如果你在模型函数中用了dlgradient(y, net.Learnables)梯度数组的顺序和网络参数顺序一致但维度信息可能带batch维度或额外维度。稳妥的做法是使用extractdata或stripdims处理后再更新。我在调试时先打印size(grads{k})和size(params{k})发现确实存在不匹配后来强制统一用extractdata拿原始数组再组合成dlarray才彻底解决。4. 训练过程、超参数选择与结果分析4.1 超参数配置照着改就能跑我把最终稳定运行的超参数整理成了表格这套参数跑12x12栅格地图500回合左右就能得到一条不错的路径。如果你的地图更大需要相应增大网络容量和训练回合数。参数值说明网络结构64-64两层全连接中间ReLU学习率1e-3Adam折扣因子0.95值太大前期收敛慢值太小容易短视Batch Size128过小临界大过大GPU/CPU占用高每步微惩罚-0.02抑制绕路撞墙惩罚-1让智能体学会避开边界到达奖励10核心正反馈epsilon初值1.0前期充分探索epsilon最小值0.05保留一定随机性epsilon衰减0.995每回合约500回合后接近最小值经验池容量高优先池2000条普通池8000条总容量10000双池采样比例高优先70%普通30%关键超参目标网络更新间隔100步硬更新4.2 训练各阶段的典型表现我把训练过程分成四个阶段这样你跑的时候可以对照一下自己的训练曲线在哪个阶段。探索期约第1到50回合epsilon很大智能体基本在地图上瞎撞撞墙全是负奖励平均回合奖励在-20到-50之间徘徊损失值抖动剧烈。这个阶段别急着调参是正常现象。学习期约第50到250回合智能体开始摸索出“走普通格子惩罚小、撞墙惩罚大”的规律平均回合奖励快速上升从负值拉升到正值的临界区域。损失值逐步下降但仍有波动。这个阶段最容易被误判为“不收敛”其实是在路上。稳定期约第250到500回合平均回合奖励稳定在8到10之间损失值减小到一个平台智能体已经能稳定到达终点。少数回合还会因为epsilon随机性绕路但整体路径质量越来越高。收敛期第500回合后epsilon降到0.05附近策略基本确定路径长度趋于平稳。我保存了这段训练结果最后展示的路径已经是一条非常贴近最优的折线路径。4.3 训练结果的可视化与分析我习惯每50回合生成一张当前策略的路径图这样能直观看到“智能体是怎么一步步学会的”。MATLAB里画轨迹只需要把智能体访问过的位置记录下来然后叠加到栅格地图上% 记录轨迹 trace startPos; % ...在循环里每次更新后记录 trace [trace; nextState]; ... % 画图 figure; imagesc(map); colormap(gray); hold on; plot(trace(:,2), trace(:,1), b-o, ... LineWidth, 1.5, MarkerSize, 4, MarkerFaceColor, b); plot(startPos(2), startPos(1), go, MarkerSize, 10, LineWidth, 2); plot(goalPos(2), goalPos(1), r*, MarkerSize, 15, LineWidth, 2); title(最终路线规划结果);收敛后的路径智能体基本能沿着障碍物边缘平滑绕过没有明显倒车和反复横跳。这说明Dueling结构在状态价值估计上确实更稳定。双经验池的作用则体现在收敛速度上我用普通DQN同环境同参数量做了对比实验普通DQN大约在350回合才开始稳定到达而双经验池Dueling-DQN在220回合左右就初次出现连续稳定到达训练效率提升大概三成。5. 实际运行中踩过的坑和排查技巧5.1 Q值发散的典型场景与对策我最开始跑的时候损失曲线直接拉飞到1e4以上看Q值输出动辄上千。排查下来最常见的原因是学习率过大。1e-2的学习率在这个任务上直接炸1e-3才稳。另一个原因是奖励量纲差异太大——到达奖励10TD误差本身在0到10之间如果网络输出Q值在数百说明梯度更新方向出了问题。如果遇到Q值发散我的检查顺序是先画Q值均值曲线看是否单调膨胀再检查奖励计算有没有出现意外的非线性。一个隐藏雷点如果目标网络和当前网络完全同步更新状态价值容易出现正反馈循环Q值越估越大直到发散。所以目标网络更新间隔一定要设得足够长。5.2 智能体原地转圈怎么办原地转圈是个常见问题。出现这个现象时我第一反应是检查撞墙惩罚和普通移动惩罚的比例关系。当撞墙惩罚和普通移动惩罚差得不明显时智能体发现“原地绕圈”和“往前走”平均收益差不多就容易陷入局部最优。我把撞墙惩罚拉到-1、普通移动惩罚压到-0.02之后原地转圈的问题基本消失。另外一个原因是epsilon衰减太慢训练后期随机探索过多智能体已经学会路径了还在到处乱试。可以适当加快epsilon衰减或者改用epsilon线性衰减到固定值后停止衰减。5.3 双经验池的“池折叠”问题双经验池有个比较隐蔽的问题当高优先池容量过大或者阈值设得太低时几乎所有样本都会被送进高优先池双池结构名存实亡。我刚开始阈值设0.1跑完一看高优先池里有8000多条这不就等于没有分池吗后来把阈值调成动态值比如每100步重新取当前所有TD误差的中位数作为新阈值效果比固定阈值好很多。这样前期的关键样本和中后期的关键样本都能被动态识别不会出现“池子折叠”。5.4 快速调试的三条实用技巧调试强化学习代码比调试普通代码麻烦因为即使逻辑有bug程序也不一定报错只是训练结果不对。我总结了三个特别有用的排查手段。第一个是固定随机种子。在MATLAB训练前执行rng(0)确保每次跑出来的结果一致这样你调参时的对比才有效。不加种子光随机性就能让同样的代码跑出差别很大的结果。第二个是减少地图规模来验证算法正确性。我调试时先用5x5无障碍地图跑通整个流程确认能收敛到最短路径后再扩大地图、加障碍。这样能快速区分是“算法实现问题”还是“环境复杂度过高问题”。第三个是记录并输出Q值矩阵。在路线的关键位置比如靠近障碍的格子打印该状态下四个动作的Q值可以直接看出网络有没有把“远离障碍”的方向估得更高。如果Q值矩阵乱成一团优先检查奖励函数和TD误差计算如果Q值矩阵已经很合理但路径还是不对问题出在动作选择或epsilon策略上。6. 这套方案还能往哪些方向扩展做完这个基础仿真之后我发现这套框架的可扩展性其实很够用。如果你想在这个项目基础上继续做点什么我建议两个方向。第一个是把栅格地图换成连续坐标空间动作从离散的四方向变成连续的速度和转角。这时候DQN家族就不够了需要换上策略梯度类算法比如DDPG、TD3或者PPO。但经验池的思想仍然适用双经验池优先采样可以无缝迁移到这些算法里解决连续控制中关键样本稀疏的问题。第二个是加入动态障碍物。当前地图是静态的智能体只需要学会一条稳定的路径。如果障碍物会移动就从“路径规划”升级成了“避障决策”。双经验池的优势会体现得更明显因为“障碍物刚好挡在前进方向上”这类样本在随机探索里占比很低优先级机制能保证这类关键样本被高频学习到。双经验池本身也可以扩展成多经验池比如三个池子分别服务于探索、利用和稀疏奖励样本。在更复杂的任务中这种“分池调度”的思路比单一优先级队列更有工程价值。MATLAB里改结构也非常方便无非就是多几个struct数组和对应的采样逻辑。最后分享一个我自己的体会强化学习仿真项目真正花时间的往往不是算法推导而是环境交互和训练循环里的无数个细节。写这个双经验池Dueling-DQN项目从能跑通到跑得稳前前后后调了两周。但只要把这套流程吃透了再去接触其他强化学习项目你会发现自己已经能够快速定位问题、调参、对比实验了。这大概才是这个课程设计或者研究项目给你留下的真正资产。
返回列表