“低空经济”四个字这几年几乎出现在每一本物流配送方向论文的引言里标题里那篇论文做的正是低空经济下的车辆与无人机协同配送路径优化。它和传统车辆路径问题最大的区别是决策对象从“一辆车跑完整条线”变成了“一辆车带着多架无人机分段接力跑完末端配送”。标题后半段又点名了两个硬要求一是用 pymoo 求解二是采用集中式协同配送模式也就是把所有车辆的路线、无人机的发射回收点、任务分配放到同一个优化模型里一次性决策。这篇帖子我会从建模、编码、算子配置到实验设置完整过一遍我复现时的做法和踩坑适合正在做相关论文复现、准备低空物流调度实验或者刚打算用 pymoo 做路径优化课题的朋友参考。1. 先把“集中式协同配送”的问题边界拆清楚1.1 车机接力在低空经济里具体怎么跑大多数论文里的场景长这样一辆卡车从仓库出发车上携带若干架多旋翼无人机。卡车开到某个合适位置后停下无人机从这里起飞去给附近几个客户送货再返回卡车或者飞到前方某个预设回收点与卡车汇合。卡车本身也会按顺序服务沿途客户等无人机回来以后一起驶往下一个节点。这里最关键的是“接力”两个字无人机不是从仓库直接飞终点而是靠卡车一路“背着”它走把无人机的航程短板绕开了。对模型来说这意味着一架无人机的飞行可以被拆成三段——从卡车出发、执行客户任务、返回卡车或飞到下一个回收点。每段都依赖卡车的时空位置所以调度比普通 VRP 复杂得多。如果我们把决策方式分成两层来理解先规划卡车路线再在给定路线上插入无人机任务这叫分散式或两阶段式同时决定卡车的访问顺序、无人机的发射与回收点、客户由谁服务所有决策一起交给优化器这就是标题里说的集中式协同配送模式。集中式的优点是不会被“先车后机”的顺序限制锁死解空间缺点是决策变量膨胀评价一次候选解还要处理卡车和无人机之间的时间同步所以论文里普遍采用智能算法而不是精确求解器。1.2 问题域的数学结构一个变量、两重目标、多组约束把这类问题写成标准模型时我通常先列一张“要素表”后面写代码和做实验全靠它约束范围。组成常见记号说明对应代码里的部分客户点集合N每个客户只能被服务一次编码与解码函数卡车路径route卡车的访问顺序含仓库起点与终点路径排列段无人机任务task(k, i, s, r)无人机 k 服务客户 i起点 s、回收点 r任务分配段目标一min T最晚完成时刻即整体配送完工时间目标 F[0]目标二min C运营成本卡车距离、无人机飞行距离和固定成本目标 F[1]约束一航程限制无人机单次飞行距离或电量不能超过上限不等式约束 G约束二载重限制无人机每次最多携带重量有限客户需求不能超过它评价函数内判断约束三时间同步卡车在回收点等待无人机或无人机等待卡车等待时间进入完工时间时间轴推演目标函数一般会写成这样的形式min F (T_completion, C_operation) T_completion max(卡车到达仓库时间, 所有无人机返回时间) C_operation 卡车总里程 * 单位里程成本 无人机总飞行里程 * 单位飞行成本约束方面航程限制是硬约束通常写成无人机任务飞行距离不超过 R_max时间同步是一种“隐式约束”它不会让解不可行但会通过等待时间把目标函数值拉高。很多初写代码的人会把所有精力放在距离约束上结果跑出来的路线在时间维度上很难看就是因为没处理好同步关系。1.3 为什么选 pymoo而不是 Gurobi 或自己重写 GA这个课题里客户点规模做到几十个以上之后精确求解器就很难在可接受时间内给出 Pareto 前沿自己从零写非支配排序遗传算法又要花大量时间在算法层——选择压力、拥挤距离、存档管理、可视化。pymoo 的价值在于把这些通用部分做好了我们只需要关注“问题本身”写 Problem 子类定义目标和约束再组合采样、交叉、变异算子。另外一点pymoo 是纯 Python 的对论文复现很友好。我不用把模型翻译成求解器特定的线性表达式所有非线性判断、时间轴推演都可以直接写在评价函数里。这对带航程、载重、时间窗口等多重约束的车机协同问题来说非常关键因为很多约束本质上是程序逻辑而不是纯数学函数。2. 从变量表走到 Python 评价函数编码、解码与约束处理2.1 我采用的染色体结构以及为什么不直接模拟二进制最开始我也试过把每个客户到某节点的分配关系编码成 0/1 变量让 pymoo 用模拟二进制交叉去优化。跑了一轮就发现不行这类问题本质是排序问题卡车路线是一段排列不是一组相互独立的连续变量。排列上的相邻关系直接决定距离单纯改某个位点会造成巨大扰动交叉之后还很容易产生重复节点。论文复现里更自然的做法是分段编码。我采用的是这样的染色体结构染色体 [卡车路径排列段] [无人机发射点选择段] [无人机回收点选择段]举个例子客户数为 6 时染色体可能长这样[4, 2, 6, 1, 3, 5] | [0, 2, 1, 0, 2, 3] | [1, 0, 3, 2, 1, 4]其中第一条排列表示卡车的客户访问顺序第二段的每个值表示安排给对应客户的无人机编号或者“由卡车服务”的标签第三段表示回收点索引。这样编排的好处是交叉和变异可以分块处理排列段用保序算子分配段用常规变异降低破环合法解的概率。这里也要提醒一句没有绝对最好的编码。如果论文里假设一架无人机一次只能服务一个客户那第二段就相当于客户到无人机的指派关系如果允许一架无人机一次载多件货连续投递那第二段还要变成“无人机子路径”解码复杂度立刻上一个台阶。2.2 decode 函数的时间轴推演逻辑评价函数不是直接拿距离算两个目标而是要把染色体“执行一遍”回溯出卡车和无人机的时间线。我复现时的 decode 逻辑大概是按排列段生成卡车的节点访问序列逐段累加卡车行驶时间遍历每个被分配了无人机的客户找到它对应的发射点和回收点计算无人机从发射点到客户、再从客户到回收点的飞行时间在回收点做同步比较先到的一方要等待另一方卡车到达仓库的时间和所有无人机回归任务完成的时刻取最大值就是完工时间。等待时间的处理最容易出错。假设卡车从发射点 P 出发去回收点 R无人机先去客户 i 再返回 R。若卡车先到 R无人机后到卡车可以原地等待若无人机先到 R它就得在空中悬停或者落地等待这个等待时长同样会算进整体完工时间。实际编码里我还会加一个很小的惩罚系数无人机长期待命会增加电量消耗如果只把悬停时间计入完工时间可能会选出“卡车疯狂赶路、无人机原地傻等”的伪最优解。2.3 约束冲突的两种常见处理罚函数和修复算子对于航程和载重这类硬约束常见选择有两种罚函数和修复算子。罚函数的写法比较直观violation max(0, drone_flight_distance - drone_max_range) out[G] [violation]pymoo 会平等对待所有不等式约束用 NSGA-II 的约束支配关系引导种群往可行域走。这种方法实现快但参数敏感罚太轻不可行解容易霸占前沿罚太重种群搜索会很扭曲。我实际操作时采用的是“先修复、后计算”的思路。对无人机超航程的任务直接尝试换一个发射点或回收点如果还是超就标记该客户改由卡车服务。这样做虽然多消耗了一点解码时间但能让种群中始终保留大量可行结构NSGA-II 跑起来更稳。修复过程要记录在哪里修复过否则同一个染色体反复修复会削弱遗传信息的连续性。3. pymoo 接入细节问题类、算子组合与收敛设置3.1 用 ElementwiseProblem 子类把模型包起来pymoo 里最常用的是继承ElementwiseProblem它每次只评估一个个体对带复杂同步推演的问题特别合适。问题类模板我一般写成这样import numpy as np from pymoo.core.problem import ElementwiseProblem class TruckDroneProblem(ElementwiseProblem): def __init__(self, env): self.env env self.n_c env[n_customers] # 示意第一段为卡车路径排列后两段为无人机发射点和回收点选择 super().__init__( n_var3 * self.n_c, n_obj2, n_constr1, xl0, xu1, ) def _evaluate(self, x, out, *args, **kwargs): route decode_permutation(x[: self.n_c]) launch decode_assignment(x[self.n_c: 2 * self.n_c]) recovery decode_assignment(x[2 * self.n_c:]) completion_time, operation_cost self.simulate( route, launch, recovery ) violation self.check_hard_constraints( route, launch, recovery ) out[F] [completion_time, operation_cost] out[G] [violation]xl 和 xu 都设成 0 和 1是因为我打算统一用映射函数把个体转成离散标签pymoo 本身的数值边界只用来保证初始采样范围真正决定变量含义的是 decoder。如果你直接用排列算子那 xl/xu 可以设置成 0 到 n-1关键是把采样、交叉、变异算子一起换成 pymoo 提供的排列类算子否则边界和编码会打架。3.2 排列段的自定义算子怎么组采样、交叉、变异pymoo 自带排列类算子三件套是PermutationRandomSampling、OrderCrossover和InversionMutation。它们专门处理排列编码能保持“每个节点出现一次”的合法性。from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.operators.sampling.rnd import PermutationRandomSampling from pymoo.operators.crossover.order import OrderCrossover from pymoo.operators.mutation.inversion import InversionMutation但这里的注意点是我的染色体包含三块并不是整条染色体都是排列。如果直接把OrderCrossover作用到全部变量上会把无人机分配段也当成排列来交换生成一堆乱码。实际做法是自定义一个分段算子或者更简单把排列段和分配段拆开放在两个问题里串行优化但这样就又回到两阶段了。我的折中方案是第一段用排列算子第二、三段用普通均匀变异。实现时给每段不同的变异概率排列段变异概率低一些避免路径被频繁打碎分配段变异概率可以高一些因为任务重新指派对解的改善往往更直接。3.3 NSGA-II 的参数种群规模、代数、变异概率实际测试里规模 30 个客户点左右时我可以直接采用下面这组配置algorithm NSGA2( pop_size200, samplingPermutationRandomSampling(), crossoverOrderCrossover(prob0.85), mutationInversionMutation(prob0.15), eliminate_duplicatesTrue, ) res minimize( problem, algorithm, (n_gen, 300), seed42, verboseTrue, save_historyTrue, )这里的eliminate_duplicatesTrue很关键。车机协同问题里不同染色体可能解码出完全相同的配送方案重复个体会让种群多样性快速下降。pymoo 会在每一代检查重复并做替换代价是稍微增加运行时间但对保持 Pareto 前沿完整度很有帮助。还有一个容易被忽视的参数是随机种子。论文复现时如果不开源完整实验配置不同种子跑出来的差异可能比算法之间的差异还大。我习惯固定一组随机种子例如 42、2025、888。最终报告里优先展示中位数种子对应的前沿而不是所有种子里最好的那个。3.4 提取最优解和 Pareto 前沿minimize返回的res对象里res.F是所有非支配解的目标值矩阵res.X是对应染色体。如果论文只需要一个折中最优解我会把两个目标做归一化后取距离原点最近的点如果论文要画前沿就直接用res.F绘图。import matplotlib.pyplot as plt F res.F plt.scatter(F[:, 0], F[:, 1], s30) plt.xlabel(Completion Time) plt.ylabel(Operation Cost) plt.title(Pareto Front) plt.show()多目标优化的结果展示一定得有取舍逻辑。不能只说“我这个算法找到了很多帕累托点”还要说明决策者最终如何从多个点里选一个。常用方案是权重法或者熵权 TOPSIS具体取决于论文设定但代码层面很简单把前沿目标值读出来算一下综合得分就行。4. 跑通后影响结果最多的几个因素与复现注意事项4.1 无人机数量与最大航程的改变会重组“可行域形状”这不是我脑补的同一组模拟客户点把无人机数量和最大航程调来调去解的结构会出现明显变化。为了说明趋势我用示意数据做了一组实验数字不代表任何论文原始结果只是反映相对走向无人机数量无人机最大航程完工时间趋势运营成本趋势观察到的现象1 架2 公里基准值偏高基准值较低绝大多数客户仍由卡车服务2 架2 公里明显下降小幅上升无人机开始覆盖偏远散点2 架5 公里继续下降大幅上升无人机绕飞距离变长成本曲线变陡3 架5 公里下降不再明显继续上升无人机之间出现重复覆盖边际收益递减原因是低空经济场景里无人机并不是越多越好。当前沿上考虑运营成本时增加一架无人机会带来固定起降和维护成本除非它能显著降低完工时间否则多出来的无人机根本不会被选中。复现论文时如果发现结果里无人机利用率很低不要急着调算法先确认目标函数里是否包含了无人机固定成本。4.2 时间同步约束才是真正拖后腿的很多论文里车辆路径长度和无人机飞行距离只占模型的一小部分最复杂的是同步。真实场景中卡车 60 公里/小时在路上跑无人机 40 公里/小时在飞两者的速度差会让“谁先到谁等待”成为常态。我举一个我实际算过的例子某无人机从发射点 A 飞到客户 i再返回回收点 B总飞行距离 4 公里卡车从 A 出发沿公路到 B距离只有 2 公里。卡车大约 2 分钟就到无人机按 40 公里/小时飞行要 6 分钟。也就是说无人机回来之前卡车要在 B 点空等 4 分钟。这 4 分钟如果不体现在目标函数里最终方案的实际执行时间就会远超计划。所以在 decode 的时候我会单独统计一个“总同步等待时间”并把它写进日志。一个健康的最优解同步等待时间不应该占总完工时间的大头。如果某一代解里等待时间占比超过 30%多半是发射点和回收点的选择出了问题而不是路径顺序的问题。这是我复现这类论文最值钱的一条经验先看等待时间别急着画前沿。4.3 评价函数慢了整个 GA 都会被拖垮集中式协同配送的评价函数包含时间轴推演比普通 VRP 的纯距离计算慢得多。我第一次跑 50 客户的实例NSGA-II 跑 500 代要将近四个小时后来发现主要是 decode 里反复用矩阵索引生成新子数组导致 Python 频繁分配内存。后来做了三处优化速度提升明显把客户坐标、需求、发射点和回收点之间距离矩阵全部预计算decode 时只查表不重算在 simulate 里避免创建大列表用简单数值变量累加时间和距离如果实例规模很大把ElementwiseProblem换成向量化版本让 pymoo 一次评估整个种群不再逐个调用 Python 函数。第三种优化收益最大但编码复杂度也会上去。建议先做前两种一般已经能覆盖大部分论文复现的需求。4.4 论文复现必须写进实验记录的清单到了写论文或者做对比实验这一步我建议把所有设置放在一个config.py里统一维护至少包括下面几项设置项为什么要记录客户点生成方式和随机种子坐标分布直接决定路径形态种子影响可比性距离计算方式欧氏距离还是道路网络距离无人机直线距离不能和卡车道路距离混用卡车速度、无人机速度速度比决定了同步等待时间是模型的核心假设最大航程、最大载重硬约束边界直接影响可行域发射回收方式无人机必须回放飞点还是可以到任意回收点两者差异很大目标是否归一化完工时间和成本量纲不同不归一化会影响前沿形状随机种子与硬件并行状态便于他人复现和理解运行时长实验记录里还要写清楚生成 Pareto 前沿后如何选点。很多论文复现卡在读不懂“为什么别人得到三个点我得到三十个点”其实就是因为目标归一化方式不同或者约束权重不同。只要把这些写清楚后续算法改进才有可靠基线。4.5 复现这类课题最稳妥的起步方式最后分享一个实测很有用的顺序不要一上来就跑双目标 NSGA-II。先把目标 F 暂时固定为单一目标也就是只最小化运营成本用遗传算法跑通 decode确认完工时间、等待时间、约束判断在逐步打印时是合理的。单目标跑通后再打开第二个目标这时候出问题基本可以定位在 Pareto 选择和多样性保持上而不是底层评价函数。另一个小技巧是故意构造一个人工小案例比如两个客户、两架无人机手算出最优解再看程序能不能找到它。这一步两三个小时就能完成但能省掉后面调试好几天的时间。我在复现车机协同问题的过程中最大的教训就是算法代码永远是最后一步先把模型假设和评价逻辑钉死pymoo 才能发挥它应有的效果。
阅读完成 · 觉得有帮助?