【数学建模学习笔记】启发式算法:模拟退火算法

发布于:2025-09-09 ⋅ 阅读:(19) ⋅ 点赞:(0)

模拟退火算法(SA):零基础小白入门指南

如果你是零基础,想了解什么是模拟退火算法,看完这篇就能懂!我们会用最简单的语言,结合具体例子和代码,带你一步步认识这个强大的优化工具。

一、什么是模拟退火算法?

模拟退火算法(Simulated Annealing,简称 SA)是一种找最优解的方法,就像在迷宫里找出口 —— 它不仅能探索眼前的路,还会偶尔尝试绕远路,避免困在死胡同(局部最优解),最终找到全局最优解。

它的灵感来自物理退火过程:金属加热后慢慢冷却时,原子会逐渐排列成能量最低的稳定状态。算法模仿这个过程:先 “高温” 时大胆探索,再 “降温” 时逐渐稳定,最终找到最优解。

二、核心概念用大白话讲

  • 最优解:我们要找的目标。比如 “如何让函数值最小”“如何让成本最低”。
  • 适应度函数:判断当前解好不好的标准。比如求函数最小值时,函数值就是适应度,值越小越好。
  • 温度:算法 “大胆程度” 的指标。温度高时,敢尝试差的解(绕远路);温度低时,只接受好的解(走捷径)。
  • 邻域函数:生成 “附近的解” 的方法。比如当前位置是(2,3),邻域函数可能生成(2.1,3.2)这样的附近点。
  • 降温:温度逐渐降低(比如每次乘以 0.95),让算法从 “探索” 慢慢转向 “收敛”。

三、算法到底在做什么?(以例子为例)

模拟退火算法的步骤就像这样:

  1. 随便找个起点:比如随机选 x1​=0.5,x2​=40,x3​=60,算出此时的函数值(适应度)。
  2. 看看附近有没有更好的点:用邻域函数生成一个附近的点(比如 x1​=0.52,x2​=39.8,x3​=60.1),算它的函数值。
  3. 决定要不要去新点:
    • 如果新点的函数值更小(更好),就直接过去;
    • 如果新点的函数值更大(更差),也不是完全不去 —— 温度高时可能 “冒险” 过去(比如有 30% 的概率),温度低时就几乎不去了。
  4. 慢慢降温:每次迭代后温度降低一点(比如从 1000 降到 950,再降到 902.5……),让 “冒险” 的概率越来越小。
  5. 记下来最好的点:每次迭代都记录目前找到的最小函数值,直到迭代结束,这个值就是结果。

四、代码实现与关键部分解析

下面我们用 Python 代码实现上述过程,每一步都配有详细注释,帮助理解:

# 导入必要的库
import numpy as np  # 用于数值计算(生成随机数、数组操作等)
import math  # 用于数学运算(比如指数函数)
import matplotlib.pyplot as plt  # 用于绘制迭代过程图

1. 适应度函数(目标函数)

适应度函数是我们判断 “解好不好” 的标准,这里就是要最小化的函数

# ----------------------
# 1. 定义要优化的目标函数(适应度函数)
# 这里以最小化函数 f(x) = -10x₁ - e^(-x₂/10 - x₃) 为例
# ----------------------
def fitness(x):
    # x是一个包含3个变量的数组:[x₁, x₂, x₃]
    x1, x2, x3 = x  # 分别取出三个变量
    # 计算函数值(目标是让这个值越小越好)
    return -10 * x1 - math.exp(-x2/10 - x3)

2. 邻域函数(生成附近的解)

邻域函数的作用是在当前解的基础上,生成一个 “附近” 的新解,就像在当前位置 “小范围探索”。

# ----------------------
# 2. 定义邻域函数(生成当前解附近的新解)
# ----------------------
def generate_neighbor(current_x, lower_bound, upper_bound, step=0.1):
    # current_x:当前解(比如 [0.5, 40, 60])
    # lower_bound/upper_bound:变量的取值范围(避免新解超出范围)
    # step:扰动幅度(步长),控制新解与当前解的距离
    
    # 给当前解加一点随机小扰动(比如0.5→0.52或0.48)
    perturbation = np.random.uniform(-step, step, size=current_x.shape)
    new_x = current_x + perturbation
    
    # 确保新解在规定范围内(比如x₁不能小于0或大于1)
    new_x = np.clip(new_x, lower_bound, upper_bound)
    return new_x

