模拟退火算法:从固体退火到Rastrigin与TSP,手写一个完整的退火求解器

摘要

模拟退火算法是优化算法中单点搜索 算法的代表,通过模拟固体降温结晶现象,从一个解出发,以概率接受劣解来跳出局部最优。本文从物理退火结晶开始,完整推导模拟退火算法的核心原理,并手写模拟退火算法解决 Rastrigin 多峰函数求最小值问题和 TSP 旅行商问题。

引言

模拟退火算法的核心源自固体降温结晶过程,对固体加热后进行缓慢平稳的降温最后得到的是晶体,反则得到的是非晶体。

模拟退火算法模拟的就是这个降温过程,在固体的每一个温度都进行停留使得固体呈现稳态才继续降温,模拟退火的稳态过程就是寻找最优解的过程。

核心原理

模拟退火算法是单点搜索算法,不同于 PSO 和 GA 等群算法,轮次迭代中仅使用一个解(状态)来进行优化,这正是它在空间复杂度上的优势。

核心字段:

固体降温中的核心字段有 状态、能量、温度和基态 ,对应到模拟退火中就是:

  • 解 x(状态):问题的一个可行解
  • 适应度函数 E(能量):衡量解好坏的标准
  • 可变参数 T(温度):用于控制跳出局部最优
  • 全局最优解 best(基态):问题中适应度最好的解

温度系数

物理退火中,热运动动能赋予了分子在高温度下跨越势能临界点的能力。在算法映射中,高温度 <math xmlns="http://www.w3.org/1998/Math/MathML"> T T </math>T 赋予了系统跨越能量障碍(即接受劣解)的高概率窗口 。为了进一步提高后期局部开发的精细度,本文引入了与温度解耦或协同的步长衰减机制: <math xmlns="http://www.w3.org/1998/Math/MathML"> S t e p ∝ T Step \propto T </math>Step∝T。

常规求最小值更新公式为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x n e w = x + s t e p ∗ r a n d ( − 1 , 1 ) x_{new} = x + step*rand(-1,1) </math>xnew=x+step∗rand(−1,1)

变化幅度 step 的设置没有固定的形式,但是需要保持 step 与 T 呈正相关,T 越大 step 越大。

Metropolis 准则

