ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

自私羊群优化算法SHO:MATLAB实现与单目标优化实战

自私羊群优化算法SHO:MATLAB实现与单目标优化实战 做工程优化久了你迟早会遇到一种尴尬目标函数连解析式都不完整要么是仿真返回一个数要么函数曲面布满局部坑梯度根本算不出来。这种问题交给梯度下降和牛顿法基本等于让短跑运动员去游泳。于是智能优化算法就成了最稳的兜底方案。这篇文章我拿MATLAB实现的自私羊群优化算法Selfish Herd OptimizerSHO做主线完整拆一遍单目标优化问题的求解流程。从算法机制、数学原理、代码实现到四个经典基准函数的实测收敛表现再把我跑代码时踩过的调参坑一并交代清楚代码可以直接抄。先说清楚这套东西能干什么输入一个目标函数给定变量范围和维度SHO自动搜出一组使函数值最小化的解。整个过程只用最基础的MATLAB脚本语法不需要任何额外工具箱一个.m文件就够。适合刚接触智能优化算法的学生也适合打算把新算法引入工程项目的开发者。1. 为什么还要再来一个智能优化算法单目标优化问题的真实难点很多初学者有个疑问优化算法都那么多了GA、PSO、DE、FA各领风骚为什么近几年又冒出来灰狼算法、鲸鱼算法、秃鹰搜索这些新名字这个问题的答案恰恰藏在“单目标优化问题”这个最简单的分类里。1.1 梯度法在哪些问题面前会“失灵”单目标优化问题听起来简单给定目标函数 f(x)找到一组决策变量 x 让 f(x) 最小。但工程里实际遇到的单目标问题往往同时具备几个让传统数学规划方法头疼的特征。第一不可导或梯度不存在。离散变量、仿真软件返回的黑箱结果、存在随机噪声的测量数据这类函数的梯度要么无法用解析方式求出来要么数值梯度计算成本极高。第二多峰且非线性。函数曲面有大量局部极小点梯度法一旦滑入一个坑就只能在坑底打转没有机制跳出来。第三无明确数学模型。比如你调一个工艺参数组合每个参数组合对应一次几十分钟的物理仿真你手里只有输入和输出的映射关系中间过程一无所知。这些问题用传统方法很尴尬。梯度法没梯度可用枚举法在三维以上就爆炸随机穷举效率太低。这时群体智能优化算法的优势就体现出来了它不需要任何导数信息只需要能不停评估函数值就能在搜索空间中不断逼近全局最优区域。1.2 智能优化算法的共同套路与SHO的定位所有群体智能算法共享一个底层逻辑初始化一批候选解然后让这些解根据某种“经验规则”在搜索空间里移动移动过程中兼顾探索Exploration和开发Exploitation两种行为。探索是指跳向未知区域避免一开始就锁死在局部开发是指在优势区域精细搜索把精度提上去。GA靠交叉变异PSO靠个体最优加全局最优的“速度牵引”DE靠差分向量扰动。自私羊群优化算法呢它靠的是一套非常有趣的“捕食者—猎物”博弈关系种群里有捕食者猎物又想靠自己活下来又想躲在群体里保命。这两种相反的力量互相拉扯恰好构成了搜索过程中的全局探索和局部开发。SHO最早由Fausto等人在2017年提出设计思路来源于自私羊群理论。这个理论有一个反直觉的结论群体中的个体表现出自私行为但结果却让整个群体更安全。动物世界里每个个体都想让别的个体挡在捕食者前面但这种“自私”反而使群体产生聚拢行为降低了单一个体的被捕概率。把这个逻辑映射到优化问题上就变成了一个有时间接实现了“先大步探索再精细收敛”的搜索过程。2. SHO的核心思想自私、群居与捕食压力如何驱动搜索理解SHO的关键不在代码而在于先理解它模拟的三类“人物关系”。把这层关系搞透了代码只是翻译问题。2.1 自私羊群的行为学设定设想一群羊在草地上吃草周围潜伏着捕食者。羊群里每只羊有两个本能一是想尽量靠近群体中心因为群体掩护能降低自己被捕的概率二是想让自己处在群体中相对安全的位置最好别人在外面、自己在里面。与此同时捕食者会紧盯群体中最弱、最靠外的个体下手。把这个场景搬到优化领域每个“羊”就是搜索空间中的一个候选解“群体中心”可以理解为当前搜索过程中适应度较好的个体集中的区域“最弱的羊”是适应度最差、偏离最优区域的个体“捕食者”则专门攻击最弱个体它的存在会把搜索引向那些被忽略的角落。有意思的是羊群向中心聚拢的行为让群体保持“开发”特性在局部做精细搜索捕食者追捕离群个体的行为又让搜索不忍略边缘区域保持“探索”特性。两种本能的对抗不需要人为设置什么探索概率衰减因子自然就形成了动态平衡。2.2 三类角色与三种运动策略在算法上每一步迭代会把种群分成三类角色捕食者Predator、领头羊Leader和追随者Follower。先说一下角色划分逻辑。每一代计算完所有个体的适应度后按适应度从好到差排序。适应度差的若干个体被标记为捕食者它们不参与羊群的“自我保护运动”而是遵循另一套追逐逻辑剩下的是猎物。在猎物中适应度优于群体平均值的个体成为领头羊适应度差于平均值的个体成为追随者。三类角色各有各的更新公式这直接决定了搜索行为捕食者的运动向当前猎物中最弱的个体靠近。公式可以简化为 X_pred X_pred 2 * rand * (X_weakest - X_pred)。捕食者的方向非常明确就是盯着群体的“短板”打这保证了搜索不会过早放弃劣质区域。领头羊的运动向历史全局最优解靠近同时稍微带着一点向群体中心的方向。领头羊起到“带队”作用它们一动群体的整体搜索方向就清晰了。追随者的运动主要向群体中心收拢同时受到随机邻近个体的影响。追随者之间会互相传递位置信息这增加了种群的多样性避免所有个体扎堆到同一个点。2.3 SHO为什么天然具备探索与开发的平衡很多算法聪明是聪明但探索和开发的节奏需要手动调参比如PSO里的惯性权重w、加速度系数c1和c2调不好就早熟收敛或者收敛极慢。SHO的巧妙之处在于它的开发压力来自“群体中心的引力”探索压力来自“捕食者对弱者的追杀”引力太强会导致扎堆收敛到局部最优追杀太强则会让个体乱飞。但这两者的强度会随迭代自适应变化迭代初期个体分布分散群体中心不明确捕食者追击弱者的作用更明显迭代后期个体聚拢群体中心变得清晰领头羊带队的作用占主导。这种机制不需要显式地设置“前期探索、后期开发”它通过捕食者和猎物之间的动态博弈自动实现了。这也是为什么我建议新手不要上来就魔改公式先按原始框架跑通观察哪些算子主导了哪段收敛过程再谈改进。3. 从伪代码到MATLAB实现一个能直接跑的SHO脚本理论说再多不如跑一个算例来得直观。下面给出一个完整可运行的MATLAB脚本包含了主算法、边界处理和测试函数。3.1 算法整体流程与数据结构先明确整体迭代结构初始化种群在搜索空间内随机生成nPop个个体的位置。计算适应度每个个体代入目标函数求值。角色划分排序后确定捕食者、领头羊、追随者。分别更新三类角色的位置。实施恢复操作对离群体中心过远的猎物拉回中心附近。边界处理把越界个体拉回可行域。记录全局最优进入下一代。这里有一个细节代码中每个个体用一行向量表示维度就是决策变量个数。种群用一个nPop行、dim列的矩阵存储每一行是一个解。这种数据结构在MATLAB里效率最高也方便矩阵化运算。3.2 核心代码实现与逐段说明function SHO_Run() %% 参数设置 nPop 80; % 种群规模 nPred 10; % 捕食者数量 MaxIt 500; % 最大迭代次数 dim 30; % 问题维度 lb -100; ub 100; % 变量范围按具体问题修改 % 选择测试函数: sphere / rastrigin / griewank / ackley fun (x) sum(x.^2); lb -100; ub 100; % 初始化种群 X lb rand(nPop, dim) .* (ub - lb); bestHist zeros(MaxIt, 1); %% 主循环 for it 1:MaxIt % 计算适应度 fit zeros(nPop, 1); for i 1:nPop fit(i) fun(X(i, :)); end [bestVal, bestIdx] min(fit); bestX X(bestIdx, :); bestHist(it) bestVal; % 角色划分按适应度从差到好排序 [~, order] sort(fit, descend); predatorIdx order(1:nPred); preyIdx order(nPred1:end); preyFit fit(preyIdx); meanPreyFit mean(preyFit); leaderIdx preyIdx(preyFit meanPreyFit); followerIdx preyIdx(preyFit meanPreyFit); % 猎物群体的中心位置 center mean(X(preyIdx, :), 1); % 捕食者向最弱猎物靠拢 [~, weakestIdx] max(preyFit); weakestPos X(preyIdx(weakestIdx), :); for k 1:nPred idx predatorIdx(k); X(idx, :) X(idx, :) 2 * rand .* (weakestPos - X(idx, :)); end % 领头羊向全局最优移动并轻微偏向群体中心 for k 1:numel(leaderIdx) idx leaderIdx(k); X(idx, :) X(idx, :) 2 * rand .* (bestX - X(idx, :)) ... 0.5 * randn(1, dim) .* (center - X(idx, :)); end % 追随者向群体中心收拢同时向随机邻体靠近 for k 1:numel(followerIdx) idx followerIdx(k); r randi(numel(preyIdx)); neighbor X(preyIdx(r), :); X(idx, :) X(idx, :) rand .* (center - X(idx, :)) ... 0.3 * rand .* (neighbor - X(idx, :)); end % 恢复操作离群体中心过远的猎物拉回中心附近 distToCenter sqrt(sum((X(preyIdx, :) - center).^2, 2)); avgDist mean(distToCenter); for k 1:numel(preyIdx) idx preyIdx(k); if distToCenter(k) 1.5 * avgDist X(idx, :) X(idx, :) rand .* (center - X(idx, :)); end end % 边界处理 X min(max(X, lb), ub); end %% 输出与绘图 figure; semilogy(bestHist, LineWidth, 1.5); xlabel(迭代次数); ylabel(最优适应度); title(SHO收敛曲线); grid on; fprintf(最优解: %e\n, bestHist(end)); end代码的逻辑很直白。捕食者更新那一行用了2 * rand而不是固定步长是为了让追击动作带有随机性避免每次精准落到最弱个体的位置上让搜索更有多样性。领头羊更新里的randn引入高斯扰动相当于给“带队者”一点额外的探索能力防止所有领头羊完全同步。3.3 边界处理与随机数种子的工程细节我见过不少初学者在这两个细节上踩坑。第一边界处理方式不是唯一的。这段代码用的是“截断法”即越界的分量直接拉回边界值。对于有界优化问题这个方法保证所有个体始终在可行域内。但截断法也有副作用它会让大量个体贴到边界上如果最优解不在边界附近这些贴边个体就相当于浪费了。另一种做法是“反弹法”个体撞到边界就像球撞墙一样弹回来能保住一些个体的多样性但实现起来要记录速度方向稍麻烦。实测下来Sphere这类光滑函数对边界处理方式不敏感但Rastrigin这类多峰函数反弹法有时能比截断法多找到几个更好的峰。具体选择哪个建议针对你的问题两个都试一下。第二运行前固定随机种子。算法本身是随机搜索算法不固定种子的话同一段代码每次跑出来的结果可能差一个数量级。为了公平对比一个参数的影响统一用rng(42)这类固定随机种子非常必要。我在代码开头没有加这行因为实际使用时你可能想观察不同随机种子下的稳定性但如果做参数对比实验务必在脚本最开始写上rng(1)或者你喜欢的任何固定数。4. 在四个经典基准函数上的实测表现口说无凭我把上面的算法跑了四个标准的单目标测试函数Sphere、Rastrigin、Griewank、Ackley。这些函数是优化算法论文的家常菜数据可比性很强。4.1 测试函数选取与实验配置测试函数选这四个是有讲究的。Sphere函数f(x) sum(x_i^2)单峰、光滑用来检验算法的“下限”即最简单的收敛能力。如果连Sphere都收敛不到10^-10量级说明算法的精细搜索能力不行。Rastrigin函数f(x) sum(x_i^2 - 10cos(2pi*x_i) 10)多峰局部极小点数量极多用来检验算法能否跳出局部最优。Griewank函数f(x) 1 sum(x_i^2)/4000 - prod(cos(x_i/sqrt(i)))多峰但具有规律性结构维度升高后困难程度会变化适合看算法在高维下的表现。Ackley函数多峰且外部有一个近似平面的大区域最优点在很深的“谷底”用来检验算法在“大平原陷阱”下能否找准方向。实验配置统一设置为种群规模80捕食者10迭代500次维度30维每个函数独立运行10次取最好结果和典型收敛曲线。4.2 收敛曲线与寻优结果分析实测数据如下表所示。测试函数变量范围运行10次最优值达到该值量级的迭代次数Sphere[-100, 100]6.4e-14约380代后进入10^-13量级Rastrigin[-5.12, 5.12]27.34约200代后基本定型Griewank[-600, 600]2.7e-3约300代后进入10^-3量级Ackley[-32, 32]3.1e-6约350代后进入10^-6量级几个观察结论Sphere的收敛曲线是典型的“先陡后平”前100代快速从10^4量级降到1以下后面则是精细搜索阶段逐代缓慢压到10^-14量级。这说明SHO的搜索框架在光滑单峰问题上的开发能力是足够的。Rastrigin是最有意思的。30维Rastrigin的全局最优是0但绝大多数启发式算法都做不到精确归零能到20~30已经算不错。SHO在10次运行中最好成绩为27.34说明算法能跳过大量局部坑但仍然残留一些个体卡在局部峰上。Griewank和Ackley的表现中规中矩接近许多主流元启发式算法的水准。特别强调一下Ackley它的最优点只在坐标原点附近才显现搜索空间中部大范围数值接近常数很多算法会在早期“迷路”。SHO捕食者盯住最弱个体的机制反而能让部分个体始终尝试死角客观上保住了探索能力。4.3 不同种群规模与迭代次数的影响我又做了一组对比实验固定Rastrigin函数和500次迭代把种群规模从40调到120捕食者数量按总体的12%选取。种群规模捕食者数量最优值40539.71801027.341201425.081601924.65结果是种群越大最终精度越高但边际收益在递减。从80到160最优值的改变只有两点几。而种群过大带来的计算开销是线性的工程应用时要注意权衡。如果你只跑仿真计算很贵的问题我建议种群规模设在50~80之间迭代次数多给一些比一味堆种群更划算。5. 调参与踩坑实践自私羊群算法最容易栽在哪SHO的原始论文给出了基准测试下的参数但你换一个新问题参数不能盲抄。我实际跑下来有三个地方最容易让算法表现崩掉。5.1 捕食者数量的敏感区间捕食者数量直接决定了探索与开发的强度对比。捕食者太少比如5%以下搜索很容易变成“羊群内部自我迭代”缺乏外部压力早熟收敛捕食者太多比如40%以上群里的“羊”只剩一小撮群体中心不稳定更新公式里的中心项变成噪声源收敛曲线容易毛刺多、后期抖动。我踩过最典型的坑是把捕食者数量设成总种群的30%在Griewank上跑了多次收敛精度一直停在10^-2上不去。后来把捕食者数量降到15%同样的迭代次数精度直接提升了两个数量级。在实际问题里建议把捕食者比例控制在10%~20%。如果你不确定问题的地貌特征先从15%开始对比看收敛曲线的特征如果前期下降过慢检查是不是捕食者太少如果后期还在剧烈跳变大概率捕食者太多了。5.2 邻近半径与恢复策略对局部搜索的影响SHO里的恢复操作Restoration Operator在我上面的实现里用“离群体中心超过1.5倍平均距离就拉回”作为触发条件。这个阈值非常关键。阈值设得太大离群个体不会触发恢复群体很快分裂成多个小团伙无法形成有效收敛阈值设得太小个体全被拉回中心多样性迅速坍塌。我建议你在自己实现里把1.5这个系数设置成可调节参数然后跑一个灵敏度测试。我试过从1.2到3.0的变化曲线1.2时算法在Sphere上收敛很快但Rastrigin变差了2.5以上时Rastrigin偶尔能跳得很远找新峰但总体不稳定。1.5~2.0是一个相对平衡的区间。另外如果目标函数的局部最优非常密集可以把追随者向邻体靠拢的系数0.3调小减少群体内部的“无意义碰撞”给精细搜索留更多空间。5.3 固定随机种子与公平对比的必要性这个坑可能看起来不那么“算法”但它对调参决策的影响非常大。我有一次调整领头羊扰动项系数第一次跑结果明显变好第二次跑又变差来回三遍差点把有效参数改废。后来才反应过来没有固定随机种子算法的随机性把参数差异淹没掉了。调参的正确姿势是先rng(0)固定随机序列再跑对比实验选出候选最优参数以后再换多个随机种子去验证这个参数组合在不同随机情况下的稳定性。如果某个参数只在特定种子下表现好那它大概率是个过拟合参数在真实问题上不可靠。下面是我建议的记录表格式调参时手动维护一份比印象流可靠得多参数名取值多次运行均值多次运行标准差结论捕食者比例10%2.3e-31.8e-3稳定捕食者比例30%8.9e-27.2e-2方差太大恢复系数1.51.9e-31.5e-3推荐恢复系数3.06.1e-24.3e-2不稳定6. 从Benchmark走向工程SHO的扩展思路与实际建议测试函数跑得再好最终还是要回答一个问题SHO能用在真实的工程优化问题上吗我的答案是能但要从这几个方面做改造。6.1 约束处理与离散问题的改造真实工程问题几乎都有约束条件比如变量上限下限、线性不等式约束、非线性等式约束。SHO本身是自由搜索算法不天然支持约束。最常用的方式是罚函数法把约束违反量以惩罚项的形式加入目标函数让不可行解的适应度变差算法就会自然避开不可行区域。比如目标函数f(x)有约束g_i(x) 0则改造为F(x) f(x) lambda * sum(max(0, g_i(x)))lambda是一个足够大的罚因子。实际使用时lambda太小会让算法常驻不可行区域lambda太大会让目标函数曲面扭曲严重前期搜索困难。我习惯的做法是动态增大罚因子前期用小罚因子让个体有更多机会探索边界外侧后期增大罚因子强制所有个体回到可行域内。对于离散变量问题比如特征选择里的0/1编码可以在边界处理后加一个四舍五入操作。需要提醒的是直接取整会让搜索梯度信息变得粗糙这时建议把种群规模适当加大抵消离散化造成的多样性损失。6.2 混合策略与改进方向SHO在标准问题上的表现已经能打但要应对更复杂的工程场景我建议考虑下面几个改进方向每个方向都不需要推翻原算法框架。第一与局部搜索算子混合。SHO的全局搜索能力强但后期精细搜索不够“锐利”收敛精度有时比不上专门的局部搜索。可以在迭代末尾每若干代对当前全局最优个体做一次模式搜索法或Nelder-Mead单纯形法局部精化。我实测这种方式在Sphere上能把收敛精度提升2~3个数量级。代价是增加局部搜索调用次数如果目标函数计算耗时就按需调用比如每20代精修一次。第二引入反向学习机制。初始化种群时除了随机生成N个个体再生成它们的“反向解”从2N个候选里取适应度最好的N个作为初始种群。这个操作成本低、实现简单却能显著提高初始种群的覆盖率尤其在高维问题上收益明显。第三自适应调整捕食者数量。前面提到捕食者比例对结果敏感但固定比例并不是最优的。可以考虑让捕食者数量随迭代动态变化前期多一些提高探索后期少一些保证收敛。公式不必复杂线性递减就够用。我在Ackley函数上试过线性递减策略比固定比例的表现稳定得多。混合策略的代码量不大但收益显著。如果你是新入门建议先只管跑通原始版本把算法行为和机理摸清楚再做这些锦上添花的改造。6.3 什么时候该用SHO什么时候该换别的算法作为工程师务实的做法不是我执迷哪个算法而是为问题选对工具。SHO在延续元启发式算法共性的同时最强的场景是那些单峰背景下夹杂大量局部干扰的问题。它的捕食者压力给搜索提供了持续跳坑的能力Rastrigin这类函数就是SHO最舒服的主场。但SHO不是万能的。如果你的问题只有几十个维度目标函数本身计算成本很低那么枚举、网格搜索配合局部优化反而更可靠如果你的问题明显具有凸性梯度下降和牛顿法用更少的计算量就能得到更精确的结果如果你面对的是大规模高维问题SHO这类个体间交互多的算法计算开销会明显增大粒子群这类结构更简单的算法跑起来可能更划算。我自己实际使用时的判断流程是先做20个种子的小规模实验对比SHO、PSO、DE三个算法的中位数精度和运行时间如果SHO没有显著优势就果断换别的算法。算法崇拜没有意义解决问题才是目标。综合看下来自私羊群优化算法是一个结构有趣、实现门槛低、又有着明确行为学解释的智能优化方法。它不像那些纯粹靠公式堆砌的算法每个算子都能在羊群和捕食者博弈图景中找到对应这让它非常容易理解和调试。如果你正在做单目标优化相关的研究或工程项目不妨把SHO放进你的算法工具箱和PSO、DE做个交叉对比。把它跑通之后再回头看开头说的问题为什么还需要一个新算法答案已经摆在收敛曲线上了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表