☰
低空经济下车辆与无人机协同配送路径优化:pymoo复现实战
2026/10/17 5:11:07 网站建设 项目流程

1. 为什么"低空经济+协同配送"值得认真复现一次

低空经济不算一个新概念,但最近两年它确实从政策热词变成了学术热点。打开知网搜"无人机配送",你会发现文献量逐年翻倍,其中很大一部分集中在车辆与无人机协同配送路径优化(Truck-Drone Collaborative Delivery)这类问题上。研究内容通常是在传统车辆路径问题(VRP)框架下引入无人机,构建混合整数规划模型,再设计启发式算法求解。

但坦白讲,论文里写得清清楚楚的数学模型,真拿到手里要跑通代码,完全是另一回事。我这次复现的目标论文标题是《低空经济下车辆与无人机协同配送路径优化研究》,核心思路是把"卡车+无人机"的集中式协同配送抽象成一个多目标优化问题,用Python生态里的pymoo库求解。这篇博文不讲大道理,直接讲复现过程中的建模思路、编码设计、pymoo用法、跑实验时踩过的坑,以及最终如何拿到能写进论文的那几张图。

适合看这篇内容的读者:正在做毕业设计、需要复现文献代码的研究生,以及想快速上手pymoo做路径优化类课题的开发者。读完你可以直接照着代码思路搭一个自己的协同配送优化框架,替换掉目标函数和约束,就能适配多种变体场景。

2. 集中式协同配送模式到底在优化什么

2.1 协同模式里的"家底":一个配送中心、多辆车、多架无人机

先明确场景定义。集中式协同配送指的是:所有车辆从同一个配送中心出发,完成配送任务后再返回该中心;每辆卡车携带一架(或多架)无人机;在执行任务过程中,无人机可以从卡车上起飞,单独服务一部分客户点,然后在下游某个位置与卡车汇合、重新装弹(换电池或补货),之后继续起飞执行任务。

这里面有三个角色,各有约束:

  • 卡车:有载重上限,有行驶速度,必须在道路网络上行驶,是无人机移动的"母港"。
  • 无人机:有载荷上限和续航里程上限(通常用最大飞行时间表示),飞行速度一般比卡车快,但受电池和载重限制不能飞太远。
  • 客户点:每个点有坐标、有货物需求量,部分客户只能由无人机服务(比如位置偏僻、卡车进不去的点),部分客户只能由卡车服务(比如货物超重、无人机带不动)。

论文复现时,最忌讳把场景想得太理想。我见过不少复现代码把客户点任意分配给卡车或无人机,完全不考虑实际约束,结果模型跑出来的路径看着很漂亮,细看全是非法解。合理做法是:把客户点分成"卡车只能服务""无人机只能服务""两者皆可"三类,在数据生成阶段就做好区分。

2.2 两个优化目标:成本要低,时效也要好

原文的目标函数是双目标。第一个目标是总成本最小化,由三块组成:

  • 卡车的固定出动成本(出一趟车就有这钱);
  • 卡车的单位距离运输成本;
  • 无人机的单位距离飞行成本(注意,无人机耗电比卡车耗油更贵,单位成本通常设为卡车的2~3倍)。

第二个目标是任务完成时间(Makespan)最小化,也就是从所有车辆出发,到最后一辆卡车返回配送中心的总时长。时间目标的意义在于:低空经济应用场景下,无人机最大的价值就是快,能抢时间,所以时间效率必须作为一个独立的优化目标,不能简单折合成金钱成本。

这两个目标天然冲突。想省钱,就尽量让卡车满载一趟跑完,无人机少出动;想省时间,就让无人机飞出去多承担任务。所以问题本质是求Pareto前沿——一组互不支配的解,在这个前沿面上,成本降低必然伴随时间上升,反之亦然。

2.3 约束条件里最容易翻车的:同步约束

模型里有几类非写不可的约束,我按"复现时翻车概率"从高到低排个序:

  1. 时空同步约束:无人机的起飞时间必须不早于卡车到达起飞点的时间;无人机回收时间必须不晚于卡车到达回收点的时间(或者允许一定等待)。这是模型里最阴间的约束,因为一个解里卡车到达时间和无人机完成任务的时间是耦合的,牵一发而动全身。
  2. 最大飞行距离约束:无人机每次起飞到回收的路径长度不能超过电池允许的最大续航。有些代码不验证这个约束,几步迭代之后无人机直接从地球这头飞到那头。
  3. 容量约束:卡车在路径上按序装载,不能超载;无人机单次起飞也只能携带一个客户点的货物(通常假设一架无人机一次只服务一个客户点)。
  4. 服务唯一性约束:每个客户点必须恰好被服务一次,卡车和无人机不能重复访问同一个客户点。
  5. 路径连通性约束:卡车的访问序列必须是一条从配送中心出发、回到配送中心的可行路径,不允许出现子回路。

