3. 模拟退火算法:物理背景、Metropolis准则、算法流程、温度衰减策略、代码实现与调参

3.1 物理背景:从铁匠铺到优化算法

模拟退火这个名字,其实是从冶金学里借来的。我当年第一次听到这个名词时,第一反应是:这跟打铁有什么关系?

你想想看,铁匠要把一块生铁锻造成一把好刀,得先把铁烧得通红,让原子获得足够能量四处乱跑,然后再慢慢冷却。冷却快了,原子来不及排列整齐,铁就变脆;冷却慢了,原子有足够时间找到能量最低的晶格位置,铁就变得坚韧。这个过程,就叫「退火」。

1983年,Kirkpatrick等人把这套物理过程搬到了优化领域。他们发现:组合优化问题里的「最优解」,就相当于金属晶体里的「最低能量状态」。而搜索过程中偶尔接受差解,就相当于高温下原子随机跳动——这恰恰是跳出局部最优的关键。

核心思想: 模拟退火算法通过控制「温度」参数,在搜索早期允许算法接受较差的解(探索),随着温度降低逐渐只接受更好的解(利用),最终收敛到全局最优附近。

3.2 Metropolis准则:接受还是拒绝?

这是模拟退火的心脏。说白了,就是回答一个问题:新解比当前解差,我到底要不要接受它?

Metropolis准则给出了一个概率公式:

P(接受差解) = exp( -ΔE / T )

其中:

  • ΔE = 新解的目标函数值 - 当前解的目标函数值(对于最小化问题,ΔE > 0 表示新解更差)
  • T = 当前温度

这个公式告诉我们三件事:

  1. ΔE 越小(差得不远),接受概率越大
  2. T 越高(温度高),接受概率越大
  3. ΔE 为负(新解更好),直接接受,概率为1

我的经验: 在实际项目中,我习惯把 ΔE 归一化到目标函数值的量级范围内,否则 exp(-ΔE/T) 很容易溢出或趋近于0。比如目标函数值在 1e4 量级,ΔE 动辄几百上千,T 如果设成 100,exp(-10) ≈ 0.000045,几乎永远不接受差解——那跟爬山法就没区别了。

3.3 算法流程:三步走

