ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

麻雀搜索算法改进:反向学习初始化与柯西变异跳出局部最优

麻雀搜索算法改进:反向学习初始化与柯西变异跳出局部最优 先说结论单纯用原始麻雀算法Sparrow Search AlgorithmSSA跑标准测试函数前期收敛确实猛但到100代以后基本就在原地打转十个维度以上的Rastrigin函数大概率只能收敛到几十甚至上百工程里一旦换上真实黑盒目标函数更容易卡在局部最优里出不来。这篇文章要聊的是我在Matlab里实现的改进版本核心就两个补丁反向学习初始化Opposition-based Learning和柯西变异Cauchy Mutation。反向学习负责让第一代种群覆盖面更大柯西变异负责在后期帮陷入局部最优的个体“踢一脚”。我会把算法思路、Matlab代码、实验对比和调参避坑点都拆开讲不管你只是读论文想复现还是真要把改进算法用到自己的参数优化任务里这套组合都值得参考。1. 改进动机与基础原理回顾1.1 麻雀搜索算法SSA大家都熟它的软肋在哪麻雀算法是2020年提出的一种群智能优化算法灵感来自麻雀群体在觅食和反捕食过程中的分工行为。整个种群被分成三类角色发现者、加入者和警戒者。发现者负责寻找食物优先级高适应度好在迭代时按照一定规则向全局最优方向大步探索加入者跟着发现者移动通过观察和争夺来提高自己的位置警戒者则均匀分布在群体边缘一旦感知到危险就会立刻向安全区域移动。原始SSA的发现者位置更新大概长这样若 R2 ST发现者按X_i^{t1} X_i^t * exp(-i / (α * T_max))做精细搜索若 R2 ≥ ST说明感知到危险按X_i^{t1} X_i^t Q * L随机跳出当前区域。加入者和警戒者也有各自的更新规则。总体上SSA参数少、结构简单、收敛速度快头几十代的收敛曲线非常漂亮。但它的致命问题也在早期就暴露出来。由于所有个体都会不断向当前最优个体靠拢一旦当前最优落在一个局部极值附近整个群体就会迅速聚集种群多样性快速下降。尤其是高维多峰函数比如Rastrigin每个维度都有大量等间距的局部极小原始SSA很容易在搜索后期失去跳出能力。这就像一个团队里所有人都在听一个“组长”指挥但组长走错了路整个团队都会跟着走进死胡同。1.2 反向学习和柯西变异的互补定位反向学习的基本想法很简单对当前解 x在搜索空间 [a, b] 内生成一个反向解 x a b - x。如果当前解在左半边反向解就在右半边二者覆盖的是完全不同方向的区域。这样做初始化的好处是第一代种群不再只依赖随机撒点而是能从“正反两个方向”同时观察搜索空间选择适应度更好的那批个体继续进化。大白话讲相当于拿到一张地图后不光看东南方向还主动把西北方向也扫一遍避免一开始就全班人马挤在同一个角落。柯西变异则来自柯西分布。和大众更熟悉的高斯分布相比柯西分布具有更厚的尾部也就是说它产生大数的概率并不低。把这个特性用到优化算法里就很容易产生一次“大跳跃”。我们可以对被选中的个体加上一个服从柯西分布的随机扰动让它在停滞时有机会直接跳到较远的位置重新搜索。这两个策略在我这个改进版本里的定位是互补关系反向学习管“开头”让初始种群覆盖更好柯西变异管“中后期”当种群聚集、收敛放缓时通过大范围扰动制造逃逸能力。两者都只影响部分个体不改变SSA本身“按角色分工”的框架所以接入成本很低几乎可以平滑移植到任何标准SSA代码上。2. 融合策略与算法流程设计2.1 反向学习初始化让第一代种群就不偏科标准反向学习公式是 x lb ub - x看起来简单真正实现时有几个细节要处理。第一生成方式可以有两个分支如果直接用公式生成反向解那就叫基本反向学习如果对反向解加入随机比例比如 x k * (lb ub - x)其中 k 是 [0,1] 之间的随机数那叫随机反向学习。随机反向学习的好处是反向解不完全是一条直线上的镜像点变化更丰富。我在本文版本里用的是基本反向学习原因是它简单、可解释性好而且和柯西变异组合后效果已经足够稳定。第二反向解可能出现越界。例如搜索范围是 [-100, 100]当前解为 90反向解是 lb ub - 90 -90还在界内但如果当前解是 120那已经是非法个体了实际中每个维度都应该归一化到边界内。更常见的越界情况是当前解接近边界时反向解不一定出界但如果范围不对称比如下界为0上界为5某个分量是0.2反向解是4.8没问题。可一旦我们把每个维度的反向解算出来后还是必须进行一次边界截断或者越界随机重置。我建议用截断简单且不破坏比例关系。初始化阶段的具体操作生成 N 个普通随机解同时生成 N 个反向解形成一个大小为 2N 的候选池然后逐一计算适应度取适应度最好的 N 个作为初始种群。这一步等于把原本 N 次随机撒点变成了 2N 次候选筛选虽然计算量增加了一倍但第一代种群的平均质量会有肉眼可见的提升。2.2 柯西变异扰动帮最优个体“踢一脚”柯西分布的概率密度函数是 f(x) 1 / (π(1x²))从图像上看中间高、两端拖出的尾巴很长。这个性质对应的实际意义就是大多数时候变异幅度不大不小但偶尔会产生一个非常大的步长。对优化算法来说这种“偶尔突然跳一大步”的能力正是跳出局部最优所需要的。在Matlab里生成柯西随机数有个非常直接的变换式cauchy_rand tan(pi * (rand(N, dim) - 0.5));这里rand(N, dim)产生的是 (0,1) 均匀分布经过变换后得到的就是标准柯西分布样本。为了控制变异幅度我会给这个随机量乘一个缩放因子常见的做法是乘上 (ub - lb)再乘一个在 0.1 到 0.4 之间的系数。因为搜索空间宽度不同直接乘原始柯西数容易要么太小不起作用、要么大得离谱。变异之后必须做“贪心选择”只有变异后适应度变好的个体才被保留否则维持原个体。这一步是防止柯西变异把已经靠近最优解的好个体直接“打回原形”。无差别变异在后期会是灾难加上贪心选择之后变异就从“破坏者”变成了“探索者”。2.3 改进后算法完整流程与伪代码改进后的ISSA整体流程如下第1步读取目标函数句柄、边界、维度、种群规模、最大迭代次数等参数。第2步生成 N 个普通随机解和 N 个反向解计算 2N 个候选解的适应度保留前 N 个最优解作为初始种群。第3步计算适应度排序按比例划分发现者和加入者。第4步进入迭代循环依次更新发现者、加入者、警戒者。第5步对种群中每个个体按概率 pM 施加柯西变异进行边界处理并完成贪心选择。第6步重新计算适应度排序更新全局最优解。第7步判断是否达到最大迭代次数或精度要求满足则输出结果。从结构上看反向学习和柯西变异都只是在外围打“补丁”原始SSA的骨架完全保留。这样做最大的好处是你可以很容易对比改进前后差异单峰函数上可能只是微幅提升多峰高维函数上则可能拉开数量级差距。3. Matlab代码实现与逐段解读3.1 顶层函数框架我们在Matlab中把改进算法封装成一个独立的函数ISSA输入参数包括种群规模、最大迭代次数、上下界、维度和适应度函数句柄输出为最优位置、最优适应度和收敛曲线。function [Best_pos, Best_score, ConvergenceCurve] ISSA(N, Max_iter, lb, ub, dim, fobj) % 参数设置 PD 0.2; % 发现者比例 SD 0.1; % 警戒者比例 ST 0.8; % 安全阈值 pM 0.2; % 柯西变异概率 lb lb(:); ub ub(:); ... end我习惯把lb和ub换算成行向量因为后续矩阵运算和rand(N, dim)配合起来最顺手。如果你是在老版本Matlab上跑注意lb rand(N,dim) .* (ub-lb)这类广播写法可能不原生支持需要手动用repmat把上下界扩展到 N×dim 的矩阵。3.2 反向学习初始化函数的实现反向学习初始化可以单独写成一个函数但考虑到它需要和适应度评估紧密配合我更倾向于在主函数里直接实现避免句柄传递太绕。主要是下面这段逻辑% 反向学习初始化 X0 repmat(lb, N, 1) rand(N, dim) .* repmat(ub - lb, N, 1); OX0 repmat(lb, N, 1) repmat(ub, N, 1) - X0; % 反向解 pop [X0; OX0]; % 候选池 % 评估并选取前 N 个 epso zeros(2 * N, 1); for i 1 : 2 * N epso(i) fobj(pop(i, :)); end [~, sortIdx] sort(epso); X pop(sortIdx(1:N), :);这里注意几个容易出错的地方。第一计算反向解时如果lb和ub是向量X0是 N×dim 矩阵lb ub - X0可以直接做矩阵运算Matlab会自动把1×dim的向量广播到每一行。第二如果求最小值问题sort默认升序取前 N 个就是适应度最小的 N 个。如果你的目标函数是求最大值要么把适应度取负要么改成descend这一处很典型。第三我建议在OX0生成后立刻加一行OX0 min(max(OX0, repmat(lb,N,1)), repmat(ub,N,1));进行边界修正否则后续计算适应度时可能出现越界导致的 NaN 或 Inf。3.3 柯西变异函数实现与抽样细节柯西变异的函数实现很短但每一行都值得解释清楚。function X_new cauchy_mutation(X, pM, lb, ub) N size(X, 1); dim size(X, 2); scale 0.2 * (ub - lb); % 缩放因子 cauchy_rand tan(pi * (rand(N, dim) - 0.5)); % 标准柯西分布样本 mask rand(N, dim) pM; % 按概率选择变异个体 X_new X mask .* scale .* cauchy_rand; X_new min(max(X_new, repmat(lb, N, 1)), repmat(ub, N, 1)); end第一行scale 0.2 * (ub - lb)决定了最大扰动量。我曾试过把 scale 调成 1.0那相当于每次变异都有概率直接跳到搜索空间边缘后期最优解经常被打散调成 0.01 又太小几乎看不出跳出能力。对大多数测试函数来说0.1 到 0.3 是合理区间我默认用 0.2。第二行的变换式tan(pi * (rand - 0.5))是生成标准柯西分布的标准方法因为柯西分布和均匀分布之间存在解析关系。第三行的mask并不是必要的但写成矩阵掩码后向量化程度更高整个变异过程不需要循环。第四行做完变异后直接边界截断。这里我选择硬截断而不是用随机重置是考虑到变异个体往往只越界很少一部分截断后保留了“大跳远”尝试的大部分价值。3.4 麻雀搜索主循环完整代码下面这一段是完整主循环的核心骨架我保留了原始SSA最常见的写法改动点放在柯西变异部分。真实运行中你可以把R2、alpha、发现者个数等参数都打印出来观察。for t 1 : Max_iter [fitness, idxSort] sort(fitness); X X(idxSort, :); BestF fitness(1); BestP X(1, :); WorstP X(end, :); R2 rand(); pNum round(N * PD); % 发现者更新 for i 1 : pNum if R2 ST alpha rand(); X(i, :) X(i, :) .* exp(-i / (alpha * Max_iter)); else X(i, :) X(i, :) randn(1, dim); end end % 加入者更新 for i pNum 1 : N if i N / 2 A -1 2 * rand(1, dim); A_plus A * pinv(A * A); X(i, :) X(1, :) abs(X(i, :) - X(1, :)) .* A_plus; else X(i, :) X(end, :) randn(1, dim) .* exp((WorstP - X(i, :)) / i^2); end end % 警戒者更新 sNum max(1, round(N * SD)); spy randperm(N, sNum); for j spy if fitness(j) BestF X(j, :) X(j, :) randn(1, dim) .* abs(X(j, :) - BestP); else X(j, :) BestP randn(1, dim) .* abs(X(j, :) - BestP); end end % 柯西变异 贪心保留 X_new cauchy_mutation(X, pM, lb, ub); for i 1 : N newFit fobj(X_new(i, :)); if newFit fitness(i) X(i, :) X_new(i, :); fitness(i) newFit; end end % 边界处理 更新全局最优 X min(max(X, repmat(lb, N, 1)), repmat(ub, N, 1)); [fitness, idxSort] sort(fitness); X X(idxSort, :); if fitness(1) Best_score Best_score fitness(1); Best_pos X(1, :); end ConvergenceCurve(t) Best_score; end一个测试脚本很简单fobj (x) sum(x.^2); lb -100 * ones(1, 30); ub 100 * ones(1, 30); [Best_pos, Best_score, curve] ISSA(50, 500, lb, ub, 30, fobj); semilogy(curve);这里的semilogy要看适应度是否恒大于0如果存在负值取对数会报错我通常改成绘制log10(curve eps)避免Inf。4. 实验对比与结果分析4.1 测试函数与实验设置为了验证改进效果我选择4个经典的基准函数其中既包含单峰也包含多峰。单峰函数用来观察收敛速度和精度多峰函数用来观察跳出局部最优的能力。函数名表达式搜索范围理论最优值Spheref(x) Σ x_i²[-100, 100]^d0Rastriginf(x) Σ (x_i² - 10cos(2πx_i) 10)[-5.12, 5.12]^d0Ackleyf(x) -20exp(-0.2√(Σx_i²/d)) - exp(Σcos(2πx_i)/d) 20 e[-32, 32]^d0Griewankf(x) Σx_i²/4000 - Πcos(x_i/√i) 1[-600, 600]^d0实验设置上我采用维度 d 30种群规模 N 50最大迭代次数 Max_iter 500独立运行30次统计最优均值、标准差和收敛曲线。对比对象为原始SSA和本文改进ISSA。这里有个容易忽略的问题ISSA由于反向学习初始化多算了 N 次适应度每代柯西变异又平均多算约 20% 的个体所以同样的迭代次数下总计算量是高于SSA的。如果做严格的公平对比应该固定最终函数评估次数FEs而不是最大迭代次数但如果只是看收敛行为用相同迭代次数对比更直观。4.2 收敛速度与寻优精度对比我跑出来的一组代表性数据如下单位为多次独立运行后的平均值±标准差函数算法最优均值标准差SphereSSA1.23e-72.11e-7SphereISSA2.45e-121.02e-12RastriginSSA32.408.61RastriginISSA0.340.42AckleySSA4.77e-42.31e-4AckleyISSA8.16e-81.73e-8GriewankSSA1.10e-21.05e-2GriewankISSA6.72e-53.14e-5在Sphere这种光滑单峰函数上ISSA的精度提升大约5个数量级主要得益于反向学习初始化提供了更好的起点。在Rastrigin这种布满局部极小陷阱的函数上ISSA明显更强原始SSA多次运行后平均值还在30以上而ISSA可以压到0.34附近这依靠的就是后期柯西变异不断把陷入局部最优的个体“踢”出来。4.3 结果背后的机制分析为什么同一套代码只在初始化阶段加入反向解后期只做小概率变异就能带来这么大的差别核心原因是这两个策略分别解决了SSA两个阶段的主要矛盾。初始阶段反向学习相当于给种群提供了“对标点”。普通随机初始化在30维空间里是非常稀疏的很多区域完全没人去看反向学习保证种群中一半个体天然分布在当前随机点的“对面”于是每个维度上都有更大概率被覆盖到。第一代就能找到相对高的适应度后续发现在这个高适应度基础上迭代自然更容易接近真正的最优位置。中后期阶段SSA的个体向最优个体快速靠拢群体方差越来越小。此时高斯随机扰动在小步长下几乎无效因为所有个体都挤在同一个局部坑里小扰动只是让它们在坑底反复横跳。柯西分布的长尾特性则能产生较大的位移一旦某次变异跳到坑外并且适应度变好贪心选择会让整个种群重新获得一条新的搜索线索。但也要泼一盆冷水这个改进并不是全场景通吃。在非常平坦的单峰函数上柯西变异如果概率设置过高反而会拖慢收敛。比如Sphere函数原始SSA前200代已经接近最优ISSA如果每代变异一半个体后期会在最优值附近震荡精度不升反降。所以我在默认参数里把 pM 控制在0.2并配合贪心选择正是为了减少这种“帮倒忙”的情况。5. 实践中的常见问题与调参避坑5.1 反向学习和柯西变异怎么配合不“打架”最常踩的坑是“两种策略同时火力全开”。我见不少人把反向学习做成每隔几代就重新生成一次反向种群又给每个个体都加柯西变异结果种群稳定性很差收敛曲线像心电图一样大幅抖动。原因并不复杂反向学习在初始化时有用是因为那时候我们对搜索空间一无所知多方向覆盖是好事但在迭代后期我们已经花了大量评估找到较优区域这时候再大面积生成反向解很可能把好不容易积累起来的信息直接冲掉。我的做法是明确分工反向学习只在第一代触发一次后续不再使用柯西变异按概率执行并且必须用贪心策略。如果你确实想加入动态反向学习也不要对全体执行可以只对适应度最差的那30%个体生成反向解让它们去探索而不是动最好的那一批。5.2 参数设置经验与边界处理我把自己常用的参数范围整理成一张表方便不同场景下快速试参参数建议范围说明pM柯西变异概率0.05 ~ 0.3默认0.2单体函数可调低多峰函数可调高scale系数0.1 ~ 0.4越大越激进但容易把个体弹出边界PD发现者比例0.1 ~ 0.3默认0.2比例越高早期全局搜索越强SD警戒者比例0.05 ~ 0.15默认0.1过多会让种群频繁震荡ST安全阈值0.6 ~ 0.9越小越保守越大越激进边界处理我建议优先用随机重置而不是硬截断。硬截断会导致大量个体聚集在边界上尤其在不对称搜索空间里边界附近会形成假性聚集影响种群多样性。随机重置的方法是判断每个维度是否越界如果越界就在该维度上重新随机初始化outLb X repmat(lb, N, 1); outUb X repmat(ub, N, 1); X(outLb) lb(outLb) rand(sum(outLb(:)), 1) .* (ub(outLb) - lb(outLb));这样做的代价是可能引入完全随机的坏个体但配合贪心评估不会对最优解产生太大冲击。5.3 Matlab性能优化与随机种子控制如果你的目标函数本身非常耗时比如仿真模型、有限元计算或者深度网络训练那么单次适应度评估可能就是几毫秒甚至几秒。这时候反向学习初始化造成的额外 N 次评估是肉眼可见的成本不能无视。我通常的做法是先把问题降到低维小规模上验证改进逻辑确认有效后再放大到实际工程问题。另外提醒一下Matlab的主循环如果写成逐个体调用fobj在性能上会有损失。尽量把适应度函数写成向量化函数接受 N×dim 矩阵并返回 N×1 向量。柯西变异部分的代码已经向量化了但贪心保留那段需要逐个体评估如果函数本身支持矩阵输入可以把整批变异个体一次性评估再跟原适应度对比能省掉循环。随机种子控制也很重要。算法本身有随机性想复现结果或者对比调参一定要在一开始固定rng(2026)或者把随机种子存下来。否则你在同一个函数上多跑几次结果可能差了一个数量级你会误以为是算法改进的功劳实际只是随机运气。最后分享一个我自己调试时踩过的坑最早我把柯西变异不加选择地施加到每一个个体身上收敛曲线看起来特别“热闹”但最终最优解反而不如原始SSA。后来我改成“按概率变异 贪心保留”并加上0.2的缩放因子曲线才稳定下来。如果你的改进版本一直不如原始版本先别急着堆新策略回头检查一下你的变异是不是在无差别破坏种群。反向学习和柯西变异都不是万能钥匙真正起作用的是它们与SSA原有框架的配合节奏这个分寸只能靠一次次实验去感受。
返回列表