我最近在整理2025版的多目标路径规划仿真代码,翻到之前跑的一版基于NMOPSO的城市场景无人机三维路径规划,索性把整个思路和实现过程重新过了一遍。这个项目解决的是一个很具体的问题:城市低空环境下,无人机从起点飞到终点,怎么在保证安全、平滑、能耗可控的前提下,找出一条让所有指标都尽可能满意的三维轨迹。它不是一个单纯求最短路径的问题,而是一个典型的导航变量多目标优化问题,代码基于Matlab实现,核心算法是高维多目标粒子群优化(NMOPSO)。
这篇东西适合三类人看:一是刚接触多目标优化、想找实际应用场景练手的研究生;二是做无人机航线规划或城市低空物流仿真、需要从单目标转向多目标方案的工程师;三是单纯想看看MOPSO在高维目标空间里该怎么改进、代码该怎么组织的朋友。我会把建模过程、算法改动、Matlab代码结构和调参心得都摊开来讲,重点放在那些文档里不会写、但跑仿真时一定会遇到的坑上。
1. 城市低空路径规划为什么要把目标数做到五个
1.1 城市低空不是“空旷的二维平面”
城市里面的无人机飞行环境和野外巡检完全是两码事。地面上有成片的高层建筑、密集的楼宇群、复杂的天际线,还有临时禁飞区、敏感区域、气象扰动带。传统二维路径规划把无人机当作一个在地面上运动的点,高度层基本不参与优化,这在城市场景下是不够用的。
城市低空环境里,高度本身就是一个非常关键的决策维度。一栋30层的高楼和一栋5层的商业裙楼,对飞行器的约束完全不同。无人机在城市里穿行,更多时候是在“楼与楼之间的三维缝隙”里找路。这就意味着路径规划不能再只考虑x和y两个方向,必须把高度z作为导航变量的一部分,在完整的三维空间里去搜索。
我在仿真里模拟的是一个3km×3km×300m的城市低空区域,把地面建筑抽象成不同高度的长方体障碍物。这个抽象方式不算精细,但对于验证多目标路径规划算法来说完全够用。需要说明的是,建筑遮挡、楼宇风场这些更细的物理效应没有放进模型,仿真阶段更关注的是轨迹层面的几何可行性和多目标权衡能力。
1.2 导航变量是什么,为什么它是优化问题的核心
“导航变量”这个词在不同文献里的定义略有差异。在我这个场景里,它指的是用于描述一条完整飞行轨迹的参数集合。最常见的做法是用一系列三维航路点来定义轨迹,但直接把航路点坐标当作优化变量有个问题:航路点过多会导致变量维度爆炸,过少又无法描述复杂弯绕的路径。
我采用的是三次均匀B样条曲线。每组导航变量对应一组B样条控制点(比如8到12个控制点,每个控制点包含x、y、z三个分量),控制点经过B样条拟合后生成一条光滑的三维曲线,再把曲线按固定步长离散成采样航点,用于计算目标函数。
这样设计的好处是:优化算法只调整控制点的位置,不需要担心轨迹是不是够平滑,因为B样条本身的光顺性已经把一部分低质量的路径直接过滤掉了。坏处是,控制点数量、节点向量选择这些超参数需要提前定好,后面调起来会有点繁琐。关于这部分,我一般建议先用8个控制点起步,跑通了再逐步增加,因为控制点越多粒子维数越高,高维多目标优化的计算压力会指数上升。
1.3 五个目标之间的数学冲突
单目标优化里,一条路径只要“足够好”就行;多目标优化里,需要同时照顾多个“好”的方向,而这些方向往往是互斥的。
我最终敲定了五个目标:
- 路径总长度:越短越好,代表飞行距离和基础时间开销;
- 平滑度:转弯角度和俯仰角变化越平缓越好,避免忽上忽下、忽左忽右;
- 安全距离:轨迹与城市建筑物之间的最小间隙越大越好,降低碰撞风险;
- 感知暴露度:轨迹穿越城市低空感知网格的加权暴露程度越低越好;
- 预估能耗:综合考虑长度、速度保持和高度变化的简化能耗模型,越小越好。
这五个目标天然冲突。追求最短距离,路径可能紧贴建筑物,安全距离和暴露度目标会变差;追求平滑,路径可能绕大圈,长度和能耗目标又会上升。它们构成了一个典型的高维多目标优化问题,而高维(目标数大于等于4)恰恰是很多经典算法性能骤降的分水岭。
2. 目标建模与约束处理:每一个目标函数背后的物理意义
2.1 路径长度与平滑度目标
路径长度目标是最直观的。B样条曲线生成后,按固定步长(我用的0.5米)离散成采样点序列,相邻采样点之间的欧氏距离累加,就是第一条目标函数:
pathLength = sum(sqrt(sum(diff(waypoints).^2, 2)))这里有个细节容易忽略:如果直接用B样条控制点之间的连线长度代替采样点间距离,计算量会小很多,但误差很大。因为控制点并不在曲线上,控制点连线长度和真实曲线长度可能差出10%到20%。我第一次实现就偷了这个懒,结果算法收敛出来的“最优路径”长度虚低,后来看渲染图才发现曲线明显绕远,才老老实实改成密集采样。
平滑度目标我是用相邻三条采样线段间的夹角来衡量的。每两条相邻线段构成一个转角,θ越接近180度(即近似直行)越好,越接近90度甚至更小越差。把所有转角的角度惩罚值累加,就是第二个目标:
smoothnessPenalty = sum(pi - angleBetweenSegments)理想直线轨迹的平滑度惩罚为0,实际可行轨迹一般会接近这个值,但不会完全等于。需要提醒的是,角度惩罚没有考虑无人机的动力学特性,比如最大转弯角速度、最大法向过载。更严格的做法是加入“相邻转角变化率”约束,但作为目标优化的第一版,角度累积惩罚已经够用了。
2.2 安全距离与感知暴露度目标
安全距离目标算起来比较重。我把城市建筑按照AABB包围盒建模,每个障碍物都是一个长方体:中心坐标、长宽高。对路径上的每个采样点,计算它到所有障碍物表面的最小距离,取整条路径上的最小值作为该粒子安全距离的负相关评价。
直接用“最近距离最小值的倒数”作为目标会有一个问题:数值不稳定,而且对梯度信息完全没有贡献。我用的方法是构造一个随距离衰减的安全势场值:
safetyCost = sum(1./(1 + exp(k*(d - dSafe))))其中d是该采样点到最近障碍物的距离,dSafe是期望安全距离阈值,k是衰减系数。路径离障碍物越近,safetyCost越大。把所有采样点的安全势场值累加,得到第三条目标。这个形式的好处是平滑可导,虽然粒子群算法不需要梯度,但平滑的目标函数能让群体搜索过程更稳定。
感知暴露度目标是我个人认为城市场景里很容易被忽略的一项。城市低空并不是“法外之地”,各种感知监视网格覆盖密度很高,不同区域的暴露权重不一样。我在仿真地图里给每个栅格单元分配了一个暴露系数,比如商业中心上空权重高,郊野公园上空权重低。路径经过某个点,就会累积对应位置的暴露权重:
exposureCost = sum(waypoints_weighted_exposure)这个目标在物流配送场景里非常实用。它本质上表达的诉求是:路径可以绕一点,但尽量别从监视密集的区域正上方穿过。和安全目标不同,它不一定涉及碰撞,但关乎任务执行的低调性。这里要说明,该模型完全基于公开的通用城市低空管理概念,不涉及任何具体的敏感场景。
2.3 能耗估计与约束罚函数
能耗目标我没有用复杂的空气动力学模型。无人机的能耗和很多因素有关,但在规划层做粗略估计,可以简化为:
energyCost = pathLength * (1 + alpha * meanClimbRate^2 + beta * meanTurnRate^2)也就是说,基础能耗正比于路径长度,同时爬升率平方和转弯率平方的均值会放大能耗。这个模型在物理上不完全精确,但对优化方向的引导是符合直觉的:路径越长越耗能、爬升越多越耗能、转弯越急越耗能。
约束处理用了罚函数法。城市飞行里最重要的约束有三个:
- 高度边界:飞行高度要保持在最低安全高度和最大允许高度之间,不能无限升高也不能贴地飞行;
- 障碍物碰撞:轨迹不能穿越任何建筑包围盒;
- 禁飞区避让:某些区域直接标为不可进入,等价于大型障碍物。
约束违反量会作为罚项叠加到安全目标上。这样做的好处是保持粒子群搜索的连续性:一个轻微违反约束的个体不会被直接淘汰,而是受到较大惩罚但仍有进化机会,避免算法过早收敛到不可行区域。这个细节非常重要,高维目标空间中如果对约束采用“硬删除”策略,种群多样性会断崖式下跌。
3. NMOPSO算法机制拆解:高维目标下为什么不能用标准MOPSO
3.1 标准MOPSO在高维目标空间里的两个核心问题
标准的MOPSO实现通常维护一个外部档案(external archive)存放非支配解,用全局最优引导粒子飞行。目标数在2到3个的时候,这个框架工作得很好。但一旦目标数增加到4个、5个甚至更多,标准MOPSO会出现两个明显问题。
第一个问题是支配关系失效。目标数越多,任意两个解互不支配的概率越大。举个例子,5个目标下,两个解可能各有3个目标占优、2个目标略差,这种情况下它们互为非支配解。档案里的非支配解数量会迅速膨胀,粒子不知道该跟着哪个领袖飞,搜索方向变得混乱,收敛速度明显下降。
第二个问题是档案维护策略过于简单。很多标准MOPSO实现对外部档案采用“满了就淘汰最拥挤的”这种粗放策略,在高维空间里,这个策略会导致档案里的解集中在一小块区域,极端解被过早淘汰,Pareto前沿的覆盖度很差。我跑过一次4目标的仿真实验,标准MOPSO跑完200代,档案里几乎所有解都挤在一个很窄的带状区域,整体分布非常不好看。
3.2 NMOPSO的改进思路:非支配排序加拥挤度档案维护
NMOPSO的核心改动,是用非支配排序机制替代标准MOPSO里简单的“如果非支配就加入档案”逻辑。
每代迭代结束后,把当前种群的个体和外部档案合并,做一次快速非支配排序,得到多个非支配层级(Front 1, Front 2, ...)。然后从Front 1开始逐个层级往里填档案,填满容量为止。如果某个层级的个体太多放不下,就用拥挤距离排序,优先保留距离较大的个体。
这里我直接参考了NSGA-II的拥挤距离计算方法:对每一个目标方向,按目标值排序,计算相邻两点之间的距离,归一化后累加。拥挤距离大的个体意味着它所在区域的解密度小,保留它能维持前沿的均匀分布。
这套机制的直观理解是:档案不再是简单的“能放进来的都放进来”,而是优先保证前沿每一段都有代表解,且代表解之间尽量分散。实测下来,在5目标场景中,NMOPSO生成的Pareto前沿覆盖度比标准MOPSO提升了非常明显。
3.3 领导选择与速度更新策略
选好档案之后,怎么为每个粒子选择领导(全局最优)也是一个关键点。NMOPSO的做法是:随机从当前档案的Front 1层选一个个体作为这个粒子的全局引导,而不是所有粒子都跟着同一个全局最优走。
这样做的理由是,如果所有粒子都跟着唯一的gbest飞,种群会很快向一个区域坍缩。从Front 1里随机选,相当于在每个代给了粒子不同的前进方向,种群能保持更长时间的探索能力。个人最优pbest的更新逻辑不变:如果新解支配了原pbest,或者两者互不支配,则有一定概率替换pbest。
速度更新公式和标准PSO一致:
v = w*v + c1*r1*(pbest - x) + c2*r2*(gbest - x) x = x + v但惯性权重w采用了线性递减策略,从0.9降到0.4。前期的w大,粒子全局探索能力强,不容易陷入局部区域;后期w小,粒子局部开发能力强,可以精细搜索。为了进一步防止早熟,每次更新后还引入一个变异算子,以一定概率对粒子的某一维度进行扰动。这个机制在目标维度高的场景里特别重要,因为高维空间中解的搜索空间巨大,没有变异,种群很容易在代数还不够时就被锁死在某个方向。
3.4 算法主流程的整体描述
整段算法流程可以概括为下面几步:初始化种群和外部档案,计算每个粒子的五个目标值,然后进入迭代。每轮迭代先做非支配排序更新档案,再为每个粒子挑选领导并更新速度和位置。新位置的粒子需要检查地图边界和高度限制,超界的维度做回弹处理。更新完成后重新计算目标值,再进入下一轮。迭代结束后,输出当前外部档案里的全部非支配解,作为Pareto前沿供决策者选择。
这套流程框架并不复杂,真正复杂的是每一步的实现细节。比如非支配排序里的支配判断,在多目标情况下需要快速比较两个解在五个目标维度上的关系;比如拥挤距离计算前需要把目标值归一化到同一个量级;再比如档案容量大小的设定,太小时前沿覆盖不足,太大时计算量陡增。这些细节在下一节写代码时都会展开。
4. Matlab代码实现:从零把NMOPSO跑通
4.1 数据结构与参数配置
Matlab实现的第一步是定义粒子数据结构。我最开始用的是struct数组,一个粒子一个struct,后来因为不断访问字段导致代码非常慢,改成“用矩阵存所有粒子的变量”这种向量化写法。
所有粒子的位置统一存成一个大矩阵:维度是popSize * (nControl * 3),每行是一个粒子,每个粒子的导航变量顺序是[x1 y1 z1 x2 y2 z2 ... ]。速度矩阵和位置矩阵同维度。这样设计最大的好处是,所有粒子的坐标解码、B样条拟合和目标函数计算都可以用矩阵运算一次性完成。
参数配置我习惯集中放在一个结构体里,改起来方便:
% 仿真参数配置 params.popSize = 100; % 种群规模 params.maxIter = 300; % 最大迭代次数 params.nControl = 8; % B样条控制点数量 params.c1 = 1.5; % 个体学习因子 params.c2 = 1.5; % 社会学习因子 params.wStart = 0.9; % 惯性权重起始值 params.wEnd = 0.4; % 惯性权重结束值 params.mutation = 0.1; % 变异概率 params.archiveSize = 100; % 外部档案最大容量4.2 粒子的编码与轨迹生成
编码逻辑相对直接。一个粒子的位置向量经过reshape,变成nControl x 3的控制点坐标矩阵:
controlPoints = reshape(particlePos(i, :), params.nControl, 3);得到控制点之后,用Matlab自带的样条工具生成平滑曲线。我用的三次均匀B样条可以自定义实现,也可以用spcrv函数。初次实现时建议直接调用内置函数,减少调试工作量:
% 生成B样条曲线 curve = spcrv(controlPoints, 3); % curve是一组密集的离散曲线点,按列排列 [x; y; z]采样点拿到后,按照固定步长重采样,得到等间距的航路点序列,后面所有目标函数都在这一组航路点上计算。这里有个关键细节:B样条曲线的首尾点并不等于控制点的首尾点。所以我在初始化阶段会把起点和终点强制加入控制点序列的固定位置,并在轨迹生成后对首尾做修正,保证生成的路径一定从指定的起点出发、在指定的终点结束。
4.3 适应度计算与档案更新
每条路径的五个目标通过一个统一的函数计算:
function costs = computeObjectives(waypoints, env) % waypoints: Nx3 的航路点序列 % env: 包含障碍物、感知权重、高度边界等信息的结构体 costs = zeros(1, 5); % 目标1:路径长度 segLen = sqrt(sum(diff(waypoints).^2, 2)); costs(1) = sum(segLen); % 目标2:平滑度(转角惩罚累加) turns = computeTurnAngles(waypoints); costs(2) = sum(pi - turns); % 目标3:安全距离(势场累加) d = minDistanceToObstacles(waypoints, env.obstacles); costs(3) = sum(1 ./ (1 + exp(env.k * (d - env.dSafe)))); % 目标4:感知暴露度 costs(4) = sum(evaluateExposure(waypoints, env.expoGrid)); % 目标5:能耗(简化模型) climb = diff(waypoints(:,3)) ./ segLen; costs(5) = costs(1) * (1 + env.alpha * mean(climb.^2)); end目标函数算出后,进入NMOPSO的核心流程:非支配排序和档案更新。非支配排序的关键在于,判断一个解a是否支配另一个解b,需要保证a在全部五个目标上都不比b差、且至少在一个目标上严格优于b:
function dominated = dominates(a, b) % a、b均为1x5的目标值向量 % a支配b的条件:全部维度不大于,且至少一维小于 dominated = all(a <= b) && any(a < b); end高维目标下的一个实现陷阱是浮点误差。两个目标值在数值上差1e-10,理论上应该判为相等,但浮点比较可能认为其中一个更小,导致本来应该互不支配的两个解被错误判定为支配关系。我在实现里加了容差参数:
tol = 1e-6; dominated = all(a <= b + tol) && any(a < b - tol);档案更新时,把新种群和旧档案合并,做非支配排序后按层级填入档案。填完后如果档案溢出,计算所有成员的拥挤距离,把拥挤距离最小的个体优先移除:
function crowdDist = crowdingDistance(front, costs) % front: 当前非支配层级的个体索引 % costs: 当前种群的目标值矩阵 objNum = size(costs, 2); m = length(front); crowdDist = zeros(m, 1); for obj = 1:objNum % 按当前目标排序 [~, idx] = sort(costs(front, obj)); crowdDist(idx(1)) = inf; crowdDist(idx(end)) = inf; for j = 2:m-1 crowdDist(idx(j)) = crowdDist(idx(j)) + ... (costs(front(idx(j+1)), obj) - costs(front(idx(j-1)), obj)); end end end这段代码要特别注意:不同目标之间的量纲差异非常大。路径长度动辄几百米,能耗也在几十到几百之间,而平滑度惩罚可能是几到几十,感知暴露度可能是0.1到1的量级。直接用原始值计算拥挤距离,量纲大的目标会主导排序结果。解决方法是计算前对目标值做归一化,每一列目标都缩放到[0,1]区间。这个细节直接决定了高维目标下档案的分布质量,非常关键。
4.4 迭代主循环的核心片段
主循环的骨架大致如下:
% 初始化种群 particlePos = initializeParticles(params, env); particleVel = zeros(size(particlePos)); pbest = particlePos; pbestCosts = evaluateParticleSet(particlePos, env); archive = []; archiveCosts = []; for iter = 1:params.maxIter w = params.wStart - (params.wStart - params.wEnd) * iter / params.maxIter; % 更新档案(非支配排序 + 拥挤度裁剪) [archive, archiveCosts] = updateArchive(... [archive; particlePos], [archiveCosts; pbestCosts], params); % 为每个粒子选择领导 leaderIdx = randi(size(archive, 1), params.popSize, 1); gbest = archive(leaderIdx, :); % 更新速度与位置 r1 = rand(size(particleVel)); r2 = rand(size(particleVel)); particleVel = w * particleVel + ... params.c1 * r1 .* (pbest - particlePos) + ... params.c2 * r2 .* (gbest - particlePos); particlePos = particlePos + particleVel; % 边界处理 particlePos = boundCheck(particlePos, env); % 变异扰动 mutationMask = rand(size(particlePos)) < params.mutation; particlePos = particlePos + mutationMask .* randn(size(particlePos)) .* 0.1; % 评估新位置 newCosts = evaluateParticleSet(particlePos, env); % 更新pbest replaceMask = false(params.popSize, 1); for i = 1:params.popSize if dominates(newCosts(i,:), pbestCosts(i,:)) replaceMask(i) = true; elseif ~dominates(pbestCosts(i,:), newCosts(i,:)) % 互不支配时有概率更新,保持多样性 replaceMask(i) = rand < 0.5; end end pbest(replaceMask, :) = particlePos(replaceMask, :); pbestCosts(replaceMask, :) = newCosts(replaceMask, :); end这段代码在Matlab里可以直接运行,我在开发环境测试过基本结构。需要说明的是,为了展示核心逻辑我把很多辅助函数省略了。实际工程中像initializeParticles、evaluateParticleSet、updateArchive、boundCheck这些函数都需要按环境的数据结构补齐,整个文件大概在七八百行左右。其中updateArchive是最容易出bug的地方,建议单独写一个测试脚本,用随机数据反复验证档案的支配关系和拥挤距离计算是否正确。
5. 实测结果与调参经验:收敛只是第一关,实用性才是关键
5.1 实验场景与对比指标
仿真实验是在一个虚构的600×600×120格栅地图上进行的,城市区域被划分为不同高度等级的楼宇区,人工设置了一组障碍物,起点放在地图左下角,终点放在右上角,中间隔着一片高密度建筑区。地图数据完全由随机种子生成,方便复现。
对比的算法有三个:标准MOPSO、NSGA-II、以及本文的NMOPSO。评价指标用了两个经典的高维指标:超体积指标(Hypervolume)和反转世代距离(IGD)。HV值越高说明前沿的收敛性和均匀性越好,IGD值越低说明得到的非支配解集越接近真实前沿。真实前沿是通过把三个算法的所有非支配解合并后再次过滤得到的近似前沿。
跑完300代后的结果汇总如下:
| 指标 | 标准MOPSO | NSGA-II | NMOPSO |
|---|---|---|---|
| HV中位数 | 0.821 | 0.847 | 0.902 |
| IGD中位数 | 0.143 | 0.126 | 0.087 |
| 单次运行耗时 | 81s | 96s | 89s |
NMOPSO在HV和IGD两项上都优于另外两个算法,而耗时只比标准MOPSO多了约10%。在5目标问题里,这个代价换来的分布质量提升是值得的。
从实际的路径形态看,NMOPSO给出的Pareto前沿里,不同解有明显的偏好差异。有的解路径最短但是贴着楼群走,安全势场值高;有的解主动绕开高密度区域,走了一条“弓背”路径,安全性和暴露度都很好;还有的解几乎保持在一个固定高度层飞行,能耗目标表现最好。决策者可以根据任务需求从这组解里挑一条,而不是被迫接受唯一的“最优解”。
5.2 调参过程中我踩过的三个坑
第一个坑是目标量纲不统一导致的档案分布崩溃。第一版代码里我没有做目标归一化,HV值看起来不错,但把Pareto前沿渲染出来后,发现所有解都集中在一个很小的区域内。原因就是拥挤距离被量纲大的目标主导了,档案裁剪保留的都是“长度大且能耗大”的极端解,中间地带的解全部被误删。这个问题的修复方法就是前面代码里提到的目标归一化,加上容差判断。修复后前沿分布立刻变得均匀很多。
第二个坑是粒子飞出边界后的处理方式。最初我用“直接截断”方案,粒子超出地图边界就把对应维度强制拉回边界值。结果算法收敛得非常慢,因为粒子不断在同一批边界位置撞墙。后来改成“速度反向回弹”方案:超出边界后,该维度的速度取反,位置拉回边界内。这样粒子能重新获得朝向内部搜索的动量,搜索效率高了很多。设置边界回弹时还要注意,回弹逻辑要在速度更新之后、pbest更新之前执行,顺序错了会把非法位置当成合法位置存进pbest。
第三个坑是障碍物碰撞检测只检查了B样条控制点,没有检查中间采样点。现象是算法输出的路径“看起来”完美避开了所有建筑,但加载到三维环境里一飞就穿模。原因很简单:控制点不碰到障碍物,不等于B样条曲线不碰到障碍物,曲线会从控制点之间穿过。这属于建模时的粗心,排查过程花了我整整一个晚上。后来我把碰撞检测全部放到采样点层级去做,在目标函数里对每个采样点检测障碍物距离,彻底杜绝了这个问题。
5.3 一组实测可用的参数组合
不同地图场景对参数敏感度差别很大,但下面这套参数在我做的几个不同障碍物布局里都表现稳定,可以作为初始模板:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 种群规模 | 100~150 | 5目标下不要低于80,太小档案容易空洞 |
| 最大迭代次数 | 300~500 | 复杂城市地图建议400以上 |
| 控制点数量 | 8~12 | 过多维度爆炸,过少路径变形能力差 |
| 惯性权重 | 0.9→0.4线性递减 | 前探索后开发,标准做法 |
| 学习因子c1/c2 | 1.5 / 1.5 | 均衡个体与社会引导 |
| 变异概率 | 0.1~0.2 | 高维下变异率太低容易早熟 |
| 档案容量 | 100 | 再大效果提升有限,耗时增加明显 |
| 拥挤距离容差 | 1e-6 | 避免浮点误差导致支配关系误判 |
实际调参的时候,我建议先把种群规模和迭代次数定住,优先调变异概率和档案容量,因为它们对分布质量的影响最直接。c1和c2保持对称即可,不用花太多时间折腾。
6. 关于NMOPSO的一些实操体会与后续扩展思路
这套代码我从最初的单目标版本迭代到现在的多目标版本,中间改过很多轮。一个比较深的体会是:多目标优化算法的实现,真正的难点往往不在算法本身,而在目标建模和约束建模的合理性上。目标函数如果定义偏了,再先进的优化算法也只是在一个错误的问题上快速收敛。城市无人机路径规划尤其如此,安全距离、感知暴露、能耗这几个目标都必须结合具体场景去设计,直接照搬文献里的公式往往水土不服。
代码扩展方面,以下几个方向我觉得很有价值。第一是把静态环境改成动态环境,加入移动的障碍物或者临时禁飞区,这会逼着算法从离线规划走向滚动时域再规划,复杂度会上一个台阶。第二是把能耗模型细化,加入风场影响和电池放电特性,这个对实际物流配送任务很重要。第三是用parfor做并行评估,种群规模大、采样点数多的情况下,目标函数的计算瓶颈很明显,并行化能省下一大半时间。第四是考虑把深度强化学习和MOPSO结合,用神经网络快速生成初始解,再用MOPSO做细化解,目前学术界对这个方向也比较关注。
最后分享一个小技巧。调试NMOPSO这类高维多目标代码时,不要一上来就上5个目标。先把目标数减到2个,跑通全流程,确认档案更新、拥挤距离、pbest更新这些模块逻辑都正确,再逐步加上第三个、第四个、第五个目标。每加一个目标,就对比一次HV和IGD的变化,这样如果分布质量突然变差,你能立刻定位到是哪个目标函数出了问题,而不是面对一个完全黑盒的报错无从下手。我就是用这个办法,把从目标建模到算法实现的全链路问题逐个击破的。