3. 降温函数(控制温度变化)

温度是算法 “大胆程度” 的核心,降温函数让温度逐渐降低,使算法从 “大胆探索” 转向 “稳定收敛”。

# ----------------------
# 3. 定义温度更新函数(降温)
# ----------------------
def cool_down(temperature, alpha=0.95):
    # 每次迭代后温度降低一点(比如1000→950→902.5...)
    # alpha:降温系数(0<alpha<1,越接近1降温越慢)
    return temperature * alpha

4. 模拟退火算法主函数(核心逻辑)

主函数整合了上述所有部分,实现 “生成新解→判断是否接受→降温→记录最优解” 的完整流程。

"""
 ----------------------
# 4. 模拟退火算法主函数
# 功能:实现模拟退火算法的完整流程,从初始解开始搜索,通过温度控制实现全局优化
# 参数说明:
#   fitness_func: 函数,适应度函数(目标函数),用于评估解的优劣
#   lower_bound: 数组,各变量的取值下限(如 [0, 1, 0] 表示x1≥0, x2≥1, x3≥0)
#   upper_bound: 数组,各变量的取值上限(如 [1, 80, 120] 表示x1≤1, x2≤80, x3≤120)
#   initial_temp: 数值,初始温度(控制探索强度的起点,越高初始探索越自由)
#   alpha: 数值,降温系数(0<alpha<1,如0.95表示每次温度变为原来的95%)
#   max_iter: 整数,最大迭代次数(算法终止条件之一,控制搜索的总步数)
# 返回值:
#   best_x: 数组,找到的最优解(使目标函数最小的变量组合)
#   best_fitness: 数值,最优解对应的函数值(目标函数的最小值)
# ----------------------
"""
def simulated_annealing(fitness_func, 
                       lower_bound, 
                       upper_bound, 
                       initial_temp=1000, 
                       alpha=0.95, 
                       max_iter=100):
    # 初始化参数
    # 随机生成一个初始解(在变量范围内均匀采样,作为搜索的起点)
    # 例如:x1在[0,1]、x2在[1,80]、x3在[0,120]中随机取值
    current_x = np.random.uniform(lower_bound, upper_bound)
    # 计算初始解的函数值(用适应度函数评估初始解的优劣)
    current_fitness = fitness_func(current_x)
    
    # 记录最优解(算法运行过程中始终保存目前找到的最好解)
    # 初始状态下,最优解就是初始解
    best_x = current_x.copy()  # 用copy避免后续修改影响最优解记录
    best_fitness = current_fitness  # 记录最优解对应的函数值
    
    # 初始化温度(从初始温度开始,控制算法的探索-收敛过程)
    temperature = initial_temp
    
    # 记录每次迭代的最优函数值(用于后续绘制迭代曲线,直观展示算法收敛过程)
    fitness_history = [best_fitness]  # 初始值加入历史记录
    
    # 开始迭代搜索(核心循环,重复执行"生成新解→判断是否接受→更新最优解→降温"流程)
    for i in range(max_iter):
        # 生成新解:通过邻域函数在当前解附近生成一个候选解
        # 新解与当前解的距离由邻域函数的步长控制,确保在变量范围内
        new_x = generate_neighbor(current_x, lower_bound, upper_bound)
        # 评估新解:计算新解的函数值,判断其优劣
        new_fitness = fitness_func(new_x)
        
        # 判断是否接受新解(遵循Metropolis准则,核心机制)
        # 情况1:新解更好(函数值更小,因为我们要找最小值)→ 直接接受新解
        if new_fitness < current_fitness:
            current_x = new_x  # 更新当前解为新解
            current_fitness = new_fitness  # 更新当前解的函数值
        # 情况2:新解更差(函数值更大)→ 以一定概率接受,避免陷入局部最优
        else:
            # 计算接受概率:温度越高、新解与当前解的差距越小,接受概率越大
            # 公式来源:模拟退火的物理背景(玻尔兹曼分布)
            probability = math.exp((current_fitness - new_fitness) / temperature)
            # 生成0-1之间的随机数,若小于接受概率则接受新解
            if np.random.rand() < probability:
                current_x = new_x  # 接受较差的新解,保持探索性
                current_fitness = new_fitness
        
        # 更新最优解:如果当前解比历史最优解更好,则更新记录
        if current_fitness < best_fitness:
            best_x = current_x.copy()  # 保存新的最优解
            best_fitness = current_fitness  # 保存新的最优函数值
        
        # 降温:按照降温系数降低温度,使算法从"探索"逐渐转向"收敛"
        # 温度降低后,接受较差解的概率减小,搜索更聚焦于优质区域
        temperature = cool_down(temperature, alpha)
        
        # 记录当前迭代的最优值:用于后续绘图分析算法性能
        fitness_history.append(best_fitness)
        
        # 打印迭代信息:实时展示算法进展,方便调试和观察收敛情况
        print(f"第{i+1}次迭代,当前最优函数值:{best_fitness:.4f}")
    
    # 绘制迭代过程图:直观展示最优函数值随迭代次数的变化
    plt.plot(fitness_history)
    plt.xlabel("迭代次数")
    plt.ylabel("最优函数值")
    plt.title("模拟退火算法迭代过程")
    plt.show()
    
    # 返回最终找到的最优解和对应的函数值
    return best_x, best_fitness
    
    # 绘制迭代过程图(看函数值如何变化)
    plt.plot(fitness_history)
    plt.xlabel("迭代次数")
    plt.ylabel("最优函数值")
    plt.title("模拟退火算法迭代过程")
    plt.show()
    
    return best_x, best_fitness

