2763 words
14 minutes
模拟退火算法
首次发布: 2025-04-08
... 次访问

概述#

模拟退火 (simulated annealing) 算法是一种用于求解最优化问题的随机化算法。它的灵感来源于物理退火过程的启发,通过模拟固体加温、等温、冷却等过程,结合 Metropolis 采样准则进行科学计算的一种启发式算法。模拟退火算法在解决组合优化问题、函数优化问题等方面表现出色,尤其适用于大规模复杂问题的求解。

特点:

  • 相较于局部搜索算法,模拟退火算法可以在较短的时间内找到最优近似解,且不会陷入局部最优解中
  • 模拟退火算法允许任意选取初始解和随机数序列,对初始值的选取较为鲁棒
  • 模拟退火算法能应用于多种组合优化问题,如旅行商问题、背包问题等

算法思想#

物理学中有原理——物质总是趋于能量最低的状态(最稳定的状态)。 退火就是物质从高能态到低能态的过程。将固体加热到高温后,分子会呈现随机排列状体,然后逐步降温使之冷却,分子会逐渐趋于有序排列,从而物质达到某种稳定的低能状态。

因此,不妨把函数下降求最小值的问题看作是一个物理系统的能量最小化问题。把优化的目标函数视为一个能量函数,即可通过模拟物理退火过程来求解最优化问题。

  • 将解 xx 看作是物理系统的状态
  • 目标函数 f(x)f(x) 即为能量函数
  • 目标函数的最优解 xoptx_{opt} 就对应于能量最低的状态
  • 设定优化的初始温度为 TT 的过程对应于加温过程
  • Metropolis 采样对应于等温过程
  • 控制参数的下降对应于冷却过程

在解空间中随机游走,使用 Metropolis 抽样准则来使解状态逐渐收敛于全局最优解。

Algorithm Simulated Annealing

Input: Initial Temperature T0T_0, Initial state x0x_0, Objective Function f(x)f(x)
Output: Optimal State xoptx_{opt}
1: Let state x=x0x = x_0, optimal state xopt=x0x_{opt} = x_0 and temperature T=T0T = T_0
2: repeat
3:      repeat
4:           Randomly generate a new neighbouring state xnew=Random(x)x_{new} = \text{Random}(x)
5:           Compute probability p(x,xnew)=min{ef(xnew)f(x)T,1}\large p(x, x_{new}) = \min\{e^{-\frac{f(x_{new}) - f(x)}{T}}, 1\}
6:           Accept the new state at a probability of p(x,xnew)p(x, x_{new}), i.e. if rand(0,1)<p(x,xnew)\text{rand}(0, 1) < p(x, x_{new}), then let x=xnewx = x_{new}
7:      until the new state is accepted
8:      if f(x)<f(xopt)f(x) < f(x_{opt}), then let xopt=xx_{opt} = x
9:      Decrease the temperature TT according to a cooling schedule
10: until stopping criteria is met
11: Return the best state xoptx_{opt} found so far

Metropolis 采样准则

  • 如果新解比当前解更优,则接受新解 (这也是为什么伪代码中会出现 min\min11 的原因)
  • 如果新解比当前解差,则以一定概率接受新解,概率为 p(x,xnew)=ef(xnew)f(x)Tp(x, x_{new}) = e^{-\frac{f(x_{new}) - f(x)}{T}},其中 TT 是当前温度。
  • 该概率随着温度的降低而减小,最终趋近于 00。 体现为在高温下,可以接受与当前状态差的多的新状态,从而避免陷入局部极小值,在低温下,接受较差的新状态的概率减小,最终趋近于 00,即不接受新状态。
  • 物理学中的 Metropolis 采样准则是基于 Boltzmann 分布的,即粒子在温度 TT 时趋于热平衡的概率为
p(x)=eΔE(x)kTZp(x) = \frac{e^{-\frac{\Delta E(x)}{kT}}}{Z}

     其中 E(x)E(x) 是能量函数,kk 是玻尔兹曼常数,ZZ 是配分函数。

