Python实战:用模式搜索法优化你的算法(附完整代码解析)

在算法优化的世界里,我们常常会遇到一些“黑盒”函数——你或许知道它的输入输出,但对内部结构一无所知,或者其导数难以计算甚至不存在。面对这类问题,传统的基于梯度的优化方法(如梯度下降法)往往束手无策。这时,一种被称为“直接搜索法”或“无导数优化”的技术便闪亮登场了。模式搜索法,正是这类方法中一个经典、直观且易于实现的代表。它不依赖于目标函数的梯度信息,仅通过系统性地探测和移动来寻找最优解,特别适合处理工程优化、参数调优等实际问题。

对于Python开发者而言,掌握模式搜索法不仅意味着多了一种解决复杂优化问题的工具,更能加深对迭代搜索和算法鲁棒性的理解。本文将从零开始,带你深入理解Hooke-Jeeves模式搜索法的核心思想,并通过一个完整的、可复用的Python实现,手把手教你如何将这一算法应用到自己的项目中。我们将超越简单的理论介绍,聚焦于代码的工程化实现、参数调优技巧以及在实际场景中的避坑指南,让你不仅能看懂,更能用得好。

1. 模式搜索法:为何在无梯度场景下它如此有效?

在机器学习的超参数调优、仿真模型的参数校准,甚至是游戏AI的行为策略搜索中,我们面对的目标函数常常是计算昂贵、噪声大或不可微的。想象一下,你要调整一个神经网络的十几个超参数,每次训练都需要数小时,你无法承受成千上万次的随机尝试,也无法计算梯度。模式搜索法提供了一种系统、高效且可控的搜索策略。

它的核心思想非常直观,可以概括为“探测-加速”两步循环:

  1. 轴向探测:从当前点出发,沿着每个坐标轴的正负方向,用一个固定的步长进行试探性移动。如果某个方向的移动能降低目标函数值(对于最小化问题),就接受这次移动,并更新当前位置。这个过程像是在黑暗中,用手杖向四周试探,寻找下坡的路。
  2. 模式移动:如果在一次完整的轴向探测后,我们找到了一个更好的点,那么说明我们可能发现了一个“有利方向”。模式移动就是沿着这个方向(从旧参考点指向新找到的点)进行一个更大的跳跃,试图加速收敛。这好比是找到了一个下坡趋势后,大胆地向前迈一大步。

这种交替进行的策略,使得模式搜索法既能细致地探索局部区域,又能抓住有利趋势进行快速推进。与完全随机的搜索(如随机搜索)相比,它更有方向性;与复杂的元启发式算法(如遗传算法)相比,它更简单、参数更少、更容易理解和控制。

注意:模式搜索法属于“局部搜索”算法。它通常从一个初始点开始,寻找附近的局部最优解。对于多峰函数,其最终结果严重依赖于初始点的选择。在实际应用中,我们常结合多次随机初始点或与其他全局搜索策略配合使用。

为了更清晰地对比模式搜索法与其他无梯度方法的特性,可以参考下表:

特性/方法 模式搜索法 (Hooke-Jeeves) 单纯形法 (Nelder-Mead) 随机搜索 贝叶斯优化
核心思想 轴向探测 + 模式加速 单纯形的反射、扩张、收缩 在定义域内随机采样 构建代理模型,平衡探索与利用
是否需要梯度
参数数量 少 (初始点、初始步长、收缩因子) 中等 (反射、扩张、收缩系数等) 极少 (仅采样策略) 多 (先验、采集函数等)
收敛速度 中等,对光滑函数较有效 中等,对低维问题有效 慢,但易于并行 较快,尤其适合昂贵函数
优点 原理简单,实现容易,内存占用小 对噪声有一定鲁棒性 极其简单,全局探索能力强 样本效率高,擅长处理昂贵函数
缺点 收敛速度可能较慢,高维问题效率低 高维性能下降,可能停滞 效率低,缺乏方向性 计算开销大,实现复杂
适用场景 低维(通常<50)、计算成本中等的黑盒函数 低维、可能带噪声的函数 快速原型验证,或作为其他算法的初始阶段 评估代价极高的函数,超参数调优

从上表可以看出,模式搜索法在简单性、可控性和实现难度上取得了很好的平衡,是入门无导数优化和解决中小规模实际问题的理想选择。

2. 算法拆解:从理论步骤到Python伪代码

理解了核心思想后,我们来形式化地描述Hooke-Jeeves模式搜索法的步骤。假设我们要最小化一个 n 维函数 f(x)

算法参数初始化

  • x0: 初始点,一个 n 维向量。
  • delta0: 初始探测步长,一个正数标量或一个 n 维向量(可为每个维度设置不同步长)。
  • alpha: 步长收缩因子,一个位于 (0, 1) 之间的数,例如 0.5。
  • epsilon: 收敛容差,一个很小的正数,当步长小于此值时算法停止。
  • k: 迭代计数器,初始为 0。

算法主循环

  1. 设置参考点:令当前参考点 y = x_k(第 k 次迭代的基点)。
  2. 轴向探测
    • 对每个维度 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
  3. 判断模式移动
    • 如果 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-61e-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,那么 PowellNelder-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实现的深入剖析和扩展,足以让你在面对众多“黑盒”优化难题时,手中多了一件既直观又强大的武器。记住,理解算法行为、合理设置参数、并善用可视化工具进行调试,是成功应用任何优化算法的关键。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