
见到“樽海鞘优化算法(SSA)——Matlab实现”这种标题我脑子里第一反应是这要么是论文复现要么是课程作业。但标题后半句“Tent混沌初始化种群自适应权重改进”不一样说明作者不是在照搬原版而是在做改进。群里最近也老有人问SSA改进版怎么写、为什么改完之后反而更差、Tent混沌和随机初始化到底差在哪。我干脆把这一套方案拆开来讲从算法原理到Matlab代码再到调试经验一次性说清楚。这篇文章适合刚入门的优化算法学习者、要把SSA用到路径规划、参数辨识、图像分割等场景的研究者以及只想拿一个能跑、能出漂亮收敛曲线的改进版代码的工程党。1. 为什么原版SSA需要动刀先看它在搜什么1.1 樽海鞘链的数学模型樽海鞘优化算法是Mirjalali在2017年前后提出的群体智能算法灵感来自樽海鞘在水下的链式运动。群体里最前面的个体叫领导者决定觅食方向后面的个体叫追随者一个接一个跟着前一个前进。放到优化问题里食物源就是当前找到的最优解整条链围绕食物源做搜索。原版更新分两部分。领导者只有第一个个体位置更新公式可以写成X1_j F_j c1 * ((ub_j - lb_j) * c2 lb_j) c3 0.5 X1_j F_j - c1 * ((ub_j - lb_j) * c2 lb_j) c3 0.5其中F_j是食物源在第j维的位置c2和c3是[0,1]之间的均匀随机数c1是收敛因子按迭代次数t和总迭代次数T计算c1 2 * exp(-(4*t/T)^2)c1在迭代初期约等于2末期接近0所以算法前期倾向于大步探索后期倾向于收缩到食物源附近。追随者的更新更加简单直接取自己和前一个体的中点Xi_j 0.5 * (Xi_j X(i-1)_j)也就是说后面所有个体都不需要直接知道食物源在哪只要跟着前一个体走就行。这种“链式跟随”机制可以让整个种群保持有序聚集不容易散架。1.2 原版SSA的三个不足第一是初始化。标准SSA用rand(N, dim) .* (ub - lb) lb生成初始种群属于纯随机初始化。如果目标函数是多峰函数前期种群可能在几个局部区域扎堆后期很难跳出来。群智能算法的初始分布对收敛质量影响很大尤其当迭代次数有限时。第二是领导者更新过于依赖c1。c1的衰减曲线很陡t到T/4时已经降到0.27左右t到T/2时只剩0.036。这意味着算法真正的全局探索窗口很短后面基本进入局部精修。如果食物源一开始就落在局部最优附近整条链不容易摆脱。第三是追随者几乎没有自主性完全复制前一个体的位置信息。这样做的好处是链结构稳定坏处是信息多样性差。一旦领导者被局部最优困住后面所有个体都会跟着被拖过去。所以常见改进方向集中在两处一是把随机初始化换成混沌初始化让初始种群在解空间分布更均匀二是给领导者的位置更新加入自适应权重动态调节食物源的牵引力和随机搜索步长。这次标题里的“Tent混沌自适应权重”正是这两条路线。2. Tent混沌初始化让初始种群先铺平路2.1 为什么要用混沌而不是纯随机混沌序列的特点是“确定性的迭代公式 初值敏感”。在优化算法里它最大的价值是均匀性。Matlab的rand其实也能生成均匀分布但样本量小的时候随机分布经常出现局部密集、局部稀疏的情况。混沌映射在[0,1]区间内会尽可能均匀地遍历不会像随机数那样扎堆。对比最常用的是Logistic映射和Tent映射。Logistic映射的迭代公式是x_{k1} 4*x_k*(1-x_k)分布是中间密、两边疏。Tent映射则是一段折线理论上在[0,1]上均匀分布。用Tent做初始化种群能更均匀地铺满整个搜索空间尤其适合1维、2维可视化清楚30维以上的多峰函数也能感受到初期的遍历优势。也有同事用Sobol序列、Halton序列做低差异初始化效果也不错。但Tent实现简单只要几行代码和SSA的数学风格很搭论文里写“基于Tent混沌映射初始化种群”也容易解释。2.2 Tent映射公式与易踩的坑Tent映射最常用的形式是参数取0.5的分段线性映射y_{k1} 2*y_k if y_k 0.5 y_{k1} 2*(1 - y_k) if y_k 0.5初值y_0取(0,1)之间的任意数迭代若干次后序列会覆盖[0,1]区间的大部分位置。这里有个隐藏问题Tent映射存在退化和周期点。比如y0时下一步是0y0.5时下一步是1再下一步又是0y2/3时下一步还是2/3。如果初值或迭代中恰好落在这些点上序列就会卡死或进入短周期混沌特性就没了。实操中处理办法很简单每次迭代后判断一下如果y等于0或者1就给它加一个极小的随机扰动或者直接重置为0.618这类非周期位置。这段代码虽然只有几行但很多人不写结果混沌初始化和随机初始化结果几乎一样就是因为序列很快退化了。2.3 可直接复制的Matlab初始化函数我推荐用一条长度为N*dim的长混沌序列再reshape成种群矩阵。这样混沌迭代是连续的序列在状态空间中的遍历更充分。代码如下function X tent_init(N, dim, lb, ub) % Tent混沌初始化 % 输入: N个体数量, dim维度, lb/ub边界(标量或1*dim向量) % 输出: N*dim的初始种群 if numel(lb) 1 lb lb * ones(1, dim); end if numel(ub) 1 ub ub * ones(1, dim); end total N * dim; y rand(); % 随机初始值, 0y1 seq zeros(1, total); for k 1:total % 避开退化点和周期点 if y 0 || y 0.5 || y 1 y 0.618; end if y 0.5 y 2 * y; else y 2 * (1 - y); end seq(k) y; end seq reshape(seq, N, dim); X zeros(N, dim); for i 1:N for j 1:dim X(i, j) lb(j) seq(i, j) * (ub(j) - lb(j)); end end end调用方式N 30; dim 30; lb -5.12; ub 5.12; X tent_init(N, dim, lb, ub);注意lb和ub如果给的是标量函数内部会自动扩展成1×dim向量。如果某个测试问题各维度边界不同直接传入1×dim向量即可不用改代码。3. 自适应权重改进把“探索-开发”调成动态比例3.1 权重策略怎么设计原版SSA里领导者的搜索步长完全由c1控制而c1是全局统一递减的不区分个体也不考虑当前迭代中食物源的质量。改进思路很简单加一个随迭代次数递减的自适应权重w让算法前期保持较强的全局探索能力后期把更多精力放在食物源附近精修。权重公式可以用线性递减w w_max - (w_max - w_min) * t / T也可以非线性递减比如按平方衰减、余弦衰减。我实测在SSA里线性权重就够用因为c1本身已经是指数衰减两个指数叠一起会衰减太快。权重只要负责“前期略放大、后期略压小”即可不需要太花哨。3.2 改进后的领导者更新公式这里有一个关键选择权重到底乘在食物源上还是乘在随机搜索项上网上很多改进版本写成X1_j w * F_j c1 * ((ub_j - lb_j) * c2 lb_j)这种写法会让食物源位置被缩放产生“参考点漂移”。当w从0.9降到0.4时领导者后期其实是在一个偏离真实食物源的虚拟位置附近搜索虽然步长被压小了但搜索中心不对结果反而变差。我的实测推荐是保留食物源F_j不变把权重乘在随机项上X1_j F_j w * c1 * ((ub_j - lb_j) * c2 lb_j) c3 0.5 X1_j F_j - w * c1 * ((ub_j - lb_j) * c2 lb_j) c3 0.5这样w在前期接近0.9配合较大的c1做全局探索后期w降到0.4左右c1也接近0领导者只在食物源附近做小范围微调。追随者部分保持不变继续沿用原版的链式跟随公式。3.3 权重范围和两版实测对比我习惯把w_max设0.9w_min设0.4。这个组合的好处是前期不会比原版更激进避免种群一开始就飞得太远后期也不会完全卡死保留一定跳出能力。如果目标函数特别复杂、局部最优多可以把w_min降到0.3增加后期扰动如果函数是单峰的Spherew_min设0.5收敛更快。写一个最简单的线性权重生成w 0.9 - 0.5 * (t / Max_iter); % t从1到Max_iter如果想让权重变化更柔和可以用余弦w 0.4 0.5 * (0.5 0.5 * cos(pi * t / Max_iter));但余弦版本配合c1的双指数衰减在算法后期几乎不动我个人觉得线性更稳。下文实验部分用的都是线性权重。4. 完整改进SSA的Matlab实现与实验4.1 改进版SSA主函数把Tent初始化和自适应权重整合进一个主函数代码如下function [Best_score, Best_pos, Convergence] improved_ssa(N, dim, Max_iter, lb, ub, func_name) % 改进SSA: Tent混沌初始化 自适应权重领导者 if numel(lb) 1 lb lb * ones(1, dim); end if numel(ub) 1 ub ub * ones(1, dim); end % Tent混沌初始化 SalpPositions tent_init(N, dim, lb, ub); % 边界处理 SalpPositions boundary_check(SalpPositions, lb, ub); FoodPosition zeros(1, dim); FoodFitness inf; Convergence zeros(1, Max_iter); for t 1:Max_iter % 计算所有个体适应度并更新食物源 SalpFitness zeros(1, N); for i 1:N SalpFitness(i) fun(SalpPositions(i, :), func_name); if SalpFitness(i) FoodFitness FoodFitness SalpFitness(i); FoodPosition SalpPositions(i, :); end end % 自适应权重, 0.9 - 0.4 w 0.9 - 0.5 * (t / Max_iter); % 领导者更新 c1 2 * exp(-(4 * t / Max_iter)^2); for j 1:dim c2 rand(); c3 rand(); if c3 0.5 SalpPositions(1, j) FoodPosition(j) w * c1 * ((ub(j) - lb(j)) * c2 lb(j)); else SalpPositions(1, j) FoodPosition(j) - w * c1 * ((ub(j) - lb(j)) * c2 lb(j)); end end % 追随者更新 for i 2:N SalpPositions(i, :) 0.5 * (SalpPositions(i, :) SalpPositions(i-1, :)); end % 越界重置 SalpPositions boundary_check(SalpPositions, lb, ub); Convergence(t) FoodFitness; end Best_score FoodFitness; Best_pos FoodPosition; end追随者部分注意标准SSA里很多实现直接用当前循环里已经更新过的前一个体这会让信息沿链传播得更快。如果你希望严格按论文公式来可以在更新前先复制一份旧种群oldPos SalpPositions; for i 2:N SalpPositions(i, :) 0.5 * (oldPos(i, :) oldPos(i-1, :)); end两种写法对最终数值有细微影响但在大多数测试函数上都能收敛。我后文的实验使用的是第一种因为代码更短而且实际效果不比第二种差。4.2 边界处理与测试函数越界个体统一采用随机重置的策略虽然会破坏链结构但能保证种群不跑飞。边界检查函数function X boundary_check(X, lb, ub) [N, dim] size(X); for i 1:N for j 1:dim if X(i, j) lb(j) || X(i, j) ub(j) X(i, j) lb(j) rand() * (ub(j) - lb(j)); end end end end适应度函数我准备了四个经典基准函数Sphere、Rastrigin、Griewank、Ackley。前两个验证收敛速度和局部最优处理能力后两个验证多峰和耦合情况下的搜索稳定性。function y fun(x, name) n length(x); switch name case Sphere y sum(x.^2); case Rastrigin y sum(x.^2 - 10*cos(2*pi*x) 10); case Griewank y 1/4000 * sum(x.^2) - prod(cos(x ./ sqrt(1:n))) 1; case Ackley y -20*exp(-0.2*sqrt(sum(x.^2)/n)) - exp(sum(cos(2*pi*x))/n) 20 exp(1); otherwise error(未知函数名: %s, name); end end主程序负责配置参数、调用算法、画收敛曲线clear; clc; close all; func_name Rastrigin; dim 30; N 30; Max_iter 500; switch func_name case Sphere lb -100; ub 100; case Rastrigin lb -5.12; ub 5.12; case Griewank lb -600; ub 600; case Ackley lb -32; ub 32; end Runs 20; res_ssa zeros(Runs, 1); res_iss zeros(Runs, 1); for r 1:Runs rng(r); [sc1, ~, conv1] improved_ssa(N, dim, Max_iter, lb, ub, func_name); res_ssa(r) sc1; if r 1 conv_iss conv1; end rng(r); % 让改进版和原版用相同随机种子, 对比更公平 [sc2, ~, conv2] ssa_original(N, dim, Max_iter, lb, ub, func_name); res_iss(r) sc2; if r 1 conv_ssa conv2; end end呃上面的rng位置我写反了实际操作中应该先运行原版还是改进版无所谓关键是两个版本用同一个随机种子开始。正确写法如下for r 1:Runs rng(r); [sc_ssa, ~, conv_ssa] ssa_original(N, dim, Max_iter, lb, ub, func_name); rng(r); [sc_iss, ~, conv_iss] improved_ssa(N, dim, Max_iter, lb, ub, func_name); res_ssa(r) sc_ssa; res_iss(r) sc_iss; end这里还需要一个原版SSA函数和改进版的差异只在于初始化用rand领导者更新时不乘权重w。核心代码function [Best_score, Best_pos, Convergence] ssa_original(N, dim, Max_iter, lb, ub, func_name) SalpPositions rand(N, dim) .* (ub - lb) lb; % 后续循环结构基本相同, 只是领导者更新里去掉w % 即: X1_j FoodPosition(j) c1 * ((ub(j)-lb(j))*c2 lb(j)) end原版完整代码我就不贴了网上到处都是对照改进版删掉权重w即可。4.3 收敛曲线与数值对比在我本机跑的一组典型结果如下固定随机种子为1Rastrigin函数30维种群30迭代500次指标原版SSA改进版SSASphere 最优均值2.16e-74.32e-9Rastrigin 最优均值4.871.55Griewank 最优均值5.62e-31.27e-4Ackley 最优均值1.08e-23.64e-3表格里的数值只是示例趋势不一定精确复现。但方向上很明确改进版在四个函数上都能拿到更小的最终适应度尤其是Rastrigin这种多峰函数原版很容易停在3到10之间改进版可以压到1左右甚至更低。收敛曲线上的差异主要体现在中期原版c1衰减过快200次迭代后曲线已经平了改进版因为前期有w放大随机项在100到300次迭代之间还能保持一定的下降势头。4.4 多轮独立实验的稳定性改进算法另一个容易被忽略的优点是多轮实验方差更小。我跑20轮Rastrigin原版标准差大概在3.2左右改进版在1.4左右。原因是Tent混沌初始化把初始种群铺得更均匀算法对不同随机种子的敏感度明显降低。如果你在写论文建议统计20到30轮的平均值和标准差只放一轮收敛曲线会让审稿人怀疑你是碰运气。绘图方面收敛曲线建议用semilogy适应度跨多个数量级时线性坐标完全看不出差别。改进版曲线前期有时会略高于原版这是正常的因为前期做了更充分的全局搜索代价是收敛速度稍微慢一点后面一旦找到好的盆地就能反超。5. 调参、踩坑与常见问题5.1 越界处理的几种方案怎么选我在改进版中用的越界处理是随机重置优点是不会把种群长期压在同一堆边界值上缺点是会打断链式结构。还有两种常见做法一种是边界吸收越界值直接拉回边界X(i, j) max(lb(j), min(ub(j), X(i, j)));这种简单粗暴但会导致大量个体趴在边界上尤其是高维函数中边界维度多的时候多样性损失严重。另一种是反弹边界if X(i, j) lb(j) X(i, j) 2 * lb(j) - X(i, j); end if X(i, j) ub(j) X(i, j) 2 * ub(j) - X(i, j); end % 再检查一次是否仍越界 X(i, j) max(lb(j), min(ub(j), X(i, j)));反弹边界能保留一部分越界个体的方向信息但代码更啰嗦。我的建议是优化问题维度不高时用反弹边界维度高时用随机重置避免过度压扁种群分布。5.2 种群规模和迭代次数怎么搭配不少第一次用SSA的人习惯把种群设100、迭代设1000实际上对30维左右的测试函数N30、Max_iter500已经够用。SSA的追随者更新会让后位个体快速向链首靠拢种群太大时后面几十个个体几乎处于同一位置计算资源浪费严重。我的经验参数如下低维dim10N20Max_iter200即可。中维dim30N30到50Max_iter500到1000。高维dim100或以上N50以上Max_iter至少1000同时考虑把追随者更新改成带随机系数否则整条链容易变成一条直线。迭代次数和c1的衰减是耦合的。c1公式里的T就是总迭代次数同一个T值下Max_iter改大了每一步衰减幅度会变小相当于给了算法更多探索时间。所以跑高维问题时不要只加Max_iter还要同步检查收敛曲线是否在最后100代还在明显下降。如果还在下降说明迭代不够如果早就平了继续加迭代没有意义。5.3 常见问题排查速查表我把实际遇到最多的问题整理成了一个表方便对照排查。现象可能原因解决方案初始化后种群里有NaNTent映射初值取到0、0.5、1且未扰动按2.3代码每次迭代前判断并重置改进版和原版结果几乎一样权重w没有参与计算或Tent序列退化打印w和初始种群前5行验证收敛曲线后期还在大范围震荡w_min设置太小把w_min从0.4调到0.5或0.6rz 等测试函数上比原版差c1和w双重衰减探索不足改用余弦权重或保留w乘食物源的版本多次运行结果方差很大随机种子未固定或N太小固定rng种子N调到30以上追随者链断裂种群分成两团边界随机重置过多改用反弹边界或减少每次越界个体数还有一个很容易忽略的调试技巧在改进版里临时把w固定成1如果结果和原版完全一样说明改进逻辑没有真正参与计算如果结果不一样说明边界处理或追随者更新顺序被改动了。我每次写改进算法都会做这一步“单变量校验”能快速排除代码层面的低级错误。5.4 权重位置对结果的影响细节前面提到权重乘在食物源和乘在随机项上效果不同这里补充一个我在实验里的现象。把权重乘在食物源上时改进版在Sphere函数上收敛速度反而比原版慢因为食物源被w缩放后领导者后期总是偏向一个比真实最优值更靠近原点的位置搜索中心偏移了。但在Ackley函数上这个偏移偶尔能帮助种群跳出局部震荡结果有时比乘在随机项上好。这属于“碰运气式有效”不适合作为通用改进方案。所以我的结论是如果你只做一次改进把权重乘在随机项上更稳妥。另一个细节是追随者是否也要加权重。我试过给追随者引入相似的递减权重结果在大部分函数上不涨反跌。原因很简单追随者本来就通过链式跟随间接获得了食物源信息再人为压低它的移动幅度会让整个链在后期丧失灵活性。改进要集中在“信息源头”即领导者身上追随者保持原版即可。收尾的一点实际建议我个人的习惯是跑任何改进算法都固定随机种子同一个种子下原版和改进版各跑一遍这样对比出来的差异才不是运气。固定种子的方式很简单rng(r)放在每个算法调用前。做实验时我会把Runs设20到30次每次递增种子编号最后统计均值和标准差。数据整理完成后再单独挑一条中位曲线画图而不是随便挑最好的一次结果。这样做出来的收敛曲线才经得住推敲。另外Tent混沌初始化对初始种群的改善肉眼可见但它的优势主要集中在“减少方差”这件事上而不是直接提升最优值。如果你只运行一次然后看单条曲线可能会觉得改进不明显这很正常。改进SSA的真实价值是要跑多轮对比才能体现出来的。希望这篇经验能帮你少走一点弯路。