自私羊群优化算法SHO:MATLAB实现与单目标优化实战
2026/9/8 13:10:28 网站建设 项目流程

做工程优化久了,你迟早会遇到一种尴尬:目标函数连解析式都不完整,要么是仿真返回一个数,要么函数曲面布满局部坑,梯度根本算不出来。这种问题交给梯度下降和牛顿法,基本等于让短跑运动员去游泳。于是智能优化算法就成了最稳的兜底方案。这篇文章我拿MATLAB实现的自私羊群优化算法(Selfish Herd Optimizer,SHO)做主线,完整拆一遍单目标优化问题的求解流程。从算法机制、数学原理、代码实现,到四个经典基准函数的实测收敛表现,再把我跑代码时踩过的调参坑一并交代清楚,代码可以直接抄。

先说清楚这套东西能干什么:输入一个目标函数,给定变量范围和维度,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 算法整体流程与数据结构

先明确整体迭代结构:

  1. 初始化种群:在搜索空间内随机生成nPop个个体的位置。
  2. 计算适应度:每个个体代入目标函数求值。
  3. 角色划分:排序后确定捕食者、领头羊、追随者。
  4. 分别更新三类角色的位置。
  5. 实施恢复操作:对离群体中心过远的猎物,拉回中心附近。
  6. 边界处理:把越界个体拉回可行域。
  7. 记录全局最优,进入下一代。

这里有一个细节:代码中每个个体用一行向量表示,维度就是决策变量个数。种群用一个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(nPred+1: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.71
801027.34
1201425.08
1601924.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做个交叉对比。把它跑通之后,再回头看开头说的问题:为什么还需要一个新算法?答案已经摆在收敛曲线上了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询