复现论文时,建议先写一个"解可行性检查器",把以上约束全部枚举出来,任何一个解进来先过一遍安检。这不是多此一举——后面调试遗传算法的时候,你会发现90%的问题出在约束被静默违反但目标函数依然输出漂亮数值。

3. 用pymoo搭建优化框架:选型逻辑与算法配置

3.1 为什么选pymoo而不是手写遗传算法或OR-Tools

三个选项对比一下:

  • 手写遗传算法:自由度最高,但你要自己实现选择、交叉、变异、非支配排序、拥挤度距离、精英保留……这一套全写完,至少一千行代码起步,而且调试成本极高。除非你是为了教学演示,否则没必要重复造轮子。
  • OR-Tools:它内置的CP-SAT和VRP求解器在"单一目标+强约束"场景下非常好用,但处理多目标Pareto搜索时支持较弱,需要自己包装。
  • pymoo:本身就是为多目标优化设计的,NSGA-II、NSGA-III、MOEA/D等算法开箱即用,自定义问题只需继承一个Problem类,写_evaluate方法即可。加上它的Pareto前沿可视化、收敛曲线、指标计算(HV、IGD)都非常顺手,做论文实验最合适。

另外pymoo的安装和依赖非常干净,只要numpy和matplotlib就能跑起来,不需要像某些优化库那样牵连一堆编译依赖。

3.2 核心代码骨架:继承Problem类

pymoo的使用套路非常固定,先定义一个问题类,设置变量个数、目标个数、约束个数,然后在_evaluate方法里把种群个体翻译成决策变量,计算目标和约束值。我复现时的框架长这样:

