scikit-opt 模拟退火算法三种流派实战:Fast、Boltzmann 与 Cauchy 的数学原理、参数调优与边界约束

原创2026-10-07 18:11:051,735 阅读
文章标签:人工智能机器学习

scikit-opt 模拟退火算法三种流派实战:Fast、Boltzmann 与 Cauchy 的数学原理、参数调优与边界约束

模拟退火(Simulated Annealing,SA)是 scikit-opt 库中用于求解连续函数最优化问题的重要启发式算法之一,其核心思想是模仿金属退火过程中"高温扰动、缓慢降温"的物理过程,借助 Metropolis 接受准则跳出局部最优。本文以 docs/zh/more_sa.md 为主线,系统讲解 scikit-opt 提供的三种模拟退火具体形式——Fast(快速退火)、Cauchy(柯西退火)、Boltzmann(玻尔兹曼退火),逐一拆解它们的随机扰动公式与降温策略,并结合 sko/SA.py 源码剖析参数含义、边界约束(lb/ub)实现与收敛停止机制。读完本文,你将能够在 examples/demo_sa.py 示例的基础上,针对不同目标函数选型、配置参数并解读收敛曲线。

一、模拟退火三种流派的统一框架

scikit-opt 中,三种 SA 流派共享同一个基类 SimulatedAnnealingBase(定义于 sko/SA.py),它们的区别只在于两个核心策略函数的实现:

  • get_new_x(x):如何从当前解 x 生成新解 x_new,即"扰动方式";
  • cool_down():每一轮外循环结束后如何降低温度 T,即"降温方式"。

两者共同决定了算法的探索(exploration)与收敛(convergence)行为。三种流派都遵循同一套主循环流程(run() 方法,见 sko/SA.py):

  1. 在当前温度下,内循环迭代 L 次,每次用 get_new_x 生成新解并计算目标函数值;
  2. 依据 Metropolis 准则 决定是否接受新解:若新解更优(df < 0)无条件接受,否则以概率 np.exp(-df / T) 接受更差的解——温度越高,越容易"容忍"差解,从而具备跳出局部最优的能力;
  3. 内循环结束后调用 cool_down() 降温;
  4. 记录本轮的全局最优 best_y,并判断是否满足停止条件。

公共参数说明

SimulatedAnnealingBase.__init__ 签名如下(默认值即源码中的默认值,见 sko/SA.py):

参数 默认值 含义
func 必填 目标函数,接收一维数组 x,返回标量适应度(本文示例为求最小值)
x0 必填 初始解,数组形状为 n_dim;解的维度由 len(x0) 自动推断
T_max 100 初始温度,算法从该温度开始退火,须满足 T_max > T_min > 0
T_min 1e-7 终止温度,温度降至该值以下时停止迭代
L 300 每个温度下的内循环迭代次数,又称"马尔可夫链长"
max_stay_counter 150 若全局最优 best_y 连续 max_stay_counter 轮保持不变,则提前停止("冷却时间")

run() 的两种停止条件在源码中体现为(sko/SA.py):温度低于 T_min(停止码 'Cooled to final temperature'),或 best_y 停滞轮数超过 max_stay_counter。其中"停滞"判定通过 isclose 实现(sko/SA.py),默认相对容差 rel_tol=1e-9、绝对容差 abs_tol=1e-30。

注意:run() 返回 (best_x, best_y) 元组;基类 SkoBase.fit(sko/base.py)仍保留但会发出 DeprecationWarning,建议统一使用 run()。

二、三种流派的数学公式与源码对照

docs/zh/more_sa.md 给出了三种流派的完整数学描述。下面逐一流派给出公式原文、对应的源码实现(均位于 sko/SA.py)以及参数说明。

2.1 Fast 快速退火(SAFast)

文档给出的公式:

u ~ Uniform(0, 1, size = d)
y = sgn(u - 0.5) * T * ((1 + 1/T)**abs(2*u - 1) - 1.0)
xc = y * (upper - lower)
x_new = x_old + xc

c = n * exp(-n * quench)
T_new = T0 * exp(-c * k**quench)

对应的源码实现(sko/SA.py)在数学上完全等价,只是把随机数统一为 r ~ Uniform(-1, 1):

# get_new_x
r = np.random.uniform(-1, 1, size=self.n_dim)
xc = np.sign(r) * self.T * ((1 + 1.0 / self.T) ** np.abs(r) - 1.0)
x_new = x + xc * self.hop

# cool_down
self.T = self.T_max * np.exp(-self.c * self.iter_cycle ** self.quench)

