scikit-opt 模拟退火算法三种流派实战:Fast、Boltzmann 与 Cauchy 的数学原理、参数调优与边界约束
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):
- 在当前温度下,内循环迭代
L次,每次用get_new_x生成新解并计算目标函数值; - 依据 Metropolis 准则 决定是否接受新解:若新解更优(
df < 0)无条件接受,否则以概率np.exp(-df / T)接受更差的解——温度越高,越容易"容忍"差解,从而具备跳出局部最优的能力; - 内循环结束后调用
cool_down()降温; - 记录本轮的全局最优
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 修改目标函数与参数,将其应用到自己的最优化任务中。