简介:面向无人机导航、路径规划及强化学习方向的技术研究与开发人员,尤其适合已了解Q-learning基本思想、希望将算法落地到三维航迹规划的读者,这套基于Matlab的完整方案覆盖了从低空环境建模、障碍物分布设定到算法验证与结果分析的闭环流程。整包共14个文件,核心为10个Matlab脚本,分别承担地形建模、障碍物设置、奖励函数设计、Q表更新与主流程串联;另附3张仿真结果截图和1份学术说明文档,便于直观对比避障效果并查阅实验分析。资源总计242KB,轻量便携,已有240人学习下载。借助该资源可理解状态空间、动作空间与奖励函数的设计细节,并完整复现Q-learning训练与测试流程;仿真结果表明算法能有效避开低空限制区与障碍物,兼顾路径安全与航迹长度。文档还结合运行结果讨论了收敛速度、参数敏感性等算法局限,给出后续改进方向,便于研究者依自身场景做参数调优与课题延伸。
1. 为什么说Q-learning做无人机三维航迹规划,瓶颈不在算法在地图粒度
第一次用Q-learning做无人机低空限制地图避障时,我跑了一整晚训练,第二天看轨迹图,无人机在山谷里绕了二十多圈才到终点,还贴着禁飞区边缘飞。后来才发现,问题根源不在算法收敛性,而在地图建模粒度——栅格选得太细,状态空间膨胀到Q表装不下;选得太粗,路径直接穿山。这篇笔记围绕MATLAB环境下用Q-learning做低空限制地图避障的完整链路展开:从DEM地形栅格化、禁飞区数字化、限高面叠加,到动作空间与奖励函数设计、训练循环实现、运行结果判读,再到五条高频踩坑的排障记录。适合正在做无人机三维航迹规划课题、想用强化学习替代传统规划算法、或者已经跑通二维Q-learning正准备向三维扩展的开发者。
2. 为什么三维航迹要选Q-learning:从MDP建模到收敛条件
2.1 Q-learning与A*、人工势场的选型差异:全局已知时它并不占优
先说结论:如果地图完全已知、长期不变、状态空间不大,A会比Q-learning快得多也稳得多。Q-learning的价值不在"更快找到路径",而在"学会一种策略"。同一张低空限制地图上要反复规划不同起终点的航线时,A每次都要全图搜索,Q-learning只需要查表,训练一次长期复用。这个差异在三维栅格地图上尤其明显,因为三维A*的启发函数设计难度比二维高,而且禁飞区、限高面这类叠加约束很难写进启发式里。
人工势场法的问题则是局部极小。无人机在狭窄山谷或者禁飞区包围的走廊里,引力势场和斥力势场很容易出现零合力点,无人机悬停或震荡。Q-learning不依赖势场模型,它把规划建模成序贯决策,通过试错积累Q值,天然跳过"力平衡"这个陷阱。代价是训练时间,但训练时间是离线成本,运行时的查表决策是微秒级的。
2.2 状态、动作与奖励:把无人机飞行决策写成MDP
Q-learning处理三维航迹规划,核心是把问题映射成马尔可夫决策过程四元组。状态空间定义为离散化的三维栅格坐标,动作空间定义为相邻栅格间的位移,状态转移在确定性环境下概率为1,奖励函数则是地形代价、禁飞区惩罚、目标奖励、航程代价的加权组合。
映射关系如下:状态s是栅格坐标(x, y, h),h是高度层;动作a是向六个邻域移动一格;奖励r由当前动作的结果决定。折扣因子γ设为0.9时,未来第10步的奖励折现后只剩约0.35,这意味着算法更看重近期收益,轨迹会倾向于尽快接近目标而不是绕远路。三维与二维的本质差别不在算法,而在状态空间规模——高度维把栅格数乘了一个因子,Q表增长是乘积性的,这正是三维实现里最容易失控的地方。
2.3 ε-greedy与Q表更新:训练收敛的两个观察点
Q-learning的更新公式是Q(s,a) ← Q(s,a) + α[r + γ·max(Q(s',a')) - Q(s,a)]。括号里的差值叫时间差分误差,表示实际收益与原有估计的偏差。α是学习率,控制每次更新的修正幅度;γ是折扣因子,控制远期收益的权重。训练采用ε-greedy策略:以概率ε随机探索,以概率1-ε选择当前Q值最大的动作。ε从0.9起步按轮次衰减到0.1左右,保证前期充分探索、后期稳定利用。
收敛观察有两个可靠指标。一是Q表变化量,每隔若干轮计算一次全表更新幅度,幅度趋近于零说明Q值稳定;二是成功率曲线,用当前策略在测试集上跑,如果成功率稳定在某个区间不再上升,基本可以判断训练完成。只看累计奖励曲线不靠谱,因为奖励塑形会让数值偏高,后面第5章会专门讲这个坑。
3. 低空限制地图建模:DEM栅格化与禁飞区数字化
3.1 低空限制地图的数据结构:地形、禁飞区与限高面
低空限制地图和普通障碍物地图的区别在于,约束来自三个叠加的层面。第一个层面是地形高程,地图范围内有一座山或连续山脊时,飞行高度低于地形的栅格全部不可通行;第二个层面是禁飞区,水平投影落在禁飞区内的所有高度层都不可通行,这是对空域规则的硬约束;第三个层面是限高面,空域管理会给一个最大飞行高度,超过这个高度层的栅格全部不可通行。三层约束叠加之后,三维栅格中每个格子要么可通行,要么不可通行,没有中间状态。
实现时我一般用一个三维整数矩阵map3D表示可通行性,取值只有两种:0表示可通行,-1表示不可通行。用-1而不是1,是为了方便和地形高度值区分,排查问题时用disp观察切片更直观。生成地图的先后顺序不影响结果,但逻辑上按"地形阻挡→禁飞区→限高面"依次置-1,容易对应到实际业务规则,也方便别人接手代码时理解建图依据。
3.2 MATLAB地形环境生成:从高程矩阵到三维栅格
下面这段MATLAB代码生成一张40×40网格、10个高度层的低空限制三维地图。地形用两个高斯函数叠加模拟山峰,实际应用时把这个位置替换成读入的DEM矩阵即可。禁飞区用二维逻辑掩膜表示,限高面通过直接置空最高两层来模拟。
% 低空限制三维地图生成 gridSize = 40; % 水平方向栅格数 layerMax = 10; % 最大高度层数 % 1) DEM地形模拟,真实场景替换为读入的DEM矩阵并裁剪到gridSize [xg, yg] = meshgrid(1:gridSize, 1:gridSize); terrain = 2 + 3*exp(-((xg-12).^2 + (yg-25).^2)/80) ... + 2*exp(-((xg-30).^2 + (yg-8).^2)/100); terrain = ceil(terrain); % 高度换算成层数,向上取整 % 2) 禁飞区掩膜,true表示该水平位置禁止穿越 noFly = false(gridSize, gridSize); noFly(30:35, 5:10) = true; noFly(12:18, 28:33) = true; % 3) 合成三维可通行性矩阵,0=可通行,-1=不可通行 map3D = zeros(gridSize, gridSize, layerMax); for h = 1:layerMax for ix = 1:gridSize for iy = 1:gridSize if h <= terrain(ix, iy) % 地形阻挡:低于地形高度的栅格 map3D(ix, iy, h) = -1; end if noFly(ix, iy) % 禁飞区整柱不可通行 map3D(ix, iy, h) = -1; end end end end % 4) 限高面以上不可通行,假设最高飞到第8层 map3D(:, :, 9:layerMax) = -1; % 5) 起点与终点坐标,必须验证可通行 start = [4, 4, 6]; % (x, y, heightLayer) goal = [36, 36, 5];代码里地形高度terrain向上取整,意味着高度等于地形层数的格子已经是障碍,飞行只能在地形之上。这是故意留的1格安全裕度,后面第5章会讲到膨胀处理时再展开。禁飞区用二维掩膜乘到三维矩阵上,写法虽然用了双重循环,但逻辑直观,地图规模不大时性能可接受。起点终点选好后最好加一行断言强制验证:
assert(map3D(start(1), start(2), start(3)) == 0, '起点不可通行'); assert(map3D(goal(1), goal(2), goal(3)) == 0, '终点不可通行');这一步能省掉后面大量无效训练时间。起点都不可通行时,整个训练过程都是在学习"怎么从障碍里逃出去",成功率永远上不去,而且问题非常隐蔽。
3.3 分辨率选择:状态空间规模与路径可行性的权衡
三维Q-learning最需要提前想清楚的是栅格分辨率。分辨率直接决定状态空间大小,而状态空间又决定Q表内存和训练轮次。下表是同一张2000m×2000m地图在不同分辨率下的规模估算,动作数按6邻域计算,Q值用double类型占8字节:
| 水平栅格尺寸 | 高度层数 | 状态空间 | Q表内存 | 路径质量 |
|---|---|---|---|---|
| 50m | 10 | 16000 | 约0.77MB | 锯齿感明显,需后处理平滑 |
| 25m | 20 | 128000 | 约6.1MB | 可接受,训练时间明显增加 |
| 10m | 50 | 2000000 | 约96MB | 内存压力大,不推荐直接建Q表 |
我常用的经验法则是:水平分辨率取飞行器最小转弯半径的2倍左右,高度层数控制在5~15层。转弯半径决定了无人机在原地绕过障碍需要的最小空间,栅格小于这个值会让规划出的轨迹在物理上不可执行。高度层数不要跟着水平分辨率等比缩放,因为大部分低空任务的高度范围本身就有限,比如50m到400m,按50m一层切10层就够用。
如果任务要求10m级精度,正确做法不是硬撑Q表,而是用50m栅格粗规划出一条走廊,再在走廊内部做局部精修。这个思路第6章还会用到。
4. Q-learning三维航迹规划实现:动作集、奖励塑形与训练循环
4.1 动作空间设计:6邻域还是26邻域
三维栅格上的动作空间有两个常用选择。6邻域对应前后左右上下六个方向,每次移动距离为一个栅格单位,实现简单、转移逻辑清晰、训练收敛快,缺点是轨迹只能走直角折线,看起来不太自然。26邻域把对角方向和斜上斜下也纳入动作集,轨迹更光滑,但动作选择变多、探索变慢,而且对角移动的欧氏距离比轴向长,奖励计算需要对移动距离做归一化,否则无人机倾向于斜着飞。
我的建议是第一次实现用6邻域,把训练流程、奖励函数、结果判读全部跑通之后,再扩展成26邻域或者加上水平8邻域作为中间方案。扩展时只需要改动作表和转移函数,Q表维度和更新公式不用动。还有一个边界细节:动作执行后如果出了地图边界或者落入-1格,状态不变但给予碰撞惩罚,这个惩罚和撞山一视同仁。很多实现里把出界动作直接跳过,这会让无人机学会贴边飞行去蹭边界,因为出界没有代价。
% 6邻域动作定义,每行对应一个方向的坐标增量 actions = [1, 0, 0; % +x -1, 0, 0; % -x 0, 1, 0; % +y 0,-1, 0; % -y 0, 0, 1; % +h 爬升 0, 0,-1]; % -h 下降动作表本身是普通矩阵,训练循环里按动作编号取出增量,加在当前位置上就是下一个候选状态。用矩阵而不是switch-case,方便后期把6行扩成26行,训练代码一行都不用改。
4.2 奖励塑形公式:每步代价、碰撞惩罚与目标奖励的配比
奖励函数是整个Q-learning实现里最像玄学的部分,但它也是三维航迹规划中决定成败的核心。纯稀疏奖励——只有到达目标给正奖励、碰撞给负奖励——在三维栅格空间里收敛极慢,因为agent在16000个状态里随机游走,碰到目标的概率太低。实际实现必须加距离引导项,让agent每走一步都能感知自己是在靠近目标还是远离目标。
function reward = getReward(nextState, goal, isCollision, dOld, dNew) if isCollision reward = -50; % 碰撞惩罚 elseif isequal(nextState, goal) reward = 100; % 到达目标 else reward = -1 + 0.3 * (dOld - dNew); % 航程代价 + 距离引导 end end参数配比有一个基本逻辑:碰撞惩罚-50与目标奖励+100,意味着一次碰撞的代价相当于50个普通步的累计代价,让agent对障碍有足够强的回避倾向。每步-1是航程代价,防止无人机为了刷距离引导奖励原地转圈。距离引导项0.3乘以前后两步到目标的欧氏距离差,当无人机朝目标方向移动时获得正奖励,远离时被扣分。这个0.3系数需要调:太大agent只顾追梯度、贴着障碍跑;太小引导信号被每步-1淹没,agent学不出趋近目标的趋势。我调参的习惯是固定碰撞和目标奖励,只动这一个系数,每组参数跑300轮看成功率曲线,不凭单条轨迹下结论。
4.3 训练循环与轨迹回放:MATLAB代码与参数说明
训练主循环可以用矩阵化的写法,对新手来说下面这种结构最直观:外层循环遍历episode,内层循环走完一个episode的每一步,每一步执行动作、获得奖励、更新Q表。
% 初始化Q表与超参数 nS = gridSize * gridSize * layerMax; nA = 6; Q = zeros(nS, nA); alpha = 0.3; % 学习率 gamma = 0.9; % 折扣因子 epsilon = 0.9; % 初始探索率 epsilonMin = 0.1; decay = 0.995; % 每轮探索率衰减系数 episodes = 800; rng(42); for ep = 1:episodes state = idx(start); % 当前状态线性索引 stepCount = 0; while ~isequal(state, idx(goal)) && stepCount < 2000 action = chooseAction(Q, state, epsilon, nA); [nextState, reward, done] = stepEnv(state, action, map3D, goal); bestNext = max(Q(nextState, :)); Q(state, action) = Q(state, action) + ... alpha * (reward + gamma * bestNext - Q(state, action)); state = nextState; stepCount = stepCount + 1; end epsilon = max(epsilon * decay, epsilonMin); endchooseAction的实现在这里不展开,逻辑就是生成一个0到1之间的随机数,小于ε时随机挑动作,否则找当前状态最大Q值对应的动作。stepEnv负责查表判断下一格是否可通行、是否到达目标、计算奖励,然后返回下一状态和是否结束。这两个子函数是训练循环的边界,把环境逻辑和算法逻辑分离,后续换地图、换动作集只改stepEnv,不动训练主循环。
训练结束后输出三样东西来判读结果。第一是收敛曲线,记录每个episode的累计奖励,曲线应该从很低的负值爬升,然后在一个区间内波动——波动正常,只要不持续下坠就说明策略在稳定;第二是成功率曲线,每隔若干episode用当前Q表跑一遍固定测试集,记录到达终点的比例;第三是轨迹可视化,把规划出的轨迹画在地形上:
% 三维轨迹与地形叠加显示 figure; surf(xg, yg, terrain, 'FaceAlpha', 0.6); hold on; plot3(traj(:,1), traj(:,2), traj(:,3), 'r-o', 'LineWidth', 2); xlabel('x栅格'); ylabel('y栅格'); zlabel('高度层'); view(45, 30); grid on;注意traj(:,3)是高度层数,和surf绘制的terrain单位一致,不需要再乘分辨率。如果轨迹贴在山体表面过去,视觉上几乎重合,说明安全裕度不足,要把地图膨胀参数调大一格。
5. 常见问题与避坑:不收敛、内存爆炸与贴障飞行
5.1 训练几十轮后轨迹仍乱撞:奖励配比失衡
现象:训练跑了300个episode,轨迹图上看无人机仍然在地图里乱转,没有形成从起点到终点的稳定通路,成功率曲线在低位徘徊。原因:碰撞惩罚与距离引导系数配比失衡。最常见的是距离引导系数0.3被改成0.8甚至1.0,agent只顾着往目标方向冲刺,撞了山也不在意,因为冲刺收益远大于碰撞惩罚;另一种情况是碰撞惩罚只给-10,agent认为绕开障碍的绕路代价比撞一下更高,每次撞完调整方向继续走,形成一个"撞-绕-撞"的坏循环。解决:把奖励函数恢复成基准配比,即碰撞-50、目标+100、每步-1、距离引导0.3,一次只改一个参数。血泪经验是同时调两个以上参数时出了问题根本分不清是哪个引起的。
5.2 细化栅格后内存暴涨:Q表存储与分辨率权衡
现象:把栅格尺寸从50m细化到20m,Q表从0.77MB涨到约6MB,MATLAB还扛得住;继续细化到10m,Q表接近96MB,训练循环每轮都要做全表扫描,速度慢得难以接受。原因:状态空间随分辨率三次方增长,三维栅格比二维更陡峭。解决:不要盲目细化栅格。先用粗栅格跑通E2E流程,确认奖励和参数没问题,再用细栅格只做局部重规划。如果确实需要全图细粒度Q表,考虑用稀疏结构只存访问过的状态——训练初期绝大多数格子没被访问过,稀疏存储能省下大量内存。
5.3 成功率震荡无法收敛:ε衰减与学习率配合
现象:训练曲线每过几十轮就出现一次大波动,成功率在40%到80%之间来回跳,感觉几十轮白练了。原因:ε衰减太快,agent过早停止探索,Q表里大量状态的动作价值没有被充分修正,一旦环境稍有变化就选错动作;或者α偏大(大于0.5),单次回放对Q值的修正幅度过大,导致学习过程震荡。解决:ε从0.9起步、每轮乘0.995、下限0.1,这样前200轮保持较高探索,后面逐步切换到利用。α用0.1到0.3之间,γ用0.9,对大多数栅格地图都是稳定的起点。判断是否是探索率问题有一个快速办法:把ε设成常数0.2跑100轮,如果成功率波动明显减小,就说明之前的衰减策略太激进。
5.4 轨迹贴着障碍物边缘飞:安全裕度与膨胀处理
现象:轨迹确实避开了所有障碍,但从三维图上看,航线距离山体表面只有一个栅格的距离,有些线段几乎贴着禁飞区边界过去。原因:建图时把障碍边界当成了精确边界,碰撞判定在进入障碍格才触发,规划出的路径没有安全裕度;实际飞行中定位误差、风扰、控制延迟都会让无人机偏离规划线,贴边航线的坠毁概率很高。解决:地图膨胀处理,把障碍物周围一圈栅格也标成不可通行。MATLAB里可以对二维掩膜做腐蚀操作再叠加到三维地图上:
% 对禁飞区掩膜做膨胀(腐蚀逻辑取反再腐蚀再取反) se = strel('square', 3); noFlyDilated = imdilate(noFly, se);膨胀的半径按需选择:旋翼机取1到2个栅格,固定翼因为转弯半径大取3个以上。注意膨胀处理要在建图阶段完成,不要试图用奖励函数实现安全裕度——奖励函数只能改变agent的选择倾向,不能保证所有状态下的路径都留出物理间距。
5.5 二维跑得顺畅,三维一放开就不收敛
现象:把高度维冻结,固定在中层跑二维Q-learning一切正常,一旦放开高度动作,训练开始发散,轨迹在高空乱窜。原因:高度维把状态空间扩大了5到10倍,奖励信号密度骤降,加上水平动作和高度动作混合,agent在海拔变化上获得的正反馈不足,学不会何时该爬升、何时该下降。解决:用课程学习思路做降维热身。先固定高度层训练150到300轮,得到一个水平避障策略;然后把水平维度的Q值保留,高度维动作的Q值清零继续训练,相当于让agent在已有的避障能力基础上只学高度调整。这个方法让我少走了很多弯路,三维训练失败时优先怀疑维度扩展带来的稀疏性,而不是算法本身。
6. 演示代码之外:蒙特卡洛验证与滚动窗口改造
训练完只看一条轨迹就写报告,早晚翻车。单条轨迹可能是运气好,恰好绕开了所有障碍。我的验证习惯是固定50次随机起终点测试,统计成功率和平均航程:
trials = 50; success = 0; pathLen = zeros(trials, 1); for i = 1:trials [traj, ok] = runEval(Q, map3D, randStart(map3D), randGoal(map3D)); if ok success = success + 1; pathLen(i) = size(traj, 1); end end fprintf('成功率: %.1f%% 平均航程: %.1f栅格\n', ... success/trials*100, mean(pathLen(pathLen>0)));成功率低于90%不建议投入使用。从固定地图到动态障碍,常见做法是滚动窗口:Q表只负责全局导航,给出一个参考方向;局部规划器只考虑前方5到10个栅格,在这个小窗口内重新规划以规避新出现的障碍,绕行后回到Q表引导的参考线上。这个混合架构是工程上最稳的方案——Q表处理静态约束,局部规划器处理动态变化,谁也不拖累谁。如果地图完全已知且没有动态障碍,另一个可用思路是用A*生成参考走廊,Q-learning只在走廊内微调,状态空间直接砍掉一个数量级,训练时间从小时级降到分钟级。
我第一次把规划轨迹接进某无人机仿真平台时,轨迹在山脊处离障碍只有一格,仿真里无人机直接撞上去了,原因是建图时没做膨胀。把地图膨胀参数从0改到1之后,仿真跑通,那一刻才真正理解:规划算法的价值不在demo里那张好看的轨迹图,而在你敢不敢把它接进真实的反馈回路。希望帮到你。
本文还有配套的精品资源,点击获取