其中 self.hop 是无边界时的默认步长(默认 10,见下文第四节),self.c = self.m * np.exp(-self.n * self.quench) 在 __init__ 中预计算。Fast 退火的特点:扰动幅度与当前温度 T 成正比、随温度下降而收缩,降温采用指数形式 T = T_max * exp(-c * k^quench),在 quench > 0 时随迭代轮数 k 快速淬火,收敛速度在三者中最快,因此被选为默认流派——源码第 212 行 SA = SAFast 表明 from sko.SA import SA 导入的就是 SAFast。

Fast 流派的专属参数(均为可选关键字参数,默认值取自源码 sko/SA.py):

参数 默认值 作用
m 1 与 n、quench 共同决定淬火常数 c = m * exp(-n * quench),c 越大降温越快
n 1 同上,参与淬火常数计算
quench 1 淬火指数,出现在 T = T_max * exp(-c * k^quench) 中,控制降温曲线的形状

2.2 Cauchy 柯西退火(SACauchy)

文档给出的公式:

u ~ Uniform(-pi/2, pi/2, size=d)
xc = learn_rate * T * tan(u)
x_new = x_old + xc

T_new = T0 / (1 + k)

对应的源码实现(sko/SA.py):

# get_new_x
u = np.random.uniform(-np.pi / 2, np.pi / 2, size=self.n_dim)
xc = self.learn_rate * self.T * np.tan(u)
x_new = x + xc

# cool_down
self.T = self.T_max / (1 + self.iter_cycle)

Cauchy 退火的扰动来自标准柯西分布(由 tan(u),u 在 (-π/2, π/2) 均匀采样生成),其厚尾特性允许偶尔产生大幅跳跃,有助于逃逸局部最优;降温采用反比例形式 T = T_max / (1 + k),前期降温快、后期降温慢。

专属参数(默认值取自 sko/SA.py):

参数 默认值 作用
learn_rate 0.5 学习率,直接缩放扰动步长 xc = learn_rate * T * tan(u)

2.3 Boltzmann 玻尔兹曼退火(SABoltzmann)

文档给出的公式:

std = minimum(sqrt(T) * ones(d), (upper - lower) / (3*learn_rate))
y ~ Normal(0, std, size = d)
x_new = x_old + learn_rate * y

T_new = T0 / log(1 + k)

对应的源码实现(sko/SA.py):

# get_new_x
a, b = np.sqrt(self.T), self.hop / 3.0 / self.learn_rate
std = np.where(a < b, a, b)
xc = np.random.normal(0, 1.0, size=self.n_dim)
x_new = x + xc * std * self.learn_rate

# cool_down
self.T = self.T_max / np.log(self.iter_cycle + 1.0)

Boltzmann 退火的扰动服从高斯(正态)分布,其标准差取 sqrt(T) 与 hop / (3 * learn_rate) 的较小值——后者本质上是把步长限制在解空间范围(upper - lower)的三分之一以内,防止扰动越界;降温采用对数形式 T = T_max / log(1 + k),是三种形式中降温最慢的,因而对解空间的搜索最充分、最细腻。

专属参数(默认值取自 sko/SA.py):

参数 默认值 作用
learn_rate 0.5 学习率,参与扰动缩放 x_new = x_old + learn_rate * y

三、三种流派的完整代码示例

以下示例全部出自 examples/demo_sa.py,目标函数为三元函数 demo_func = x[0]**2 + (x[1]-0.05)**2 + x[2]**2,其全局最小值点约为 (0, 0.05, 0)。

3.1 Fast 快速退火

from sko.SA import SAFast

sa_fast = SAFast(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150)
sa_fast.run()
print('Fast Simulated Annealing: best_x is ', sa_fast.best_x, 'best_y is ', sa_fast.best_y)

3.2 Cauchy 柯西退火

from sko.SA import SACauchy

sa_cauchy = SACauchy(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150)
sa_cauchy.run()
print('Cauchy Simulated Annealing: best_x is ', sa_cauchy.best_x, 'best_y is ', sa_cauchy.best_y)

3.3 Boltzmann 玻尔兹曼退火

from sko.SA import SABoltzmann

sa_boltzmann = SABoltzmann(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150)
sa_boltzmann.run()
print('Boltzmann Simulated Annealing: best_x is ', sa_boltzmann.best_x, 'best_y is ', sa_boltzmann.best_y)

参数事实提示:上述示例中的 q=0.99 沿用了 scikit-opt 文档中的写法,但从 sko/SA.py 源码看,SAFast、SACauchy、SABoltzmann 三个类的 __init__ 只通过 **kwargs 读取 m/n/quench 或 learn_rate,并不会读取 q。也就是说 q=0.99 会被 **kwargs 静默吸收、不产生任何实际效果。需要精确控制时,应改用上述专属参数。

四、带边界约束(bounds)的用法