冷却过程

  • 冷却过程是模拟退火算法的关键,决定了算法的收敛速度和最终结果的质量。
  • 冷却过程可以采用线性冷却、指数冷却、对数冷却等方式。常用的冷却方式是指数冷却,即每次迭代时将温度乘以一个小于 11 的常数 α\alpha,即 Tnew=αToldT_{new} = \alpha T_{old},其中 0<α<10 < \alpha < 1
  • 也可以采用线性冷却,即每次迭代时将温度减去一个常数 CC,即 Tnew=ToldCT_{new} = T_{old} - C,其中 CC 是一个小于 ToldT_{old} 的常数。
  • 冷却速率的选择会影响算法的收敛速度和最终结果的质量。冷却速率过快可能导致算法陷入局部最优解,冷却速率过慢则会导致算法收敛速度过慢。

初始温度的选择

  • 初始温度的选择会影响算法的收敛速度和最终结果的质量。初始温度过高可能导致算法收敛速度过快,初始温度过低则可能导致算法陷入局部最优解。
  • 初始温度的选择可以通过经验法则、试验法则等方式进行选择。常用的经验法则是将初始温度设置为目标函数值的范围,即 T0=max(f(x))min(f(x))T_0 = \max(f(x)) - \min(f(x)),其中 f(x)f(x) 是目标函数。
def simulated_annealing(objective_function, initial_state, random_neighbor, 
                       initial_temp=100, cooling_rate=0.95, min_temp=1e-10, max_iterations=1000):
    """
    模拟退火算法的通用实现
    
    参数:
    - objective_function: 目标函数,用于评估解的质量
    - initial_state: 初始解状态
    - random_neighbor: 产生随机邻居解的函数
    - initial_temp: 初始温度
    - cooling_rate: 冷却率 (0 < cooling_rate < 1)
    - min_temp: 最小温度,算法终止条件
    - max_iterations: 每个温度下的最大迭代次数
    
    返回:
    - 找到的最优解
    - 最优解对应的目标函数值
    """
    current_state = initial_state
    current_energy = objective_function(current_state)
    
    best_state = current_state
    best_energy = current_energy
    
    temp = initial_temp
    
    # 存储能量变化,用于可视化
    energy_history = [current_energy]
    temp_history = [temp]
    
    # 主循环
    while temp > min_temp:
        for _ in range(max_iterations):
            # 生成新状态
            new_state = random_neighbor(current_state)
            new_energy = objective_function(new_state)
            
            # 计算能量变化
            delta_energy = new_energy - current_energy
            
            # Metropolis准则:接受更好的解或以一定概率接受较差的解
            if delta_energy < 0 or random.random() < math.exp(-delta_energy / temp):
                current_state = new_state
                current_energy = new_energy
                
                # 更新全局最优解
                if current_energy < best_energy:
                    best_state = current_state
                    best_energy = current_energy
            
            energy_history.append(current_energy)
            temp_history.append(temp)
        
        # 降温
        temp *= cooling_rate
    
    return best_state, best_energy, energy_history, temp_history

# 示例:寻找函数 f(x) = x^2 + 10*sin(x) 在区间 [-10, 10] 的最小值
def objective_function(x):
    return x**2 + 10*np.sin(x)

def random_neighbor(x, step_size=0.5):
    """生成当前解附近的随机解"""
    return x + (random.random() - 0.5) * step_size

# 执行模拟退火算法
initial_x = random.uniform(-10, 10)
best_x, best_value, energy_history, temp_history = simulated_annealing(
    objective_function, 
    initial_x, 
    random_neighbor,
    initial_temp=100,
    cooling_rate=0.97,
    max_iterations=100
)

print(f"找到的最优解 x = {best_x:.6f}")
print(f"对应的函数值 f(x) = {best_value:.6f}")

实例应用#

旅行商问题 (TSP)#

数学描述为,给定 nn 个城市和它们之间的距离 dij, i,j{1,2,,n}d_{ij},\ i,j \in \{1, 2, \dots, n\},要求找到一条经过每个城市一次且仅一次的最短路径。