模拟退火算法使用适应度函数 E(x) 来衡量当前解 x 的好坏,设置 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E = E ( x n e w ) − E ( x ) \Delta E = E(x_{new}) - E(x) </math>ΔE=E(xnew)−E(x),求最小值问题中 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E ≤ 0 \Delta E \le 0 </math>ΔE≤0 说明新解 <math xmlns="http://www.w3.org/1998/Math/MathML"> x n e w x_{new} </math>xnew 更优会直接接受 ,反则当 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E > 0 \Delta E>0 </math>ΔE>0 则说明新解没有旧解好,理论上应该拒绝新解,但是为了避免全局最优会有概率接受新解。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P ( Accept ) = { 1 , Δ E ≤ 0 e − Δ E T , Δ E > 0 P(\text{Accept}) = \begin{cases} 1, &\Delta E \le 0 \\ e^{-\frac{\Delta E}{T}}, & \Delta E > 0 \end{cases} </math>P(Accept)={1,e−TΔE,ΔE≤0ΔE>0

按照 Metropolis 准则 接受劣解的概率为 <math xmlns="http://www.w3.org/1998/Math/MathML"> e − Δ E T e^{-\frac{\Delta E}{T}} </math>e−TΔE (公式源于统计力学中的 Boltzmann 分布,系统处于能量 E 的状态的概率正比于 <math xmlns="http://www.w3.org/1998/Math/MathML"> e − E T e^{-\frac{E}{T}} </math>e−TE),将 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E T \frac{\Delta E}{T} </math>TΔE 看作一个整体时图像为 <math xmlns="http://www.w3.org/1998/Math/MathML"> e − x e^{-x} </math>e−x:

算法开始时, T 非常高, <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E T \frac{\Delta E}{T} </math>TΔE 就会很小接近于 0 , <math xmlns="http://www.w3.org/1998/Math/MathML"> e − Δ E T e^{-\frac{\Delta E}{T}} </math>e−TΔE 接近于 1 ,算法接受劣解的概率非常高 ,随着轮次迭代 T 下降同时趋近于稳定 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E \Delta E </math>ΔE 也会变小, <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E T \frac{\Delta E}{T} </math>TΔE 就会很大, <math xmlns="http://www.w3.org/1998/Math/MathML"> e − Δ E T e^{-\frac{\Delta E}{T}} </math>e−TΔE 接近于 0 ,算法接受劣解的概率非常低

温度更新

算法逐步迭代寻找最优解,T 也会随着轮次逐步减低,常用公式为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T n e w = α T , α ∈ [ 0.85 , 0.99 ] T_{new} = \alpha T,\alpha∈[0.85,0.99] </math>Tnew=αT,α∈[0.85,0.99]

<math xmlns="http://www.w3.org/1998/Math/MathML"> α \alpha </math>α 类似于学习率用于控制温度下降的速度 , <math xmlns="http://www.w3.org/1998/Math/MathML"> α \alpha </math>α 越小温度下降得越快,算法整体执行的次数也会减少,但是整体效果可能会变差。

算法执行往往会设置一个阈值 tolerance(如 1e-6) ,当 T < tolerance 时代表算法结束。

温度初始化

温度的核心作用体现在算法的迭代轮次接受劣解的概率 <math xmlns="http://www.w3.org/1998/Math/MathML"> e − Δ E T e^{-\frac{\Delta E}{T}} </math>e−TΔE ,如果面对不同规模的问题设置相同的初始化 T_start 时会出现问题,若设置 T_start = 100

  • 小规模问题设 E_new = 10 , E = 8 , ΔE = 2,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> P = e − 1 50 ≈ 0.98 P = e^{-\frac{1}{50}} \approx 0.98 </math>P=e−501≈0.98,过高
  • 大规模问题设 E_new = 1000 , E = 800 , ΔE = 200,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> P = e − 2 ≈ 0.13 P = e^{-2} \approx 0.13 </math>P=e−2≈0.13,过低

发现同一个 T 对不同规模的问题影响不同,理论上 T 应该与问题规模呈正相关,根据 <math xmlns="http://www.w3.org/1998/Math/MathML"> P = e − Δ E T P = e^{-\frac{\Delta E}{T}} </math>P=e−TΔE 可以反推得到:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T s t a r t = − Δ E l n P s t a r t T_{start} =-\frac{\Delta E}{lnP_{start}} </math>Tstart=−lnPstartΔE

<math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E \Delta E </math>ΔE 整体代表的是问题的规模,可以在算法开始之前取100个 Δ E = E(rand(limit)_1) - E(rand(limit)_2),最后取 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E a v g \Delta E_{avg} </math>ΔEavg作为问题规模代表。

<math xmlns="http://www.w3.org/1998/Math/MathML"> P s t a r t P_{start} </math>Pstart 代表的就是开始时对于劣解的接受程度往往初始化为 p_start=0.85,最终公式为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> T s t a r t = − Δ E a v g l n 0.85 T_{start} =-\frac{\Delta E_{avg}}{ln0.85} </math>Tstart=−ln0.85ΔEavg

算法流程

按照模拟退火算法的核心原理实现算法。

参数初始化

  • L :温度 T 下算法的迭代次数
  • p_start :初始化劣解接受概率,常设置p_start=0.85
  • t_end :最低温度阈值,当T < tolerance算法结束
  • alpha :温度更新控制参数
  • fitness_history :保存可用于查看算法流程

温度初始化

根据推导的公式 <math xmlns="http://www.w3.org/1998/Math/MathML"> T s t a r t = − Δ E a v g l n 0.85 T_{start} =-\frac{\Delta E_{avg}}{ln0.85} </math>Tstart=−ln0.85ΔEavg 进行温度的初始化,其中的位置参数只有 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E a v g \Delta E_{avg} </math>ΔEavg ,其计算发生在模拟退火算法执行之前,可以随机取多个 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E \Delta E </math>ΔE 最后取均值。

如取 100 个 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E \Delta E </math>ΔE ,即每个轮次随机两个解 x1 和 x2,然后计算 ΔE =E(x1) - E(x2),100 轮后取均值即可得到 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ E a v g \Delta E_{avg} </math>ΔEavg,最后代入 <math xmlns="http://www.w3.org/1998/Math/MathML"> T s t a r t = − Δ E a v g l n 0.85 T_{start} =-\frac{\Delta E_{avg}}{ln0.85} </math>Tstart=−ln0.85ΔEavg 。

获取新状态

评估完当前状态的好坏后,就会获取下一个状态,其结果可能更好也可能更坏。

1.在函数求最值问题中常用的获取状态公式为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> x n e w = x + s t e p ∗ r a n d ( − 1 , 1 ) x_{new} = x + step*rand(-1,1) </math>xnew=x+step∗rand(−1,1)

同时还要进行边界处理,新状态不允许超出边界如边界为[-5,5],获取x_new = 5.2时就越界了。

  • 直接截断 :超出边界的状态直接设置为边界此时x_new = 5.2-->5,但是会存在一个很大问题,如果在达到右边界x = 5后,获取新状态时如果随机到整数则还是x_new = 5,此时ΔE=0无条件接受新状态,算法会将大量的算力被浪费在边界的同一个点上。
  • 镜面反弹:以边界为对称轴投影越界的状态到界内,这个做法更合理。

2.在TSP旅行商问题中常用的获取状态公式为:

旅行商问题中 x 是所有城市序号的组合如[3,2,35,......,7,3]是一个经过所有城市的回路。

  • 交换算子:任取两个城市交换其位置
  • 逆序算子:任取一个区间内城市逆序
  • 插入算子:随机拔出一个城市插入到随机位置中

逻辑实现

算法的整体逻辑实现为:

  1. 在限制内随机生成初始状态 x
  2. 初始化起始温度值 T
  3. 保存历史最优数据
  4. 温度外循环,计算温度内移动的幅度
  5. 温度内循环,获取新位置并判断是否更新

手写 SA

Rastrigin

使用模拟退火算法实现二维 Rastrigin 函数求最小值问题最小值点在(0,0)值为 0 。

单点搜索

python 复制代码
"""模拟退火算法 SA (Simulated Annealing)
    摘要:模拟退火算法通过模拟物理现象,解决寻找最优解问题,并且实现概率更新到更差的位置以跳出局部最优。
    引言:模拟退火算法源自物理中固体降温结晶原理,缓慢降温(退火)则得到晶体,快速降温得到的则是非晶体。
    核心原理:
        状态(解):问题的一个可行解
        能量(适应度函数):衡量状态好坏的标准
        温度:可变参数,用来计算跳出局部最优的概率
        基态(全局最优解):问题的最优解
    算法流程(最小值问题举例):
        1.定义问题--适应度函数 E,解 x 的维度,解的区间
        2.定义参数--迭代轮次 L,温度 T 初始值及变化函数
        3.更新条件--ΔE = E(x_new) - E(x),ΔE < 0 说明新位置 x_new 更优,否则 x 更优
        4.概率更新--当 ΔE > 0 时 x 更优,但是避免全局最优问题,在概率 P = e^-(ΔE/T) 更新
        5.全局最优--避免最后一轮时概率 P = e^-(ΔE/T) 更新,迭代中保存历史最优 x_best
    核心问题:
        T 的初始化:T_Start 应该随着问题规模的变大而变大,而不是固定的,按照 P = e^-(ΔE/T) 反算 T = -ΔE/lnP,
            通常设置 P_Start = 0.85,ΔE 可以在退火之前进行计算 100 轮迭代下 ΔE 的均值 ΔE_avg。
        T 的变化:随着轮次 L 上升 T 应该下降,二者呈负相关。T = α * T,(α属于0-1)L 越大 α 越大,反则越小。
"""

import numpy as np


class SA:
    """模拟退火算法 SA """

    def __init__(self, L=100, dim=1, p_start=0.85, t_end=1e-6, alpha=0.98):
        """初始化"""
        self.L = L
        self.dim = dim
        self.p_start = p_start
        self.t_end = t_end
        self.alpha = alpha
        self.fitness_history = None
        self.x_history = None

    def _template_init(self, fitness_function, limit):
        """
        初始化温度 T_start
            T_start = -ΔE_avg/lnP_start
        """
        # 1.迭代 100 轮计算 ΔE
        delta_E_list = []
        for i in range(100):
            x1 = np.random.uniform(limit[:, 0], limit[:, 1], size=self.dim)
            x2 = np.random.uniform(limit[:, 0], limit[:, 1], size=self.dim)
            delta_E = abs(fitness_function(x1) - fitness_function(x2))
            delta_E_list.append(delta_E)
        # 2.计算 ΔE_avg ,代入计算 T_start
        delta_E_avg = np.mean(delta_E_list)
        t_start = -delta_E_avg / np.log(self.p_start)
        return t_start

    def _get_next(self, x, step_size, limits):
        """
        计算新解位置
            x_new = x + step_size * rand(-1,1)
        """
        # 1.添加随机扰动
        dx = np.random.uniform(-step_size, step_size, size=self.dim)
        x_new = x + dx

        # 2. 反射边界处理
        lb, ub = limits[:, 0], limits[:, 1]
        # 处理上限越界:如果 x > ub,反弹回 ub - (x - ub) = 2*ub - x
        over_ub = x_new > ub
        x_new[over_ub] = 2 * ub[over_ub] - x_new[over_ub]

        # 处理下限越界:如果 x < lb,反弹回 lb + (lb - x) = 2*lb - x
        under_lb = x_new < lb
        x_new[under_lb] = 2 * lb[under_lb] - x_new[under_lb]

        return x_new

    def optimize(self, fitness_function, limits):
        """算法逻辑"""
        # 1,初始化参数
        limits = np.atleast_2d(limits)  # 确保至少是二维的
        if limits.shape[0] == 1 and self.dim > 1:
            limits = np.tile(limits, (self.dim, 1))
        x = np.random.uniform(limits[:, 0], limits[:, 1], size=self.dim)  # 利用NumPy自动广播生成单解
        fitness = fitness_function(x)

        template = self._template_init(fitness_function, limits)  # 初始化温度
        template_start = template

        x_best = np.copy(x)  # 保存历史最优解
        fitness_best = fitness

        self.fitness_history = [fitness]
        self.x_history = [np.copy(x)]

        # 2.迭代计算
        while template > self.t_end:
            # 外层循环控制 步长
            cur_step_size = (limits[:, 1] - limits[:, 0]) * (template / template_start) * 0.1

            for _ in range(self.L):
                # 内层更新位置
                x_new = self._get_next(x, cur_step_size, limits)

                # 计算 ΔE
                fitness_new = fitness_function(x_new)
                delta_E = fitness_new - fitness

                # 更新
                if delta_E < 0:
                    # 一定更新
                    x = x_new
                    fitness = fitness_new
                    if fitness_new < fitness_best:
                        x_best = np.copy(x)
                        fitness_best = fitness
                else:
                    # 概率更新 P = e^-(ΔE/T)
                    p = np.exp(-delta_E / template)
                    if np.random.rand() < p:
                        x = x_new
                        fitness = fitness_new

                self.fitness_history.append(fitness)
                self.x_history.append(np.copy(x))

            # 更新温度
            template *= self.alpha

        return x_best, fitness_best

主函数测试结果代码:

python 复制代码
if __name__ == "__main__":
    def func(x):
        return 10 * len(x) + np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))


    sa = SA(dim=2)
    limit = [[-5.12, 5.12], [-5.12, 5.12]]
    x_best, fitness_best = sa.optimize(func, limit)

    print(f'最小值位置:{x_best}')
    print(f'最佳适应值:{fitness_best}')

    import matplotlib.pyplot as plt

    # 允许 matplotlib 显示中文
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False

    # 提取历史记录
    fitness_history = np.array(sa.fitness_history)
    x_history = np.array(sa.x_history)
    total_steps = len(fitness_history)

    # ---------------- 图 1:收敛曲线图 ----------------
    plt.figure(figsize=(10, 5), dpi=100)
    plt.plot(fitness_history, color='#1f77b4', linewidth=1.5, label='当前解适应度')
    plt.axhline(y=0, color='r', linestyle='--', alpha=0.6, label='理论全局最优值(0)')
    plt.title(f'模拟退火算法(SA)收敛曲线(总评估次数: {total_steps}次)', fontsize=12)
    plt.xlabel('内层迭代总步数 (Evaluation Steps)', fontsize=10)
    plt.ylabel('适应度函数值 (Energy / Fitness)', fontsize=10)
    plt.grid(True, linestyle=':', alpha=0.6)
    plt.legend(loc='upper right')

    # 局部放大图提示:你会看到前中期曲线上有很多"向上跳跃"的红点,那就是 Metropolis 准则在发挥作用
    plt.tight_layout()
    plt.show()

    # ---------------- 图 2:2D 状态空间搜索轨迹图 ----------------
    # 1. 准备 Rastrigin 函数的等高线数据
    X1 = np.linspace(-5.12, 5.12, 200)
    X2 = np.linspace(-5.12, 5.12, 200)
    X1, X2 = np.meshgrid(X1, X2)
    Z = 10 * 2 + (X1 ** 2 - 10 * np.cos(2 * np.pi * X1)) + (X2 ** 2 - 10 * np.cos(2 * np.pi * X2))

    plt.figure(figsize=(8, 7), dpi=100)
    # 画出绚丽的背景等高线
    contour = plt.contourf(X1, X2, Z, levels=30, cmap='viridis', alpha=0.8)
    plt.colorbar(contour, label='Rastrigin 函数值')

    # 画出 SA 单解的移动轨迹(用渐变色代表时间先后:由浅入深)
    colors = plt.cm.autumn(np.linspace(0, 1, total_steps))
    plt.scatter(x_history[:, 0], x_history[:, 1], c=colors, s=3, alpha=0.6, label='解移动轨迹')

    # 标出起点、终点和理论最优
    plt.plot(x_history[0, 0], x_history[0, 1], 'go', markersize=8, label='初始随机起点')
    plt.plot(x_best[0], x_best[1], 'ro', markersize=8, label='算法寻优终点')
    plt.plot(0, 0, 'b*', markersize=12, label='理论全局中心(0,0)')

    plt.title('模拟退火在二维 Rastrigin 函数上的空间搜索轨迹', fontsize=12)
    plt.xlabel('x1', fontsize=10)
    plt.ylabel('x2', fontsize=10)
    plt.xlim(-5.12, 5.12)
    plt.ylim(-5.12, 5.12)
    plt.legend(loc='lower left')
    plt.tight_layout()
    plt.show()