三种流派均通过 lb(下界)与 ub(上界)限制解的搜索范围,这一能力由 SimulatedAnnealingValue 基类实现(sko/SA.py)。其处理规则如下:

  • lb 与 ub 必须同时给出或同时省略,否则抛出 ValueError;
  • 传入的 lb/ub 会被广播为 n_dim 维数组(即 np.array(lb) * np.ones(self.n_dim)),因此既支持 lb=[-1, 1, -1], ub=[2, 3, 4] 这种逐维边界,也支持 lb=-1 这种标量边界(会被广播为所有维度下界均为 -1);
  • 需满足 ub > lb,且维度与 x0 一致;
  • 边界的存在会生成扰动步长 self.hop(默认 hop = ub - lb),无边界时 hop 默认为 10;
  • 每次扰动后用 np.clip(x_new, self.lb, self.ub) 将新解钳制回边界内(三种流派的 get_new_x 末尾均有此操作)。

带边界约束的完整示例:

4.1 Fast + bounds

from sko.SA import SAFast

sa_fast = SAFast(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150,
                 lb=[-1, 1, -1], ub=[2, 3, 4])
sa_fast.run()
print('Fast Simulated Annealing with bounds: best_x is ', sa_fast.best_x, 'best_y is ', sa_fast.best_y)

4.2 Boltzmann + bounds(标量下界广播用法)

from sko.SA import SABoltzmann

sa_boltzmann = SABoltzmann(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150,
                           lb=-1, ub=[2, 3, 4])
sa_boltzmann.run()
print('Boltzmann Simulated Annealing with bounds: best_x is ', sa_boltzmann.best_x, 'best_y is ', sa_boltzmann.best_y)

4.3 Cauchy + bounds

from sko.SA import SACauchy

sa_cauchy = SACauchy(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, q=0.99, L=300, max_stay_counter=150,
                     lb=[-1, 1, -1], ub=[2, 3, 4])
sa_cauchy.run()
print('Cauchy Simulated Annealing with bounds: best_x is ', sa_cauchy.best_x, 'best_y is ', sa_cauchy.best_y)

五、结果读取与收敛曲线可视化

三种流派运行后都会在实例上保存历史信息,可用来绘制收敛曲线(参考 README.md 第 4.1 节):

  • best_x / best_y:全局最优解与最优适应度;
  • best_y_history(即 generation_best_Y):每一轮降温后的全局最优适应度序列,见 sko/SA.py;
  • best_x_history(即 generation_best_X):对应的最优解序列。

以下代码绘制"当前最优适应度"随迭代的累积最小值曲线,可以看到算法逐步逼近全局最优的过程:

import matplotlib.pyplot as plt
import pandas as pd

sa = SAFast(func=demo_func, x0=[1, 1, 1], T_max=1, T_min=1e-9, L=300, max_stay_counter=150)
sa.run()
plt.plot(pd.DataFrame(sa.best_y_history).cummin(axis=0))
plt.show()

六、实战选型建议

结合三种流派的公式与源码特性,给出选型参考:

  • 追求收敛速度:优先 SAFast(也是库默认流派),其指数淬火降温最快,适合目标函数较光滑、对求解时长敏感的场景;
  • 强多峰、易陷入局部最优:优先 SACauchy,柯西分布厚尾带来的大幅跳跃能力更强,反比例降温中后期保持较高温度;
  • 精细搜索、需要慢速充分收敛:优先 SABoltzmann,对数降温最慢,高斯扰动细腻,但所需迭代轮数更多,可适当调大 L 与 max_stay_counter。

参数调优时重点关注:T_max 决定初始扰动强度(过小会过早收敛到局部最优);L 决定每个温度下的搜索链长(影响求解质量与耗时);max_stay_counter 决定"耐心"(过小会提前停止)。三种流派均支持 lb/ub 边界约束,实际工程中建议始终给出合理边界,既能加速收敛也能避免无效区域的计算。

七、延伸:SA 在组合优化中的应用

除连续函数优化外,sko/SA.py 还提供 SA_TSP(sko/SA.py),用于旅行商问题等组合优化:它继承 SimulatedAnnealingBase,将扰动策略替换为交换(swap)、反转(reverse)、换位(transpose)三种路径变异算子(定义于 sko/operators/mutation.py),降温采用 T = T_max / (1 + log(1 + k))。其调用方式见 examples/demo_sa_tsp.py:

from sko.SA import SA_TSP

sa_tsp = SA_TSP(func=cal_total_distance, x0=range(num_points), T_max=100, T_min=1, L=10 * num_points)
best_points, best_distance = sa_tsp.run()
print(best_points, best_distance, cal_total_distance(best_points))

至此,scikit-opt 的三种模拟退火流派已全部覆盖:从统一框架、数学公式、源码实现,到边界约束、收敛可视化与选型策略,你可以直接基于 examples/demo_sa.py 修改目标函数与参数,将其应用到自己的最优化任务中。

登录后查看全文
scikit-opt