1. 从“周期波动”到“全局寻优”正余弦算法的直觉理解最近在复现一些经典的元启发式优化算法时我又把正余弦优化算法Sine Cosine Algorithm, SCA拿出来跑了一遍。这个算法在2016年由Seyedali Mirjalili提出结构异常简洁但效果却常常出人意料地好尤其是在处理那些维度不高、但存在大量局部最优点的“多峰”函数时。很多人第一次看到SCA的公式会觉得它像是一个在正弦和余弦函数之间随机切换的“无头苍蝇”但如果你把它想象成一个在解空间里进行“探索”与“开发”的智能体其背后的逻辑就清晰多了。简单来说SCA模拟的是数学上正弦和余弦函数的周期性波动行为。在优化过程中每个候选解可以理解为搜索代理会根据当前最优解的位置结合正弦或余弦函数的振荡来更新自己的位置。这种振荡机制本质上是在平衡两种核心的搜索策略大幅度的正弦/余弦摆动对应“探索”Exploration和小幅度的、围绕最优解的精细调整对应“开发”Exploitation。算法通过一个自适应的参数巧妙地控制着从全局广域搜索到局部精细挖掘的过渡。对于多元函数寻优问题比如我们要找一个10维Rastrigin函数的最小值SCA这种机制能有效避免过早陷入某个局部最优的“坑”里增加找到全局最优解的概率。我之所以重新关注它是因为在处理一些工程优化问题时比如神经网络超参数调优、天线阵列设计发现像粒子群PSO、遗传算法GA这类更流行的算法有时会因为参数设置不当而收敛过快或陷入停滞。SCA的代码量极少核心更新公式就几行但调整其关键参数带来的性能变化非常直观是一个理解“探索-开发”权衡Exploration-Exploitation Trade-off的绝佳教学模型同时也具备解决实际中低维复杂优化问题的潜力。接下来我将拆解SCA的核心原理、手把手实现它并分享在多元函数测试集上调试参数、避免常见陷阱的实战经验。2. SCA的核心机制一个公式里的探索与开发哲学SCA的精华全部浓缩在其位置更新公式中。我们假设在一个D维的搜索空间中有N个搜索代理即候选解。对于第i个代理在第t次迭代时的位置 X_i^t其更新方式如下X_i^{t1} \begin{cases} X_i^t r_1 \times \sin(r_2) \times | r_3 P_i^t - X_i^t |, \text{if } r_4 0.5 \ X_i^t r_1 \times \cos(r_2) \times | r_3 P_i^t - X_i^t |, \text{if } r_4 \geq 0.5 \end{cases}这个公式看起来有点复杂但拆开看每个部分都有明确的物理意义和控制作用。理解每个参数是灵活运用SCA的关键。2.1 公式拆解四个随机数驱动的智能移动X_i^t: 当前代理的当前位置。这是更新的起点。P_i^t: 这是算法的“指南针”。在标准SCA中它指的是当前种群中全局最优解的位置。注意有些改进变体会让P_i^t指向个体历史最优或邻域最优但基础版本就是全局最优。代理更新的方向始终受到这个最优位置的吸引。| r_3 P_i^t - X_i^t |: 这计算了当前代理与目标位置全局最优之间的绝对距离。它决定了更新步长的“基数”。距离越远潜在的移动幅度就越大。sin(r_2) 或 cos(r_2): 这是定义移动方向的核心。r_2是一个在 [0, 2π] 范围内均匀分布的随机数。正弦和余弦函数在这个区间内会产生介于[-1, 1]之间的值。这个值决定了代理是向着最优解移动值为正还是背离最优解移动值为负亦或是进行垂直方向的探索。r_4这个0-1之间的随机数以50%的概率决定本次更新使用正弦还是余弦组件这增加了搜索行为的不可预测性和多样性。r_1: 这是整个算法中最重要的控制参数。它不是一个常数而是一个随着迭代次数t自适应递减的值。通常的更新方式是 r_1 a - t \times (a / T_{max}) 其中a是一个常数通常设为2T_max是最大迭代次数。在迭代初期r_1值较大接近2此时sin(r_2)或cos(r_2)的系数大代理的移动幅度大算法倾向于在解空间中进行大范围的“探索”Exploration避免陷入局部最优。随着迭代进行r_1线性减小到接近0此时更新项的影响变小代理主要在全局最优解附近进行小范围的精细“开发”Exploitation收敛到最终解。2.2 探索与开发的动态平衡我们可以通过几个极端情况来感受这个平衡当 |sin(r_2)| 或 |cos(r_2)| ≈ 1 且 r_1 较大时代理会进行一个长距离的跳跃。如果这个值是正的它会大幅靠近全局最优如果是负的它甚至会大幅远离当前最优区域跳到另一个可能更优的区域去探索。这是“探索”的典型表现。当 sin(r_2) 或 cos(r_2) ≈ 0 时无论r_1多大更新量都接近0代理几乎停留在原地。这看起来像“停滞”但在高维复杂空间中这种短暂的停留可能是在为下一次探索积蓄“方向”。当 r_1 变得很小时无论正弦余弦值如何整个更新项的幅度都很小。此时所有代理都紧密地围绕在全局最优解周围进行微调算法进入纯粹的“开发”阶段旨在提高解的精度。这种通过r_1线性控制搜索范围的方式是SCA最核心的设计。它模拟了智能优化中一个普遍原则先粗搜后精搜。然而这种线性的递减策略也是SCA的主要改进点之一我们后面会谈到。3. 手把手实现用Python为多元函数构建SCA优化器理论清晰之后实现一个基础的SCA优化器就非常直接了。我们将以寻找一个经典的多峰测试函数——Rastrigin函数的最小值为例。这个函数在原点(0,0,...,0)处有全局最小值0但存在大量按正弦波排列的局部最优点非常适合测试算法的全局搜索和逃离局部最优的能力。Rastrigin函数的公式为 f(x) 10n \sum_{i1}^{n} [ x_i^2 - 10\cos(2\pi x_i) ] 其中n是维度x_i ∈ [-5.12, 5.12]。3.1 环境准备与问题定义我们首先定义要优化的函数和搜索空间边界。import numpy as np import matplotlib.pyplot as plt def rastrigin(x): 计算Rastrigin函数值。 参数: x: 一个一维numpy数组代表一个候选解。 返回: 函数值 (float). n len(x) return 10 * n np.sum(x**2 - 10 * np.cos(2 * np.pi * x)) # 定义问题维度、搜索边界 dim 10 # 我们测试一个10维问题 lower_bound -5.12 upper_bound 5.123.2 SCA核心类的实现接下来我们实现一个SCA类它将包含初始化种群、更新位置、迭代优化等所有逻辑。class SineCosineAlgorithm: def __init__(self, objective_func, dim, pop_size, lower_bound, upper_bound, max_iter, a2): 初始化SCA优化器。 参数: objective_func: 目标函数要求最小化。 dim: 问题维度。 pop_size: 种群大小搜索代理数量。 lower_bound: 每个维度的下界标量或长度为dim的列表。 upper_bound: 每个维度的上界标量或长度为dim的列表。 max_iter: 最大迭代次数。 a: 参数r1的初始值控制探索范围默认为2。 self.objective_func objective_func self.dim dim self.pop_size pop_size self.lb np.array(lower_bound) if np.isscalar(lower_bound) else np.array(lower_bound) self.ub np.array(upper_bound) if np.isscalar(upper_bound) else np.array(upper_bound) self.max_iter max_iter self.a a # 初始化种群位置和适应度 self.positions np.random.uniform(self.lb, self.ub, (self.pop_size, self.dim)) self.fitness np.apply_along_axis(self.objective_func, 1, self.positions) # 记录全局最优 self.best_fitness_idx np.argmin(self.fitness) self.best_solution self.positions[self.best_fitness_idx].copy() self.best_fitness self.fitness[self.best_fitness_idx] # 记录收敛曲线 self.convergence_curve [] def update_position(self, iteration): 根据SCA公式更新所有搜索代理的位置。 参数: iteration: 当前迭代次数 (从0开始)。 # 计算当前迭代的自适应参数 r1 r1 self.a - iteration * (self.a / self.max_iter) for i in range(self.pop_size): for j in range(self.dim): # 生成随机数 r2, r3, r4 r2 2 * np.pi * np.random.rand() r3 2 * np.random.rand() # 通常r3在[0,2]以一定概率使项为负增加探索方向 r4 np.random.rand() # 核心更新公式 if r4 0.5: new_value self.positions[i, j] r1 * np.sin(r2) * np.abs(r3 * self.best_solution[j] - self.positions[i, j]) else: new_value self.positions[i, j] r1 * np.cos(r2) * np.abs(r3 * self.best_solution[j] - self.positions[i, j]) # 边界处理对于越界的位置采用随机重置策略 if new_value self.lb[j] or new_value self.ub[j]: # 策略1直接重置为边界内随机值探索性更强 # new_value np.random.uniform(self.lb[j], self.ub[j]) # 策略2反射边界开发性更强 if new_value self.lb[j]: new_value 2 * self.lb[j] - new_value elif new_value self.ub[j]: new_value 2 * self.ub[j] - new_value # 如果反射后仍越界则钳位到边界 new_value np.clip(new_value, self.lb[j], self.ub[j]) self.positions[i, j] new_value def run(self): 执行SCA优化主循环。 返回: best_solution: 找到的全局最优解。 best_fitness: 对应的最优适应度值。 convergence_curve: 每次迭代的最优适应度记录。 print(fSCA开始优化 {self.dim} 维问题种群大小: {self.pop_size}, 最大迭代: {self.max_iter}) self.convergence_curve.append(self.best_fitness) for t in range(self.max_iter): # 1. 更新所有代理的位置 self.update_position(t) # 2. 计算新位置的适应度 new_fitness np.apply_along_axis(self.objective_func, 1, self.positions) self.fitness new_fitness # 3. 更新全局最优解 current_best_idx np.argmin(self.fitness) current_best_fitness self.fitness[current_best_idx] if current_best_fitness self.best_fitness: self.best_fitness current_best_fitness self.best_solution self.positions[current_best_idx].copy() self.best_fitness_idx current_best_idx # 4. 记录本次迭代的最优值 self.convergence_curve.append(self.best_fitness) # 可选打印进度 if (t1) % 100 0: print(fIteration {t1}/{self.max_iter}, Best Fitness: {self.best_fitness:.6f}) print(f优化完成。最优解: {self.best_solution}) print(f最优适应度: {self.best_fitness}) return self.best_solution, self.best_fitness, self.convergence_curve3.3 执行优化与可视化现在我们可以实例化这个优化器并运行它同时绘制收敛曲线来观察算法的表现。# 参数设置 pop_size 30 max_iterations 1000 # 创建SCA优化器实例并运行 sca_optimizer SineCosineAlgorithm(objective_funcrastrigin, dimdim, pop_sizepop_size, lower_boundlower_bound, upper_boundupper_bound, max_itermax_iterations, a2) best_sol, best_fit, conv_curve sca_optimizer.run() # 可视化收敛过程 plt.figure(figsize(10, 6)) plt.plot(conv_curve, linewidth2) plt.title(SCA Optimization Convergence Curve (Rastrigin Function)) plt.xlabel(Iteration) plt.ylabel(Best Fitness Value (log scale)) plt.yscale(log) # 使用对数坐标能更清晰地看到后期的细微变化 plt.grid(True, whichboth, ls--, alpha0.5) plt.show() # 打印最终结果 print(\n 最终优化结果 ) print(f找到的最优解近似值: {best_sol}) print(f对应的Rastrigin函数值: {best_fit}) print(f理论全局最优值: 0.0) print(f误差: {best_fit - 0.0})运行这段代码你会看到SCA在1000代内对10维Rastrigin函数的优化过程。收敛曲线通常会显示前期快速下降探索阶段中后期缓慢逼近开发阶段。由于函数的复杂性SCA可能无法精确找到0但通常能找到一个非常接近的近似解这已经证明了其全局寻优能力。4. 调参实战与性能瓶颈让SCA真正work起来把算法跑起来只是第一步。要让SCA在具体问题上表现优异甚至超过一些更复杂的算法参数调优和理解其局限性至关重要。很多论文和博客只展示成功案例却很少提调试过程中踩的坑。4.1 关键参数的影响与调优策略种群大小 (pop_size): 这是最重要的参数之一。我的经验是对于D维问题pop_size至少设置在5*D到10*D之间。种群太小多样性不足容易陷入局部最优种群太大每次迭代的计算开销剧增收敛速度变慢。对于10-30维的问题30-100是个不错的起点。参数a(控制r1的初始值): 原始论文设为2。a决定了迭代初期探索的“最大步长”。如果问题搜索空间非常大或者你怀疑最优解可能离初始随机种群很远可以尝试稍微增大a例如2.5或3给予算法更强的“探索冲动”。反之如果搜索空间相对紧凑或平滑可以减小a。最大迭代次数 (max_iter): 这决定了算法有多少时间进行“开发”。对于多峰函数需要足够的迭代次数让算法在探索到有希望的盆地后再进行精细搜索。一个实用的方法是观察收敛曲线当曲线在连续很多代如50-100代几乎变成水平线时说明算法已经收敛继续迭代收益很小。可以设置早停机制。r3的范围: 原始公式中r3在[0,2]。r3 1时r3 * P_i^t会放大最优解的影响力促使代理更积极地向其靠拢r3 1时则会减弱这种吸引力甚至产生反向分量当与正弦/余弦的负值结合时增强探索。有些改进版本会将r3设置为一个随机数其绝对值大于1的概率更高以加强开发。注意参数调优没有银弹。最可靠的方法是针对你的特定问题设计一个小规模的参数实验例如使用网格搜索或随机搜索在多个随机种子上运行比较平均性能。4.2 SCA的固有局限与常见“坑点”线性递减的r1可能不是最优策略SCA采用r1 a - t*(a/T)的线性递减。这意味着在迭代中期探索和开发的转换是匀速的。但对于许多复杂问题我们可能希望在前期更长时间地探索然后在某个点快速切换到深度开发。这是SCA一个主要的改进方向例如可以采用非线性递减策略r1 a * (1 - t/T)^b其中b是一个指数可以控制递减的曲线形状。对高维问题100维的“维度灾难”和大多数元启发式算法一样SCA在高维空间中的搜索效率会急剧下降。随着维度增加解空间呈指数级膨胀随机搜索的有效性降低。此时种群大小需要极大增加计算成本变得不可接受。对于超高维问题通常需要结合降维、分治策略或使用专门针对高维优化的算法变种。边界处理的微妙影响在update_position函数中我提供了两种边界处理策略随机重置和反射。随机重置更具探索性但可能破坏有希望的搜索方向。反射更像物理反弹能保留部分动量但可能导致代理在边界附近“振荡”。我个人的经验是对于像Rastrigin这样最优解在空间中央的函数反射策略有时效果更好而对于最优解可能在边界上的问题随机重置可能更有助于探索边界区域。这是一个需要根据问题先验知识来调整的点。“停滞”与早熟收敛有时你会发现算法在迭代早期就找到了一个相对较好的解然后所有代理迅速聚集到该点附近由于r1变小再也跳不出来了。这就是早熟收敛。对策包括1) 增加种群大小2) 在算法中引入“重启”机制当种群多样性低于某个阈值时重新初始化一部分代理3) 采用动态的r3或更复杂的更新公式如结合莱维飞行Levy Flight来偶尔产生长距离跳跃。4.3 一个简单的改进尝试非线性递减的r1我们可以轻松修改上面的SCA类尝试一种非线性递减策略看看效果。只需修改update_position方法中计算r1的那一行# 将原来的线性递减 # r1 self.a - iteration * (self.a / self.max_iter) # 改为指数递减例如b2 b 2 r1 self.a * (1 - iteration / self.max_iter) ** b这种改变会让r1在迭代前期下降较慢维持更久的探索在后期下降加快快速转入开发。你可以在同一测试函数上对比两种策略的收敛曲线和最终精度感受其差异。5. 超越基准测试SCA在更复杂场景下的应用思考在标准测试函数上跑通算法只是学习的开始。将SCA应用于实际问题时我们需要考虑更多。5.1 处理约束优化问题现实中的优化问题大多带有约束条件例如g(x) 0或h(x) 0。标准的SCA并不直接处理约束。常用的方法有罚函数法这是最直接的方法。将约束违反的程度作为一个惩罚项加到目标函数值上。例如新目标函数F(x) f(x) λ * Penalty(x)其中Penalty(x)衡量违反约束的程度λ是一个很大的正数罚因子。这样违反约束的解会有很差的适应度在进化中被淘汰。难点在于罚因子λ的选择需要调整。可行解优先规则在比较两个解的优劣时优先比较它们的约束违反程度。只有都是可行解满足约束时才比较原始目标函数值。这种方法在SCA更新最优解P_i^t的逻辑中需要修改。修复算子对于越界或违反约束的解不直接丢弃而是通过一个确定的规则将其“修复”到可行域内。这需要针对具体约束设计专门的修复逻辑。5.2 与其他算法的混合策略“没有免费的午餐”定理告诉我们没有一种算法能在所有问题上都最好。混合策略是提升性能的常见手段。SCA与局部搜索结合在SCA的每一代或每隔若干代对当前全局最优解best_solution执行一次局部搜索如梯度下降、Nelder-Mead单纯形法。这相当于在SCA的全局框架下嵌入了局部求精的能力能显著提高解的精度。这种模式常被称为“Memetic Algorithm”。SCA与PSO或DE的混合可以借鉴粒子群算法中的“个体历史最优”概念让SCA的代理不仅受全局最优吸引也受自身历史最优位置吸引增加搜索的多样性。或者在SCA的更新公式中以一定概率采用差分进化DE的变异策略来生成新的候选解。5.3 离散化与组合优化SCA原生是为连续空间优化设计的。对于旅行商问题TSP、调度问题等组合优化问题需要设计离散化的SCA版本。核心在于重新定义“位置”和“移动”位置表示一个解可以表示为一个排列permutation例如[2,4,1,3]代表城市的访问顺序。移动操作正弦余弦更新公式产生的连续值不能直接用于排列。需要设计特殊的“操作符”例如将连续值通过某种规则如Smallest Position Value, SPV转换为排列。定义基于正弦余弦振荡的“交换”、“插入”、“逆序”等邻域搜索操作。将更新公式中的加减法替换为针对离散结构的特定操作如OX交叉、两点交换等。这部分的实现比连续问题复杂得多通常需要结合具体问题的领域知识。在我自己的项目中曾用SCA来优化一个小型神经网络的学习率和层节点数。我将连续值的SCA输出通过取整和映射转换为离散的超参数组合。虽然它不是最先进的超参优化工具如贝叶斯优化但其实现简单、并行评估方便的特点在计算资源有限、参数空间不大的情况下是一个快速验证的实用选择。关键在于你要清楚算法的边界在哪里——SCA擅长中低维、黑箱、多峰的连续或可离散化的问题对于超高维、梯度信息可用、或约束极其复杂的问题可能需要更专门的方法。理解原理亲手实现再针对具体场景做调整和融合这才是掌握一个优化算法的正确姿势。