结果分析

模拟退火算法是单点搜索算法,全局中更新的只有一个状态,所以结果会出现很大的随机性,非常不稳定,容易陷入局部最优问题。

观察到收敛曲线震荡是因为算法接受了劣解,所以会出现适应度跳变的情况。

多点并行

本文设计了一种多起点并行退火策略(Multi-start Parallel SA) 。该策略在搜索空间内并行部署 <math xmlns="http://www.w3.org/1998/Math/MathML"> M M </math>M 个独立的退火马尔可夫链。虽然单链仍保持单点勘探(Exploration)特征,但群体层面的多种子广度覆盖,显著降低了单一单点算法对初始状态的敏感性,起到了全局统计学保底的作用。

整体修改不大,只需要将涉及到 x 的位置由(dim,)-->(m_seeds,dim)转化为矩阵计算。

python 复制代码
"""模拟退火算法(并行)"""

import numpy as np


class SA_Parallel:
    """模拟退火算法(并行)"""

    def __init__(self, L=100, dim=1, p_start=0.85, t_end=1e-6, alpha=0.98, m_seeds=1):
        """初始化"""
        self.L = L
        self.dim = dim
        self.p_start = p_start
        self.t_end = t_end
        self.alpha = alpha
        self.fitness_history = None
        self.m_seeds = m_seeds

    def _template_init(self, fitness_function, limit):
        """
        初始化温度 T_start
            T_start = -ΔE_avg/lnP_start
        """
        # 1.迭代 100 轮计算 ΔE
        delta_E_list = []
        for i in range(100):
            x1 = np.random.uniform(limit[:, 0], limit[:, 1], size=self.dim)
            x2 = np.random.uniform(limit[:, 0], limit[:, 1], size=self.dim)
            delta_E = abs(fitness_function(x1) - fitness_function(x2))
            delta_E_list.append(delta_E)
        # 2.计算 ΔE_avg ,代入计算 T_start
        delta_E_avg = np.mean(delta_E_list)
        t_start = -delta_E_avg / np.log(self.p_start)
        return t_start

    def _get_next(self, x, step_size, limits):
        """
        计算新解位置
            x_new = x + step_size * rand(-1,1)
        """
        # 1.添加随机扰动
        dx = np.random.uniform(-step_size, step_size, size=x.shape)
        x_new = x + dx

        lb, ub = limits[:, 0], limits[:, 1]  # 形状 (dim,)

        # 1. 处理上限越界:如果 x_new > ub,则反弹回 2*ub - x_new,否则保持原样
        x_new = np.where(x_new > ub, 2 * ub - x_new, x_new)

        # 2. 处理下限越界:如果 x_new < lb,则反弹回 2*lb - x_new,否则保持原样
        x_new = np.where(x_new < lb, 2 * lb - x_new, x_new)

        return np.clip(x_new, lb, ub)

    def optimize(self, fitness_function, limits):
        """算法逻辑"""
        # 1,初始化参数
        limits = np.atleast_2d(limits)  # 确保至少是二维的
        if limits.shape[0] == 1 and self.dim > 1:
            limits = np.tile(limits, (self.dim, 1))
        # 生成 m_seeds 个种子
        x = np.random.uniform(limits[:, 0], limits[:, 1], size=(self.m_seeds,self.dim))
        fitness = np.array([fitness_function(ind) for ind in x])

        template = self._template_init(fitness_function, limits)  # 初始化温度
        template_start = template

        # 全局历史最优记录 (从所有种子中挑个最好的)
        best_idx = np.argmin(fitness)
        x_best = np.copy(x[best_idx])
        fitness_best = fitness[best_idx]

        # 用于画图的收敛历史(记录每一轮里所有解之中的最好值)
        self.fitness_history = [fitness_best]

        # 2.迭代计算
        while template > self.t_end:
            # 外层循环控制 步长
            cur_step_size = (limits[:, 1] - limits[:, 0]) * (template / template_start) * 0.1

            for _ in range(self.L):
                # 内层更新位置
                x_new = self._get_next(x, cur_step_size, limits)

                # 计算所有种子的 ΔE
                fitness_new = np.array([fitness_function(ind) for ind in x_new])
                delta_E = fitness_new - fitness

                # 对每一个独立的退火进行判定
                for s in range(self.m_seeds):
                    if delta_E[s] < 0:
                        x[s] = x_new[s]
                        fitness[s] = fitness_new[s]
                        # 更新全局最优
                        if fitness_new[s] < fitness_best:
                            x_best = np.copy(x_new[s])
                            fitness_best = fitness_new[s]
                    else:
                        p = np.exp(-delta_E[s] / template)
                        if np.random.rand() < p:
                            x[s] = x_new[s]
                            fitness[s] = fitness_new[s]

                # 记录当前这一步的全局最优历史,平滑
                # self.fitness_history.append(fitness_best)
                # 记录当前这一步,整个种群里实时表现最好的个体的适应度,震荡
                current_population_best = np.min(fitness)
                self.fitness_history.append(current_population_best)

            # 更新温度
            template *= self.alpha

        return x_best, fitness_best

测试效果主函数:

python 复制代码
if __name__ == "__main__":
    def func(x):
        return 10 * len(x) + np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))


    sa = SA_Parallel(dim=2,m_seeds=5)
    limit = [[-5.12, 5.12], [-5.12, 5.12]]
    x_best, fitness_best = sa.optimize(func, limit)

    print(f'最小值位置:{x_best}')
    print(f'最佳适应值:{fitness_best}')

    import matplotlib.pyplot as plt

    # 允许 matplotlib 显示中文
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False

    # 提取历史记录
    fitness_history = np.array(sa.fitness_history)
    total_steps = len(fitness_history)

    # 收敛曲线图
    plt.figure(figsize=(10, 5), dpi=100)
    plt.plot(fitness_history, color='#1f77b4', linewidth=1.5, label='当前解适应度')
    plt.axhline(y=0, color='r', linestyle='--', alpha=0.6, label='理论全局最优值(0)')
    plt.title(f'模拟退火算法(SA)收敛曲线(总评估次数: {total_steps}次)', fontsize=12)
    plt.xlabel('内层迭代总步数 (Evaluation Steps)', fontsize=10)
    plt.ylabel('适应度函数值 (Energy / Fitness)', fontsize=10)
    plt.grid(True, linestyle=':', alpha=0.6)
    plt.legend(loc='upper right')

    # 局部放大图提示:你会看到前中期曲线上有很多"向上跳跃"的红点,那就是 Metropolis 准则在发挥作用
    plt.tight_layout()
    plt.show()
