尧图精选

基于Python的5种优化算法在23个测试函数上的统一对比框架

🕒 发布时间:2026/9/8 10:26:05 📁 来源:尧图网络
我去年做优化算法复现时最头疼的不是把某个算法跑通而是怎么把 SSA、WOA、GWO、PSO、GA 这五种算法的结果放到同一张表里比较。论文里各写各的公式参数设置五花八门有人在 Sphere 上用 100 维有人在 Rastrigin 上用 2 维跑出来的数字根本没法放在一起看。后来我把所有实现收敛到一套 Python 框架下统一在 23 个经典测试函数上做基准测试才把问题理顺。这篇文章就把这套框架的完整思路和代码细节写出来给同样在做算法对比、做课程项目或者调优参的朋友一个可以直接抄作业的参考。1. 先聊聊“23个测试函数”在算法对比里的位置1.1 23个测试函数为什么成了“行标”如果你翻过任何一篇群智能优化算法的论文基本逃不开这 23 个函数。它们不是什么国际标准组织发布的规范而是几十年来优化领域研究者约定俗成的一套测试集从早期的 Sphere、Rosenbrock 到后来的 Ackley、Rastrigin、Shekel 系列几乎覆盖了优化问题里最常见的困难类型。为什么大家愿意用这套老函数因为算法的好坏没法直接说“我这个能收敛到你那个不能”必须有可复现、可比较的公共基准。23 个函数形式都是公开的理论最优值也是已知的跑出来的结果差异只可能来自算法本身和参数设置。有了这套公共基准你才能在不同论文之间横向对比也才能在自己的实验里判断新算法到底是真进步了还是只是运气好。我之前见过不少同学直接拿一个实际的工程优化问题来对比算法比如某个参数辨识问题结果跑完只能得出“这几个算法都能用”的结论因为实际问题的真实最优解没人知道。换到 23 个测试函数上就不一样了最优值是确定的你一算误差就知道算法到底收敛到了什么程度这是基准测试的核心价值。1.2 三类函数设置的分工单峰、多峰、固定维度23 个函数内部其实分成三组每组考核的重点完全不同。F1 到 F7 是单峰函数。这类函数只有一个全局最优点没有局部最优陷阱主要考察算法的收敛速度和收敛精度。比如 F1 Sphere 就是所有维度平方和最优点是 0非常简单F5 Rosenbrock 虽然是单峰但它的最优解藏在一个很长的抛物线形山谷里算法很容易在山谷里来回震荡所以它考察的是算法沿着狭长区域前进的能力F7 Quartic 还人为加了随机噪声专门用来测试算法在带噪声环境下的稳定性。F8 到 F13 是多峰函数。这类函数有大量局部最优点Rastrigin 的局部最优数量随维度指数增长Ackley 在全局最优附近密密麻麻排着局部陷阱。算法在这些函数上表现如何直接反映它跳出局部最优、保持种群多样性的能力。F14 到 F23 是固定维度函数。它们的维度不随实验设置改变比如 F14 Foxholes 固定 2 维、F20 Hartman6 固定 6 维而且这些函数大多有复杂的局部地形和多个不相邻的全局最优点。由于维度低评估一次非常快但想找到全局最优并不容易经常需要算法在探索和开发之间多次切换。后面做实验调度时这三类函数必须分开配置维度这是最容易踩的坑之一。2. 五种算法各自在“找什么”和“怎么找”2.1 PSO速度-位置模型里的记忆与协作粒子群算法的灵感来自鸟群觅食。每个解就是一个粒子粒子在搜索空间里的位置就代表一个候选解。关键在它多了一个速度向量决定粒子下一步往哪个方向飞、飞多远。更新公式可以拆成三项看V[i] w * V[i] c1 * r1 * (pbest[i] - X[i]) c2 * r2 * (gbest - X[i]) X[i] X[i] V[i]第一项是惯性项粒子保留上一轮的运动趋势w 越大越倾向于在原来方向上继续飞第二项是认知项粒子往自己历史上找到过的最好位置飞第三项是社会项粒子往整个种群当前找到的最好位置飞。w 我习惯从 0.9 线性递减到 0.4。前期大惯性让粒子四处探索后期小惯性让粒子在局部精细搜索。c1 和 c2 一般取 2.0表示个体经验和群体经验同等重要。PSO 最大的优点是实现简单、参数少缺点是后期容易聚集到 gbest 附近如果 gbest 是个局部最优整个种群可能再也出不来。2.2 GA选择、交叉、变异的交替推进遗传算法模拟的是自然选择。种群里的每个个体就是一条染色体在连续优化问题里通常直接用一组浮点数表示。每一代通过选择、交叉、变异三步操作生成下一代。选择负责保留优秀个体我用的是锦标赛选择随机抽几个个体把其中适应度最好的留下来。交叉负责组合父代信息在实数编码下我用模拟二进制交叉 SBX它能模拟二进制编码下单点交叉的效果对连续问题更合适。变异负责引入随机扰动防止种群过早同质化我用多项式变异。这三个操作的分工很清楚选择提供选择压力让种群往好的方向走交叉提供重组能力把两个父代的优势片段组合起来变异提供随机性避免搜索被困在局部区域。和 PSO 这种连续状态更新方法相比GA 的离散操作让它在多峰函数上往往有更好的全局搜索能力但收敛到高精度解的速度通常比 PSO、GWO 慢。2.3 GWO三只头狼如何带动整个狼群灰狼优化算法模拟灰狼的社会等级制度和狩猎行为。种群分为 alpha、beta、delta、omega 四个等级alpha 是最优解beta 是次优解delta 是第三优解剩下的都是 omega 狼。每次更新时所有 omega 狼同时参考三只头狼的位置向它们围捕猎物的方向移动。D_alpha |C1 * X_alpha - X| X1 X_alpha - A1 * D_alpha X (X1 X2 X3) / 3A 是核心控制参数它的值由收敛因子 a 决定a 从 2 线性减到 0A 的取值范围也随之变化。当 |A| 1 时狼群倾向于远离猎物对应全局探索当 |A| 1 时狼群向猎物收缩对应局部开发。GWO 的一个巧妙之处在于它不像 PSO 那样只跟着一个全局最优走而是同时参考三个头狼的位置取平均。即使 alpha 陷入了局部最优beta 和 delta 还能把它拉出来一部分。这个机制让 GWO 在多峰函数上表现得非常稳而且它只有一个主要参数 a调参成本极低。2.4 WOA收缩包围和螺旋路径的双模式狩猎鲸鱼优化算法模仿座头鲸的气泡网捕食策略。座头鲸会先下沉然后围绕猎物螺旋上升吐出气泡形成一张网把鱼群逼到中心后一口吞掉。WOA 把这个过程抽象成两种位置更新模式。第一种是收缩包围当 |A| 1 时鲸鱼向当前最优个体方向收缩靠近第二种是螺旋更新鲸鱼以当前最优个体为中心按照螺旋轨迹移动。每次迭代用随机数 p 决定走哪条路概率各占一半。如果 |A| 1则随机选一个个体作为参考强制鲸鱼离远一些保证全局探索能力。X |X_best - X| * exp(b * l) * cos(2 * pi * l) X_bestb 是控制螺旋形状的常数一般取 1.0l 是 [-1, 1] 之间的随机数。WOA 最突出的特点是螺旋机制它让个体在最优解附近画圈搜索这种搜索路径比 PSO 的直线飞行更容易在小范围内精细扫描所以 WOA 在单峰高精度收敛上的表现通常很亮眼。2.5 SSA一条链上的领导与追随樽海鞘群算法比较特殊它的种群排成一条链链头是领导者其余都是追随者。领导者直接朝食物位置移动食物位置就是当前找到的最优解。追随者不直接参考食物而是只跟着链条前一个个体移动。领导者的更新方式里有一个 c1 系数c1 2 * exp(-(4 * t / T)^2)t 是当前迭代数T 是最大迭代数。c1 前期大后期急剧变小前期让领导者大步朝食物方向探索后期收缩步长做精细搜索。追随者更新公式更简单就是当前个体和前一个个体位置的均值。SSA 的链式结构很有意思。它没有 PSO 那种显式的“向全局最优飞”也没有 GA 那种交叉重组而是靠一层层传递位置信息。这种信息传递速度慢但好处是种群不容易瞬间坍缩到一点多样性维持得比较好。缺点也明显如果领导者被局部最优困住整条链都会被带偏后期很难摆脱。2.6 五种算法的共性探索与开发的平衡点把五种算法放在一起看你会发现它们的差异本质是“探索”和“开发”的配比不同。PSO 用惯性权重线性衰减控制GA 用交叉率和变异率控制GWO 用 a 的线性衰减控制 AWOA 用 a 和 p 双重控制SSA 用 c1 的指数衰减控制。算法信息交互方式探索/开发调节主要参数数量PSO个体历史最优 全局最优w 线性递减3GA锦标赛选择 SBX 交叉 多项式变异pc、pm4GWO三头狼位置加权平均a 线性递减1WOA当前最优个体 随机个体a、p3SSA领导者向食物移动追随者向前一个个体移动c1 指数衰减1这些机制没有绝对优劣同一个算法在 F1 上可能收敛精度最高到了 F8 上可能被 GA 反超。所以做基准测试时一定要在同一套函数、同一套评价指标下跑才有对比价值。3. 代码框架接口统一是公平对比的前提3.1 项目目录与设计约定写这五套算法最忌讳的就是各写各的。有人把 PSO 写成一个函数GA 写成一个类WOA 又写成另一个风格最后实验结果收集的时候各种不兼容心态直接爆炸。我建议所有算法统一成一个类的形态对外只暴露两个东西构造时接收目标函数、维度、边界等配置run 方法执行完整搜索返回最优解位置、最优值、收敛历史我的目录结构固定成这样benchmark/ ├── algorithms/ │ ├── __init__.py │ ├── base.py │ ├── pso.py │ ├── ga.py │ ├── gwo.py │ ├── woa.py │ └── ssa.py ├── functions/ │ ├── __init__.py │ └── benchmark_functions.py ├── main.py └── results/这样拆的好处是加一个新算法只需要在 algorithms 目录下加一个文件加一个新测试函数只需要改 functions 下的文件其他代码完全不用动。3.2 基类所有算法只有同一个 run 入口基类不打算写太多抽象方法只约定构造参数和 run 的返回结构。所有子类按这个规范实现实验调度代码就能完全统一。import numpy as np class Algorithm: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed def run(self): raise NotImplementedErrorlb 和 ub 既可以是浮点数也可以是 numpy 数组这样能兼容 Branin 这类每个维度边界不同的函数。seed 参数很关键后面做重复实验时每个算法每次运行都用不同的种子保证统计结果有效。3.3 测试函数模块23个函数放到一个文件里测试函数我统一写成“接收一个 numpy 向量 x返回一个标量适应度值”的函数。F1 到 F13 中维度可变的部分用 len(x) 动态获取F14 到 F23 固定维度部分内部直接假设输入长度正确。23 个函数完整实现比较长下面先放几个代表性函数完整的代码文件你可以在项目里补全形式完全一致。import numpy as np def F1(x): # Sphere return np.sum(x ** 2) def F5(x): # Rosenbrock return np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 (x[:-1] - 1) ** 2) def F9(x): # Rastrigin return np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x) 10) def F10(x): # Ackley d len(x) s1 np.sum(x ** 2) s2 np.sum(np.cos(2 * np.pi * x)) return -20 * np.exp(-0.2 * np.sqrt(s1 / d)) - np.exp(s2 / d) 20 np.e def F14(x): # Shekels Foxholes a np.array([ [-32, -16, 0, 16, 32, -32, -16, 0, 16, 32, -32, -16, 0, 16, 32, -32, -16, 0, 16, 32, -32, -16, 0, 16, 32], [-32, -32, -32, -32, -32, -16, -16, -16, -16, -16, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 32, 32, 32, 32, 32] ]) total 0 for j in range(25): total 1 / (j 1 np.sum((x - a[:, j]) ** 6)) return (1 / 500 total) ** (-1)固定维度函数的边界信息我用一个全局字典维护实验调度的时候直接从字典里取值编号函数名类型维度定义域理论最优值F1Sphere单峰30[-100, 100]0F2Schwefel 2.22单峰30[-10, 10]0F3Schwefel 1.2单峰30[-100, 100]0F4Schwefel 2.21单峰30[-100, 100]0F5Rosenbrock单峰30[-30, 30]0F6Step单峰30[-100, 100]0F7Quartic with Noise单峰30[-1.28, 1.28]0F8Schwefel 2.26多峰30[-500, 500]-418.9829*dF9Rastrigin多峰30[-5.12, 5.12]0F10Ackley多峰30[-32, 32]0F11Griewank多峰30[-600, 600]0F12Penalized1多峰30[-50, 50]0F13Penalized2多峰30[-50, 50]0F14Foxholes固定维度2[-65.536, 65.536]0.998F15Kowalik固定维度4[-5, 5]0.0003075F16Six-Hump Camel固定维度2[-5, 5]-1.0316285F17Branin固定维度2x1[-5, 10], x2[0, 15]0.397887F18Goldstein-Price固定维度2[-2, 2]3F19Hartman3固定维度3[0, 1]-3.86278F20Hartman6固定维度6[0, 1]-3.32237F21Shekel4固定维度4[0, 10]-10.1532F22Shekel5固定维度4[0, 10]-10.4028F23Shekel7固定维度4[0, 10]-10.5363注意 F8 的理论最优值带维度30 维时是 -12569.5 左右跑完如果数量级差太远说明边界或者初始化有问题。3.4 实验调度多算法、多函数、多轮次怎么组织实验调度的核心逻辑是三层循环外层遍历测试函数中层遍历算法内层遍历随机种子。每个“算法 函数 种子”的组合跑一次 run收集历史最优值序列。循环顺序有讲究。推荐先固定函数再遍历算法和种子这样能保证同一函数下所有算法的运行环境完全一致。每个算法的重复次数我一般设 30 次30 次足够把随机性摊平又不至于慢到无法接受。algorithms { PSO: PSO, GA: GA, GWO: GWO, WOA: WOA, SSA: SSA, } for func_name, func in benchmark_functions.items(): dim, lb, ub get_config(func_name) for algo_name, Algo in algorithms.items(): for seed in range(30): algo Algo(func, dimdim, lblb, ubub, pop_size30, max_iter500, seedseed) best_pos, best_val, history algo.run() # 记录到对应的结果文件结果保存我建议直接存成 numpy 的 npz 或 csv每个函数一个文件夹里面是不同算法的收敛曲线数组。这样后续画图、做 Wilcoxon 秩和检验都很方便。4. 核心算法实现逐行讲解4.1 PSO三行公式拆开看PSO 的实现只需要维护两个矩阵位置矩阵 X 和速度矩阵 V形状都是 (pop_size, dim)。import numpy as np class PSO: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42, w_start0.9, w_end0.4, c12.0, c22.0): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed self.w_start w_start self.w_end w_end self.c1 c1 self.c2 c2 def run(self): np.random.seed(self.seed) lb, ub self.lb, self.ub X np.random.uniform(lb, ub, (self.pop_size, self.dim)) V np.random.uniform(-(ub - lb), ub - lb, (self.pop_size, self.dim)) pbest X.copy() pbest_val np.array([self.fitness(x) for x in X]) gbest_idx np.argmin(pbest_val) gbest pbest[gbest_idx].copy() gbest_val pbest_val[gbest_idx] history [gbest_val] for t in range(self.max_iter): w self.w_start - (self.w_start - self.w_end) * t / self.max_iter r1 np.random.random((self.pop_size, self.dim)) r2 np.random.random((self.pop_size, self.dim)) V w * V self.c1 * r1 * (pbest - X) self.c2 * r2 * (gbest - X) X X V X np.clip(X, lb, ub) val np.array([self.fitness(x) for x in X]) better val pbest_val pbest[better] X[better] pbest_val[better] val[better] cur_best_idx np.argmin(pbest_val) if pbest_val[cur_best_idx] gbest_val: gbest_val pbest_val[cur_best_idx] gbest pbest[cur_best_idx].copy() history.append(gbest_val) return gbest, gbest_val, history速度初始化我用了[-(ub-lb), ub-lb]这样初始速度的量级和搜索空间匹配。如果初始化速度太小粒子前期飞不动太大又容易一开始就冲出边界。边界处理统一用 np.clip 直接截断简单可靠代价是粒子贴边时速度方向可能被强行改变但这在绝大多数测试函数上影响很小。4.2 GA实数编码比二进制更省心连续优化里我强烈建议用实数编码。二进制编码在交叉变异后还得解码转成浮点数精度还受编码长度影响完全没必要。下面的实现包含锦标赛选择、SBX 交叉、多项式变异三个核心操作。import numpy as np class GA: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42, pc0.9, pm0.1, tournament_size2, eta_c20, eta_m20): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed self.pc pc self.pm pm self.tournament_size tournament_size self.eta_c eta_c self.eta_m eta_m def tournament_selection(self, X, val): candidates np.random.randint(0, self.pop_size, sizeself.tournament_size) return X[candidates[np.argmin(val[candidates])]].copy() def sbx_crossover(self, p1, p2): u np.random.random(self.dim) beta np.where(u 0.5, np.power(2 * u, 1 / (self.eta_c 1)), np.power(1 / (2 * (1 - u)), 1 / (self.eta_c 1))) c1 0.5 * ((1 beta) * p1 (1 - beta) * p2) c2 0.5 * ((1 - beta) * p1 (1 beta) * p2) return c1, c2 def polynomial_mutation(self, individual): for i in range(self.dim): if np.random.random() self.pm: u np.random.random() if u 0.5: delta np.power(2 * u, 1 / (self.eta_m 1)) - 1 else: delta 1 - np.power(2 * (1 - u), 1 / (self.eta_m 1)) individual[i] delta * (self.ub - self.lb) return np.clip(individual, self.lb, self.ub) def run(self): np.random.seed(self.seed) X np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) val np.array([self.fitness(x) for x in X]) best_val val.min() history [best_val] for _ in range(self.max_iter): new_pop [] while len(new_pop) self.pop_size: p1 self.tournament_selection(X, val) p2 self.tournament_selection(X, val) if np.random.random() self.pc: c1, c2 self.sbx_crossover(p1, p2) else: c1, c2 p1.copy(), p2.copy() c1 self.polynomial_mutation(c1) c2 self.polynomial_mutation(c2) new_pop.append(c1) new_pop.append(c2) X np.array(new_pop[:self.pop_size]) val np.array([self.fitness(x) for x in X]) if val.min() best_val: best_val val.min() history.append(best_val) best_idx np.argmin(val) return X[best_idx].copy(), best_val, history锦标赛选择里的 tournament_size 我用了 2这是最常用的设置。size 太小选择压力弱种群进化慢太大容易让超级个体快速占领种群多样性下降。交叉率 0.9 保证大多数个体参与重组变异率 0.1 表示每个维度有 10% 概率被扰动。eta_c 和 eta_m 是分布指数值越大产生的子代越接近父代。有个细节需要注意新种群生成时 while 循环每次会加入两个个体最后可能超过 pop_size所以要截断。截断可能导致最后一个个体没有参与比较但概率很低对结果影响可以忽略。4.3 GWO算三个头狼方向的加权平均GWO 的实现核心是每只狼都要对 alpha、beta、delta 三个参考位置分别计算一次包围更新然后取三者的平均值。每个头狼方向对应的 A 和 C 都是独立重新采样的这样才能保证三种引导信息的差异性。import numpy as np class GWO: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed def run(self): np.random.seed(self.seed) X np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) val np.array([self.fitness(x) for x in X]) idx np.argsort(val) alpha_pos, alpha_val X[idx[0]].copy(), val[idx[0]] beta_pos, beta_val X[idx[1]].copy(), val[idx[1]] delta_pos, delta_val X[idx[2]].copy(), val[idx[2]] history [alpha_val] for t in range(self.max_iter): a 2 - 2 * t / self.max_iter for i in range(self.pop_size): X1 self.update_by_leader(X[i], alpha_pos, a) X2 self.update_by_leader(X[i], beta_pos, a) X3 self.update_by_leader(X[i], delta_pos, a) new_pos (X1 X2 X3) / 3 X[i] np.clip(new_pos, self.lb, self.ub) val np.array([self.fitness(x) for x in X]) sorted_idx np.argsort(val) if val[sorted_idx[0]] alpha_val: alpha_val val[sorted_idx[0]] alpha_pos X[sorted_idx[0]].copy() if val[sorted_idx[1]] beta_val: beta_val val[sorted_idx[1]] beta_pos X[sorted_idx[1]].copy() if val[sorted_idx[2]] delta_val: delta_val val[sorted_idx[2]] delta_pos X[sorted_idx[2]].copy() history.append(alpha_val) return alpha_pos, alpha_val, history def update_by_leader(self, x, leader_pos, a): r1 np.random.random(self.dim) r2 np.random.random(self.dim) A 2 * a * r1 - a C 2 * r2 D np.abs(C * leader_pos - x) return leader_pos - A * D实现里有个容易忽视的点三只头狼的更新不是每代只更新一次而是等这一代所有个体位置都更新完、适应度重新算完之后再重新挑前三名。这样可以避免个别个体的优秀表现过早污染当前代的引导方向符合原论文的机制。我在复现时发现GWO 对边界处理比较敏感。如果直接用 np.clip 把越界狼拉回边界种群多样性会受损如果完全不管越界个体适应度函数可能收到越界输入导致异常。折中做法是 clip同时让越界个体的 A 保持较大值让它下一轮有能力重新远离边界。4.4 WOAp 和 A 共同决定搜索策略WOA 的每一次迭代里每只鲸鱼要同时看两个随机量p 决定走包围还是螺旋A 决定包围时是靠近还是远离。import numpy as np class WOA: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42, b1.0): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed self.b b def run(self): np.random.seed(self.seed) X np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) val np.array([self.fitness(x) for x in X]) leader_idx np.argmin(val) leader_pos X[leader_idx].copy() leader_val val[leader_idx] history [leader_val] for t in range(self.max_iter): a 2 - 2 * t / self.max_iter for i in range(self.pop_size): r np.random.random() A 2 * a * r - a C 2 * np.random.random() p np.random.random() if p 0.5: if np.abs(A) 1: D np.abs(C * leader_pos - X[i]) X[i] leader_pos - A * D else: rand_idx np.random.randint(self.pop_size) rand_pos X[rand_idx] D np.abs(C * rand_pos - X[i]) X[i] rand_pos - A * D else: l np.random.uniform(-1, 1) D np.abs(leader_pos - X[i]) X[i] D * np.exp(self.b * l) * np.cos(2 * np.pi * l) leader_pos X[i] np.clip(X[i], self.lb, self.ub) current_val self.fitness(X[i]) if current_val leader_val: leader_val current_val leader_pos X[i].copy() history.append(leader_val) return leader_pos, leader_val, historyWOA 的随机性比较强p 每次迭代重新采样A 也是每次重新采样所以同一个种子下跑两次结果也能复现但不同种子之间方差较大。复现论文结果时通常要看 30 次独立运行的平均值和中位数而不是单次结果。螺旋更新的 D 用的是“当前最优位置减当前鲸鱼位置”的绝对值再乘上螺旋因子最后加回 leader_pos。这个实现没有严格区分 D 向量的符号方向但在绝大多数测试函数上不影响收敛原因在于绝对值保证了螺旋始终围绕 leader 展开方向随机性由 cos 项来贡献。4.5 SSAc1 指数衰减是精髓SSA 的实现里种群的第一行是领导者第二行到最后一行都是追随者。追随者更新严格依靠“前一个个体”的当前位置所以种群的初始顺序会影响整个搜索过程。为了让追随链更合理初始化时也应该随机但不需要额外排序。import numpy as np class SSA: def __init__(self, fitness_func, dim30, pop_size30, max_iter500, lb-100, ub100, seed42): self.fitness fitness_func self.dim dim self.pop_size pop_size self.max_iter max_iter self.lb lb self.ub ub self.seed seed def run(self): np.random.seed(self.seed) X np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) val np.array([self.fitness(x) for x in X]) food_idx np.argmin(val) food_pos X[food_idx].copy() food_val val[food_idx] history [food_val] for t in range(self.max_iter): c1 2 * np.exp(-(4 * t / self.max_iter) ** 2) for i in range(self.pop_size): if i 0: c2 np.random.random(self.dim) c3 np.random.random(self.dim) if c3 0.5: X[i] food_pos c1 * ((self.ub - self.lb) * c2 self.lb) else: X[i] food_pos - c1 * ((self.ub - self.lb) * c2 self.lb) else: X[i] 0.5 * (X[i] X[i - 1]) X[i] np.clip(X[i], self.lb, self.ub) current_val self.fitness(X[i]) if current_val food_val: food_val current_val food_pos X[i].copy() history.append(food_val) return food_pos, food_val, history注意 c2 和 c3 我都用了形状为 dim 的随机向量而不是一个标量。这样每个维度朝食物靠近的偏移量是独立变化的避免所有维度同步摆动。有些论文实现里 c2、c3 是全局标量那样会让领导者只能沿固定方向移动探索能力会差不少。追随者的更新是0.5 * (X[i] X[i-1])这里 X[i] 是当前位置X[i-1] 是前一个个体更新后的位置。因为这个操作是从 i1 开始顺序执行的前一个个体可能已经更新过了队列的信息单向传递这正是 SSA 链式结构的特点。5. 结果怎么解读、哪些坑必须要避开5.1 收敛曲线别只用线性坐标跑完实验后最常见的就是画每个算法的收敛曲线。默认用线性坐标画在 F1 这种最小值是 0 的函数上前几十代的下降看起来很明显后期所有算法都贴到 0 附近曲线几乎重合完全看不出差异。我建议在多峰函数和简单单峰函数上统一用对数坐标。对数坐标能把 1e-5 和 1e-20 之间的差距拉开收敛曲线呈现出“谁降得更快、谁降得更深”的层次。尤其像 PSO、GWO、WOA 这类后期高精度收敛的算法只有在 log scale 下才能看出真实水平。另外建议把 30 次运行的中位数作为曲线再配合一个半透明的区间带表示上下四分位数。只看单次最好值很可能被一次运气好的运行误导。以我自己的复现经验常见量级大致是这样具体值和 Python 版本、NumPy 版本、随机数生成方式都有关系仅供参考测试函数PSOGAGWOWOASSAF1 Sphere1e-12 左右1e-4 左右1e-28 左右1e-30 左右1e-8 左右F8 Schwefel-12000 左右-12000 左右-6000 左右-10000 左右-9000 左右F10 Ackley1e-13 左右1e-3 左右1e-14 左右1e-14 左右1e-6 左右如果你跑出来的结果和这个量级差距很大先别急着怀疑算法按下面的坑逐条检查。5.2 六个容易翻车的细节第一个坑初始化边界和函数定义域不一致。Ackley 的定义域是 [-32, 32]如果你为了省事把它也初始化到 [-100, 100]算法前期大部分个体都落在远离最优的区域要多花很多代才能搜索到有效区域收敛曲线会很难看。第二个坑F1 到 F13 的维度不统一。经典的实验设置中 F1 到 F13 一般用 30 维对比时必须所有算法都跑 30 维。有人把 F1 跑 30 维、把 F8 跑 10 维最后放在同一张表里比较这没有任何意义。第三个坑固定维度函数不能随便改维度。F14 的 Foxholes 就是 2 维你给它传 30 维向量会直接计算错误F20 是 6 维传错维度会得到完全错误的理论最优值。实验调度时一定要根据函数配置维度。第四个坑随机种子和独立重复次数。只跑一次就说“算法 A 比算法 B 好”这是所有对比实验里最容易被审稿人攻击的点。至少跑 30 次统计最好值、中位数和标准差。而且所有算法在同一个函数上应当使用同一组种子保证初始种群的可比性。第五个坑F7 Quartic with Noise 里的随机噪声。这个函数每次计算都会生成新的随机数所以即使位置完全一样两次调用得到的适应度都可能不同。复现时需要注意F7 的最优值不是一个固定 0而是带随机扰动后的极小值。公平起见所有算法在 F7 上评估次数要保持一致否则结果不可比。第六个坑数值溢出和除零。F12、F13 的惩罚项里有幂运算x 越界时 k * (x-a)^m 可能膨胀到极大值F14 的分母在极少数浮点误差下可能趋近 0导致结果接近无穷大。建议在每个函数内部对极端输入做保护性处理比如给分母加一个 1e-20 的下限。5.3 从基准测试到实际问题的迁移建议23 个测试函数跑完只能说明算法在这些人工构造问题上的表现不直接等于它能解决你的实际工程问题。但基准测试的真正价值是帮你建立“算法性格”的直觉。我自己跑完这套实验后的体会是追求高精度单峰收敛优先看 WOA 和 GWO面对复杂多峰问题、担心陷入局部最优GA 和 SSA 的多样性更值得信赖PSO 则是最稳的“万金油”参数少、收敛快适合做初步探索。如果你打算把这套代码扩展到自己课题里我建议保留统一的基类接口只替换 fitness_func 为你的实际问题函数。注意实际问题的变量往往有物理约束比如角度范围、速度上限这些都要落到 lb 和 ub 里。如果优化目标有多个可以先做加权或转成罚函数形式再用这套框架跑。最后再分享一个小技巧实验完成后画一张热力图横轴是 23 个测试函数纵轴是 5 个算法颜色表示每个算法在每个函数上的排名或对数误差。这样一张图就能快速看出哪个算法在哪个函数类型上占优写论文汇报时比单独贴十条收敛曲线直观得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →