ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

模拟退火算法:从原理到Python实战,解决复杂优化问题

模拟退火算法:从原理到Python实战,解决复杂优化问题 1. 从“烧铁淬火”到函数寻优模拟退火算法初印象如果你曾经为了找到一个复杂函数的最低点或最高点而绞尽脑汁比如在机器学习中调整几十个超参数或者在工程设计中优化多个相互制约的变量那你一定体会过“维数灾难”的恐怖。传统的梯度下降法就像蒙眼下山一旦掉进一个小坑局部最优解就爬不出来了。这时候我们需要的是一种更“聪明”的、能主动跳出局部陷阱的搜索策略。模拟退火算法正是这样一种灵感来源于金属热处理工艺的全局优化算法。我第一次接触这个算法是为了解决一个供应链网络中的仓库选址问题。目标函数里有运输成本、建设成本、覆盖半径等多个变量相互影响非线性程度很高。梯度下降试了几次结果严重依赖于初始点效果很不稳定。直到用了模拟退火才算找到了一个在成本和效率上都能接受的“较优解”。它不一定能保证找到数学上绝对的最优解但在处理复杂的、多峰的优化问题时其稳定性和找到“满意解”的能力非常突出。简单来说模拟退火模仿的是固体退火过程先将固体加温至熔化使其内部粒子排列从有序变为无序然后徐徐冷却粒子逐渐趋于有序最终在常温时达到基态此时内能最小。在算法中“温度”是一个控制参数“内能”对应我们的目标函数值。算法通过以一定概率接受“更差”的解来避免陷入局部最优随着“温度”降低接受差解的概率也越来越小搜索逐渐稳定在全局最优解附近。接下来我们就深入这个算法的核心看看如何用Python把它应用到多变量函数优化上。2. 算法核心机理为什么“接受变差”反而能找到更好理解模拟退火关键在于弄懂它如何平衡“探索”和“利用”。纯粹的随机搜索是盲目的探索而梯度下降是贪婪的利用只往眼前最陡的方向走。模拟退火的精髓在于它在早期高温阶段鼓励大胆探索甚至接受更差的解在后期低温阶段则趋于保守利用主要接受更好的解。2.1 状态产生与接受准则算法的“心脏”算法的每一次迭代可以看作是从当前解X_old尝试“跳”到一个新解X_new。这个新解通常是在当前解附近通过一个简单的扰动函数生成的比如对于连续函数可以在每个变量维度上加一个服从正态分布N(0, sigma)的随机扰动。import numpy as np def generate_new_solution(x_old, bounds, step_size0.1): 在当前解附近产生一个新解。 x_old: 当前解一维数组。 bounds: 每个变量的取值范围列表如 [(min1, max1), (min2, max2), ...]。 step_size: 控制扰动大小的参数。 x_new x_old np.random.randn(len(x_old)) * step_size # 确保新解不超出定义域边界 for i in range(len(x_new)): x_new[i] np.clip(x_new[i], bounds[i][0], bounds[i][1]) return x_new产生新解后就需要决定是否接受它。这里引入了Metropolis准则这是整个算法的灵魂计算目标函数值的变化delta_E f(X_new) - f(X_old)。对于最小化问题delta_E 0意味着新解更优。判断是否接受如果delta_E 0新解更优总是接受。如果delta_E 0新解更差则以一个概率P exp(-delta_E / T)接受它。其中T是当前的温度。这个概率公式P exp(-delta_E / T)是理解退火过程的关键温度T很高时即使delta_E很大解差很多exp(-delta_E / T)也会接近1算法几乎以100%的概率接受差解。这时算法行为接近随机搜索广泛探索解空间。温度T很低时exp(-delta_E / T)会变得很小除非delta_E非常接近0解只有一点点差否则接受差解的概率极低。这时算法行为接近局部搜索如梯度下降在当前位置精细挖掘。delta_E的影响在相同温度下解变得越差delta_E越大接受它的概率就越低。这很符合直觉我们可以容忍一点点退步去绕过一个小山丘但不会为了探索而跳下一个悬崖。def metropolis_accept(delta_e, temperature): 根据Metropolis准则判断是否接受新解。 if delta_e 0: return True else: # 防止温度为零导致除零错误同时零温度下只接受更优解 if temperature 1e-10: return False accept_probability np.exp(-delta_e / temperature) return np.random.rand() accept_probability2.2 退火进度表算法的“节奏大师”退火进度表控制着温度T如何从初始高温T0下降到终止低温T_end。冷却太快淬火系统可能来不及跳出局部最优而凝固在非晶态冷却太慢退火则计算时间会无法接受。常见的降温方式是指数降温T_{k1} alpha * T_k其中alpha是衰减系数通常取0.8到0.99之间。alpha越接近1降温越慢搜索越充分。除了降温策略退火进度表还包括马尔可夫链长度L在每个温度T下进行L次状态转移产生新解并判断接受的尝试。L太小在每个温度下搜索不充分L太大计算开销增加。一个常见的策略是让L与问题的维度成正比。终止条件通常设定为温度低于某个阈值T_end或者连续若干个温度下最优解都没有改善。def simulated_annealing(func, bounds, t0100, t_end1e-7, alpha0.95, max_iter1000): 模拟退火算法主框架。 func: 目标函数接受一维数组输入返回标量。 bounds: 变量边界列表。 t0: 初始温度。 t_end: 终止温度。 alpha: 温度衰减系数。 max_iter: 最大迭代次数温度下降次数。 # 初始化在边界内随机产生一个初始解 dim len(bounds) x_current np.array([np.random.uniform(low, high) for (low, high) in bounds]) f_current func(x_current) x_best, f_best x_current.copy(), f_current t t0 iteration 0 while t t_end and iteration max_iter: # 内循环在当前温度下进行L次尝试 l dim * 100 # 马尔可夫链长度简单设为维度100倍 for _ in range(l): x_new generate_new_solution(x_current, bounds, step_size0.1) f_new func(x_new) delta_e f_new - f_current if metropolis_accept(delta_e, t): x_current, f_current x_new, f_new # 更新历史最优解 if f_new f_best: x_best, f_best x_new, f_new # 降温 t * alpha iteration 1 # 可以在这里打印日志观察温度和最优值的变化 # print(fIter {iteration}, T{t:.4e}, f_best{f_best:.6f}) return x_best, f_best3. 实战优化一个经典的多峰测试函数理论说得再多不如动手跑一个例子。我们选用一个经典的、具有多个局部极小点的函数——Rastrigin函数来测试我们的模拟退火算法。这个函数在优化领域常被用来测试算法的全局搜索能力。Rastrigin函数的公式为f(x) A*n Σ_{i1}^{n} [x_i^2 - A * cos(2πx_i)]其中A通常取10n是变量维度。当所有x_i 0时函数取得全局最小值0。但这个函数在搜索空间内布满了大量的局部极小点对算法跳出局部最优的能力要求很高。3.1 Python实现与可视化我们先在二维空间n2上实现并可视化这个函数看看它的“地形”有多复杂。import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D def rastrigin(x, A10): Rastrigin函数x可以是一维数组单个点或二维数组多个点每行一个点。 # 确保x至少是二维数组便于向量化计算 x np.asarray(x) if x.ndim 1: x x.reshape(1, -1) n x.shape[1] return A * n np.sum(x**2 - A * np.cos(2 * np.pi * x), axis1) # 定义搜索范围 bounds [(-5.12, 5.12), (-5.12, 5.12)] # 生成网格点用于绘图 x1 np.linspace(bounds[0][0], bounds[0][1], 100) x2 np.linspace(bounds[1][0], bounds[1][1], 100) X1, X2 np.meshgrid(x1, x2) X_grid np.column_stack([X1.ravel(), X2.ravel()]) Z rastrigin(X_grid).reshape(X1.shape) # 绘制3D曲面图 fig plt.figure(figsize(12, 5)) ax1 fig.add_subplot(121, projection3d) surf ax1.plot_surface(X1, X2, Z, cmapcoolwarm, alpha0.8, linewidth0) ax1.set_xlabel(x1) ax1.set_ylabel(x2) ax1.set_zlabel(f(x)) ax1.set_title(Rastrigin Function (3D Surface)) # 绘制等高线图 ax2 fig.add_subplot(122) contour ax2.contourf(X1, X2, Z, levels50, cmapcoolwarm) ax2.set_xlabel(x1) ax2.set_ylabel(x2) ax2.set_title(Rastrigin Function (Contour)) plt.colorbar(contour, axax2) plt.tight_layout() plt.show()运行这段代码你会看到一个像“蛋盒”一样的曲面上面密密麻麻布满了凹陷局部极小点。全局最小值(0, 0)位于中心最底部但算法从随机点出发很容易被困在任何一个周围的“小坑”里。3.2 运行模拟退火并追踪过程现在我们将前面实现的模拟退火算法应用到这个函数上并记录下搜索路径和最优值的变化直观感受算法的“退火”过程。def simulated_annealing_with_trace(func, bounds, t0100, t_end1e-7, alpha0.95, max_iter150): 带追踪记录的模拟退火算法。 dim len(bounds) x_current np.array([np.random.uniform(low, high) for (low, high) in bounds]) f_current func(x_current) x_best, f_best x_current.copy(), f_current t t0 iteration 0 # 记录历史数据用于分析 history {temperature: [], f_current: [], f_best: [], x_current: [], x_best: []} while t t_end and iteration max_iter: history[temperature].append(t) history[f_current].append(f_current) history[f_best].append(f_best) history[x_current].append(x_current.copy()) history[x_best].append(x_best.copy()) l dim * 100 for _ in range(l): x_new generate_new_solution(x_current, bounds, step_size0.5) # 步长稍大便于探索 f_new func(x_new) delta_e f_new - f_current if metropolis_accept(delta_e, t): x_current, f_current x_new, f_new if f_new f_best: x_best, f_best x_new, f_new t * alpha iteration 1 # 记录最终状态 history[temperature].append(t) history[f_current].append(f_current) history[f_best].append(f_best) history[x_current].append(x_current.copy()) history[x_best].append(x_best.copy()) return x_best, f_best, history # 运行算法 best_solution, best_value, history simulated_annealing_with_trace(rastrigin, bounds, t050, alpha0.9, max_iter100) print(f找到的最优解: x {best_solution}, f(x) {best_value}) print(f理论全局最优解: x [0, 0], f(x) 0)3.3 结果分析与可视化我们可以绘制三条关键的曲线来观察算法行为温度下降曲线展示退火进度。当前解函数值变化曲线可以看到算法如何上下跳动特别是在高温阶段。历史最优解函数值变化曲线这是最关键的它应该是一个单调不增的曲线最终趋于稳定。# 绘制算法过程分析图 fig, axes plt.subplots(2, 2, figsize(14, 10)) iterations list(range(len(history[temperature]))) # 1. 温度变化 axes[0, 0].semilogy(iterations, history[temperature], b-, linewidth2) axes[0, 0].set_xlabel(迭代次数) axes[0, 0].set_ylabel(温度 (对数坐标)) axes[0, 0].set_title(温度下降曲线) axes[0, 0].grid(True, alpha0.3) # 2. 当前解函数值 axes[0, 1].plot(iterations, history[f_current], g-, alpha0.7, linewidth1) axes[0, 1].set_xlabel(迭代次数) axes[0, 1].set_ylabel(f(当前解)) axes[0, 1].set_title(当前解函数值变化 (允许变差)) axes[0, 1].grid(True, alpha0.3) # 3. 历史最优解函数值 axes[1, 0].plot(iterations, history[f_best], r-, linewidth2) axes[1, 0].set_xlabel(迭代次数) axes[1, 0].set_ylabel(f(历史最优解)) axes[1, 0].set_title(历史最优解函数值变化 (单调不增)) axes[1, 0].grid(True, alpha0.3) # 4. 在等高线图上绘制搜索路径 x_current_path np.array(history[x_current]) x_best_path np.array(history[x_best]) ax axes[1, 1] contour ax.contourf(X1, X2, Z, levels50, cmapcoolwarm, alpha0.7) # 绘制当前解的游走路径带透明度显示早期探索 ax.plot(x_current_path[:, 0], x_current_path[:, 1], yo-, markersize3, linewidth0.5, alpha0.5, label当前解路径) # 绘制历史最优解的演进路径 ax.plot(x_best_path[:, 0], x_best_path[:, 1], k*-, markersize5, linewidth1.5, label最优解演进) ax.set_xlabel(x1) ax.set_ylabel(x2) ax.set_title(搜索路径可视化 (在等高线图上)) ax.legend() plt.colorbar(contour, axax) plt.tight_layout() plt.show()从这些图中你可以清晰地看到温度曲线指数下降初期降温快后期慢。当前解曲线在高温初期剧烈震荡频繁接受差解体现了“探索”特性随着温度降低震荡幅度减小逐渐稳定。历史最优解曲线呈阶梯式下降每次下降都意味着算法发现了一个更优的区域并成功跳了过去。最终值可能非常接近0。搜索路径图当前解黄点的路径杂乱无章遍布整个区域而历史最优解黑星连线的路径则清晰地显示出向全局最优点(0,0)收敛的趋势。这张图完美诠释了模拟退火“大胆探索谨慎收敛”的过程。4. 调参经验与避坑指南让算法真正为你所用模拟退火算法不难实现但想让它高效工作参数调优是关键。这些参数没有放之四海而皆准的“最优值”必须结合具体问题调整。下面是我在多个项目中总结出的一些经验。4.1 关键参数解析与调优策略初始温度T0作用决定了算法初期的探索能力。T0越高初期接受差解的概率越大探索范围越广。设置方法一个经验法则是让初始时接受差解的概率在一个较高水平例如0.8。可以通过采样一些随机解计算目标函数值的标准差sigma_f然后设定T0 K * sigma_fK是一个较大的数如10。更简单的方法是先设一个较大的值如100, 1000根据初期接受率来调整。如果初期接受率远低于50%说明T0太低如果接近100%说明T0太高。终止温度T_end作用决定算法何时停止。温度越低接受差解的概率越小算法趋于稳定。设置方法通常设为一个非常小的正数如1e-7,1e-8。也可以结合最大迭代次数max_iter来使用。在实践中我常观察历史最优值曲线当连续多个温度周期如10个最优值都没有任何改善时就可以提前终止这比固定T_end更高效。温度衰减系数alpha作用控制降温速度是影响算法性能最敏感的参数之一。设置方法alpha越接近1降温越慢在每个温度下搜索越充分找到更好解的可能性越大但计算时间越长。通常取值范围在[0.8, 0.99]。对于复杂问题建议使用0.95或更高。一个进阶策略是采用自适应降温例如根据当前温度下解的接受率来动态调整alpha如果接受率高说明还没充分搜索可以慢点降接受率低则可以快点降。马尔可夫链长度L作用决定了在每个温度下进行多少次尝试即在“恒温”阶段搜索的深度。设置方法太短会导致搜索不充分太长则浪费计算资源。一个常见的启发式规则是L 100 * nn为变量维度。也可以根据问题规模动态调整或者设定为直到在该温度下解的状态分布趋于稳定例如连续若干次尝试都无法产生被接受的新解为止。新解产生函数扰动步长作用决定了从当前解“跳”到新解的距离。步长太大容易跳过最优解附近区域步长太小搜索效率低下容易陷入局部。设置方法步长应与变量的定义域范围相关。例如可以设定为定义域宽度的某个比例如1/20。一个更鲁棒的方法是使用自适应步长在高温时使用较大步长进行全局探索在低温时使用较小步长进行局部精细搜索。这可以通过让步长与当前温度的平方根成正比来实现step_size scale * sqrt(T)其中scale是一个基础缩放因子。4.2 常见问题与解决方案问题算法运行很久但结果依然很差甚至不如随机搜索。可能原因1初始温度T0设置过低导致算法从一开始就缺乏探索能力迅速陷入初始点附近的局部最优。解决方案提高T0确保算法初期有足够的“活力”进行大范围探索。观察前几次迭代的接受率应保持在较高水平如50%。可能原因2降温速度太快alpha太小系统还来不及跳出局部最优就“淬火”凝固了。解决方案增大alpha如从0.85调到0.95让降温过程更平缓。或者采用更复杂的降温策略如对数降温。可能原因3马尔可夫链长度L太短在每个温度下还没找到更好的方向就降温了。解决方案增加L或者实现更智能的内循环终止条件如连续N次拒绝新解则跳出内循环。问题算法收敛速度太慢无法满足实时性要求。可能原因1alpha太接近1或者T_end设得太小导致总迭代次数过多。解决方案适当降低alpha如从0.99降到0.9或提高T_end如从1e-8升到1e-5。同时可以加入基于最优解改进停滞的提前终止条件。可能原因2目标函数func计算过于复杂每次评估耗时很长。解决方案这是模拟退火以及其他基于迭代采样的算法的固有瓶颈。可以考虑使用更高效的编程方式向量化、利用JIT编译如Numba如果可能用代理模型如响应面模型、神经网络来近似复杂的目标函数在代理模型上进行优化。问题结果不稳定每次运行得到的最优解差异很大。可能原因这是随机优化算法的通病。由于初始解随机且搜索过程具有随机性每次运行的结果都会有波动。解决方案多次运行取最优这是最直接有效的方法。独立运行算法多次如10-30次取所有结果中最好的一个作为最终输出。这虽然增加了计算量但极大地提高了找到高质量解的概率。设置随机种子在开发调试阶段固定随机数种子可以保证结果可复现便于比较不同参数设置的效果。改进解的产生方式如果问题有领域知识可以在随机扰动中加入启发式信息引导搜索方向减少盲目性。4.3 一个可复用的、参数鲁棒的SA类实现基于以上经验我通常会封装一个更健壮、功能更完整的模拟退火类方便在不同项目中调用。class AdvancedSimulatedAnnealing: 一个增强版的模拟退火算法实现包含自适应步长、提前终止等功能。 def __init__(self, func, bounds): self.func func self.bounds np.array(bounds) self.dim len(bounds) self.best_solution_history [] self.best_value_history [] self.temperature_history [] def solve(self, t0None, t_end1e-7, alpha0.95, max_iter1000, max_stagnation20, run_times1): 求解函数。 run_times: 独立运行次数返回多次运行中的最佳结果。 max_stagnation: 最优解连续未改进的迭代次数用于提前终止。 final_best_solution None final_best_value float(inf) all_history [] for run in range(run_times): # 自动估计初始温度如果未提供 if t0 is None: t0 self._estimate_initial_temperature() # 初始化 x_current np.array([np.random.uniform(low, high) for (low, high) in self.bounds]) f_current self.func(x_current) x_best, f_best x_current.copy(), f_current t t0 stagnation_counter 0 iteration 0 # 初始化自适应步长参数 initial_step_scale (self.bounds[:, 1] - self.bounds[:, 0]) * 0.1 # 定义域宽度的10% while t t_end and iteration max_iter and stagnation_counter max_stagnation: # 自适应步长随温度降低而减小 current_step_scale initial_step_scale * np.sqrt(t / t0) # 内循环 l self.dim * 50 # 基础链长 accept_count 0 for _ in range(l): # 产生新解使用自适应步长 x_new x_current np.random.randn(self.dim) * current_step_scale # 边界处理 x_new np.clip(x_new, self.bounds[:, 0], self.bounds[:, 1]) f_new self.func(x_new) delta_e f_new - f_current if delta_e 0 or np.random.rand() np.exp(-delta_e / t): x_current, f_current x_new, f_new accept_count 1 if f_new f_best: x_best, f_best x_new, f_new stagnation_counter 0 # 找到更优解重置停滞计数器 # 记录历史 self.best_solution_history.append(x_best.copy()) self.best_value_history.append(f_best) self.temperature_history.append(t) # 判断是否停滞 if iteration 0 and self.best_value_history[-1] self.best_value_history[-2] - 1e-12: stagnation_counter 1 else: stagnation_counter 0 # 自适应降温可选根据接受率微调alpha accept_rate accept_count / l # 如果接受率太低说明温度降得太快或步长不合适这里仅作记录 # print(fIter {iteration}, T{t:.2e}, f_best{f_best:.6f}, AcceptRate{accept_rate:.3f}) # 降温 t * alpha iteration 1 # 更新全局最优 if f_best final_best_value: final_best_value f_best final_best_solution x_best.copy() all_history.append((self.best_solution_history.copy(), self.best_value_history.copy())) # 清空历史记录为下一次运行准备 self.best_solution_history.clear() self.best_value_history.clear() self.temperature_history.clear() print(f运行 {run1}/{run_times} 完成 本次最优值: {f_best:.6f}) print(f\n{run_times} 次运行中的全局最优值: {final_best_value:.6f}) print(f最优解: {final_best_solution}) return final_best_solution, final_best_value def _estimate_initial_temperature(self, num_samples100): 通过随机采样估计目标函数值的波动范围用于设置初始温度。 random_samples np.array([np.random.uniform(low, high, num_samples) for (low, high) in self.bounds]).T f_values np.array([self.func(x) for x in random_samples]) sigma_f np.std(f_values) # 设置初始温度使得初始接受概率较高 estimated_t0 10 * sigma_f if sigma_f 1e-10 else 100 print(f估计的初始温度 T0: {estimated_t0:.2f}) return estimated_t0 # 使用增强版算法 asa AdvancedSimulatedAnnealing(rastrigin, bounds) best_sol, best_val asa.solve(t050, alpha0.9, max_iter80, max_stagnation15, run_times5)这个类提供了自动估计初始温度、自适应步长、基于停滞的提前终止以及多次运行取最优的功能在实际项目中比基础版本更可靠。通过调整run_times参数你可以在计算时间和解的质量之间做一个很好的权衡。对于那个仓库选址问题我就是用类似的代码设置了run_times10最终稳定地找到了比梯度下降法好得多的方案。模拟退火算法就像一位有耐心的登山者不贪图眼前的捷径愿意为了找到最高的山峰而暂时走下坡路。理解并掌握它能让你在面对复杂、非凸的优化问题时多一件强大的武器。
返回列表