结果分析
  • 记录适应度方法1:仅在全局最优的适应度更新时记录,曲线整体平滑下降
  • 记录适应度方法2:记录每一次迭代中所有种子的最优适应度,会有概率 P 往上走,曲线震荡

TSP 问题

旅行商问题,寻找经过所有位置的最短回路。

  • 初始化状态或更新状态:由修改一个数变成修改一个序列的顺序。
  • 路径长度计算:计算一个路径的长度
  • 算子选择:可以选择一个,也可以多个混合
python 复制代码
"""模拟退火算法(并行) TSP问题"""

import numpy as np


class SA_Parallel:
    """模拟退火算法(并行)"""

    def __init__(self, L=100, p_start=0.85, t_end=1e-6, alpha=0.98, m_seeds=1):
        """初始化"""
        self.L = L
        self.p_start = p_start
        self.num_cities = None
        self.t_end = t_end
        self.alpha = alpha
        self.fitness_history = None
        self.m_seeds = m_seeds

    def _template_init(self, dist_matrix):
        """
        初始化温度 T_start
            T_start = -ΔE_avg/lnP_start
        """
        # 1.迭代 100 轮计算 ΔE
        delta_E_list = []
        for i in range(100):
            x1 = np.random.permutation(self.num_cities)
            x2 = np.random.permutation(self.num_cities)
            delta_E = abs(self.get_single_path_distance(x1, dist_matrix) -
                          self.get_single_path_distance(x2, dist_matrix))
            delta_E_list.append(delta_E)
        # 2.计算 ΔE_avg ,代入计算 T_start
        delta_E_avg = np.mean(delta_E_list)
        t_start = -delta_E_avg / np.log(self.p_start)
        return t_start

    def get_single_path_distance(self, path, dist_matrix):
        """ 计算单条 TSP 路径的回路总长度 """
        go_distance = np.sum(dist_matrix[path[:-1], path[1:]])
        back_distance = dist_matrix[path[-1], path[0]]
        return go_distance + back_distance

    def _get_next(self, x, step_size):
        """
        计算新解位置
            策略1:交换算子,交换任意两个城市位置
            策略2:逆序算子,逆序随机一个区间的城市(使用)
            策略3:插入算子,随机拔出一个城市,随机插入到路径
        """
        max_span = max(15, int(step_size))
        x_new = np.copy(x)

        # 交换算子0.2+逆序算子0.8
        for s in range(self.m_seeds):
            if np.random.rand() < 0.8:
                # ------ 策略 1:逆序算子(保持你测试出的保底 15 跨度) ------
                span = np.random.randint(2, max_span + 1)
                start_idx = np.random.randint(0, self.num_cities - span + 1)
                end_idx = start_idx + span
                x_new[s, start_idx:end_idx] = x_new[s, start_idx:end_idx][::-1]
            else:
                # ------ 策略 2:基于步长约束的交换算子 ------
                # 随机选第一个城市
                idx1 = np.random.randint(0, self.num_cities)
                # 第二个城市不能离得太远,受当前步长限制(后期退化为近邻交换,极其精准)
                max_swap_dist = max(2, int(step_size * 0.5))
                swap_dist = np.random.randint(1, max_swap_dist + 1)

                # 环形边界处理(防止越界)
                idx2 = (idx1 + swap_dist) % self.num_cities

                # 执行硬交换
                x_new[s, idx1], x_new[s, idx2] = x_new[s, idx2], x_new[s, idx1]

        return x_new

    def optimize(self, dist_matrix):
        """算法逻辑"""
        # 1.初始化原始路径
        self.num_cities = dist_matrix.shape[0]
        x = np.array([np.random.permutation(self.num_cities) for _ in range(self.m_seeds)])
        # 初始化温度
        template = self._template_init(dist_matrix)
        template_start = template

        # 全局历史最优记录 (从所有种子中挑路径最短的)
        dist = np.array([self.get_single_path_distance(path, dist_matrix) for path in x])
        best_idx = np.argmin(dist)
        x_best = np.copy(x[best_idx])
        dist_best = dist[best_idx]

        # 用于画图的收敛历史
        self.fitness_history = [dist_best]

        # 2.迭代计算
        while template > self.t_end:
            # 外层循环控制 步长
            cur_step_size = (self.num_cities * 0.5) * (template / template_start)

            for _ in range(self.L):
                # 内层更新位置
                x_new = self._get_next(x, cur_step_size)

                # 计算所有种子的 ΔE
                dist_new = np.array([self.get_single_path_distance(path, dist_matrix) for path in x_new])
                delta_E = dist_new - dist

                # 对每一个独立的退火进行判定
                for s in range(self.m_seeds):
                    if delta_E[s] < 0:
                        x[s] = x_new[s]
                        dist[s] = dist_new[s]
                        # 更新全局最优
                        if dist_new[s] < dist_best:
                            x_best = np.copy(x_new[s])
                            dist_best = dist_new[s]
                    else:
                        p = np.exp(-delta_E[s] / template)
                        if np.random.rand() < p:
                            x[s] = x_new[s]
                            dist[s] = dist_new[s]

                # 记录当前这一步的全局最优历史,平滑
                # self.fitness_history.append(dist_best)
                # 记录当前这一步,整个种群里实时表现最好的个体的适应度,震荡
                current_population_best = np.min(dist)
                self.fitness_history.append(current_population_best)

            # 更新温度
            template *= self.alpha

        return x_best, dist_best

