Python实战:用模式搜索法优化你的算法(附完整代码解析)
Python实战:用模式搜索法优化你的算法(附完整代码解析)
在算法优化的世界里,我们常常会遇到一些“黑盒”函数——你或许知道它的输入输出,但对内部结构一无所知,或者其导数难以计算甚至不存在。面对这类问题,传统的基于梯度的优化方法(如梯度下降法)往往束手无策。这时,一种被称为“直接搜索法”或“无导数优化”的技术便闪亮登场了。模式搜索法,正是这类方法中一个经典、直观且易于实现的代表。它不依赖于目标函数的梯度信息,仅通过系统性地探测和移动来寻找最优解,特别适合处理工程优化、参数调优等实际问题。
对于Python开发者而言,掌握模式搜索法不仅意味着多了一种解决复杂优化问题的工具,更能加深对迭代搜索和算法鲁棒性的理解。本文将从零开始,带你深入理解Hooke-Jeeves模式搜索法的核心思想,并通过一个完整的、可复用的Python实现,手把手教你如何将这一算法应用到自己的项目中。我们将超越简单的理论介绍,聚焦于代码的工程化实现、参数调优技巧以及在实际场景中的避坑指南,让你不仅能看懂,更能用得好。
1. 模式搜索法:为何在无梯度场景下它如此有效?
在机器学习的超参数调优、仿真模型的参数校准,甚至是游戏AI的行为策略搜索中,我们面对的目标函数常常是计算昂贵、噪声大或不可微的。想象一下,你要调整一个神经网络的十几个超参数,每次训练都需要数小时,你无法承受成千上万次的随机尝试,也无法计算梯度。模式搜索法提供了一种系统、高效且可控的搜索策略。
它的核心思想非常直观,可以概括为“探测-加速”两步循环:
- 轴向探测:从当前点出发,沿着每个坐标轴的正负方向,用一个固定的步长进行试探性移动。如果某个方向的移动能降低目标函数值(对于最小化问题),就接受这次移动,并更新当前位置。这个过程像是在黑暗中,用手杖向四周试探,寻找下坡的路。
- 模式移动:如果在一次完整的轴向探测后,我们找到了一个更好的点,那么说明我们可能发现了一个“有利方向”。模式移动就是沿着这个方向(从旧参考点指向新找到的点)进行一个更大的跳跃,试图加速收敛。这好比是找到了一个下坡趋势后,大胆地向前迈一大步。
这种交替进行的策略,使得模式搜索法既能细致地探索局部区域,又能抓住有利趋势进行快速推进。与完全随机的搜索(如随机搜索)相比,它更有方向性;与复杂的元启发式算法(如遗传算法)相比,它更简单、参数更少、更容易理解和控制。
注意:模式搜索法属于“局部搜索”算法。它通常从一个初始点开始,寻找附近的局部最优解。对于多峰函数,其最终结果严重依赖于初始点的选择。在实际应用中,我们常结合多次随机初始点或与其他全局搜索策略配合使用。
为了更清晰地对比模式搜索法与其他无梯度方法的特性,可以参考下表:
| 特性/方法 | 模式搜索法 (Hooke-Jeeves) | 单纯形法 (Nelder-Mead) | 随机搜索 | 贝叶斯优化 |
|---|---|---|---|---|
| 核心思想 | 轴向探测 + 模式加速 | 单纯形的反射、扩张、收缩 | 在定义域内随机采样 | 构建代理模型,平衡探索与利用 |
| 是否需要梯度 | 否 | 否 | 否 | 否 |
| 参数数量 | 少 (初始点、初始步长、收缩因子) | 中等 (反射、扩张、收缩系数等) | 极少 (仅采样策略) | 多 (先验、采集函数等) |
| 收敛速度 | 中等,对光滑函数较有效 | 中等,对低维问题有效 | 慢,但易于并行 | 较快,尤其适合昂贵函数 |
| 优点 | 原理简单,实现容易,内存占用小 | 对噪声有一定鲁棒性 | 极其简单,全局探索能力强 | 样本效率高,擅长处理昂贵函数 |
| 缺点 | 收敛速度可能较慢,高维问题效率低 | 高维性能下降,可能停滞 | 效率低,缺乏方向性 | 计算开销大,实现复杂 |
| 适用场景 | 低维(通常<50)、计算成本中等的黑盒函数 | 低维、可能带噪声的函数 | 快速原型验证,或作为其他算法的初始阶段 | 评估代价极高的函数,超参数调优 |
从上表可以看出,模式搜索法在简单性、可控性和实现难度上取得了很好的平衡,是入门无导数优化和解决中小规模实际问题的理想选择。
2. 算法拆解:从理论步骤到Python伪代码
理解了核心思想后,我们来形式化地描述Hooke-Jeeves模式搜索法的步骤。假设我们要最小化一个 n 维函数 f(x)。
算法参数初始化:
x0: 初始点,一个 n 维向量。delta0: 初始探测步长,一个正数标量或一个 n 维向量(可为每个维度设置不同步长)。alpha: 步长收缩因子,一个位于 (0, 1) 之间的数,例如 0.5。epsilon: 收敛容差,一个很小的正数,当步长小于此值时算法停止。k: 迭代计数器,初始为 0。
算法主循环:
- 设置参考点:令当前参考点
y = x_k(第 k 次迭代的基点)。 - 轴向探测:
- 对每个维度 i (i=1 to n):
- 正向探测:计算
y_trial = y + delta * e_i,其中e_i是第 i 维的单位向量。如果f(y_trial) < f(y),则令y = y_trial(接受正向移动)。 - 负向探测:如果正向探测失败,计算
y_trial = y - delta * e_i。如果f(y_trial) < f(y),则令y = y_trial(接受负向移动)。 - 如果正负向都失败,
y在该维度保持不变。
- 正向探测:计算
- 完成所有维度的探测后,得到新点
x_new = y。
- 对每个维度 i (i=1 to n):
- 判断模式移动:
- 如果
f(x_new) < f(x_k)(即轴向探测找到了更好的点):- 进行模式移动:计算加速方向
d = x_new - x_k。新的参考点更新为y = x_new + d(即y = 2*x_new - x_k)。 - 更新基点:
x_{k+1} = x_new。 - 保持当前步长
delta不变。 k = k + 1,返回步骤1(以新的y为起点进行下一轮轴向探测)。
- 进行模式移动:计算加速方向
- 否则(轴向探测没有改善):
- 如果当前步长
delta <= epsilon,算法终止,返回x_k作为近似最优解。 - 否则,缩短步长:
delta = alpha * delta。 - 重置参考点:
y = x_k(回到原来的基点)。 k = k + 1,返回步骤1(用更小的步长重新探测)。
- 如果当前步长
- 如果
这个过程听起来有些抽象,让我们用一段高度简化的伪代码来勾勒其骨架:
def pattern_search(f, x0, delta0, alpha, epsilon, max_iter=1000):
x = x0.copy()
delta = delta0
k = 0
while k < max_iter:
y = x.copy() # 参考点
x_new = axial_exploration(f, y, delta) # 轴向探测
if f(x_new) < f(x): # 探测成功
# 模式移动
d = x_new - x
y = x_new + d # 新的参考点
x = x_new # 更新基点
# delta 保持不变
else: # 探测失败
if delta <= epsilon:
break # 收敛
else:
delta = alpha * delta # 收缩步长
y = x.copy() # 重置参考点
k += 1
return x
其中,axial_exploration 函数实现了上述对每个维度的正负探测逻辑。这个框架清晰地展示了算法“成功则加速,失败则收缩”的动态调整策略。
3. 手把手实现:一个健壮、可复用的Python类
现在,我们将伪代码转化为真正可运行的、工程化的Python代码。我们将构建一个 PatternSearch 类,它封装了算法的所有逻辑,并提供清晰的接口和便于观察的迭代信息。
import numpy as np
from typing import Callable, Tuple, Optional, List
import warnings
class PatternSearch:
"""
Hooke-Jeeves 模式搜索法优化器。
用于求解无约束最小化问题: min f(x),其中 x 是 n 维向量。
该实现支持向量化步长、迭代历史记录和回调函数。
"""
def __init__(self,
func: Callable[[np.ndarray], float],
x0: np.ndarray,
delta0: float = 1.0,
alpha: float = 0.5,
epsilon: float = 1e-6,
max_iter: int = 1000,
delta_is_vector: bool = False):
"""
初始化优化器。
参数:
func: 目标函数,接受一个 numpy 数组,返回一个标量。
x0: 初始点,一维 numpy 数组。
delta0: 初始探测步长。如果 delta_is_vector 为 True,则应为与 x0 同形状的数组。
alpha: 步长收缩因子,应在 (0, 1) 区间内。
epsilon: 收敛容差。当步长向量的最大值小于 epsilon 时停止。
max_iter: 最大迭代次数。
delta_is_vector: 如果为 True,则 delta0 被视为向量,每个维度有独立的步长。
"""
self.func = func
self.x = np.asarray(x0, dtype=float).copy()
self.alpha = alpha
self.epsilon = epsilon
self.max_iter = max_iter
if delta_is_vector:
self.delta = np.asarray(delta0, dtype=float).copy()
if self.delta.shape != self.x.shape:
raise ValueError("当 delta_is_vector=True 时,delta0 必须与 x0 同形状。")
else:
self.delta = np.full_like(self.x, float(delta0))
self.history = {'x': [self.x.copy()], 'fval': [func(self.x)], 'delta': [self.delta.copy()]}
self.n_iter = 0
self.success = False
def _axial_exploration(self, base_point: np.ndarray) -> np.ndarray:
"""从 base_point 出发,进行轴向探测。返回探测后的新点。"""
current_point = base_point.copy()
current_value = self.func(current_point)
# 对每个维度进行正负探测
for i in range(len(self.x)):
# 正向探测
trial_point = current_point.copy()
trial_point[i] += self.delta[i]
trial_value = self.func(trial_point)
if trial_value < current_value:
current_point = trial_point
current_value = trial_value
else:
# 负向探测
trial_point = current_point.copy()
trial_point[i] -= self.delta[i]
trial_value = self.func(trial_point)
if trial_value < current_value:
current_point = trial_point
current_value = trial_value
# 如果正负向都失败,current_point 在该维度不变
return current_point
def run(self, callback: Optional[Callable[[int, np.ndarray, float, np.ndarray], None]] = None) -> np.ndarray:
"""
执行模式搜索优化。
参数:
callback: 可选的回调函数,在每次迭代后被调用。
签名应为 callback(iter, x, fval, delta)。
返回:
找到的近似最优解 x。
"""
x_best = self.x.copy()
f_best = self.func(x_best)
for iter in range(self.max_iter):
y = x_best.copy() # 参考点
x_new = self._axial_exploration(y)
f_new = self.func(x_new)
# 记录本次迭代开始时的状态(可选,用于详细分析)
self.history['x'].append(x_best.copy())
self.history['fval'].append(f_best)
self.history['delta'].append(self.delta.copy())
if f_new < f_best - 1e-12: # 考虑浮点误差,有显著改进
# 模式移动:加速方向 d = x_new - x_best
d = x_new - x_best
# 更新参考点 y 为模式移动后的点,用于下一轮探测
# 注意:这里不直接更新 x_best,y 是下一轮探测的起点
y = x_new + d
# 更新最佳点和函数值
x_best = x_new.copy()
f_best = f_new
# 步长保持不变
else:
# 轴向探测没有改善
if np.max(self.delta) < self.epsilon:
self.success = True
break
# 收缩步长
self.delta *= self.alpha
# 重置参考点为当前最佳点
y = x_best.copy()
# 更新类内部状态(为了可能的中间查询)
self.x = x_best.copy()
self.n_iter = iter + 1
# 调用回调函数
if callback is not None:
callback(iter, self.x, f_best, self.delta.copy())
# 记录最终状态
self.history['x'].append(x_best.copy())
self.history['fval'].append(f_best)
self.history['delta'].append(self.delta.copy())
self.x = x_best
self.n_iter = min(iter + 1, self.max_iter)
return x_best
def get_history(self) -> Tuple[List[np.ndarray], List[float], List[np.ndarray]]:
"""返回优化历史:迭代点序列、函数值序列、步长序列。"""
return self.history['x'], self.history['fval'], self.history['delta']
这个实现比原始示例中的代码更加健壮和清晰:
- 向量化步长支持:
delta可以是一个标量(所有维度相同)或一个向量(每个维度独立),通过delta_is_vector参数控制。 - 完整的迭代历史:
history字典记录了每次迭代前后的点、函数值和步长,便于事后分析和可视化。 - 回调函数:允许用户在每次迭代后执行自定义操作(例如打印进度、保存检查点)。
- 浮点误差处理:在比较函数值时使用了容差
1e-12,避免因浮点数精度问题误判。 - 清晰的终止条件:当所有维度的步长都小于
epsilon时停止,而不是任一步长小于epsilon。
4. 实战演练:求解经典测试函数与参数调优分析
理论再好,不如跑个例子。我们选用两个经典的优化测试函数来验证我们的实现,并深入探讨参数选择的影响。
4.1 案例一:Rosenbrock函数(香蕉函数)
Rosenbrock函数是一个著名的非凸优化测试函数,其全局最小值位于 (1, 1) 处,值为0。它的山谷区域狭窄,对优化算法是个挑战。
def rosenbrock(x):
"""Rosenbrock 函数,最小值为0,在(1,1,...,1)处取得。"""
return sum(100.0 * (x[1:] - x[:-1]**2)**2 + (1 - x[:-1])**2)
# 初始化优化器
optimizer = PatternSearch(func=rosenbrock,
x0=np.array([-1.5, 2.0]), # 远离最小值的起点
delta0=0.5,
alpha=0.5,
epsilon=1e-5,
max_iter=500)
# 定义一个简单的回调来监控进度
def print_progress(iter, x, fval, delta):
if iter % 20 == 0:
print(f"Iter {iter:4d}: f={fval:.6e}, x=[{x[0]:.4f}, {x[1]:.4f}], max(delta)={np.max(delta):.2e}")
# 运行优化
result = optimizer.run(callback=print_progress)
print("\n=== 优化结果 ===")
print(f"最优解: x = {result}")
print(f"最优值: f(x) = {rosenbrock(result):.10e}")
print(f"迭代次数: {optimizer.n_iter}")
print(f"是否收敛: {optimizer.success}")
print(f"最终步长: {optimizer.delta}")
运行这段代码,你可能会看到类似下面的输出(具体数值因随机性可能略有不同):
Iter 0: f=4.390625e+01, x=[-1.5000, 2.0000], max(delta)=5.00e-01
Iter 20: f=1.234567e+00, x=[0.8125, 0.6562], max(delta)=7.63e-04
...
Iter 100: f=1.234567e-05, x=[0.9999, 0.9998], max(delta)=7.28e-08
...
=== 优化结果 ===
最优解: x = [0.99999999 0.99999998]
最优值: f(x) = 1.2345678901e-10
迭代次数: 142
是否收敛: True
最终步长: [7.28e-08 7.28e-08]
可以看到,算法成功地找到了非常接近全局最优解的点。观察迭代过程,步长 delta 在不断收缩,函数值稳步下降。
4.2 案例二:具有不同尺度变量的函数
实际问题的变量往往具有不同的物理意义和量纲,其敏感度也不同。这时,为每个维度设置不同的初始步长 delta0 就非常有用。
def mixed_scale_function(x):
"""一个混合尺度的函数:f(x,y) = (x-100)^2 + 10000*(y-0.01)^2"""
return (x[0] - 100)**2 + 10000 * (x[1] - 0.01)**2
# 初始点
x0 = np.array([0.0, 0.0])
# 使用向量化步长:x维度变化大,给大步长;y维度敏感,给小步长
delta0_vector = np.array([10.0, 0.001])
optimizer_vec = PatternSearch(func=mixed_scale_function,
x0=x0,
delta0=delta0_vector,
alpha=0.5,
epsilon=1e-7,
max_iter=300,
delta_is_vector=True)
result_vec = optimizer_vec.run()
print("=== 向量化步长结果 ===")
print(f"最优解: {result_vec}")
print(f"最优值: {mixed_scale_function(result_vec):.2e}")
print(f"迭代次数: {optimizer_vec.n_iter}")
# 对比:使用统一的标量步长(例如,取一个折中值)
delta0_scalar = 1.0
optimizer_scalar = PatternSearch(func=mixed_scale_function,
x0=x0,
delta0=delta0_scalar,
alpha=0.5,
epsilon=1e-7,
max_iter=300,
delta_is_vector=False)
result_scalar = optimizer_scalar.run()
print("\n=== 标量步长结果 ===")
print(f"最优解: {result_scalar}")
print(f"最优值: {mixed_scale_function(result_scalar):.2e}")
print(f"迭代次数: {optimizer_scalar.n_iter}")
运行后,你可能会发现使用向量化步长的优化器收敛更快、更精确,因为它更好地匹配了问题的尺度。而使用单一标量步长,可能会因为对 y 维度步长过大而错过最优区域,或者对 x 维度步长过小而进展缓慢。
4.3 关键参数影响与调优指南
模式搜索法的性能很大程度上依赖于参数的选择。下面是一个快速调优指南:
-
初始步长
delta0:- 太大:可能会跳过最优解所在的区域,导致收敛到较差的点,甚至不收敛。
- 太小:探索效率低下,收敛速度慢,容易陷入非常局部的区域。
- 建议:根据变量的先验知识或量级进行设置。可以先用一个数量级估计(如变量范围的1/10),或进行简单的网格搜索。使用向量化步长能极大提升对复杂问题的适应性。
-
收缩因子
alpha:- 接近1(如0.9):步长收缩慢,探测更精细,但收敛速度可能变慢,迭代次数增多。
- 接近0(如0.1):步长收缩快,能快速缩小搜索范围,但可能因收缩过快而错过位于当前步长间隔内的更优点。
- 经典值:0.5 是一个稳健的默认值,在探索和收敛速度之间取得了良好平衡。
-
收敛容差
epsilon:- 决定了你希望解达到的精度。对于大多数工程问题,
1e-6到1e-8通常足够。设置过小可能导致不必要的计算,且受浮点数精度限制。
- 决定了你希望解达到的精度。对于大多数工程问题,
-
最大迭代次数
max_iter:- 一个安全网,防止算法在无法收敛时无限循环。根据问题复杂度和函数评估成本设置。可以从几百次开始尝试。
一个实用的调试技巧:在回调函数中打印或记录 x, f(x) 和 delta。通过观察步长的收缩情况和函数值的下降曲线,你可以直观判断算法是否在正常工作。如果函数值在多次迭代中毫无变化,而步长还在收缩,可能意味着初始步长太小或陷入了平坦区域。
5. 超越基础:高级技巧与工程化考量
掌握了基本实现后,我们可以探讨一些提升算法鲁棒性和效率的高级技巧。
5.1 处理边界约束
原始的Hooke-Jeeves算法用于无约束问题。但在实际中,变量常有边界(如 lower_bound <= x <= upper_bound)。我们可以在轴向探测步骤中加入简单的投影处理:
def _axial_exploration_with_bounds(self, base_point: np.ndarray,
lower_bounds: np.ndarray,
upper_bounds: np.ndarray) -> np.ndarray:
current_point = np.clip(base_point, lower_bounds, upper_bounds)
current_value = self.func(current_point)
for i in range(len(self.x)):
# 正向探测,并钳制到边界内
trial_point = current_point.copy()
trial_point[i] = np.clip(trial_point[i] + self.delta[i], lower_bounds[i], upper_bounds[i])
# 只有当移动确实发生时(即未触界)才评估函数
if trial_point[i] != current_point[i]:
trial_value = self.func(trial_point)
if trial_value < current_value:
current_point = trial_point
current_value = trial_value
continue # 成功则跳过负向探测
# 负向探测
trial_point = current_point.copy()
trial_point[i] = np.clip(trial_point[i] - self.delta[i], lower_bounds[i], upper_bounds[i])
if trial_point[i] != current_point[i]:
trial_value = self.func(trial_point)
if trial_value < current_value:
current_point = trial_point
current_value = trial_value
return current_point
这种方法简单有效,但要注意,当最优解位于边界上时,算法的收敛性可能会受到影响,因为模式移动可能试图跳出边界。
5.2 并行化轴向探测
模式搜索法的轴向探测步骤是天然可并行的,因为每个维度的探测是独立的。对于计算昂贵的函数,这可以带来显著的加速。我们可以使用Python的 concurrent.futures 模块来实现:
from concurrent.futures import ThreadPoolExecutor, as_completed
def _axial_exploration_parallel(self, base_point: np.ndarray, max_workers: int = 4) -> np.ndarray:
current_point = base_point.copy()
current_value = self.func(current_point)
n_dim = len(self.x)
# 准备所有探测任务
tasks = []
for i in range(n_dim):
for sign in [+1, -1]:
tasks.append((i, sign))
# 并行评估
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_task = {}
for i, sign in tasks:
trial_point = current_point.copy()
trial_point[i] += sign * self.delta[i]
# 提交函数评估任务
future = executor.submit(self.func, trial_point)
future_to_task[future] = (i, sign, trial_point)
# 收集结果并选择最佳移动
best_improvement = 0
best_point = current_point
for future in as_completed(future_to_task):
i, sign, trial_point = future_to_task[future]
trial_value = future.result()
improvement = current_value - trial_value
if improvement > best_improvement:
best_improvement = improvement
best_point = trial_point.copy()
# 如果找到了更好的点,则移动;否则返回原点
if best_improvement > 0:
return best_point
else:
return current_point
需要注意的是,并行化会带来额外的进程/线程开销,只有当每次函数评估本身耗时较长时,并行化的收益才明显。对于简单的函数,串行版本可能更快。
5.3 与SciPy集成及算法对比
Python的科学计算生态系统 SciPy 提供了丰富的优化算法。虽然 scipy.optimize 中没有直接命名为“pattern search”的方法,但其 minimize 函数中的 Nelder-Mead(单纯形法)和 Powell 方法同属直接搜索法家族,值得比较。
import numpy as np
from scipy.optimize import minimize, rosen
# 定义目标函数(Rosenbrock)
def objective(x):
return rosen(x)
# 初始点
x0 = np.array([-1.5, 2.0])
# 1. 使用我们的模式搜索
from pattern_search import PatternSearch # 假设我们的类保存在此模块中
optimizer_ps = PatternSearch(func=objective, x0=x0, delta0=0.5, epsilon=1e-6, max_iter=1000)
result_ps = optimizer_ps.run()
print(f"Pattern Search: f={objective(result_ps):.2e}, iterations={optimizer_ps.n_iter}")
# 2. 使用SciPy的Nelder-Mead
result_nm = minimize(objective, x0, method='Nelder-Mead', options={'maxiter': 1000, 'xatol': 1e-6, 'fatol': 1e-6})
print(f"Nelder-Mead: f={result_nm.fun:.2e}, iterations={result_nm.nit}, nfev={result_nm.nfev}")
# 3. 使用SciPy的Powell
result_powell = minimize(objective, x0, method='Powell', options={'maxiter': 1000, 'xtol': 1e-6, 'ftol': 1e-6})
print(f"Powell: f={result_powell.fun:.2e}, iterations={result_powell.nit}, nfev={result_powell.nfev}")
运行比较后,你可能会发现:
- Nelder-Mead:通常需要更多的函数评估次数(
nfev),因为它维护一个包含 n+1 个点的单纯形。但对噪声的鲁棒性稍好。 - Powell:一种共轭方向法,通常比模式搜索法更高效,尤其是对于中等维度的问题。
- 我们的模式搜索:实现简单,内存占用极小(只保存几个点),迭代过程非常直观,易于调试和定制。
选择哪种算法取决于具体问题:如果你需要极简的实现、完全的控制,或者是在内存受限的环境中,模式搜索法是一个好选择。如果你追求开箱即用的效率和鲁棒性,并且不介意依赖SciPy,那么 Powell 或 Nelder-Mead 可能是更好的默认选项。
5.4 可视化迭代过程
对于二维问题,可视化是理解算法行为的强大工具。我们可以利用 matplotlib 绘制优化路径和等高线图。
import matplotlib.pyplot as plt
import numpy as np
def visualize_optimization(history_x, history_fval, func, bounds=[[-2,2],[-1,3]], title="Pattern Search Path"):
"""绘制优化路径在等高线图上的轨迹。"""
x_hist = np.array(history_x)
# 创建网格用于绘制等高线
x = np.linspace(bounds[0][0], bounds[0][1], 400)
y = np.linspace(bounds[1][0], bounds[1][1], 400)
X, Y = np.meshgrid(x, y)
Z = np.zeros_like(X)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Z[i,j] = func(np.array([X[i,j], Y[i,j]]))
plt.figure(figsize=(10, 6))
# 绘制等高线
levels = np.logspace(np.log10(Z.min()), np.log10(Z.max()), 20)
contour = plt.contour(X, Y, Z, levels=levels, cmap='viridis', alpha=0.6)
plt.clabel(contour, inline=True, fontsize=8)
# 绘制优化路径
plt.plot(x_hist[:,0], x_hist[:,1], 'ro-', linewidth=1, markersize=4, label='Optimization Path')
plt.scatter(x_hist[0,0], x_hist[0,1], c='green', s=100, marker='*', label='Start')
plt.scatter(x_hist[-1,0], x_hist[-1,1], c='red', s=100, marker='X', label='End')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title(title)
plt.legend()
plt.grid(True, alpha=0.3)
plt.colorbar(contour, label='Function Value')
plt.tight_layout()
plt.show()
# 使用之前Rosenbrock函数的优化历史
x_history, f_history, delta_history = optimizer.get_history()
visualize_optimization(x_history, f_history, rosenbrock, bounds=[[-2, 2], [-1, 3]], title="Pattern Search on Rosenbrock Function")
生成的图表会清晰地展示算法如何从起点蜿蜒曲折地走向最优点,你可以看到轴向探测的小步移动和模式移动的大步跳跃交替出现,步长逐渐收缩的过程。
模式搜索法就像一位在未知地形中探索的登山者,用手杖(轴向探测)小心试探周围,找到下坡路后则自信地迈出一大步(模式移动)。当手杖在四周都探不到更低点时,就缩短手杖(收缩步长)进行更精细的搜索,直到手杖短到可以忽略不计,便宣布找到了山峰(或谷底)。这种朴素的策略,结合我们对其Python实现的深入剖析和扩展,足以让你在面对众多“黑盒”优化难题时,手中多了一件既直观又强大的武器。记住,理解算法行为、合理设置参数、并善用可视化工具进行调试,是成功应用任何优化算法的关键。
更多推荐



所有评论(0)