解空间 X\mathcal{X} 是遍历每个城市恰好一次的回路,可以表示为 {1,2,,n}\{1, 2, \dots, n\} 的全部循环排列。记状态为 x=(x1,x2,,xn)x = (x_1, x_2, \dots, x_n),其中 xix_i 表示第 ii 个城市的编号。目标函数为路径长度,即 f(x)=dx1x2+dx2x3++dxn1xn+dxnx1f(x) = d_{x_1 x_2} + d_{x_2 x_3} + \dots + d_{x_{n-1} x_n} + d_{x_n x_1}

新解的产生可以通过互换操作插入操作逆序操作来实现。

  • 互换操作:随机选择两个城市 iijj,交换它们在路径中的位置。
  • 插入操作:随机选择一个城市 ii 和一个位置 jj,将城市 ii 插入到路径中的位置 jj
  • 逆序操作:随机选择两个城市 iijj,将它们之间的路径逆序。

e.g. 设当前路径为 x=(1,2,3,4,5,6,7,8,9)x = (1, 2, 3, 4, 5, 6, 7, 8, 9),互换操作产生的新解为 xnew=(1,8,3,4,5,6,7,2,9)x_{new} = (1, \underline{8}, 3, 4, 5, 6, 7, \underline{2}, 9),插入操作产生的新解为 xnew=(1,8,2,3,4,5,6,7,9)x_{new} = (1, \underline{8}, 2, 3, 4, 5, 6, 7, 9),逆序操作产生的新解为 xnew=(1,8,7,6,5,4,3,2,9)x_{new} = (1, \underline{8, 7, 6, 5, 4, 3, 2}, 9)

import numpy as np
import random
import math
import matplotlib.pyplot as plt
from typing import List, Tuple

def generate_distance_matrix(n_cities: int, seed: int = 42) -> np.ndarray:
    """
    生成随机城市坐标和距离矩阵
    
    参数:
    - n_cities: 城市数量
    - seed: 随机种子
    
    返回:
    - distances: 距离矩阵
    - coords: 城市坐标
    """
    random.seed(seed)
    np.random.seed(seed)
    
    # 生成随机坐标
    coords = np.random.rand(n_cities, 2) * 100
    
    # 计算距离矩阵
    distances = np.zeros((n_cities, n_cities))
    for i in range(n_cities):
        for j in range(n_cities):
            if i != j:
                # 欧几里得距离
                distances[i, j] = np.sqrt(np.sum((coords[i] - coords[j])**2))
    
    return distances, coords

def calculate_path_length(path: List[int], distances: np.ndarray) -> float:
    """
    计算路径长度
    
    参数:
    - path: 路径
    - distances: 距离矩阵
    
    返回:
    - path_length: 路径长度
    """
    path_length = 0
    for i in range(len(path) - 1):
        path_length += distances[path[i], path[i+1]]
    # 加上从最后一个城市回到起点的距离
    path_length += distances[path[-1], path[0]]
    return path_length

def random_path(n_cities: int) -> List[int]:
    """
    生成随机路径
    
    参数:
    - n_cities: 城市数量
    
    返回:
    - path: 随机路径
    """
    path = list(range(n_cities))
    random.shuffle(path)
    return path

def generate_neighbor(path: List[int]) -> List[int]:
    """
    生成邻居解(使用三种操作之一:互换、插入或逆序)
    
    参数:
    - path: 当前路径
    
    返回:
    - new_path: 新路径
    """
    new_path = path.copy()
    n = len(path)
    
    # 随机选择操作类型
    operation = random.choice(["swap", "insert", "reverse"])
    
    if operation == "swap":
        # 互换操作:随机选择两个城市并交换它们的位置
        i, j = random.sample(range(n), 2)
        new_path[i], new_path[j] = new_path[j], new_path[i]
    
    elif operation == "insert":
        # 插入操作:随机选择一个城市,并将其移动到一个新位置
        i, j = random.sample(range(n), 2)
        city = new_path.pop(i)
        new_path.insert(j, city)
    
    else:  # operation == "reverse"
        # 逆序操作:随机选择两个位置,并将它们之间的路径逆序
        i, j = sorted(random.sample(range(n), 2))
        new_path[i:j+1] = reversed(new_path[i:j+1])
    
    return new_path