使用 berlin52数据集 进行测试算法的能力:

python 复制代码
if __name__ == "__main__":
    # 1. berlin52数据集 52个坐标
    coords = np.array([[565.0, 575.0], [25.0, 185.0], [345.0, 750.0], [945.0, 685.0], [845.0, 655.0],
                       [880.0, 660.0], [25.0, 230.0], [525.0, 1000.0], [580.0, 1175.0], [650.0, 1130.0],
                       [1605.0, 620.0], [1220.0, 580.0], [1465.0, 200.0], [1530.0, 5.0], [845.0, 680.0],
                       [725.0, 370.0], [145.0, 665.0], [415.0, 635.0], [510.0, 875.0], [560.0, 365.0],
                       [300.0, 465.0], [520.0, 585.0], [480.0, 415.0], [835.0, 625.0], [975.0, 580.0],
                       [1215.0, 245.0], [1320.0, 315.0], [1250.0, 400.0], [660.0, 180.0], [410.0, 250.0],
                       [420.0, 555.0], [575.0, 665.0], [1150.0, 1160.0], [700.0, 580.0], [685.0, 595.0],
                       [685.0, 610.0], [770.0, 610.0], [795.0, 645.0], [720.0, 635.0], [760.0, 650.0],
                       [475.0, 960.0], [95.0, 260.0], [875.0, 920.0], [700.0, 500.0], [555.0, 815.0],
                       [830.0, 485.0], [1170.0, 65.0], [830.0, 610.0], [605.0, 625.0], [595.0, 360.0],
                       [1340.0, 725.0], [1740.0, 245.0]])

    # 2. 一次性预计算距离矩阵 (C语言级矩阵广播,速度极快)
    num_cities = len(coords)
    dist_matrix = np.sqrt(np.sum((coords[:, np.newaxis, :] - coords[np.newaxis, :, :]) ** 2, axis=-1))

    # 3. 实例化算法
    sa_tsp = SA_Parallel(L=300, m_seeds=12, alpha=0.98)
    x_best, dist_best = sa_tsp.optimize(dist_matrix)

    print(f"【优化成功】")
    print(f"最优路线城市顺序:\n{x_best}")
    print(f"跑出来的最短总回路长度:{dist_best:.2f} (官方理论最优值是:7542)")

    import matplotlib.pyplot as plt

    # 1. 允许 matplotlib 显示中文
    plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'sans-serif']
    plt.rcParams['axes.unicode_minus'] = False

    # 2. 创建画布 (左图看收敛速度和震荡,右图看最终路线拓扑)
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

    # ---- 左图:收敛历史曲线 ----
    fitness_history = np.array(sa_tsp.fitness_history)
    ax1.plot(fitness_history, color='#1f77b4', linewidth=1.5, label='当前种群最优')
    ax1.axhline(y=7542, color='r', linestyle='--', alpha=0.7, label='官方理论最优 (7542)')
    ax1.set_title("模拟退火算法 - 协同赛马收敛轨迹", fontsize=12, fontweight='bold')
    ax1.set_xlabel("内层迭代总步数 (外层循环 × L)", fontsize=10)
    ax1.set_ylabel("TSP 回路总长度", fontsize=10)
    ax1.grid(True, linestyle=':', alpha=0.6)
    ax1.legend(loc='upper right')

    # ---- 右图:Berlin52 最终路线可视化 ----
    # 按照求出的最优顺序重排坐标,别忘了闭环(首尾相连)
    best_path_idx = list(x_best) + [x_best[0]]
    ordered_coords = coords[best_path_idx]

    # 画出城市节点
    ax2.scatter(coords[:, 0], coords[:, 1], color='#e377c2', s=40, zorder=3, label='城市')
    # 给城市标上序号
    for i, (x, y) in enumerate(coords):
        ax2.text(x + 10, y + 10, str(i), fontsize=8, color='#333333', zorder=4)

    # 画出巡回连线
    ax2.plot(ordered_coords[:, 0], ordered_coords[:, 1], color='#2ca02c', linewidth=2, alpha=0.8, label='最优航线')
    ax2.set_title(f"Berlin52 最终路线轨迹 (总长: {dist_best:.2f})", fontsize=12, fontweight='bold')
    ax2.set_xlabel("X 坐标", fontsize=10)
    ax2.set_ylabel("Y 坐标", fontsize=10)
    ax2.grid(True, linestyle=':', alpha=0.5)
    ax2.legend(loc='lower left')

    plt.tight_layout()
    plt.show()

