简介:这份资源面向无人机任务分配方向的学习者与开发者,提供一套基于Python与粒子群优化算法(PSO)的多无人机任务分配系统完整实现。项目将每架无人机视为一个粒子,以位置表示任务分配方案、速度表示调整方向,通过迭代搜索最优策略,并围绕任务紧急程度、载荷能力、续航时间等要素构建适应度函数,兼顾负载与飞行距离等约束。资源包共14个文件,以9个Python脚本为核心,涵盖主流程、距离计算、适应度评估、解码与PSO迭代等模块,另附4张PNG结果图与1个gitattributes配置,整体约1.38MB,结构紧凑便于阅读与二次开发。目前已有1789人学习下载。读者可据此掌握从问题建模、适应度设计到算法调参的完整思路,并参考运行结果图验证分配效果,适合作为课程设计、毕业设计或算法入门的实践素材。
1. 多无人机任务分配为什么需要粒子群优化:从一组炸机说起
三架无人机同时起飞执行区域侦察,任务点 12 个,结果两架撞在同一片空域,第三架电量耗尽在半路。复盘时发现,问题不在飞控,而在起飞前那张任务分配表——谁去哪个点、按什么顺序飞、什么时候返航,全是拍脑袋定的。这就是多无人机任务分配要解决的核心问题:在任务点数量、无人机数量、续航、载荷、时间窗等多重约束下,找到一组让整体代价最小的分配方案。
粒子群优化算法(PSO)之所以常被拿来做这件事,是因为它天然适合连续和离散混合的优化问题,实现门槛低,几十行 Python 就能跑起来,而且收敛速度在中小规模任务分配上表现稳定。这篇文章面向的是已经会用 Python 写基本脚本、想把这套方案真正落地到多机调度场景的工程师。我会从建模、编码、参数、避坑一路讲到怎么验证结果是不是真的可用,不绕弯子。
2. 把任务分配写成 PSO 能吃的数学模型
2.1 决策变量怎么定义:0-1 矩阵还是任务序列
多无人机任务分配的第一道坎不是算法,是建模。最常见的两种编码方式:一种是 0-1 分配矩阵,行是无人机、列是任务点,矩阵元素为 1 表示该机执行该任务;另一种是任务序列编码,每个粒子是一个长度为任务数的排列,排列中每个位置的值表示执行该任务的无人机编号。
0-1 矩阵的优点是直观,缺点是维度随无人机数×任务数膨胀,粒子搜索空间大,而且容易产生无效解(一个任务被多机认领,或某机没分到任务)。任务序列编码的优点是维度固定为任务数,天然保证每个任务只被分配一次,缺点是丢失了执行顺序信息,需要额外一层排序逻辑。
我一般会选任务序列编码,原因是它把「每个任务必须被且仅被分配一次」这个硬约束直接编码进了粒子结构里,省掉大量罚函数调参的麻烦。具体做法是:粒子位置向量长度等于任务数 N,每个分量的取值范围是 [1, M+1),M 是无人机数量,取整后得到该任务归属的无人机编号。
import numpy as np def decode_particle(position, n_tasks, n_uavs): """ 将连续位置向量解码为任务分配方案 position: 长度 n_tasks 的浮点向量 n_tasks: 任务点数量 n_uavs: 无人机数量 返回: 长度 n_tasks 的整数数组,每个元素是无人机编号(0-based) """ # 将连续值映射到 [0, n_uavs) 区间并取整 normalized = np.clip(position, 0, 1 - 1e-9) assignment = np.floor(normalized * n_uavs).astype(int) return assignment这段代码的关键在np.clip那一行。PSO 迭代中粒子位置可能飞出 [0,1] 边界,如果不做裁剪,floor之后可能得到负数或超出无人机编号范围的值,后续计算代价函数时直接索引越界。裁剪上界用1 - 1e-9而不是 1,是为了避免floor(1.0 * n_uavs)恰好等于 n_uavs 导致越界。这个细节看起来小,但在实际跑的时候,少了这个 1e-9,程序会在第几十代突然崩掉,排查起来很费时间。
2.2 代价函数:飞行距离、时间窗和负载均衡怎么加权
分配方案好不好,全靠代价函数说话。多无人机任务分配的代价通常由三部分组成:总飞行距离、时间窗惩罚、负载均衡惩罚。
总飞行距离的计算依赖每架无人机执行其分配到的任务点的顺序。由于任务序列编码本身不含顺序,需要先按某种规则排序。常见做法是对每架无人机分配到的任务点,用最近邻策略生成一条飞行路径,再累加距离。最近邻不是最优,但计算快,在 PSO 迭代几千次的场景下,用精确 TSP 求解器不现实。
时间窗惩罚针对的是任务点有最早开始时间和最晚结束时间约束的情况。如果无人机到达任务点的时间早于最早开始时间,需要等待;晚于最晚结束时间,则产生惩罚。负载均衡惩罚衡量各无人机任务量的方差,避免一架忙死、一架闲着。
def cost_function(assignment, tasks, uavs, w1=1.0, w2=10.0, w3=5.0): """ assignment: 每个任务归属的无人机编号 tasks: 任务点列表,每个含坐标、时间窗 uavs: 无人机列表,每个含起始坐标、速度 w1/w2/w3: 距离/时间窗/负载均衡权重 """ total_distance = 0.0 time_penalty = 0.0 task_counts = np.zeros(len(uavs)) for uav_id, uav in enumerate(uavs): # 取出该无人机负责的任务点 assigned = [tasks[i] for i in range(len(tasks)) if assignment[i] == uav_id] task_counts[uav_id] = len(assigned) if not assigned: continue # 最近邻排序生成飞行路径 current = uav['start'] remaining = assigned[:] elapsed = 0.0 while remaining: dists = [np.linalg.norm(np.array(current) - np.array(t['pos'])) for t in remaining] idx = int(np.argmin(dists)) nxt = remaining.pop(idx) d = dists[idx] total_distance += d elapsed += d / uav['speed'] # 时间窗惩罚 if elapsed < nxt['earliest']: elapsed = nxt['earliest'] elif elapsed > nxt['latest']: time_penalty += (elapsed - nxt['latest']) current = nxt['pos'] # 负载均衡:任务数标准差 balance_penalty = np.std(task_counts) return w1 * total_distance + w2 * time_penalty + w3 * balance_penalty权重 w1、w2、w3 的设定直接决定优化方向。w2 设得过大,算法会优先满足时间窗,可能牺牲总距离;w3 设得过大,任务会被均匀切分,但每架无人机的飞行路径可能变得绕。我的经验是先把 w1 固定为 1.0,w2 取 5 到 20 之间,w3 取 1 到 10 之间,跑几组对比看帕累托前沿,再根据实际业务容忍度定。如果时间窗是硬约束,w2 要大到让违反时间窗的解在进化中被淘汰。
3. 用 Python 把 PSO 主循环跑起来
3.1 粒子群初始化与速度更新公式的代码实现
PSO 的核心就两个公式:速度更新和位置更新。速度更新决定粒子下一步往哪飞、飞多快,位置更新决定粒子落在哪。标准公式里,速度受三部分影响:惯性项(保持当前方向)、个体认知项(向自己历史最优靠拢)、社会认知项(向全局最优靠拢)。
class PSO: def __init__(self, n_particles, n_tasks, n_uavs, max_iter, w=0.7, c1=1.5, c2=1.5): self.n_particles = n_particles self.n_tasks = n_tasks self.n_uavs = n_uavs self.max_iter = max_iter self.w = w # 惯性权重 self.c1 = c1 # 个体学习因子 self.c2 = c2 # 社会学习因子 # 初始化位置和速度 self.positions = np.random.rand(n_particles, n_tasks) self.velocities = np.random.uniform(-0.1, 0.1, (n_particles, n_tasks)) self.pbest_pos = self.positions.copy() self.pbest_cost = np.full(n_particles, np.inf) self.gbest_pos = None self.gbest_cost = np.inf def step(self, cost_fn): for i in range(self.n_particles): # 解码并计算代价 assignment = decode_particle(self.positions[i], self.n_tasks, self.n_uavs) cost = cost_fn(assignment) # 更新个体最优 if cost < self.pbest_cost[i]: self.pbest_cost[i] = cost self.pbest_pos[i] = self.positions[i].copy() # 更新全局最优 if cost < self.gbest_cost: self.gbest_cost = cost self.gbest_pos = self.positions[i].copy() # 速度与位置更新 r1 = np.random.rand(self.n_particles, self.n_tasks) r2 = np.random.rand(self.n_particles, self.n_tasks) self.velocities = (self.w * self.velocities + self.c1 * r1 * (self.pbest_pos - self.positions) + self.c2 * r2 * (self.gbest_pos - self.positions)) # 速度限幅,防止粒子飞太快跳过最优解 self.velocities = np.clip(self.velocities, -0.2, 0.2) self.positions = np.clip(self.positions + self.velocities, 0, 1 - 1e-9)速度限幅那行np.clip(self.velocities, -0.2, 0.2)是血泪经验。不限幅的话,粒子速度会随着迭代累积越来越大,位置在边界反复弹跳,全局最优附近根本停不下来。限幅范围一般取位置范围的 10% 到 20%,这里位置范围是 [0,1],所以取 0.2 是合理的。
3.2 惯性权重和学习因子的调参套路
PSO 的调参玄学主要集中在三个参数:惯性权重 w、个体学习因子 c1、社会学习因子 c2。
惯性权重控制粒子保持原有速度的趋势。w 大,全局搜索能力强,但收敛慢;w 小,局部搜索能力强,但容易早熟收敛。常见做法是线性递减:迭代初期 w 取 0.9 鼓励探索,后期降到 0.4 鼓励收敛。实现上就是在每次 step 里根据当前迭代次数动态调整 w。
c1 和 c2 的典型取值是各 1.5 到 2.0。c1 大于 c2,粒子更相信自己的经验,适合多峰问题;c2 大于 c1,粒子更倾向于跟随全局最优,收敛快但可能陷入局部最优。多无人机任务分配的场景下,我一般让 c1 略大于 c2,比如 c1=1.8、c2=1.4,因为任务分配的解空间往往有多个局部最优,太早聚集到全局最优未必是好事。
| 参数 | 推荐范围 | 作用 | 调大后果 | 调小后果 |
|---|---|---|---|---|
| w | 0.4~0.9 | 惯性权重 | 探索强、收敛慢 | 收敛快、易早熟 |
| c1 | 1.4~2.0 | 个体学习因子 | 解多样性高 | 趋同快 |
| c2 | 1.4~2.0 | 社会学习因子 | 收敛快、易局部最优 | 搜索散漫 |
| 粒子数 | 20~50 | 种群规模 | 计算量大 | 覆盖不足 |
| 迭代数 | 100~500 | 终止条件 | 耗时 | 未收敛 |
粒子数不是越多越好。任务数在 20 以内时,30 个粒子跑 200 代基本能收敛到可接受解。粒子数加到 100,耗时翻三倍,解的质量提升可能不到 5%。这个投入产出比在实际项目里要算清楚。
4. 多无人机任务分配避坑:5 个让我返工的问题
4.1 粒子解码后出现空任务或重复分配
现象:跑完 PSO,输出分配方案时发现某个任务没有被任何无人机认领,或者同一个任务出现在两架无人机的任务列表里。
原因:如果用的是 0-1 矩阵编码,粒子位置取整后可能产生全零列(任务无人认领)或某列多个 1(重复分配)。这是编码方式本身的缺陷,不是算法 bug。
解决:换用任务序列编码,或者在使用 0-1 矩阵时加修复算子——对全零列随机选一架无人机置 1,对多 1 列只保留一个 1。修复算子要放在代价计算之前,否则无效解会污染 pbest 和 gbest。
4.2 代价函数权重没调好导致方案不可用
现象:算法收敛了,代价曲线很漂亮,但输出的方案里有一架无人机被分了 8 个任务,另一架只有 1 个,或者所有任务都堆在时间窗边缘。
原因:代价函数里负载均衡项或时间窗项的权重太小,被总距离项压制了。
解决:先把各项代价归一化到相近量级再加权。比如总距离除以最大可能距离,时间窗惩罚除以最大允许延迟,负载均衡用标准差本身就在 0 到任务数之间。归一化之后,权重才有可比性。我一般会跑一组权重组合,画出帕累托前沿,让业务方选一个可接受的平衡点。
4.3 算法早熟收敛到局部最优
现象:迭代到 30 代左右,gbest 就不再变化,但把结果画在地图上一看,明显有更优的分配方式。
原因:种群多样性丧失太快,所有粒子挤在同一个局部最优附近。常见诱因是 c2 过大、w 过小,或者粒子数太少。
解决:三个手段组合用。一是惯性权重线性递减,前期保持探索;二是加变异算子,每迭代 20 代对最差的 10% 粒子做随机重置;三是多跑几次取最优,PSO 本身是随机算法,单次结果不代表真实水平。
4.4 任务点数量增加后计算时间爆炸
现象:任务数从 10 增加到 30,单次迭代耗时从 0.1 秒涨到 3 秒,跑 200 代要 10 分钟。
原因:代价函数里对每架无人机的任务列表做最近邻排序,复杂度是 O(n²),任务数增加时平方级增长。如果还在 Python 层用循环算距离,更慢。
解决:把距离计算向量化。用 numpy 的广播机制一次性算出所有任务点之间的距离矩阵,最近邻排序时直接查表。另外,如果任务规模确实大,考虑把 PSO 和局部搜索结合,用 PSO 做粗分配,再用 2-opt 做路径精调,减少 PSO 迭代次数。
4.5 结果每次跑都不一样,无法复现
现象:同样的参数、同样的数据,两次运行得到不同的分配方案,没法做对比实验。
原因:PSO 初始化用了随机数,没有固定随机种子。
解决:在程序入口设np.random.seed(42),并把种子作为参数记录下来。如果要对比不同参数的效果,固定种子后只改参数,这样差异才归因于参数本身。另外,最终方案不要只取 gbest,把 pbest 里代价接近的几个方案都输出,人工看一眼,有时候次优解在实际执行中更鲁棒。
5. 验证分配方案是否真的可用:三个我必做的检查
跑出结果只是第一步,方案能不能用是另一回事。我一般会做三个检查。
第一个是可视化检查。把任务点和无人机起始点画在二维平面上,用不同颜色标出每架无人机的任务归属,连线画出飞行路径。肉眼看一遍,有没有交叉飞行、有没有某架无人机路径明显绕远。这一步能发现很多代价函数没捕捉到的问题,比如两架无人机航线交叉带来的碰撞风险。
import matplotlib.pyplot as plt def visualize(assignment, tasks, uavs): colors = plt.cm.tab10(np.linspace(0, 1, len(uavs))) for uid, uav in enumerate(uavs): pts = [uav['start']] + [tasks[i]['pos'] for i in range(len(tasks)) if assignment[i] == uid] xs, ys = zip(*pts) plt.plot(xs, ys, 'o-', color=colors[uid], label=f'UAV{uid}') plt.legend() plt.title('Task Assignment Result') plt.show()第二个是约束校验。写一个独立的校验函数,不依赖代价函数,逐条检查:每个任务是否只被分配一次、每架无人机任务数是否超过上限、时间窗是否满足、总航程是否超过续航。这个函数和代价函数分开写,避免代价函数里的 bug 掩盖约束违反。
第三个是敏感性测试。把任务点坐标随机扰动 5%,重新跑 PSO,看分配方案变化大不大。如果扰动一点点,方案就完全变了,说明这个解处在悬崖边上,实际执行时 GPS 误差或风偏就可能导致任务失败。稳定的解应该在轻微扰动下保持结构不变。
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| 任务覆盖 | 统计每个任务被分配次数 | 全部为 1 |
| 续航约束 | 累加每机航程 | 不超过最大航程 |
| 时间窗 | 模拟飞行时间线 | 无违反 |
| 鲁棒性 | 坐标扰动 5% 重跑 | 方案结构不变 |
| 负载均衡 | 任务数标准差 | 小于阈值 |
最后说一个我自己的习惯:每次调完参数,把配置和结果一起存成 JSON,包括随机种子、粒子数、迭代数、权重、最终代价、运行时间。过一周回头看,能快速定位是哪次改动导致效果变差。这个习惯帮我省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取