def simulated_annealing_tsp(distances: np.ndarray, 
                           initial_temp: float = 1000.0, 
                           cooling_rate: float = 0.995, 
                           min_temp: float = 1e-8,
                           iterations_per_temp: int = 100) -> Tuple[List[int], float, List[float], List[float]]:
    """
    使用模拟退火算法解决旅行商问题
    
    参数:
    - distances: 城市间距离矩阵
    - initial_temp: 初始温度
    - cooling_rate: 冷却率
    - min_temp: 最小温度
    - iterations_per_temp: 每个温度下的迭代次数
    
    返回:
    - best_path: 找到的最佳路径
    - best_length: 最佳路径长度
    - length_history: 路径长度历史
    - temp_history: 温度历史
    """
    n_cities = distances.shape[0]
    
    # 初始化随机路径
    current_path = random_path(n_cities)
    current_length = calculate_path_length(current_path, distances)
    
    best_path = current_path.copy()
    best_length = current_length
    
    temp = initial_temp
    
    # 存储历史数据用于可视化
    length_history = [current_length]
    temp_history = [temp]
    
    # 主循环
    while temp > min_temp:
        for _ in range(iterations_per_temp):
            # 生成新解
            new_path = generate_neighbor(current_path)
            new_length = calculate_path_length(new_path, distances)
            
            # 计算能量差
            delta = new_length - current_length
            
            # Metropolis准则:接受更好的解或以一定概率接受较差的解
            if delta < 0 or random.random() < math.exp(-delta / temp):
                current_path = new_path
                current_length = new_length
                
                # 更新全局最优解
                if current_length < best_length:
                    best_path = current_path.copy()
                    best_length = current_length
            
            length_history.append(current_length)
            temp_history.append(temp)
        
        # 降温
        temp *= cooling_rate
    
    return best_path, best_length, length_history, temp_history

def plot_tsp_solution(coords, path, title="TSP Solution"):
    """绘制TSP解决方案"""
    plt.figure(figsize=(10, 8))
    
    # 绘制城市
    plt.scatter(coords[:, 0], coords[:, 1], c='red', s=50)
    
    # 绘制路径
    for i in range(len(path)):
        j = (i + 1) % len(path)
        plt.plot([coords[path[i], 0], coords[path[j], 0]], 
                 [coords[path[i], 1], coords[path[j], 1]], 'b-')
    
    # 添加城市编号标签
    for i, coord in enumerate(coords):
        plt.text(coord[0] + 1, coord[1] + 1, str(i), fontsize=10)
    
    plt.title(title)
    plt.grid(True)
    plt.tight_layout()
    plt.show()

def plot_convergence(length_history, temp_history):
    """绘制收敛过程"""
    plt.figure(figsize=(14, 6))
    
    # 绘制路径长度变化
    plt.subplot(1, 2, 1)
    plt.plot(length_history)
    plt.xlabel('迭代次数')
    plt.ylabel('路径长度')
    plt.title('路径长度随迭代变化')
    plt.grid(True)
    
    # 绘制温度变化
    plt.subplot(1, 2, 2)
    plt.plot(temp_history)
    plt.xlabel('迭代次数')
    plt.ylabel('温度')
    plt.title('温度随迭代变化')
    plt.yscale('log')
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()

# 主程序
if __name__ == "__main__":
    # 生成问题实例(20个城市)
    n_cities = 20
    distances, coords = generate_distance_matrix(n_cities)
    
    # 运行模拟退火算法
    initial_temp = 100.0
    cooling_rate = 0.995
    iterations_per_temp = 50
    
    print("开始求解TSP问题...")
    best_path, best_length, length_history, temp_history = simulated_annealing_tsp(
        distances, initial_temp, cooling_rate, min_temp=0.01, iterations_per_temp=iterations_per_temp
    )
    
    print(f"找到的最佳路径: {best_path}")
    print(f"最佳路径长度: {best_length:.2f}")
    
    # 可视化结果
    plot_tsp_solution(coords, best_path, f"TSP Solution - Length: {best_length:.2f}")
    plot_convergence(length_history, temp_history)

Comments Section