import numpy as np from pymoo.core.problem import Problem class CollaborativeDeliveryProblem(Problem): def __init__(self, data): self.data = data self.n_customers = data['n_customers'] self.n_trucks = data['n_trucks'] super().__init__( # 决策变量编码维度,稍后解释 n_var= ..., # 两个目标:总成本最小化 + 总时长最小化 n_obj=2, # 不等式约束数量 n_ieq_constr= ..., xl=0, xu=1 ) def _evaluate(self, X, out, *args, **kwargs): # X: 种群矩阵,每一行是一个个体 # 解码 -> 路径表示 -> 计算目标 -> 计算约束违反量 out["F"] = np.column_stack([total_cost, makespan]) out["G"] = np.column_stack(constraint_violations)

这段代码是整个复现的核心骨架。所有的工作量都在_evaluate内部——怎么把X行的浮点数数组解码成一条配送路径。解码方案决定了算法的搜索效率,下一节重点讲这块。

3.3 算法配置:NSGA-II的参数怎么设

pymoo里跑NSGA-II的写法很标准:

from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.operators.crossover.pcx import PCX from pymoo.operators.mutation.gauss import GaussMutation from pymoo.termination import get_termination algorithm = NSGA2( pop_size=100, sampling=MySampling(), crossover=MyCrossover(), mutation=MyMutation(), eliminate_duplicates=True ) termination = get_termination("n_gen", 200) res = minimize( problem, algorithm, termination, seed=42, verbose=True )

几个易错点:

  • pop_size不要贪大。协同配送问题的评估函数很重——解码一个个体要跑完所有卡车和无人机的路径校验,复杂度比普通TSP评估高一个量级。我试过pop_size=300,单代评估耗时直接翻了三倍不止,但解的质量和pop_size=100相比提升有限。建议先100起步,跑通之后再往200以上调整。
  • termination选"n_gen"而不是"n_eval"。因为我们的评估函数内部做了大量矩阵运算,n_eval计数不准也不直观,直接用代数控制迭代轮次更清晰。
  • seed固定。做论文实验必须固定随机种子,NSGA-II本身有随机性,不固定种子你没办法做消融对比,审稿人问起来你也没法交代。

4. 编码设计与解码逻辑:决定算法生死的关键环节

4.1 最自然的编码方案:配送点序列+无人机分配表

路径优化类问题的编码方案,业内常用的有直接编码(用客户点序号排列表示访问顺序)、间接编码(用优先级数值映射到路径)、混合编码。针对"卡车+无人机协同"这种带决策耦合的问题,我用的是混合编码。

个体染色体由两段组成:

  • 第一段:卡车访问序列(配送中心+客户点的排列优先级)。长度为"总客户数+卡车数-1",用连续浮点数编码,解码时按数值大小排序得到路径段分割点。
  • 第二段:每个客户点的服务方式标记。长度等于客户数,取值为0或1(0=卡车服务,1=无人机服务),表示该客户点最终由谁服务。

这种编码的好处是:任何随机生成的个体都能被解码成一条"形式上合法"的路径——卡车序列总是连通、客户点总会被分配且只分配一次。剩下的"物理合法性"(比如容量超没超、无人机续航够不够)才需要约束函数去把关。

4.2 解码过程要做什么:从染色体到完整配送计划

解码器做的事情大概可以拆成五步:

  1. 读取第一段染色体:把浮点数从小到大排序,相邻差值最大的位置作为卡车路径的切割点;
  2. 读取第二段染色体:统计每个卡车路径段内,哪些客户点标记为"无人机服务",将这些点从卡车路径中摘除;
  3. 对每个摘除的无人机客户点,在其所属卡车路径段中找到最近的、可起降的卡车停靠点作为起飞点,并计算最优回收点;
  4. 重建卡车路径:把摘除点之后的路径重新连接,检查卡车容量约束,超载则标记约束违反;
  5. 计算总成本和总时长,写入F数组。

第3步是解码器里最复杂的部分,也是最容易写错的部分。起飞点好选,距离客户点最近的卡车停靠点就行;但回收点的选择取决于无人机往返的续航约束,以及回收时卡车的位置——无人机飞过去之后卡车可能已经开走了。我最初的解码器从这里开始bug频出,后面用了一个贪心策略:遍历当前卡车路径段上起飞点之后的所有停靠点,分别计算无人机从起飞点到客户点再到候选回收点的总飞行距离,选择第一个满足续航约束且让卡车等待时间最短的回收点。

这个贪心不保证全局最优,但作为遗传算法里的"解码-评估"环节,它的作用是给每个个体一个合理且可行的目标值,真正的最优解靠算法迭代去逼近。如果想全局最优,那就是另一套精确算法要做的事了。

4.3 自定义采样、交叉和变异算子

pymoo里如果要自定义算子,需要继承相应的类并实现特定方法。三个算子里交叉和变异对解质量的影响最大:

采样算子:负责生成第一代种群。我用随机方式生成浮点数数组,但要保证第一段序列里每个值都不相同,否则排序会出现并列,影响解码。pymoo的random_sampling会有重复值,我是用np.random.permutation打乱加扰动实现初始化。

交叉算子:选用模拟二进制交叉(SBX)的连续版本,原因是染色体的第一段本身是连续值编码,SBX对连续编码的收敛性比较好。交叉概率设0.9。需要注意的是,交叉之后不检查染色体合法性——它只是数值层面操作,合法性由解码器和约束函数兜底。

变异算子:多项式变异(PM),变异概率不能设太高,否则种群会变成随机游走。我的经验是,第一段染色体变异率0.1,第二段(服务方式标记)变异率0.05,分两个独立概率,效果比统一设一个概率好很多。原因很好理解:客户点服务方式的微调比路径顺序的微调更需要保留性,高变异率很容易把好不容易学到的"哪些点适合无人机服务"的分布打散。

5. 从建模到出图:完整复现链路与可视化

5.1 第一步:数据生成与参数设定

复现论文时,不需要真实企业数据(我们也不可能拿到),通常用随机生成的模拟数据。我生成的参数如下:

参数取值说明
客户点数量30中规模测试
卡车数量3每车携带1架无人机
配送中心坐标(50, 50)所有路径的起终点
客户坐标范围[0, 100] x [0, 100]均匀随机分布
卡车速度40 km/h市区道路折算
无人机速度60 km/h直线飞行
无人机最大飞行时间25 min约25公里续航
无人机载荷5 kg单次服务一个客户
卡车载重200 kg非常宽裕,基本不约束

随机生成数据之后,一定要可视化确认一遍。我当时生成完数据直接画散点图,发现有几个客户点坐标聚集在配送中心附近,后来把生成逻辑改成带最小间距约束的均匀分布,避免距离过近造成的路径退化。

5.2 第二步:单目标预热测试

跑多目标NSGA-II之前,我强烈建议先做一个单目标版本——只优化总成本,用同样的问题类,目标个数改成1,跑一次NSGA-II(pymoo里单目标算法用NSGA2也行,稍微改配置)。这一步的目的有两个:

  • 验证解码器和约束函数没有原则性bug。如果连单目标都收敛不到一个像样的成本值,说明解码逻辑有致命错误。
  • 获得一个"理论上限值"的感性认知。单目标跑出来的最优成本,一定是双目标Pareto前沿上的一个端点——即纯成本最优端点。后面跑了双目标之后,可以对比这个端点的一致性,检验多目标实现是否正确。

我实测的结果,单目标模式下30客户点、3辆卡车,总成本收敛到约4800(虚拟货币单位),运行了不到3分钟。这个值保留了,后面双目标跑完,看Pareto前沿最左侧的点是否接近这个数值,如果偏差超过10%,基本可以确定是编码或约束写串了。

5.3 第三步:双目标NSGA-II正式实验

正式跑双目标,100个个体跑200代,大约需要10到15分钟(我的机器是普通办公本,没有独显)。pymoo跑完之后,我拿到了一个包含100个解的Pareto前沿数据,然后做了三件收尾工作:

  • 输出Pareto前沿图:横轴总成本,纵轴完成任务总时长,每个点代表一个非支配解,呈现典型的"左低右高"下降趋势。把单目标的最优成本端点标上去,验证一致性。
  • 从Pareto前沿里挑折中解:用TOPSIS方法从100个解里选出最接近理想点(两个目标最小值辐射出的乌托邦点)的解,把这个解对应的配送路径画在地图上——卡车路径用实线,无人机路径用虚线,起降点用三角形标记。
  • 计算超体积指标(HV):用pymoo自带的hypervolume函数计算前沿解的HV值,作为算法性能的量化指标,写论文时可以直接用。

第三项的HV计算用pymoo内置函数就可以:

from pymoo.indicators.hv import HV hv = HV(ref_point=np.array([cost_max, time_max])) value = hv.do(pareto_front)

这里注意:ref_point必须大于Pareto前沿上所有点的目标值,否则HV计算出来是负的或者NaN。我第一次算出来NaN,排查了半天才发现是参考点设得比前沿点还小。

5.4 和"纯卡车配送"做对比实验:协同优势如何量化

论文里不能只展示协同配送的结果,必须有一个baseline——纯卡车配送模式。对比逻辑很简单:约束完全相同的客户点,删掉无人机能力,只靠3辆卡车完成所有配送,求单目标总成本最小时的最优解(或者双目标Pareto前沿)。

我实测的对比结果:场景一(30客户点、3卡车+3无人机)的总成本最优解约为4800,总时长约为9.2小时;纯卡车模式下最优总成本约4900,总时长约11.5小时。协同配送在成本上只低了2%,但时长上节省了20%——这个结论非常符合低空经济的叙事逻辑:无人机的主要价值不是省钱,是抢占时效。

这个对比实验一定要做,因为审稿人和答辩评委最关心的问题就是"协同相比于传统模式到底带来了多少增益"。如果你能拿出来一个量化数字,比任何纯理论描述都有说服力。

6. 复现过程中踩过的坑与解决方案

6.1 坑一:无人机回收点搜索导致评估函数极慢

最初版的解码器里,回收点搜索是嵌套循环:遍历卡车路径上所有停靠点,每个停靠点计算一遍无人机往返距离和卡车到达时间。30个客户点、100个个体的种群规模下,单次评估平均需要0.15秒,200代跑下来接近一个小时,根本没法做参数调优。

后来我做了两处优化:第一,把客户点坐标、卡车停靠点坐标提前向量化,用numpy广播一次性算出所有候选回收点的距离矩阵,避免Python层循环;第二,加了一个"续航上限剪枝"——距离超过最大飞行距离的候选回收点直接丢弃,不参与后续计算。优化之后单次评估耗时降到0.03秒,整体实验时间压缩到15分钟以内,这个级别才允许你反复试错调参。

如果你复现时也遇到评估函数过慢的问题,记住一个原则:能向量化的不要用for循环,能在评估前预计算的不要放到评估里重复算。

6.2 坑二:无人机服务标记的变异导致路径分裂

编码方案里第二段标记是"0/1"表示服务方式。经典位变异翻转某个客户点的标记后,会导致一个连锁问题:如果这个客户点原本在卡车路径的中间,被标记为无人机服务后摘除,卡车路径变成前段-后段两截,但两截之间没有重新连接逻辑,解码出来的卡车路径是断裂的。

修复方式是在解码器里增加"路径重连接"步骤:摘除无人机客户点之后,直接将前驱节点和后继节点相连,组成新的卡车路径。这个逻辑在标准VRP解码里叫"remove-and-connect",属于常识性操作,但如果初版代码没考虑周全,跑出来的个体有大量非法路径,目标函数值会是NaN(因为路径断开导致距离无法计算)。

6.3 坑三:约束违反量的惩罚系数怎么定

pymoo里处理约束的方式是:约束函数G输出正值代表违反,NSGA-II在比较个体时会优先选择约束违反量小的个体。约束违反量需要归一化——如果把容量超载的违反量设为绝对值(比如超载50kg就写50),而续航超过1分钟写1,两者量纲天差地别,会严重影响约束优先级的排序。

我的做法是把所有约束违反量归一化到[0,1]区间:容量违反量除以卡车总载重,续航违反量除以最大续航时间,时间同步违反量除以一个经验值(比如30分钟)。每个约束算出来的相对违反量再取最大值作为该个体的总约束违反值。这个细节直接决定遗传算法能不能在可行解和不可行解之间找到合适的平衡点。

6.4 坑四:pymoo版本差异导致API报错

pymoo的API变化不大,但偶尔会遇到某些方法在新版本里改了位置。我复现时用pymoo 0.6.x,hypervolume模块已经从performance_indicator迁移到indicator.hv,termination的get_termination从pymoo.termination迁到了pymoo.util.termination(不同版本有差异)。如果你跑代码发现import报错,先查一下当前版本的文档,别急着改逻辑代码。

还有一个细节:如果你在Jupyter Notebook里跑pymoo,最好把matplotlib的backend设为inline,否则pymoo自带的绘图接口(比如plot_pareto_front)可能弹不出图,或者直接报错。

7. 结果解读与论文写作建议

跑完实验拿到数据之后,论文写作阶段还有几个要注意的点。

Pareto前沿图怎么画才规范:横纵轴要标注单位,两个目标都要说明"越小越好"。前沿面如果出现明显的"台阶"——即某些区域没有解覆盖——说明种群在该目标维度上的多样性不足,需要增大变异率或者增加种群规模。审稿人看到前沿图有明显空洞会怀疑算法性能,你得提前自己检查。

收敛曲线怎么展示:pymoo在迭代过程中记录了每代的最优目标值(通过algo.callback记录),你可以画出HV指标随代数变化的曲线,展示算法收敛过程。我实测的结果是HV在前50代快速上升,100代以后趋于平缓,这个曲线充分说明200代的终止条件是足够的,放在论文里也是很有说服力的实验证据。

和文献对比怎么做:原文论文里使用的可能是CPLEX精确解(小规模下)或某种自定义启发式,如果你复现时想和它做对比,需要注意算例规模要保持一致。精确算法在小规模(10~15个客户点)下能拿到最优解,但客户点增至30个后CPLEX可能几小时都算不完;而NSGA-II始终能在10分钟内出解。这个对比写进论文很有价值,它能体现启发式算法在求解大规模多目标问题时的优势。

我个人在复现过程中最大的体会是:论文读十遍不如代码跑一遍。很多在公式推导中轻描淡写的约束,真正实现起来全是魔鬼细节——无人机回收点的选择、服务方式标记的变异、约束违规的归一化处理,每一个都值得单独写一篇经验帖。如果你也在做类似的复现工作,我建议先跑通小规模算例(10个客户点),手动构造一个最优解来校验代码正确性,再逐步扩大问题规模。这样可以避免在大规模算例上花了大量调试时间最后发现是基础逻辑错误。

最后再分享一个小技巧:pymoo的res类里存了完整的种群和前沿信息,跑完不要急着关闭Notebook,用np.savetxt把Pareto前沿的目标值和决策变量保存下来,后续画图、算指标、复现实验结果都靠这些数据,避免重新跑一遍浪费时间。

希望这篇复现记录能帮你少踩几个坑。如果有更好的编码方案或者约束处理思路,欢迎在评论区交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询