结果分析:

最终回路总长与理论最优值7542的偏差控制在±500以内(即相对误差约±6.6%以内)。

上图为仅使用 逆序算子 策略的解法,整体趋近结果但是由于策略限制后期的细节优化上无法精准。

实验结果表明,单一的逆序算子Inversion Operator 容易陷入特定拓扑死结。而引入 2-Opt 交换算子(Swap Operator) 进行 20% 的邻域微调,能够有效破坏路径交叉,显著提升中后期细节微调的精度。

第三方库

SciPy 库提供的双重退火算法(Dual Annealing)展现出极佳的性能,其核心在于:一方面利用 Tsallis 广义非延伸统计力学 建立非高斯长尾访问分布(Visiting Distribution),允许解在空间中实现 Lévy 飞行式的大跨度全局跃迁;另一方面在降温各阶段无缝嵌入 L-BFGS-B 局部拟牛顿算子,实现了"全局粗勘"向"局部高精微雕"的双重耦合。

python 复制代码
@_transition_to_rng("seed", position_num=10)
def dual_annealing(func, bounds, args=(), maxiter=1000,
                   minimizer_kwargs=None, initial_temp=5230.,
                   restart_temp_ratio=2.e-5, visit=2.62, accept=-5.0,
                   maxfun=1e7, rng=None, no_local_search=False,
                   callback=None, x0=None):
                   
    # 1.边界处理
    if isinstance(bounds, Bounds):
        bounds = new_bounds_to_old(bounds.lb, bounds.ub, len(bounds.lb))

    if x0 is not None and not len(x0) == len(bounds):
        raise ValueError('Bounds size does not match x0')

    lu = list(zip(*bounds))
    lower = np.array(lu[0]) # 下界
    upper = np.array(lu[1]) # 上界
    # restart_temp_ratio:重启温度比率,用于重新退火
    if restart_temp_ratio <= 0. or restart_temp_ratio >= 1.:
        raise ValueError('Restart temperature ratio has to be in range (0, 1)')
    # 检测边界合法性
    if (np.any(np.isinf(lower)) or np.any(np.isinf(upper)) or np.any(
            np.isnan(lower)) or np.any(np.isnan(upper))):
        raise ValueError('Some bounds values are inf values or nan values')
    if not np.all(lower < upper):
        raise ValueError('Bounds are not consistent min < max')
    if not len(lower) == len(upper):
        raise ValueError('Bounds do not have the same dimensions')

    # 封装适应度函数
    func_wrapper = ObjectiveFunWrapper(func, maxfun, *args)

    # 局部优化器参数
    minimizer_kwargs = minimizer_kwargs or {}

    # 局部优化器
    minimizer_wrapper = LocalSearchWrapper(
        bounds, func_wrapper, *args, **minimizer_kwargs)

    # 随机数生成器
    rng_gen = check_random_state(rng)
    # 能量管理,管理当前解、历史最优解等状态信息
    energy_state = EnergyState(lower, upper, callback)
    energy_state.reset(func_wrapper, rng_gen, x0)
    # 重启温度阈值计算
    temperature_restart = initial_temp * restart_temp_ratio
    # 生成新的候选解(长尾分布)
    visit_dist = VisitingDistribution(lower, upper, visit, rng_gen)
    # 协调整个退火流程的核心控制器
    strategy_chain = StrategyChain(accept, visit_dist, func_wrapper,
                                   minimizer_wrapper, rng_gen, energy_state)
    need_to_stop = False
    iteration = 0
    message = []
    # 返回结果
    optimize_res = OptimizeResult()
    optimize_res.success = True
    optimize_res.status = 0

    t1 = np.exp((visit - 1) * np.log(2.0)) - 1.0
    # 循环
    while not need_to_stop:
        for i in range(maxiter):
            # 温度计算
            s = float(i) + 2.0
            t2 = np.exp((visit - 1) * np.log(s)) - 1.0
            temperature = initial_temp * t1 / t2
            # 最大迭代次数检查
            if iteration >= maxiter:
                message.append("Maximum number of iteration reached")
                need_to_stop = True
                break
            # 重启检查
            if temperature < temperature_restart:
                energy_state.reset(func_wrapper, rng_gen)
                break
            # 核心退火步骤
            val = strategy_chain.run(i, temperature)
            if val is not None:
                message.append(val)
                need_to_stop = True
                optimize_res.success = False
                break
            # 局部搜索
            if not no_local_search:
                val = strategy_chain.local_search()
                if val is not None:
                    message.append(val)
                    need_to_stop = True
                    optimize_res.success = False
                    break
            iteration += 1

    # 设置返回结果
    optimize_res.x = energy_state.xbest
    optimize_res.fun = energy_state.ebest
    optimize_res.nit = iteration
    optimize_res.nfev = func_wrapper.nfev
    optimize_res.njev = func_wrapper.ngev
    optimize_res.nhev = func_wrapper.nhev
    optimize_res.message = message
    return optimize_res

