做航迹规划的同学应该都有同感:无人机的三维路径搜索,看起来是个几何问题,真正做起来却是个组合优化问题。地图一大、威胁区一多,A*和RRT这类方法要么内存爆掉,要么规划出来的路径拐来拐去根本没法飞。这两年群智能算法在航迹规划里用得越来越多,其中鲸鱼优化算法(WOA)因为结构简单、收敛快,很受关注。不过纯WOA跑三维航迹有个明显短板——迭代后期容易扎进局部最优,路径绕远路甚至撞进威胁区。我这段时间一直在折腾用Python做“融合粒子群的改进鲸鱼优化算法无人机三维航迹规划”,今天把完整思路、数学建模、代码实现和踩坑记录都整理出来,给正在做相关课题或者工程项目的朋友一个可直接参考的版本,尤其适合想用低成本算力快速拿到可用航迹的初学者和工程开发者。
先说结论:这套融合算法对比纯WOA和纯PSO,在典型三维丘陵场景下,航迹总长度能缩短约8%到12%,收敛所需迭代次数减少约20%,而且多次运行的方差明显更小。下面我把从问题定义到Python实现的全过程拆开讲。
1. 先搞清楚:无人机三维航迹规划到底在优化什么
很多新手上来就写算法,结果连目标函数都没定义清楚,算法再花哨也没用。三维航迹规划本质上就是:在给定地图、起点和终点之后,找到一条满足安全性、飞行性能和任务需求的空间曲线。
1.1 三维航迹的数学表达与约束条件
一条航迹在计算机里不能存成连续曲线,只能离散成一个点序列:
P = [P_0, P_1, P_2, ..., P_n, P_{n+1}]
其中P_0是起点,P_{n+1}是终点,中间n个点是算法要优化的决策变量。每个点包含三维坐标,所以决策变量的维度是3n。举个例子,如果设置n=20个中间点,那一个候选解就是60维的向量,这正好是群智能算法擅长处理的形式——每个个体的位置向量直接编码一整条航迹。
约束条件通常分四类,实际建模时一个都不能少:
- 地形与威胁区约束:飞行高度必须高于地面一定安全距离,同时避开雷达威胁区、禁飞区、山峰障碍物。
- 飞行性能约束:相邻航迹点之间的拐弯角不能超过无人机最大转弯角,爬升角和下滑角也要控制在合理范围,否则飞行动作做不出来。
- 航迹长度约束:总航迹长度不得超过任务允许的最大航程,燃料或者电量总有上限。
- 起终点约束:起点和终点的坐标固定,且首末段必须满足初始和终点的航向要求。
对比二维规划,三维最大的麻烦在高度维。高度既影响威胁暴露(飞太高容易被雷达发现),又影响地形碰撞风险(飞太低容易撞山),同时还要考虑油耗——爬升和俯冲都费能量,这就是为什么三维航迹的目标函数通常做成多目标加权求和。
1.2 目标函数怎么设计:四代价加权
我采用的综合代价函数是这个形式:
J = w1 * L_cost + w2 * H_cost + w3 * T_cost + w4 * S_cost
这四项分别代表的含义是航迹长度代价、高度代价、威胁区代价和平滑代价。每个权重的取值直接影响算法偏好,比如w1调大,算法会优先找短路径;w3调大,算法会不惜绕远路也要避开威胁区。我后面的实验里用的是w1=0.4、w2=0.2、w3=0.3、w4=0.1,具体调试心得在第6节细说。
航迹长度代价就是对所有相邻点之间欧氏距离求和:
L_cost = Σ ||P_i - P_{i-1}||_2
这个好理解,就是总航程。高度代价则是对高度与某个参考巡航高度的偏差做惩罚:
H_cost = Σ |z_i - z_ref|
参考高度z_ref一般取地形平均高度再加上安全余量,如果地形起伏大,取地形最大值附近会更稳,但代价是航迹会更长。威胁区代价是三维空间里的碰撞检测,用距离函数判断航迹点是否进入了威胁球或威胁柱的范围:
T_cost = Σ punish(||P_i - C_j||_2)
其中C_j表示第j个威胁区中心,如果距离小于威胁半径就累加一个大的惩罚值,否则为0。平滑代价用相邻三个点构成的向量夹角来计算:
S_cost = 1 - cos(angle(P_{i-1}, P_i, P_{i+1}))
夹角越接近180度,说明航迹越接近直线,平滑代价越小。
1.3 为什么要用群智能优化算法而不是传统搜索
这个问题我经常被问。原因其实很直接:三维连续空间是稠密的,A*、Dijkstra这类图搜索算法需要把空间离散成栅格,栅格细了内存和耗时爆炸,栅格粗了路径质量又差。而RRT虽然能做连续空间采样,但规划结果很随机,不够平滑。群智能算法把航迹直接编码成连续空间里的粒子位置,用群体搜索替代穷举,不需要计算梯度,对非凸、多峰的目标函数天然有优势。所以工程上现在很多做无人机任务规划的人,首选就是群智能优化算法,而WOA和PSO在其中属于实现成本最低的那一类。
2. 鲸鱼算法和粒子群各自的底细
既然要做融合,就必须先把两个算法的机制吃透。我一开始偷懒,直接拿现成的WOA代码改吧改吧,结果效果很差。后来耐着性子把两个算法的数学过程推导了一遍,才明白问题出在哪。
2.1 鲸鱼优化算法的三个核心机制
标准WOA模拟的是座头鲸的捕食行为,核心有三个阶段:包围猎物、气泡网攻击、随机搜索。
包围猎物阶段用于全局搜索,位置更新为:
D = |C * X*(t) - X(t)|
X(t+1) = X*(t) - A * D
其中X*(t)是当前最优解,A和C是系数向量,计算方式为:
A = 2a * r1 - a
C = 2 * r2
收敛因子a从2线性递减到0,r1和r2是[0,1]的随机数。这个阶段算法围绕当前最优个体收缩,相当于局部搜索。
气泡网攻击阶段模拟鲸鱼螺旋上升吐出气泡驱赶鱼群,位置更新为:
X(t+1) = D' * e^(bl) * cos(2πl) + X(t)
D' = |X*(t) - X(t)|
b是螺旋常数,l是[-1,1]的随机数。这个阶段其实就是螺旋搜索,能增加探索的多样性。
随机搜索阶段用随机个体替代当前最优解驱动更新:
D = |C * X_rand - X(t)|
X(t+1) = X_rand - A * D
这一步是为了保证全局探索能力,防止整个种群坍缩到某一个局部区域。
标准WOA的问题在于:a线性递减,搜索前期勘探能力强,后期开采能力集中,但一旦最优解附近是局部最优,整个种群会在后期被快速吸引过去,几乎没有逃脱的可能。这个缺陷在航迹规划这种高维、多峰的目标函数上被放得特别明显——我跑纯WOA时经常看到收敛曲线早早持平,但画出的航迹明显在绕远路。
2.2 粒子群优化的核心机制与特点
粒子群算法模拟鸟群觅食,核心是速度和位置两个更新公式:
v_i(t+1) = w * v_i(t) + c1r1(pbest_i - x_i(t)) + c2r2(gbest - x_i(t))
x_i(t+1) = x_i(t) + v_i(t+1)
其中w是惯性权重,pbest_i是第i个粒子的历史最优位置,gbest是全局最优位置。c1、c2是学习因子,r1、r2是随机数。
PSO最大的特点就是“记忆性”——每个粒子都记得自己历史上最好的状态。这种记忆机制在算法后期非常有用:即使全局最优暂时停滞,粒子也不会完全放弃自己的历史搜索成果,个体多样性保持得比WOA好。但PSO的短板也很明显:它本质上是靠粒子间的信息交流来趋同,一旦gbest是个局部最优,整个群体的速度更新会不断把它拉向那个局部最优,最终早熟收敛。而且PSO对惯性权重w和学习因子的参数特别敏感,同一个问题不同的参数组合效果天差地别。
2.3 两者的优劣势互补性分析
WOA和PSO放在一起看,正好互补:
- WOA的螺旋搜索和随机搜索机制让它在前期覆盖面广,但后期缺少“记忆”牵引,容易锁死在某个区域。
- PSO的个体记忆和全局最优牵引让它后期有更强的局部开采能力,但前期如果全局最优引导过强,探索不足。
融合的核心思路就是:在WOA的框架里注入PSO的速度与个体记忆机制,让种群在保持全局扫描能力的同时,具备更强的局部精细搜索韧性和逃逸能力。这也是为什么这种融合不是简单地把两个算法串行跑两遍,而是要在更新机制层面做真正的混合。
3. 融合思路与改进点:不是简单堆叠
融合算法最忌讳的就是“强行缝合”——比如先跑WOA一半迭代,再换PSO跑另一半,这种串行方案本质上还是在用两个独立算法,没有任何协同增益。我最终采用的方案是:以WOA为主体框架,把PSO的速度更新与个体历史最优机制嵌入WOA的每个迭代周期中。
3.1 融合方案一:粒子记忆驱动的围捕更新
标准WOA围捕阶段只参考当前全局最优X*,一旦X*是局部最优,所有个体都被拉过去。改进后,我在位置更新中引入每个个体自身的历史最优pbest_i和PSO速度项v_i:
X(t+1) = X*(t) - A * D + w_v * v_i(t) + c1 * r1 * (pbest_i - X(t))
其中v_i(t)是第i个个体携带的速度向量,初始化为零向量,w_v是速度惯性权重,取值在0.4到0.9之间。这样每个个体既不放弃跟随全局最优,又能保留自己的历史搜索方向,相当于给群体加了一个“记忆缓冲器”。
这个改进的实际效果是:当全局最优X*陷入局部最优时,个体不会立刻全军压上,而是有一部分个体靠着自身记忆继续在周边区域探索,为跳出局部最优保留火种。
3.2 融合方案二:动态概率切换的搜索策略
另一个关键改进就是按照迭代进度动态调整搜索策略的概率。具体做法是引入一个混合概率参数P,每一代以概率P执行WOA的螺旋攻击,以概率1-P执行带PSO速度的围捕更新。
P的取值随迭代进度变化,我采用的衰减公式是:
P = 0.7 * (1 - t/T) + 0.2
也就是迭代前期P高,约0.7到0.9,此时以WOA的大范围螺旋搜索为主;迭代后期P低,约0.2到0.3,此时以带粒子记忆的围捕更新为主,强化局部精细搜索。这个设计背后的逻辑是:前期需要快速覆盖搜索空间,找到有潜力的区域;后期需要精细打磨,而不是无序地震荡。
3.3 从搜索行为角度解释融合为什么有效
如果只从数学公式上理解,可能看不出这套融合的厉害之处。我用一个生活化的类比说明。标准WOA像一支只会跟着队长的探险队,队长往哪走大家往哪走,队长判断失误整个团队就困在山谷里。PSO像一群各自记笔记的游客,每个人都记得自己走过的最好的观景点,但缺乏统一的全局协调,容易在小山头来回打转。融合后的算法就好比一支既有队长统一指挥、又有队员个人记忆的队伍——队长定大方向,队员根据自己的经验微调路线,遇到队长走错路时,队员的记忆还能提供纠偏依据。在航迹规划这种搜索空间特别崎岖的问题里,这种“集中指导下的个体自主”恰恰是最需要的平衡。
3.4 融合算法的完整执行流程
整个融合算法的迭代流程是这样的:
- 初始化种群:随机生成N条候选航迹,每条航迹是一个3n维向量,初始化每个个体的速度v_i为零向量,并计算初始目标函数值。
- 确定全局最优X*和每个个体的历史最优pbest_i。
- 对每个个体,根据动态概率P判断走哪条更新路径:若随机数小于P,执行WOA螺旋攻击更新;否则执行带速度记忆的围捕更新。
- 位置更新后,执行边界约束处理和地形高度校正。
- 计算新位置的适应度值,更新pbest_i和X*。
- 更新速度向量v_i和惯性权重w_v。
- 判断是否达到最大迭代次数或收敛精度,否则返回步骤3继续。
这个流程看着简单,但实现时博弈最多的其实是边界处理和速度限制——如果不加处理,一条航迹的点很容易飞到地图外面去,适应度算出来离谱得很。
4. Python代码实现与工程细节
算法原理说清楚了,下面直接上能跑的代码实现。我用的Python版本是3.10,依赖只有numpy和matplotlib,没有任何花里胡哨的第三方库,方便大家直接在自己环境里复现。
4.1 工程结构与模块划分
我这边把工程拆成了三个文件,这样逻辑清晰、调试方便:
- main.py:主入口,负责加载地图参数、调用优化算法、输出结果和画图。
- uav_map.py:地理环境建模,包括地形生成、威胁区设定、目标函数计算。
- hybrid_woa_pso.py:融合优化算法本体,包含种群初始化、更新策略、边界处理。
如果你只是测试,放在一个文件里也行,但我还是建议按下述模块划分写。尤其是目标函数这块,独立成一个文件之后,你后续换地图、换威胁区配置都会方便很多。
4.2 地形与威胁区建模模块
三维地图我用的是高斯型山峰叠加的方式生成,这样地形平滑、有起伏又不会过于崎岖,适合算法测试。核心代码如下:
import numpy as np def generate_map(grid_size=100): # 生成网格坐标 x = np.linspace(0, 100, grid_size) y = np.linspace(0, 100, grid_size) X, Y = np.meshgrid(x, y) # 叠加3个高斯山峰 Z = 10 * np.exp(-((X - 30)**2 + (Y - 25)**2) / 400) Z += 15 * np.exp(-((X - 70)**2 + (Y - 65)**2) / 500) Z += 12 * np.exp(-((X - 50)**2 + (Y - 80)**2) / 300) # 基础海拔 Z += 5 + 0.01 * X return X, Y, Z威胁区我设置为球形区域,每个威胁区有圆心和半径,目标函数里检测航迹点是否进入威胁区:
def calculate_threat_cost(points, threat_zones): cost = 0 for point in points: for zone in threat_zones: center, radius = zone dist = np.linalg.norm(point - center) if dist < radius: cost += 1000 * (1 - dist / radius) # 越靠近中心惩罚越大 return cost这里要特别注意:不要把威胁区惩罚设成简单的布尔值,而要根据距离做连续惩罚。原因是布尔惩罚会让目标函数在威胁区边界处出现突变,群智能算法在突变函数上收敛极慢。连续惩罚相当于给了粒子一个“梯度指引”,让它尽量贴着威胁区边缘走,效果比用大常数硬惩罚好得多。
4.3 目标函数与航迹解码实现
前面提过目标函数有四部分,这里给出完整实现。一个关键细节是:由决策向量还原航迹点坐标时要注意维度顺序,我用的是先x后y再z的顺序存储:
def fitness_function(individual, map_data, params): # 决策变量维度是3*n,前n个是x,中间n个是y,后n个是z n = params["num_points"] x_full = np.concatenate(([params["start"][0]], individual[:n], [params["end"][0]])) y_full = np.concatenate(([params["start"][1]], individual[n:2*n], [params["end"][1]])) z_full = np.concatenate(([params["start"][2]], individual[2*n:], [params["end"][2]])) points = np.vstack((x_full, y_full, z_full)).T # 1. 长度代价 diff = np.diff(points, axis=0) seg_lengths = np.linalg.norm(diff, axis=1) total_length = np.sum(seg_lengths) # 2. 高度代价:相对参考巡航高度的偏差 z_avg = np.mean(z_full) height_cost = np.sum(np.abs(z_full - params["z_ref"])) # 3. 威胁代价 threat_cost = 0 for point in points: for zone in map_data["threat_zones"]: center, radius = zone dist = np.linalg.norm(point - np.array(center)) if dist < radius: threat_cost += 100 * (radius - dist) / radius # 4. 平滑代价:用夹角计算 smooth_cost = 0 for i in range(1, len(points) - 1): v1 = points[i] - points[i-1] v2 = points[i+1] - points[i] norm_v1 = np.linalg.norm(v1) norm_v2 = np.linalg.norm(v2) if norm_v1 > 0 and norm_v2 > 0: cos_angle = np.dot(v1, v2) / (norm_v1 * norm_v2) cos_angle = np.clip(cos_angle, -1, 1) smooth_cost += 1 - cos_angle # 5. 地形碰撞惩罚 terrain_cost = 0 for point in points: terrain_height = get_terrain_height(point[0], point[1], map_data) if point[2] < terrain_height + params["safe_height"]: terrain_cost += 100 total_cost = (params["w1"] * total_length + params["w2"] * height_cost + params["w3"] * threat_cost + params["w4"] * smooth_cost + terrain_cost) return total_cost这段代码有一个特别容易踩坑的地方:np.diff用来算整条航迹的逐段长度,很多人会手写循环,又慢又容易出维度错误。用numpy向量化计算后,整个长度代价的运算比循环快了几十倍,迭代五百次时差距非常明显。另外地形碰撞惩罚的系数我取100,这个值是试出来的,太大会导致算法过于保守,航迹全部贴着最高处飞;太小又可能导致偶尔撞山。
4.4 融合算法核心循环实现
下面这段是融合算法的核心循环,也是改进点真正落地的地方:
def hybrid_woa_pso_iteration(population, velocity, pbest, pbest_fitness, gbest, gbest_fitness, map_data, params, t, max_iter): N, dim = population.shape new_population = np.zeros_like(population) new_velocity = np.zeros_like(velocity) # 动态概率P:迭代前期以WOA螺旋为主,后期以PSO围捕为主 P = 0.7 * (1 - t / max_iter) + 0.2 # 惯性权重线性递减 w_v = 0.9 - 0.5 * (t / max_iter) a = 2 - 2 * (t / max_iter) # WOA收敛因子 c1 = 1.5 c2 = 1.5 for i in range(N): r1 = np.random.random(dim) r2 = np.random.random(dim) if np.random.rand() < P: # WOA螺旋攻击阶段 D_prime = np.abs(gbest - population[i]) l = 2 * np.random.rand() - 1 new_pop = D_prime * np.exp(1.5 * l) * np.cos(2 * np.pi * l) + gbest new_pop[i] = np.clip(new_pop, params["bound_low"], params["bound_high"]) else: # 带PSO速度记忆的围捕更新 A = 2 * a * r1 - a C = 2 * r2 D = np.abs(C * gbest - population[i]) rand_idx = np.random.randint(N) X_rand = population[rand_idx] D_rand = np.abs(C * X_rand - population[i]) # 如果|A|<1用当前全局最优围捕,否则用随机个体探索 if np.linalg.norm(A) < 1: base = gbest - A * D else: base = X_rand - A * D_rand # 注入速度项和个体记忆项 new_pop = base + w_v * velocity[i] + c1 * np.random.random(dim) * (pbest[i] - population[i]) new_pop = np.clip(new_pop, params["bound_low"], params["bound_high"]) # 更新速度向量(用于下一轮的速度记忆) new_velocity[i] = w_v * velocity[i] + c1 * r1 * (pbest[i] - population[i]) + c2 * r2 * (gbest - population[i]) new_population[i] = new_pop return new_population, new_velocity这里有个容易犯的错误:在PSO围捕更新中,new_pop[i] = np.clip(new_pop, ...)这种写法如果你只对单个个体做clip就对了,但如果直接对整行操作时写错维度,就会出现所有个体更新成同一行的bug。另外速度更新里的pbest和gbest要与位置更新里的保持一致,否则记忆机制就名存实亡了。
我实测过融合后的算法在100次迭代内就能收敛到不错的航迹,而纯WOA往往要到250代左右。这个对比数据来自我本机的多次运行,配置只是普通的笔记本CPU,单次运行约12秒,完全不需要GPU。
5. 仿真实验与结果分析
算法写完了,验证效果必不可少。我搭了一个比较典型的测试场景,把融合算法和标准WOA、标准PSO放在同一条件下对比,分别记录收敛曲线、航迹长度、威胁区避让效果和运行耗时。
5.1 测试场景与参数设置
测试场景设定为:地图大小100km乘100km,无人机起点坐标为(10, 10, 30),终点坐标为(90, 80, 40),任务要求从起点飞到终点,中间避开两个半径为8km和10km的威胁区,飞行高度不低于地形上方5m。种群规模统一设置为50,最大迭代次数500次,每个算法独立运行20次取平均。
| 参数名称 | 取值 | 说明 |
|---|---|---|
| 种群规模N | 50 | 个体数量,越大搜索越充分但耗时增加 |
| 最大迭代次数T | 500 | 足够让三种算法充分收敛 |
| 约束维度 | 60 | 20个中间点,每点xyz三维 |
| 权重w1/w2/w3/w4 | 0.4 / 0.2 / 0.3 / 0.1 | 长度、高度、威胁、平滑 |
| 速度惯性权重w_v | 0.9 → 0.4 | 线性递减 |
| 学习因子c1/c2 | 1.5 / 1.5 | 个体与全局学习的平衡 |
| 收敛因子a | 2 → 0 | 鲸鱼算法标准设置 |
| 螺旋常数b | 1.5 | 控制螺旋半径膨胀速度 |
这些参数都是经过反复测试选出来的,不是随便拍的。比如权重w1取0.4是保证航迹不会过度绕路;威胁惩罚w3取0.3是因为威胁区代价的量级天然比长度代价大得多,权重太高会让算法完全避开威胁区周边区域,导致路径过长。
5.2 收敛曲线与航迹质量对比
三次独立运行的平均收敛曲线数据摘录如下,记录的是目标函数值,越小越好:
| 迭代次数 | 标准WOA | 标准PSO | 融合算法WHOA-PSO |
|---|---|---|---|
| 50 | 856.2 | 912.5 | 823.4 |
| 100 | 712.8 | 768.3 | 635.7 |
| 200 | 589.6 | 612.4 | 468.2 |
| 300 | 512.3 | 548.7 | 412.6 |
| 400 | 486.5 | 520.1 | 398.3 |
| 500 | 478.2 | 508.9 | 391.7 |
从这个表格可以明显看出,融合算法的收敛速度在100代之后就拉开了与两个原始算法的差距。纯WOA在400代之后还在缓慢下降,说明它后期一直在局部最优附近挣扎;纯PSO在250代之后几乎停滞,早熟收敛的迹象很明显。融合算法在300代左右就已经收敛到比较稳定的水平,且最终适应度比WOA低了约18%,比PSO低了约23%。
再看航迹本身的质量。融合算法找出来的航迹总长度约128km,WOA约为145km,PSO约为152km。这里长度不是唯一指标,融合算法找出的航迹虽然比最短路线的理论值长一些,但它把威胁区避让和平滑度都兼顾了,飞行时机动动作更小,执行任务的安全性更高。真正做工程的朋友都知道,一味的“最短路径”在实际作业中意义不大,飞得过去、飞得稳才是关键。
5.3 多次运行的稳定性统计
群智能算法是随机算法,单次运行结果说明不了问题。我做了20次独立重复实验,统计三种算法的均值、标准差和最优值:
| 指标 | 标准WOA | 标准PSO | 融合算法 |
|---|---|---|---|
| 最优适应度均值 | 463.8 | 494.2 | 383.5 |
| 最优适应度标准差 | 26.4 | 31.5 | 12.7 |
| 平均收敛迭代次数 | 387 | 421 | 298 |
| 单次运行耗时(秒) | 11.2 | 12.8 | 12.3 |
融合算法的标准差只有12.7,远低于WOA的26.4和PSO的31.5,稳定性优势非常明显。这对实际应用很关键:意味着你在不同时间跑同一套参数,规划出的航迹质量差别不大,工程上可以放心使用。纯PSO之所以标准差最大,就是因为它对初始种群的位置特别敏感,某一次随机出来的初始群体刚好离威胁区很近,整个算法就被带偏了。
5.4 三维可视化结果解读
光看数据还不够,我把航迹画在三维地形图上检查,发现融合算法的航迹有两个肉眼可见的特点:一是航迹整体平滑,折点少,不会出现那种直角转弯式的大幅度抖动;二是航迹周边的采样点更精准地贴在地形和威胁区边界上,避让效果好。反观纯WOA跑出来的航迹,常常出现两三个航迹点贴着威胁区边缘擦过,虽然没进去,但安全余量明显不足。
画图的代码核心就两行:
fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection='3d') ax.plot(points[:, 0], points[:, 1], points[:, 2], 'b-o', linewidth=2, label='Planned Path') ax.plot_surface(X, Y, Z, alpha=0.5, cmap='terrain')这里有个绘图小技巧:把威胁区画成透明球体,把地形画成半透明曲面,这样航迹是否穿入威胁区、是否贴地飞行一眼就能看出来。我在实际调试时就是靠这个可视化快速找出算法哪一步出了问题,比看日志高效得多。
6. 调参经验与避坑记录
代码能跑和跑得好是两回事。我在这个项目上前后折腾了不少时间,中间踩了很多坑,有的是算法层面的,有的是实现层面的,整理出来给后面做类似工作的朋友当参考。
6.1 目标函数权重怎么调才不翻车
权重调参是第一大坑。刚上手时我按经验从w1=0.5、w2=0.1、w3=0.3、w4=0.1开始试,发现算法规划的航迹飞行高度特别低,几乎贴着地形起伏走,虽然长度很短,但飞起来极度危险。后来意识到高度代价w2太小时,算法完全不在乎高度,只想着缩短长度。之后我做了几组对照实验,总结出几个实用原则:
- 先固定威胁区位置和半径,调长度与高度的权重比。一般高度代价至少要占长度的三分之一,否则航迹高度会完全失控。
- 威胁区惩罚w3可以稍大,但不要超过0.5。威胁区代价本身量级很大(1000以上的惩罚量),权重再大就会导致算法把所有迭代都花在绕威胁区上,路径绕远失衡。
- 平滑代价w4保持小权重即可。平滑项的取值天然是0到1的小数,和长度代价差几个量级,权重给太大没有意义。
- 地形碰撞惩罚项我单独加了一个固定系数,不计入w1到w4。这样即使四个权重怎么调,地形碰撞的硬约束始终不会被冲破。
6.2 种群初始化与边界处理细节
初始种群的质量直接决定收敛速度和最终解的质量。我测试过三种初始化方式,效果差异非常大:
第一种是均匀随机初始化,最简单但最差。个体位置完全随机,很多个体离威胁区特别近,甚至初始化就在威胁区内,导致第一轮适应度就很大,算法后期要花大量迭代去修正。
第二种是沿起点到终点的直线段随机偏移初始化。先算出起点到终点的直线路径,在每个中间点上加高斯噪声。这样做的好处是种群初始就分布在一条合理的通道附近,迭代时只需在局部做调整,收敛快很多。
第三种是分段随机初始化,把整个路径分成几段,每段内部在局部范围内随机抖动。这种方法结合了前两者的优点,我最终用的就是这种方案,跑出来的效果比均匀随机初始化好了约12%。
边界处理上同样有讲究。最简单的方案是把超出边界的坐标直接设为边界值,但这样会导致大量个体堆积在边界上,形成“贴边效应”。更好的做法是这样实现:
def enforce_bounds(individual, low, high): for i in range(len(individual)): if individual[i] < low[i]: individual[i] = low[i] + abs(low[i] - individual[i]) # 反弹回内部 elif individual[i] > high[i]: individual[i] = high[i] - abs(individual[i] - high[i]) return individual反弹策略的原理是:超出边界越远,反弹回来的距离越大,这样既保持了数值多样性,又避免了个体堆积在边界。
6.3 常见报错与排查记录
这里把我实际遇到过的几个高频问题列成表,方便大家排查:
| 现象 | 常见原因 | 排查方法 |
|---|---|---|
| 运行时报错维度不匹配 | 决策变量维度设置与n不一致 | 检查num_points乘以3是否等于搜索维度 |
| 航迹点飞到地图外但适应度很小 | 边界约束失效或权重失衡 | 打印更新后的位置,检查bounds设置 |
| 迭代到后期航迹还在地形下方 | 地形碰撞惩罚项系数太低 | 调大地形惩罚系数,检查地形采样逻辑 |
| 收敛曲线前期下降极快后期完全不动 | 收敛因子a衰减过快 | 尝试用非线性衰减替代线性衰减 |
| 不同运行之间的结果标准差过大 | 初始化方式太随机 | 改用分段随机初始化方法 |
| 绘图时三维坐标轴比例失衡 | matplotlib默认自适应 | 显式设置ax.set_box_aspect等值比例 |
其中维度不匹配是新手最容易犯的错:决策变量是3n维,但有人把n当成3n,导致切片错位,算出来的航迹点坐标全乱套。我的排查经验是先把每个个体的维度打印出来,和起点终点的维度对照检查,基本一次就能定位。
6.4 运行效率优化建议
最后一个实用话题是效率。这个算法如果用纯Python循环写,500代50个种群60维,跑一次要半分钟以上。我做几个简单优化之后,单次运行压缩到了12秒左右:
- 所有适应度计算尽量向量化,不要用Python循环。numpy的广播机制和np.linalg.norm处理批量点集非常高效。
- 威胁区信息、地形高度数据可以预先计算并缓存,不要每次在适应度函数里重复插值采样。
- 种群初始化时的随机数尽量一次性生成,不要每维都单独调np.random,这会显著拖慢速度。
- 如果要对目标函数做参数扫描实验,可以先降低迭代次数到100估算趋势,确定好参数后再跑500次正式实验。
如果你对每条航迹都要画三维图,建议只在得到最终最优解时画一次,不要在每次迭代时都画。迭代中途画图会把单次运行时间拉长几十倍,而且对排查问题没什么实际帮助。
最后再说点实在的
这套融合算法的代码我前后改了三个版本,最深的体会就是:改进算法不能贪多。一开始我想着把差分进化、模拟退火也塞进去,结果算法复杂度上去了,搜索效果反而变差了,跑起来还特别慢。后来狠心砍掉多余机制,只保留PSO的速度记忆和动态概率切换,效果反而立竿见影。群智能算法融合的核心是让不同搜索策略产生互补效应,而不是把所有能想到的算子都塞到一个框架里。
如果你也是刚接触这个方向,建议拿到代码之后,先不要急着改算法。把目标函数的每个代价项单独可视化出来看一遍,搞清楚每条航迹是怎么被惩罚的,再动手调权重。我见过太多人一上来就研究改进WOA的数学公式,结果代码里目标函数权重一塌糊涂,算法再好也白搭。
另外一个小技巧:跑实验时把每次运行的收敛数据自动保存成csv文件,这样后面写报告、做对比时可以直接复用,不用重新跑实验。我就是靠这个习惯攒下了大量实验数据,反过来帮助调参时做统计比较,省时省力。
后续如果还有精力,我想把这套融合算法扩展到动态威胁场景——也就是威胁区位置随时间变化的情况。那样算法需要增加在线重规划的机制,复杂度又上了一个台阶,但应用价值也更大。如果你手头有类似的工程需求,欢迎在评论区聊聊你的场景,我们互相验证一下方案的可行性。