模拟退火的流程其实很简洁,我把它总结成三步:

  1. 初始化:设定初始温度 T₀、终止温度 T_end、衰减率 α,随机生成初始解 x₀
  2. 外循环(降温):在每个温度下,执行内循环;然后按衰减策略降温
  3. 内循环(搜索):在当前温度下,重复 N 次:
    • 从当前解 x 生成邻域解 x'
    • 计算 ΔE = f(x') - f(x)
    • 如果 ΔE < 0,接受 x'
    • 否则,以概率 exp(-ΔE/T) 接受 x'

当温度降到 T_end 以下,或者连续若干次降温都没有改进时,算法终止。

我曾经踩过的坑: 内循环次数设得太少。有一次做旅行商问题(TSP),我设了内循环次数 = 城市数量,结果算法跑出来跟随机搜索差不多。后来改成 10 倍城市数量,效果才明显改善。内循环次数决定了「在当前温度下充分探索」的程度,太少等于没降温就急急忙忙收工。

3.4 温度衰减策略:怎么降温?

降温策略直接影响算法能不能找到好解。我整理了几种常见策略:

策略名称 公式 特点 适用场景
指数衰减 T = T₀ × αk 最常用,α 通常取 0.85~0.99 通用场景,调参简单
线性衰减 T = T₀ - k × ΔT 降温快,容易陷入局部最优 对解质量要求不高、追求速度
对数衰减 T = T₀ / ln(1 + k) 理论收敛性最好,但太慢 学术研究、小规模问题
自适应衰减 根据接受率动态调整 效果好,但实现复杂 工业级应用、调参经验丰富

我个人最常用的是指数衰减,α 取 0.95 左右。为什么?因为它在「降温速度」和「搜索质量」之间取得了不错的平衡。α 太大(比如 0.999),降温太慢,跑起来急死人;α 太小(比如 0.8),降温太快,跟没退火一样。

一个实用技巧: 我习惯在代码里加一个「回温」机制——如果连续若干次降温都没有改进,就把温度回升到上一个有效温度。这相当于给算法一次「重启」的机会,有时候能救回来。

3.5 代码实现:Python 实战

下面是一个完整的模拟退火实现,用来求解一个简单的函数最小值问题。我尽量把注释写清楚,方便你直接拿去改。

import numpy as np
import matplotlib.pyplot as plt

def objective(x):
    """目标函数:f(x) = x^2 + 4*sin(5x) + 0.1*x"""
    return x**2 + 4*np.sin(5*x) + 0.1*x

def simulated_annealing(
    x0,                # 初始解
    T0=100,            # 初始温度
    T_end=1e-3,        # 终止温度
    alpha=0.95,        # 衰减率
    inner_iters=100,   # 内循环次数
    step_size=0.5      # 邻域搜索步长
):
    x_current = x0
    f_current = objective(x_current)
    x_best = x_current
    f_best = f_current
    
    T = T0
    history = []
    
    while T > T_end:
        for _ in range(inner_iters):
            # 生成邻域解:在当前解附近随机扰动
            x_new = x_current + np.random.uniform(-step_size, step_size)
            f_new = objective(x_new)
            
            delta = f_new - f_current
            
            # Metropolis 准则
            if delta < 0:
                x_current = x_new
                f_current = f_new
            else:
                p = np.exp(-delta / T)
                if np.random.rand() < p:
                    x_current = x_new
                    f_current = f_new
            
            # 记录全局最优
            if f_current < f_best:
                x_best = x_current
                f_best = f_current
        
        history.append((T, f_best))
        T *= alpha  # 指数衰减
    
    return x_best, f_best, history

# 运行
x_opt, f_opt, hist = simulated_annealing(x0=2.0)
print(f"最优解: x = {x_opt:.4f}, f(x) = {f_opt:.4f}")

调参建议: 这段代码里,step_size 很关键。设太大,新解可能跳到很远的地方,算法像无头苍蝇;设太小,只能在原地打转,探索能力不足。我一般从 step_size = 0.1 * (变量取值范围) 开始试,然后根据接受率调整——理想接受率在 0.4~0.6 之间。

3.6 调参实战:四个关键旋钮

模拟退火的调参,说白了就是拧四个旋钮。我按重要性排个序:

  1. 初始温度 T₀:太高了浪费时间,太低了直接变爬山法。一个经验法则是:让初始接受率在 0.8 左右。你可以先跑一小段,统计一下差解的 ΔE 均值,然后反推 T₀。
  2. 衰减率 α:0.85~0.99 之间。α 越大,搜索越精细,但耗时越长。我一般从 0.95 开始调。
  3. 内循环次数:跟问题规模正相关。对于 100 维的问题,内循环 500~1000 次不算多。
  4. 邻域生成方式:这是最容易被忽略的。对于连续问题,高斯扰动 vs 均匀扰动差别很大;对于离散问题,交换、反转、插入等操作的选择直接影响搜索效率。

我曾经犯过的错: 有一次做排产优化,我用了均匀扰动生成邻域解,结果算法总是在几个局部最优之间来回跳,就是找不到全局最优。后来改成高斯扰动(标准差随温度降低而减小),效果立竿见影。邻域生成方式一定要跟问题结构匹配,这是模拟退火调参里最「艺术」的部分。

3.7 知识结构总览

下面这张图把模拟退火的核心脉络串起来了,方便你对照复习。

模拟退火算法知识结构图 物理背景 Metropolis准则 算法流程 温度衰减策略 代码实现 调参实战 退火 → 优化 能量 → 目标函数 P = exp(-ΔE/T) ΔE<0 直接接受 初始化 → 外循环 内循环 → 终止 指数 / 线性 / 对数 Python 完整实现 T₀ / α / 内循环 / 邻域

3.8 小结

模拟退火是我个人非常喜欢的一种算法。它不要求目标函数可导,不要求凸性,实现起来也不复杂。但它的「温柔」也意味着——如果你不认真调参,它可能跑得比随机搜索还差。

记住三个关键点:Metropolis准则给了你跳出局部最优的勇气,温度衰减策略决定了你能走多远,邻域生成方式决定了你每一步踩得准不准。把这三点吃透了,模拟退火就是你工具箱里一把趁手的兵器。