5. 运行算法并查看结果

最后,我们设置参数并运行算法,看看它如何找到最优解:

# ----------------------
# 5. 主程序:运行算法
# ----------------------
if __name__ == "__main__":
    # 定义变量的取值范围(x₁: [0,1],x₂: [1,80],x₃: [0,120])
    lower = np.array([0, 1, 0])    # 下限
    upper = np.array([1, 80, 120]) # 上限
    
    # 算法参数
    initial_temperature = 1000  # 初始温度
    cooling_rate = 0.95         # 降温系数
    max_iterations = 100        # 最大迭代次数
    
    # 运行模拟退火算法
    best_solution, best_value = simulated_annealing(
        fitness_func=fitness,
        lower_bound=lower,
        upper_bound=upper,
        initial_temp=initial_temperature,
        alpha=cooling_rate,
        max_iter=max_iterations
    )
    
    # 输出结果
    print("\n最终结果:")
    print(f"最优解(x₁, x₂, x₃):{best_solution}")
    print(f"最优函数值:{best_value:.4f}")

python语法基础补充

if __name__ == "__main__": —— 主程序入口判断

这是 Python 中用于区分 “模块运行” 和 “模块导入” 的经典语法:

  • 当这个脚本被直接运行时(比如python script.py),__name__变量会被自动赋值为"__main__",此时if条件成立,内部代码会执行。

  • 当这个脚本被作为模块导入到其他脚本时(比如import script),__name__会被赋值为模块名("script"),此时if条件不成立,内部代码不会执行。

作用:让脚本既能作为独立程序运行,又能作为模块被其他程序导入(导入时不执行主程序逻辑)。

五、结果能说明什么?

在上述例子中,迭代 100 次后,最终找到的最优解通常接近函数值 -10.0。从迭代过程的图表(适应度随迭代次数变化)能看到:

  • 一开始函数值波动较大(温度高,敢尝试差的解);
  • 后来逐渐稳定在 -10.0 左右(温度低,找到最优解后不再变化)。

这说明算法成功从 “探索” 阶段过渡到 “收敛” 阶段,最终找到了全局最优解。

六、为什么要用模拟退火算法?

  • 适合解决复杂问题:比如旅行商问题(找最短路线)、工厂排班、参数优化等。
  • 不容易陷在局部最优:比如爬山时,它不会只盯着眼前的小山坡,而是可能先爬上一个稍高的坡,再找到更高的山。

七、如何修改代码解决自己的问题?

  • 如果你想优化其他函数,只需修改 fitness 函数中的计算公式。
  • 如果你有更多变量或不同的取值范围,修改 lower 和 upper 数组即可(比如 2 个变量就设为 np.array([a, b]) 和 np.array([c, d]))。
  • 可以调整 initial_temperature(初始温度)、cooling_rate(降温系数)、max_iterations(迭代次数)等参数,让算法更适合你的问题。

网站公告

今日签到

点亮在社区的每一天
去签到