简介:一套基于Python与粒子群优化算法实现的多无人机任务分配系统完整源码包,面向无人机任务调度、智能算法应用方向的开发者与学习者。资源针对多无人机协同场景下的任务分配难题,使用PSO将每个可行分配方案编码为粒子,通过位置与速度迭代搜索最优解,涵盖主程序、适应度计算、距离与时间处理、全局变量、解码及约束检查等模块,可作为课程设计或研究项目的直接参考。压缩包体积约1.38MB,共14个文件,其中9个Python脚本构成核心算法框架,4张PNG图直观展示无人机飞行轨迹、散点图与方案对比,另附Git配置文件便于工程管理。目前已有1789人学习下载,适合具备基础Python语法、想深入理解粒子群优化在组合优化中落地方法的读者。通过阅读与运行代码,可掌握任务优先级、负载限制等真实约束下的PSO建模与调参思路,并迁移到其他路径规划或资源调度场景。
1. 多无人机任务分配为什么需要粒子群优化:从指派问题到NP-hard
多无人机任务分配(Multi-UAV Task Allocation)在工程上不是一个“多跑几个循环”就能解决的问题:当无人机数量从 3 架增加到 10 架、任务点从 5 个增加到 20 个,纯暴力枚举的组合数会瞬间爆炸,而经典匈牙利算法只适用于一对一匹配,面对“一架无人机依次执行多个任务”“不同任务价值不同”“无人机续航不同”这些现实约束时就无能为力了。粒子群优化算法(PSO)正好是这类离散组合优化问题的实用解:它不要求目标函数可导,不依赖初始解质量,代码量小且易于并行,非常适合在 Python 里快速实现一套多无人机任务分配原型系统。这篇笔记面向正在做毕业设计、竞赛或工程预研的开发者,讲清楚从数学建模到 Python 落地的完整链路,以及那些让 PSO 从“能跑”到“跑得好”的关键细节。
2. 把任务分配写成数学模型:目标函数、约束与编码方式
2.1 问题定义与符号
在写任何优化代码之前,第一步是把任务分配描述成数学问题。我这里采用最常见的“多旅行商问题”变体:有 N 架无人机,M 个需要执行的任务点,每架无人机从基地出发,访问分配给它的任务点后返回基地。每架无人机有最大航程限制,每个任务只能被一架无人机执行一次,目标是让所有无人机的总飞行代价最小,同时尽量均衡各无人机的负载。
用符号表示就是:决策变量为“哪个无人机去哪个任务点以及访问顺序”,目标函数包含总航程、任务完成时间、任务价值收益等多个维度。实际工程里很少只优化单一目标,通常是把多个目标加权成一个综合适应度值,或者用 Pareto 多目标优化。但对于初版系统,我建议先从单目标加权做起,后续再扩展。
约束条件里最容易被忽略的是“时序约束”:某些任务有最早开始时间,或者任务之间存在先后依赖关系。如果一开始就把这些约束全部塞进 PSO,粒子群会很难收敛,因为可行解空间被切得太碎。我一般先做无约束版本,跑通链路,再逐步加入约束惩罚项。
2.2 连续PSO如何适配离散任务分配
原始 PSO 是为连续实数优化设计的,粒子位置是一个实数向量,速度更新公式是 v = wv + c1r1*(pbest-x) + c2r2(gbest-x)。而任务分配是离散决策问题,所以必须做编码映射。常见有两种做法:一种是“实数编码 + 排序映射”,另一种是“直接离散化位置”。
我推荐第一种,原因是它保持了标准 PSO 的速度-位置更新机制,不需要改动核心公式。具体做法是:每个粒子的位置向量长度等于任务点数 M,每一维的值是个实数。每次迭代后,按这 M 个值从小到大排序,排序结果对应的索引就是任务执行顺序;然后按无人机的航程或任务容量约束,把这个顺序切分成若干段,分别分配给各无人机。这样“粒子位置”和“任务分配方案”之间就建立了一对一映射关系。
第二种做法是直接把位置向量每一维取整成无人机编号,但这样做的问题在于:速度更新后取整会丢失梯度信息,而且位置空间不连续,粒子很容易在边界震荡。实测下来,排序映射的收敛稳定性远好于直接取整,新手做系统时不要走弯路。
2.3 最小可跑通的PSO代码
下面给出一个最简实现,先让你看到 PSO 核心循环长什么样。这个版本不做任何工程化封装,只用来验证“代码能不能跑通”:
import numpy as np # 任务点坐标(首尾是基地),这里随便造 6 个点 points = np.array([[0, 0], [2, 3], [5, 1], [7, 4], [3, 6], [6, 2], [0, 0]]) def calc_distance(order): total = 0.0 for i in range(len(order) - 1): total += np.linalg.norm(points[order[i]] - points[order[i + 1]]) return total n_particles = 30 n_tasks = len(points) - 2 # 去掉首尾基地,实际任务点 pos = np.random.rand(n_particles, n_tasks) * 20 - 10 vel = np.random.rand(n_particles, n_tasks) * 2 - 1 pbest = pos.copy() pbest_score = np.full(n_particles, 1e9) gbest = pos[0].copy() gbest_score = 1e9 w, c1, c2 = 0.8, 1.5, 1.5 def decode(x): order = np.argsort(x) + 1 # 加 1 是因为 0 号是基地 return np.concatenate(([0], order, [len(points) - 1])) for iter in range(200): for i in range(n_particles): order = decode(pos[i]) score = calc_distance(order) if score < pbest_score[i]: pbest_score[i] = score pbest[i] = pos[i].copy() if score < gbest_score: gbest_score = score gbest = pos[i].copy() for i in range(n_particles): r1, r2 = np.random.rand(2) vel[i] = w * vel[i] + c1 * r1 * (pbest[i] - pos[i]) + c2 * r2 * (gbest - pos[i]) pos[i] = pos[i] + vel[i] print("最优路径:", decode(gbest)) print("最短距离:", gbest_score)这段代码的逻辑说明:decode函数是核心,np.argsort(x)把实数位置向量转成任务序列,前后补上基地索引 0 和最后一个点,就得到一个闭合路径。calc_distance按当前路径顺序累加欧氏距离作为适应度值。速度更新公式里r1、r2是每次迭代重新采样的随机数,用来维持粒子多样性。
参数说明:w=0.8是惯性权重,控制粒子继承上一时刻速度的程度;c1=1.5是自我认知系数,c2=1.5是社会认知系数。这两个系数取 1.5 左右是 PSO 的常用经验值,不需要每次改。粒子数 30 对 6 个任务点来说已经足够,任务点增加到 30 个以上时建议把粒子数提到 80~120。
3. 设计与实现多无人机任务分配系统:模块划分与核心流程
3.1 系统整体架构与数据流
把 PSO 从“函数”升级成“系统”,需要想清楚模块边界。一个可维护的多无人机任务分配系统至少包含四个模块:任务参数输入、粒子群初始化、迭代优化引擎、结果解析与输出。输入模块负责读取任务点坐标、无人机数量、最大航程、任务价值等参数;迭代优化引擎负责维护粒子群状态、计算适应度、更新速度和位置;结果解析模块把最优粒子的位置向量还原成“每架无人机飞哪些任务点”的调度表。
数据流方向是单向的:原始参数 -> 编码后的粒子群 -> 适应度评估 -> 速度和位置更新 -> 直到满足终止条件 -> 解码输出。这里最容易犯的错误是把“解码”功能散落到各个模块里,导致后续想调整约束时到处改代码。我在实际系统里会定义统一的Solution类,粒子的位置向量和解析后的调度方案绑定在一起,迭代过程中只操作位置向量,最终只解码一次。
3.2 初始化、适应度与速度更新
初始化时,粒子位置向量每一维应在 [-10, 10] 范围内均匀随机,这样排序映射产生的任务序列初始就具有较高的随机性。速度初始化一般取位置范围的 10%~20%,也就是 [-2, 2] 左右。如果速度初始值太大,粒子一开始会剧烈震荡,适应度曲线呈现锯齿状;太小则收敛很慢。
适应度函数设计是整个系统最重要的部分。我的建议是把它拆成“代价项 + 约束惩罚项”两个层次。代价项包括总航程、任务完成总时间、任务收益的负值等;约束惩罚项用于处理航程超限、任务遗漏、无人机负载不均等情况。工程上常用的是“外点罚函数法”:约束破坏得越严重,罚项越大,粒子会被自然拉回可行域。
速度更新部分要特别注意边界处理。位置向量虽然理论上是无界的,但如果某些维度长期徘徊在很大数值上,argsort的排序结果会被少数极端值主导,其他维度失去区分度。我一般在每次更新后做位置裁剪,限制在 [-100, 100] 内,速度裁剪在 [-20, 20] 内,防止数值溢出。
3.3 用Python实现完整迭代
下面是一个更接近工程可用的版本,加入了“多无人机切分”和“航程约束惩罚”:
import numpy as np class PSOAllocator: def __init__(self, points, n_uavs, max_range, n_particles=60, max_iter=300): self.points = np.array(points) self.n_uavs = n_uavs self.max_range = max_range self.n_particles = n_particles self.max_iter = max_iter self.n_tasks = len(points) - 2 self.w, self.c1, self.c2 = 0.9, 1.5, 1.5 def compute_distance(self, order): # order 是任务点索引列表,不含基地;这里自动加上首尾基地 seq = [0] + [t + 1 for t in order] + [len(self.points) - 1] dist = 0.0 for i in range(len(seq) - 1): dist += np.linalg.norm(self.points[seq[i]] - self.points[seq[i + 1]]) return dist def split_tasks(self, order): # 按“累计航程不超限”贪心切分任务给多架无人机 segments = [] start_idx = 0 current_uav = 0 current_dist = 0.0 base = self.points[0] for i in range(len(order)): task_idx = order[i] + 1 d_to_task = np.linalg.norm(base - self.points[task_idx]) d_task_to_base = np.linalg.norm(self.points[task_idx] - base) if current_dist + d_to_task + d_task_to_base > self.max_range: segments.append((current_uav, order[start_idx:i])) current_uav += 1 start_idx = i current_dist = 0.0 if i > start_idx: current_dist += np.linalg.norm(self.points[task_idx] - self.points[order[i - 1] + 1]) else: current_dist = d_to_task segments.append((current_uav, order[start_idx:])) return segments def fitness(self, x): order = np.argsort(x) segments = self.split_tasks(order) total_dist = 0.0 penalty = 0.0 for uav, task_list in segments: seq = [0] + [t + 1 for t in task_list] + [len(self.points) - 1] d = 0.0 for i in range(len(seq) - 1): d += np.linalg.norm(self.points[seq[i]] - self.points[seq[i + 1]]) total_dist += d if d > self.max_range: penalty += (d - self.max_range) * 10 # 未覆盖所有任务的惩罚 assigned = set() for _, task_list in segments: assigned.update(task_list) missing = self.n_tasks - len(assigned) return total_dist + penalty + missing * 1000 def optimize(self): n = self.n_particles m = self.n_tasks pos = np.random.uniform(-10, 10, (n, m)) vel = np.random.uniform(-2, 2, (n, m)) pbest = pos.copy() pbest_fit = np.array([self.fitness(p) for p in pos]) gbest = pos[np.argmin(pbest_fit)].copy() gbest_fit = np.min(pbest_fit) for _ in range(self.max_iter): for i in range(n): f = self.fitness(pos[i]) if f < pbest_fit[i]: pbest_fit[i] = f pbest[i] = pos[i].copy() if f < gbest_fit: gbest_fit = f gbest = pos[i].copy() r1, r2 = np.random.rand(2) vel = self.w * vel + self.c1 * r1 * (pbest - pos) + self.c2 * r2 * (gbest - pos) pos += vel pos = np.clip(pos, -100, 100) vel = np.clip(vel, -20, 20) return gbest, gbest_fit, self.split_tasks(np.argsort(gbest))[:self.n_uavs]逻辑说明:split_tasks按“当前无人机累计航程是否超限”来切分任务序列,超限就把剩余任务给下一架无人机。fitness中missing * 1000是任务遗漏惩罚,确保 PSO 优先寻找能覆盖所有任务的解。optimize里位置和速度的裁剪避免了数值发散。
参数说明:max_range是无人机最大航程,需要根据你的任务场景设定,例如测绘任务中一架无人机单次飞行 20 公里,这个值就设 20。切割方式用的是贪心,虽然不一定全局最优,但和 PSO 配合时能给粒子一个“可行基础”,比让粒子随机拼凑出可行解要高效得多。注意最后的返回值只取了前self.n_uavs个 segment,如果实际切分出的段数超过无人机数量,说明任务分配失败,需要增大max_range或调整惩罚系数。
3.4 结果输出与可视化
优化结束后,用户最关心的是“每架无人机具体飞哪些点”。我会输出一个字典,键是无人机编号,值是该无人机的任务点序列,同时输出总航程和单机负载。为了验证算法效果,有必要把迭代过程中的最优适应度记录下来,画成收敛曲线。如果你的系统带界面,可以在地图上标注航线,查看有没有交叉和明显绕路。
可视化部分我用 Matplotlib 绘制航线,颜色区分不同无人机,基地用五角星标记。收敛曲线单独画一个子图,x 轴是迭代次数,y 轴是适应度值。这两张图在论文或项目汇报中几乎是必须的,它们能从视觉上证明“PSO 确实在收敛”,而不是随机乱跑。
4. 避坑与常见问题:PSO收敛陷阱与工程化坑
4.1 位置更新后任务编号越界
现象:解码时发现任务点编号超出实际范围,或者任务序列里出现了重复的任务点。原因:直接用四舍五入取整方式把实数位置映射成任务编号,速度更新后数值溢出,再加上没有做越界检查。解决:采用argsort排序映射,这种天然不重复;同时对位置向量加边界裁剪。还有一个容易忽视的点:基地索引和任务索引混用,任务点从 1 编号而基地是 0,写decode函数时要统一偏移量。
4.2 粒子群陷入局部最优,后期收敛不动
现象:迭代到 100 代后,最优适应度曲线变成水平直线,换一种随机种子效果也差不多。原因:惯性权重固定不变,粒子群过早聚集到某个局部极值附近,失去了探索能力;或者粒子数太少,搜索空间覆盖不足。解决:把惯性权重从 0.9 线性递减到 0.4,前 30% 迭代保持较大权重用于全局搜索,后面逐渐变小做局部精细搜索。同时把粒子数从 30 提升到 80~150,并设置停滞检测:如果连续 50 代最优适应度没有改善,就对部分粒子的位置做随机重置。
4.3 适应度函数量纲不一致,让某个目标主导
现象:总航程单位是公里,数值大概几十;任务价值单位是“分”,数值可能上千。加权求和后,PSO 只会优化数值大的那一项,另一项目标形同虚设。原因:没有做归一化。解决:在计算适应度前,先对每个目标做 min-max 归一化,或者用参考值相除,让所有目标都在 [0,1] 或者同一数量级。另一个常见做法是给每个目标乘一个权重系数,系数的量纲等于目标量纲的倒数,但这样调参很玄学,不如归一化来得直接。
4.4 任务遗漏,粒子总在“非完整解”上打转
现象:最终结果里部分任务点没有被任何无人机访问,但总航程很小。原因:惩罚项系数设置太小,例如missing * 1000里的 1000 相对总航程不够大,一些粒子发现放弃任务点能大幅降低航程,于是偏向缺陷解。解决:加大遗漏惩罚,对任务价值高的点使用更高的惩罚系数;或者直接在解码阶段强制把未分配的任务点随机插入某段路径中。我常用的手段是双重保险:惩罚系数设到 1e5,解码后再补一遍。
4.5 性能瓶颈在大规模任务点
现象:任务点个数超过 100 时,单次适应度计算就要遍历整条路径,300 次迭代乘以 100 个粒子,耗时几十秒。原因:用了纯 Python 循环,且每次解码都重新计算整个路径。解决:将距离矩阵提前算好,用矩阵索引替代逐点计算np.linalg.norm;适应度计算用向量化操作;还可以用并行池化评估每个粒子的适应度,因为粒子之间互不影响。实测把距离矩阵化之后,100 个任务点下单次迭代从 0.5 秒降到 0.1 秒左右。
5. 进阶技巧:约束处理、离散映射与收敛验证
5.1 位置向量到任务序列的稳健映射
排序映射虽然好用,但有一种情况会翻车:位置向量各维度的值差异极小,argsort的结果会因为浮点噪声而随机抖动。建议在解码前对位置向量做一次“扰动”,给每维加上一个极小的随机值,例如x + np.random.normal(0, 1e-6, size=x.shape),然后再排序。这一步能显著提高结果稳定性,尤其是在迭代后期粒子接近收敛时。
5.2 带时间窗的任务约束怎么加
如果你的系统里任务有时限,最直接的做法是把违反时间窗的时长作为惩罚加入适应度。假设任务点 j 的允许开始时间是 [earliest_j, latest_j],解码后按顺序累加飞行时间得到到达时间 arr_j,那么惩罚项可以设计为:
time_penalty = 0.0 arr = 0.0 for t in task_list: eta = max(0, earliest[t] - arr) # 早到等待时间 late = max(0, arr - latest[t]) # 迟到时间 time_penalty += 10 * eta + 100 * late arr += service_time[t] + travel_time[t]这里的核心是把“约束”变成“连续的惩罚值”,这样 PSO 的梯度信息(虽然是离散的)不容易丢失。迟到的惩罚系数比早到更高,因为迟到通常意味着任务失败。工程上如果你需要严格满足时间窗,可以用修复算子:调整任务顺序使所有任务在时间窗内,但这样会破坏 PSO 的搜索自由度,我建议只在最终方案输出前使用。
5.3 收敛性验证与参数鲁棒性检查
系统跑完后,不要只写“算法收敛了”。我会额外做三件事:第一,用多个随机种子分别运行 20 次,统计最优解的均值、标准差,标准差接近 0 说明算法稳定,较大说明需要增大迭代次数或粒子数。第二,画收敛曲线时把“每代最优”和“每代平均”画在同一个图里,如果平均曲线下降而最优曲线长期不变化,说明粒子多样性不足。第三,把 PSO 的结果和贪心算法对比,差距在 5% 以内说明系统可靠,如果差 30% 以上,先检查适应度函数是不是写错了。
我最常踩的坑是拿默认参数直接跑大规模问题。固定 w=0.8, c1=c2=1.5 对 30 个任务点可行,但对 200 个任务点,每代更新的随机性不足,收敛曲线下降得很慢。建议先做一个参数敏感性测试:固定其他参数,单独改变 w 的初始值,记录最终适应度,选择使结果最稳定的组合。这一步看起来费时间,却能让你的系统从“纸面能跑”变成“换数据也能用”。
做多无人机任务分配系统一年下来,我最大的体会是:PSO 本身不难,难的是“让算法理解你的工程约束”。编码方式、惩罚系数、切分策略这三者之间互相影响,改一个就得重新调其他两个。所以最后给你一个实用习惯:每次调整都保留一份当时的参数和结果曲线,方便回溯。希望这套从建模到填坑的路径能帮你少走几个弯路。
本文还有配套的精品资源,点击获取