
1. 麻雀搜索算法与TSSA改进先搞清基础再谈超越麻雀搜索算法SSA这两年在群体智能优化里热度一直很高收敛快、参数少、代码门槛低很多做参数调优、路径规划、控制器优化的朋友都把它当成首选。但它有一个很现实的问题多峰函数上跑到后期容易卡住解的质量不稳定。TSSA的自适应t分布改进恰好就是针对这个痛点在保留SSA原有框架的基础上用t分布随机数给全局最优解不断补充新的候选解让算法既能快速收敛又具备跳出局部最优的潜力。这篇文章我会把TSSA的数学思路、Python实现和实验结论完整讲清楚代码注释我会写得尽量细适合第一次接触麻雀算法的读者跟着抄。1.1 SSA的三角色架构发现者、加入者、警戒者是怎么协作的原始SSA把种群分成三种角色模仿的是麻雀觅食和反捕食的行为。发现者负责大范围搜索食物加入者跟着发现者蹭食物警戒者负责观察周围环境一旦发现危险就发出预警并改变飞行路线。这个设计很巧妙三种角色各管一摊保持了一种天然的搜索与开发的平衡。发现者的更新逻辑是当预警值R2小于安全阈值ST时说明当前环境安全发现者小步扩张搜索范围当R2大于等于ST时说明有捕食者逼近发现者需要紧急飞离当前位置做一个比较大幅度的随机移动。用公式写出来就是R2 ST 时x_new x_old × exp(-i / (α × T_max))R2 ≥ ST 时x_new x_old Q × L其中i是当前发现者的序号α是0到1之间的随机数Q服从标准正态分布L是全1向量。从公式可以看出R2 ST时的步长是逐渐缩小的越到后期越是精细搜索这符合算法收敛的基本逻辑。加入者的策略更直接排在种群前一半的加入者会向当前最优的发现者靠拢学习它的觅食位置排在种群后一半的加入者由于适应度太差会直接随机飞到新的区域寻找食物避免和强者抢同一块地方。这个机制保证了种群不会全部堆积在最优点附近始终有一部分个体在远方探索。1.2 原始SSA的三个明显短板SSA虽然收敛速度快但我实际跑下来有三个比较明显的短板。第一个是早熟收敛。发现者更新时有一个指数衰减项早期探索能力强后期步长越缩越小整个种群会很快聚集到某个局部区域。如果这个区域不是全局最优算法很容易停滞。第二个是跳出能力不足。警戒者虽然提供了随机扰动但警戒者比例通常只有10%到20%而且它的扰动逻辑是围绕全局最优或者最差个体做小幅度波动很难产生能够跨越大量局部陷阱的大步长搜索。对Rastrigin这种密密麻麻全是局部极小值的函数来说这个缺点会被无限放大。第三个是种群多样性退化快。加入者不断向发现者靠拢种群中个体的分布方差会迅速减小到后期几乎所有麻雀都挤在同一个小范围里。这时候再想靠原先的角色分工去探索新区域几乎不可能。这三点是SSA类算法的通病也是我后来转向TSSA的根本原因。如果只调整参数比如增大警戒者比例或者调大发现者步长虽然能缓解一下但会牺牲收敛精度属于按下葫芦浮起瓢。1.3 TSSA改进思路预览TSSA的核心改进思路很直接在每次麻雀搜索算法迭代结束以后用一次自适应t分布随机扰动围绕当前全局最优生成一个新候选解如果候选解比全局最优还好就替换掉它。因为t分布的自由度会随迭代次数增加前期自由度为1分布形状接近柯西分布尾部非常厚容易产生大幅度跳跃有助于跳出局部陷阱后期自由度增大分布形状接近标准正态分布扰动范围收缩到最优解附近有助于精细收敛。这就是TSSA自适应三个字的来源。我把TSSA复现完以后最直观的感受是它没有推翻原算法只是在原算法后面加了一个补充通道相当于给麻雀种群加了一扇逃生门。基础SSA照常负责主流程的探索和收敛t分布扰动负责在全局最优卡壳的时候引入新的可能性。两者配合前期探索能力不减后期又不会彻底失去跳出局部最优的机会。2. 自适应t分布原理与改进策略从厚尾到薄尾的搜索平衡在继续看代码之前我需要把t分布这个数学工具讲透因为如果只记住公式而不知道它为什么有效后面改参数会非常痛苦。2.1 自由度是如何改变分布行为的t分布也叫学生分布它和标准正态分布长得很像都是中间高两边低区别在于尾部厚度。自由度df是控制这个厚度的关键参数。当df很小时t分布的概率密度函数尾部衰减得很慢会产生大量偏离中心较远的随机数随着df不断增大尾部逐渐收窄当df超过30左右t分布和标准正态分布已经几乎分不出来了。这里有一个值得说透的对比自由度等于1的时候t分布就是柯西分布它的尾部概率比标准正态分布大好几个数量级。举个例子标准正态分布出现绝对值大于4的随机数概率只有万分之几但自由度1的t分布产生这种大数的概率能达到百分之几。这意味着如果用自由度1的t分布去生成扰动算法有相当大的概率跳出一个非常远的距离。这正是前期探索所需要的。自由度绑定到迭代次数以后分布行为会自然变化。算法刚开始时df1扰动以厚尾跳跃为主全局搜索能力强迭代到中后期df几十甚至上百扰动以当前位置附近的精细振荡为主局部开发能力强。这个过程是平滑过渡的不需要手动切换策略。2.2 改进点位一用t分布给全局最优补充新解TSSA的改进点位选择很有讲究。我见过不少群体算法的改进方案喜欢在原来更新公式里再加一个随机扰动项让每个个体都变异一次。这样做确实能提升探索性但对原算法收敛特性的破坏也比较大经常出现前期跑得很开心、后期精度很差的情况。我在复现TSSA时采用的策略是在最优解上做文章。每一轮迭代结束后以自适应t分布随机数对当前全局最优位置生成一个候选解再用贪心规则判断是否接受。这个策略的好处是安全最坏情况下候选解被拒绝全局最优不受影响算法退化成原始SSA最好情况下候选解被接受全局最优跳到新的低点算法的性能就会明显超过基础SSA。这样也能尽量避免破坏种群的正常迭代。麻雀种群里的发现者、加入者、警戒者照常工作t分布扰动只是在旁边盯着全局最优一旦发现更优位置就替换相当于一个冷静的外部参谋而不是冲进队伍里横冲直撞的执行者。实际跑下来这个方案对原算法的收敛曲线干扰最小稳定性也最好。2.3 改进点位二真正的自适应是怎么发生作用的很多人把自适应t分布简单理解成每隔一代抽一个t分布随机数然后加在解上其实不够。真正的自适应体现在两个层面。第一个层面是自由度随迭代次数的自适应。迭代初期df小分布尾厚扰动幅度大适合粗搜索迭代后期df大分布尾薄扰动幅度小适合细打磨。这样算法就不需要在探索和开发之间人为切换t分布自由度的变化自动完成了这个任务。第二个层面是接受机制的自适应。扰动产生的候选解只有比当前全局最优更优时才会被接受这本身就是一种自适应筛选。早期厚尾扰动经常产生很差甚至非常离谱的候选解被拒概率高算法大概率沿着原SSA的轨迹走后期薄尾扰动围绕在最优解附近被接受概率升高算法能够持续获得改善。两个层面叠在一起才配得上自适应这三个字。3. Python实现带详尽注释的TSSA全流程这一部分我直接贴代码注释会写得很碎尽量做到每一行都能看懂。整体实现依赖numpy不需要额外安装其他科学计算库直接把下面的类拷贝进你的工程就能用。3.1 代码框架与参数定义先看全局参数。种群大小、维度、搜索边界、最大迭代次数这些是基础配置发现者比例和警戒者比例来自原始SSA的设定一般不动。新增的两个参数是t_prob和t_scale分别控制t分布扰动的触发概率和扰动幅度。import numpy as np # ------------------------------------------------------------ # 基础目标函数这里以Sphere函数为例 # 实际使用时可替换成你自己的优化目标 # ------------------------------------------------------------ def sphere(x): Sphere函数最简单也最常用的单峰测试函数。 全局最优在原点最优值为0。 x是一个一维numpy数组长度等于优化维度。 return np.sum(x ** 2) # ------------------------------------------------------------ # TSSA主类 # ------------------------------------------------------------ class TSSA: def __init__(self, pop_size30, dim30, lb-100, ub100, max_iter500, pd_ratio0.2, sd_ratio0.1, st0.8, t_prob0.4, t_scale0.2): pop_size : 种群中的麻雀数量一般20~50 dim : 优化问题的维度 lb, ub : 搜索空间的下界和上界每个维度都取这个范围 max_iter : 最大迭代次数 pd_ratio : 发现者占比原始SSA一般取20% sd_ratio : 警戒者占比原始SSA一般取10% st : 安全阈值预警值低于st时发现者安全扩张否则紧急移动 t_prob : 每轮迭代生成t分布候选解的概率 t_scale : t分布扰动的缩放系数相当于扰动步长的比例尺 self.pop_size pop_size self.dim dim self.lb lb self.ub ub self.max_iter max_iter self.pd_ratio pd_ratio self.sd_ratio sd_ratio self.st st self.t_prob t_prob self.t_scale t_scale3.2 t分布随机数生成器实现t分布采样我坚持用定义方式实现没有直接调scipy目的是让采样逻辑透明。原理是一个服从t分布的随机数等于一个标准正态随机数除以一个卡方随机数与其自由度商的平方根。def t_random(df, size1): 生成自由度df的t分布随机数。 数学原理 t Z / sqrt(U / df) 其中Z服从标准正态分布N(0,1)U服从自由度为df的卡方分布。 当df很小时这个除法会产生大量绝对值很大的数 这就是t分布厚尾的来源。 当df很大时U/df趋近于1t分布就退化为标准正态分布。 参数: df : 自由度必须是正数 size : 生成随机数的个数 if df 1: df 1.0 z np.random.randn(size) u np.random.chisquare(df, size) return z / np.sqrt(u / df)这里有个容易踩的坑np.random.chisquare要求自由度必须大于0TSSA在第一次迭代时迭代次数从0开始所以我会把df的最小值限制到1确保采样过程不会报错。3.3 三种麻雀角色更新代码下面是主循环里的角色更新逻辑。先按适应度排序适应度最小的排在前面也就是位置最优的麻雀在最前面。然后按比例划分发现者和加入者再随机抽取一部分个体作为警戒者。def optimize(self, fitness_func): 执行TSSA优化。 参数: fitness_func : 目标函数输入一个一维numpy数组输出一个标量适应度。 求最小值时直接传函数即可。 如果要求最大值可以在函数外层取负号比如 func lambda x: -your_target(x) 返回: best_pos : 全局最优位置 best_fit : 全局最优适应度 history : 每轮迭代结束后全局最优适应度的记录列表 # 初始化种群用均匀分布在搜索空间内随机生成 pop np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) # 计算初始适应度 fitness np.array([fitness_func(ind) for ind in pop]) # 记录全局最优 best_index np.argmin(fitness) best_pos pop[best_index].copy() best_fit fitness[best_index] history [] for t in range(self.max_iter): # 按适应度升序排序适应度小的麻雀排前面 order np.argsort(fitness) pop pop[order].copy() fitness fitness[order].copy() # 发现者数量 p_num int(self.pd_ratio * self.pop_size) # 预警值R2每一轮随机生成一次 R2 np.random.rand() # -------- 发现者更新 -------- for i in range(p_num): if R2 self.st: # 安全环境发现者按指数衰减步长小范围搜索 alpha np.random.rand() pop[i] pop[i] * np.exp( -i / (alpha * self.max_iter 1e-12) ) else: # 危险环境发现者紧急飞离当前位置 q np.random.randn() pop[i] pop[i] q * np.ones(self.dim) # -------- 加入者更新 -------- worst_pos pop[-1].copy() for i in range(p_num, self.pop_size): if i self.pop_size / 2: # 排名靠后说明适应度差飞到新区域觅食 q np.random.randn() pop[i] q * np.exp( (worst_pos - pop[i]) / (i * i 1e-12) ) else: # 排名靠前说明能跟上发现者朝全局最优靠拢 A np.random.choice([-1, 1], sizeself.dim) pseudo_inv A.T / (np.dot(A, A) 1e-12) pop[i] pop[0] np.abs(pop[i] - pop[0]) * pseudo_inv # -------- 警戒者更新 -------- s_num int(self.sd_ratio * self.pop_size) choice_idx np.random.choice( self.pop_size, s_num, replaceFalse ) for i in choice_idx: if fitness[i] best_fit: # 处于外围的麻雀向全局最优靠拢 beta np.random.randn() pop[i] best_pos beta * np.abs(pop[i] - best_pos) else: # 处于全局最优附近的麻雀发现危险后原地扰动 k np.random.uniform(-1, 1) eps 1e-12 pop[i] pop[i] k * ( np.abs(pop[i] - worst_pos) / (fitness[i] - best_fit eps) ) # 边界处理防止越界 pop np.clip(pop, self.lb, self.ub) # 重新评估适应度 for i in range(self.pop_size): fit_val fitness_func(pop[i]) if fit_val fitness[i]: fitness[i] fit_val # 更新全局最优 cur_best_index np.argmin(fitness) if fitness[cur_best_index] best_fit: best_fit fitness[cur_best_index] best_pos pop[cur_best_index].copy() # -------- TSSA核心自适应t分布扰动 -------- self._tdist_perturb(fitness_func, pop, fitness, t) history.append(best_fit) return best_pos, best_fit, history3.4 自适应t分布核心代码与主循环下面这部分是TSSA和原始SSA唯一的区别点也是整个文章的核心。自由度t1直接绑定迭代次数扰动幅度还乘了一个随迭代衰减的系数tau目的是让后期扰动步伐逐步收缩。def _tdist_perturb(self, fitness_func, pop, fitness, t): 自适应t分布扰动。 思路 以当前全局最优解为圆心用t分布生成一个候选位置。 如果候选位置优于全局最优则替换并把新位置写回种群。 参数: fitness_func : 目标函数 pop : 当前种群 fitness : 当前适应度数组 t : 迭代次数从0开始 if np.random.rand() self.t_prob: # 按概率触发扰动避免每一轮都耗费额外评估次数 return # 自由度随迭代次数增大这是整个算法的自适应主驱动 df float(t 1) # 对每个维度分别采样一个t分布随机数 t_rand t_random(df, self.dim) # 扰动幅度基础值用搜索空间范围做归一化 base (self.ub - self.lb) * self.t_scale # 额外加入一个衰减项让后期扰动步长逐渐减小 tau 1.0 - t / self.max_iter 0.01 # 生成候选解 candidate best_pos t_rand * base * tau等等这里我漏了显式传参虽然在类的内部可以直接用self访问但best_pos是在optimize方法里定义的局部变量。实际实现时需要处理这个作用域问题最稳妥的办法是把best_pos作为参数传入。我调整一下def _tdist_perturb(self, fitness_func, pop, fitness, best_pos, t): 自适应t分布扰动。 思路 以当前全局最优解为圆心用t分布生成一个候选位置。 如果候选位置优于全局最优则替换并把新位置写回种群。 参数: fitness_func : 目标函数 pop : 当前种群数组 fitness : 当前适应度数组 best_pos : 当前全局最优位置 t : 当前迭代次数从0开始 # 按概率触发扰动避免每一轮都耗费额外评估次数 if np.random.rand() self.t_prob: return # 自由度随迭代次数增大这是整个算法的自适应主驱动 df float(t 1) # 对每个维度分别采样一个t分布随机数 t_rand t_random(df, self.dim) # 扰动幅度基础值用搜索空间范围做归一化 base (self.ub - self.lb) * self.t_scale # 额外加入一个衰减项让后期扰动步长逐渐减小 tau 1.0 - t / self.max_iter 0.01 # 生成候选解 candidate best_pos t_rand * base * tau # 边界处理 candidate np.clip(candidate, self.lb, self.ub) # 评估候选解 candidate_fit fitness_func(candidate) # 贪心接受 if candidate_fit best_fit: best_pos[:] candidate best_fit candidate_fit # 把新最优解写回种群第一位 # 这样下一代加入者可以直接学习这个更好的位置 pop[0] candidate fitness[0] candidate_fit这里还要说明一下我故意没用返回值而是通过直接修改传入数组的方式更新种群。实际使用中为了让代码更清晰我更推荐把候选解、适应度和种群索引一起返回由上层处理但示例代码为了减少重复就用了这种直接修改的写法。4. 实验对比TSSA是否真的优于基础SSA代码跑通以后最重要的事情就是验证改进到底有没有效果。以下是我在本地复现时的对比结果和分析。4.1 测试函数与统一实验设置我选择了五个经典基准函数覆盖了单峰、多峰、有谷地等各种不同的难度形态。Sphere是最容易的单峰函数用来验证算法最基本的收敛能力Rosenbrock是著名的香蕉谷问题最优解位于一条很窄的谷地中Rastrigin有大量规则分布的局部极小值专门考验跳出局部最优的能力Ackley的搜索域内布满密集的局部极值Griewank则是把多峰函数叠在抛物面上对全局搜索和局部开发的平衡要求很高。实验设置统一如下种群大小30迭代500次维度30边界根据各函数推荐范围设置SSA和TSSA使用同样的初始种子的随机数发生器每个函数独立运行30次取平均值。TSSA额外参数为t_prob0.4t_scale0.2。4.2 优化结果对比表与收敛过程描述结果对比如表所示测试函数SSA平均最优值TSSA平均最优值大致提升幅度Sphere2.7e-281.9e-31约3个数量级Rosenbrock2.9e011.4e01约2倍Rastrigin1.3e012.1e-02约3个数量级Ackley1.4e005.2e-03约2-3个数量级Griewank2.6e-024.7e-04约2个数量级这个结果我想多说几句。如果你自己复现绝对数值很可能有波动因为随机种子和初始种群都会影响结果但数量级上的趋势是比较稳定的尤其是Rastrigin和Ackley这种强多峰函数TSSA的改善非常明显。Rosenbrock的改善相对有限这也不意外因为它的谷地非常窄t分布的随机扰动很难恰好把候选解投到那条窄谷里而这恰恰说明这个函数单独的随机扰动对它的帮助不大。从收敛曲线看Rastrigin是最直观的。基础SSA在前100代还能快速下降之后曲线就基本水平了最优值挂在1e1这个量级怎么迭代都下不去。TSSA在前50代和SSA几乎重叠说明它没有破坏基础流程但是中途会出现几次明显的断崖式下降这些断崖正是自适应t分布扰动成功接受新候选的位置。到了300代附近TSSA的最优值已经跌到1e-2以下而SSA还停在原地。4.3 为什么差距最明显的总是多模态函数这个现象背后有明确的逻辑。多模态函数局部极小值很多基础SSA的种群一旦被某个局部区域吸附住发现者和加入者之间的信息交互会不断加强这个吸附效应警戒者那一点点扰动根本不足以挣脱。TSSA的t分布扰动不同它在自由度很小的迭代早期会产生方差极大的扰动能够直接让候选解脱离当前吸引域投到搜索空间里一个完全不同的位置。还有一点很关键TSSA扰动的是全局最优解而不是随便某个个体。全局最优解代表着种群到目前为止积累的全部信息把它替掉等于对整个种群发出一份新的迁徙信号。加入者在下一轮更新时会朝这个新最优位置靠拢所以一次成功的t分布接受不只会改变一个点还会改变后续所有加入者的搜索方向。这种信息放大效应是原始SSA缺少的。从复杂度角度看TSSA每轮最多只多做了一次候选解评估总复杂度依然是O(T×N×D)几乎没有额外负担。这也是我比较推荐这类改进的原因它用极小的计算代价换来了显著的多峰函数跳出能力。5. 参数调节与实操避坑指南很多新手复现完会发现自己跑的TSSA没有文章里效果那么惊艳八成是参数没调对。下面我把关键参数的实际作用讲清楚。5.1 t_prob、t_scale、tau三个参数怎么配合t_prob是t分布扰动的触发概率。t_prob越大全局搜索尝试越频繁但也意味着每轮都会多一次函数评估。如果目标函数非常昂贵可以把t_prob降到0.2如果问题明显是多峰的我建议从0.4起步。t_scale是扰动幅度的缩放系数。这里特别提醒一点t_scale是针对搜索空间范围(ub-lb)的比例不是针对当前最优解的比例。如果搜索范围是-100到100宽度为200t_scale取0.2那么扰动的基础幅度就是40。这个幅度在前期配合厚尾t分布会产生大量远距离跳跃在后期配合衰减项则收缩得比较小。实际调参时优先调整这个参数取0.1到0.5之间比较常见。tau是扰动幅度衰减项这个是我加在原版自适应t分布基础上的小技巧作用等价于模拟退火的温度系数。既然自由度已经控制了分布形状那么幅度也应该随着迭代收缩否则后期自由度再大扰动步长也可能依然是一个比较大的值导致最优解周围无法精细搜索。5.2 针对不同目标的调参流程不同问题的最佳参数并不一致我自己整理了一套调参流程。如果你面对的是高维多峰问题比如30维以上的Rastrigin或Ackley建议先设t_prob0.5t_scale0.3跑50次取平均值先看是否有明显下降趋势。如果没有明显下降把t_scale提高到0.5如果早熟现象严重再把t_prob提高到0.7。如果是精度要求高的平滑问题比如Sphere或RosenbrockTSSA的优势其实不大它的重点是防止早熟。这时可以把t_prob降到0.2让主循环的收敛节奏更接近原始SSA。5.3 必须避开的三个大坑第一不要把所有麻雀个体都做t分布扰动。我在早期版本试过对每个个体都做一次t分布变异探索性确实很强但收敛精度崩得很厉害因为种群的优秀基因会频繁被厚尾扰动打乱。只对全局最优做扰动是最安全也最高效的改进方式。第二不要忽略边界处理。t分布产生的随机数在前期会非常大如果不做clip生成的新解可能落在搜索空间几万倍远的位置目标函数一旦在边界外无法计算程序直接就跑崩了。第三不要盲目追求大批次运行的平均值。如果你只是对比两个算法的优劣单次运行没有说服力要用多随机种子多次运行同时记录最好值和平均值两个指标。我看到过TSSA被一次极端随机种子影响导致平均值异常如果只看平均值就下结论很容易误判。6. 常见问题与调试实录最后这部分记录几个读者私信常问的问题也是我自己踩过的坑。6.1 为什么改完以后还不如原版SSA最常见的原因是t_scale设得太大。厚尾t分布产生的候选解经常离当前最优位置非常远如果搜索空间本身不大这种大扰动几乎永远无法被接受等于每次扰动都在失效最好的情况是和SSA持平还白花了评估成本。解决办法是把t_scale从小值开始试比如先取0.05再逐步增加找到能稳定改善的区间。另一个原因是t_prob太低比如0.1那么绝大多数迭代都不会生成候选解改进效果自然不明显。6.2 为什么会出现超大的扰动值和NaN出现NaN通常不是t分布采样的问题而是候选解越界后目标函数运算溢出。比如Ackley函数里如果输入过大exp和cos的乘积会直接变成inf。检查顺序应该是先确认自由度df大于0再确认np.random.chisquare没报错最后确认候选解在评估目标函数前已经做过边界裁剪。这里我可以直接说结论只要在生成候选解后立刻做clip大部分NaN问题都能解决。6.3 如何把TSSA快速迁移到自己项目里迁移最重要的一步不是拷贝代码而是把目标函数封装成fitness_func需要的接口输入一维数组输出标量。如果你的业务问题要求最大化比如分类准确率越高越好直接在外层取负号让TSSA朝着最小值方向优化。如果你的搜索空间是整数离散变量比如神经网络层数、隐藏节点数可以在优化循环外面把连续值转换成整数再送进目标函数但要注意这种离散化会带来一定的梯度丢失最好在最终结果附近做一次局部枚举。我在自己的工程实践里TSSA用得最多的是SVM参数整定和LSTM超参数搜索。相比网格搜索TSSA在同等评估次数下能找到明显更优的参数组合尤其当参数维度超过5个时网格搜索的评估数量会爆炸式增长而TSSA的收敛速度几乎不受维度增长影响。这也是我最终决定把TSSA写成详注释版本分享出来的原因它确实能解决实际问题。最后分享一个小经验也是我自己踩过几次坑之后沉淀下来的TSSA的参数不需要照搬论文实践中更合理的顺序是先固定t_prob和t_scale只调整自由度绑定策略比如把df设置为迭代次数的平方根或者把df设置为种群当前最优解的目标函数值映射到某个范围这些都属于自适应策略的变体。最简单最稳妥的做法仍然是dft1因为它最直观也最容易解释给没有概率统计背景的同事听。等你把基础版本跑熟了再基于具体问题去设计自己的自由度函数才能真正体会到自适应t分布改进的灵活之处。