Main 函数实现 Rastrigin 函数求最小值:

python 复制代码
""" 第三方库实现 SA"""

from scipy.optimize import dual_annealing
import numpy as np


def func(x):
    return 10 * len(x) + np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))

if __name__ == "__main__":
    bounds = [[-5.12,5.12],[-5.12,5.12]]

    res = dual_annealing(
        func,
        bounds=bounds,
        seed=5
    )
    print(f'最小值位置:{res.x}')
    print(f'最佳适应值:{res.fun}')

参数实验

整个算法实现下来,有两个超参数对算法的结果有很大影响:

  • 冷却速率 α :用于控制降温的速度, α 越小降温越快 ,α=0.85, 0.90, 0.95, 0.99

α 越大如 0.99 时的图像一直在震荡因为温度下降得慢 T 始终很大导致接受劣解 P 很大,但是结果是优于其他情况的,因为降得慢所以迭代的次数更多。

  • 初始接受概率 P_start:对劣解的接受程度,P_start=0.7, 0.85, 0.95

P_start 越大代表整体对劣解的接受概率越大曲线越震荡。

前沿进展

  • 量子退火与经典退火的融合:算法在后期低温时,不需要费力爬坡,而是可以直接"穿墙而过",横向击穿极高、极窄的局部阻碍势垒。这极大地加快了在高度非线性(如 Rastrigin 或超大规模 TSP)问题中的收敛速度。

  • 强化学习作为内核嵌入 SA :算法能够"边跑边学"。在高温期自动加大步长去冲撞大局,在低温期敏锐地捕捉到陷入局部的信号,自动切换为高精度的近邻"微雕手术",实现了邻域搜索拓扑结构的动态自适应闭环控制

总结

模拟退火算法是优化算法中单点搜索算法的代表,搜索空间复杂度低、Metropolis准则跳出局部最优、理论上以概率1收敛。

当前实现中目前种子之间互不相识。如果在代码的每 <math xmlns="http://www.w3.org/1998/Math/MathML"> K K </math>K 轮外层循环后,加入一个信息共享机制 ------比如表现最差的种子有概率被表现最好的种子"强行同化"或在其周围重新采样,这就是典型的并行回火(Parallel Tempering)或群体协同退火

维度 SA PSO GA
搜索模式 单点+概率接受 群体+社会分享 群体+自然进化
核心机制 Metropolis准则 速度/位置更新 选择/交叉/变异
空间复杂度 O(d) O(N·d) O(N·d)
跳出局部最优 温度控制下的随机跳跃 个体记忆+随机项 变异+交叉重组
适用场景 组合优化(TSP等) 连续函数优化 组合+连续

参考文献:

  1. Kirkpatrick, S., Gelatt, C. D., & Vecchi, M. P. (1983). Optimization by simulated annealing. Science, 220(4598), 671-680.
  2. Metropolis, N., Rosenbluth, A. W., Rosenbluth, M. N., Teller, A. H., & Teller, E. (1953). Equation of state calculations by fast computing machines. The Journal of Chemical Physics, 21(6), 1087-1092.
  3. Swendsen, R. H., & Wang, J. S. (1986). Replica-exchange online simulation of spin glasses. Physical Review Letters, 57(21), 2607.
  4. Lin, S., & Kernighan, B. W. (1973). An effective heuristic algorithm for the traveling-salesman problem. Operations Research, 21(2), 498-516.
相关推荐
努力努力再努力wz6 小时前
【Redis入门系列】:从 hashtable到 listpack:深入理解 Hash 底层编码、字段级过期、核心命令与缓存应用
开发语言·数据结构·数据库·c++·redis·算法·缓存
zhaokuangkuang_6 小时前
Java学习
java·学习·算法
陈辛chenxin6 小时前
【数据挖掘01】相似度算法大全(万字讲解)
算法·数据挖掘·代理模式
无限进步_6 小时前
【C++】智能指针的设计逻辑:RAII与资源安全
c++·算法·安全
江屿风7 小时前
C++OJ题经验总结(竞赛)2
开发语言·c++·笔记·算法
mjhcsp7 小时前
P1034 [NOIP 2002 提高组] 矩形覆盖题解
算法·深度优先
AI职业加油站7 小时前
从政策到实战:人工智能算法工程师证书的完整价值分析
人工智能·python·学习·算法·职场和发展
炸膛坦客7 小时前
嵌入式 - 数据结构与算法:(1-11)排序算法 - 选择排序(Selection Sort)
数据结构·算法·排序算法
艾iYYY7 小时前
详解string类的基础用法
c语言·开发语言·c++·算法