3. 爬山搜索算法:从原理到实战改进

爬山算法,说白了就是「往高处走」。在自动对焦里,我们把图像清晰度评价函数看作一座山,山顶就是最清晰的位置。算法从当前点出发,每次朝清晰度值更高的方向移动一步,直到找不到更高的点为止。

听起来很简单对吧?但我在实际项目中踩过不少坑。今天咱们就把爬山算法的底裤扒干净,看看它到底怎么工作,又该怎么改进。

3.1 爬山算法的基本原理

想象你蒙着眼站在一座山上,想走到山顶。你只能用手杖探探周围的地面——哪边高就往哪边挪一步。这就是爬山算法的核心思想。

核心流程:

  1. 从当前位置出发,计算当前清晰度值
  2. 朝某个方向移动一步,计算新位置的清晰度
  3. 如果新位置更清晰,继续朝这个方向走
  4. 如果新位置更模糊,换个方向试试
  5. 重复直到找不到更清晰的位置

我习惯把爬山算法分成两种模式:

模式 策略 适用场景
最速爬山 比较左右两个方向,选更优的那个 清晰度曲线比较平滑时
简单爬山 固定一个方向,走到不能走再换 实时性要求高、计算资源有限时

嗯,这里要注意:最速爬山虽然每一步都选最优方向,但计算量翻倍。我在做手机摄像头对焦时,就因为这个原因选了简单爬山——每帧图像的处理时间只有几毫秒,经不起折腾。

3.2 步长与方向判断

步长是爬山算法的命门。步长太大,容易跳过山顶;步长太小,对焦慢得像蜗牛爬。

我的经验法则:

  • 初始步长设为镜头全行程的 5%-10%
  • 接近峰值时步长缩小到 1%-2%
  • 步长调整策略:连续 3 步都上升,步长加倍;遇到下降,步长减半

方向判断其实更讲究。我见过不少新手直接写:

// 错误示范:只比较当前和下一个位置
if (sharpness[next] > sharpness[current]) {
    move_forward();
} else {
    move_backward();
}

这样做的问题在哪?如果当前正好在山脊上,左右都下降,算法就傻眼了。我曾经在一个项目中遇到这种情况,镜头在峰值附近来回震荡,死活停不下来。

正确的做法是加一个「死区」判断:

// 改进版:加入死区和方向记忆
if (sharpness[next] - sharpness[current] > DEAD_ZONE) {
    direction = FORWARD;
    move_forward();
} else if (sharpness[current] - sharpness[next] > DEAD_ZONE) {
    direction = BACKWARD;
    move_backward();
} else {
    // 在死区内,认为已经到达峰值
    stop();
}

避坑指南:死区大小要根据图像噪声水平来设。我曾经把死区设得太小,结果在低光照环境下,算法被噪声干扰得来回乱跳。后来我改成根据图像亮度自适应调整死区阈值,问题才解决。

3.3 局部极值与全局极值问题

这是爬山算法最大的痛点。你想想看,如果清晰度曲线有多个波峰,爬山算法只能爬到最近的那个山顶,不一定是全局最高的那个。

我画了一张图来说明这个问题:

爬山算法的局部极值问题 镜头位置 清晰度 局部极值 全局极值 起点 从起点出发,算法只能爬到最近的波峰(局部极值),错过更高的全局极值

为什么会这样?因为爬山算法是「贪心」的——它只看眼前一步,不看全局。这在单峰曲线上没问题,但实际场景中,由于镜头像差、光照变化、物体纹理分布等因素,清晰度曲线经常出现多个波峰。

我遇到过的典型案例:有一次做显微镜自动对焦,样本表面有划痕和污点。划痕处清晰度很高,但那是局部特征,不是真正的焦平面。爬山算法每次都停在划痕位置,气得我差点砸键盘。后来我改用粗扫描+精细爬山的策略才搞定。

3.4 改进型爬山算法

针对局部极值问题,业界有很多改进方案。我挑几个实战中效果最好的说说。

3.4.1 多起点爬山法

说白了就是「广撒网」。在镜头行程上选多个起始点,分别跑爬山算法,最后取所有结果中清晰度最高的那个。

// 多起点爬山伪代码
function multi_start_hill_climbing() {
    start_points = [0, 0.25, 0.5, 0.75, 1.0]; // 5个起始位置
    best_position = 0;
    best_sharpness = 0;
    
    for (pos in start_points) {
        result = hill_climbing(pos); // 从每个起点跑爬山
        if (result.sharpness > best_sharpness) {
            best_sharpness = result.sharpness;
            best_position = result.position;
        }
    }
    return best_position;
}

实战建议:起始点数量不是越多越好。我一般设 3-5 个,再多就浪费计算时间了。起始点分布要均匀,最好覆盖整个行程范围。

3.4.2 模拟退火爬山法

这个思路很有意思——允许算法「偶尔往下走」,就像金属退火时原子偶尔获得能量跳出晶格。刚开始允许跳出的概率大,随着时间推移概率逐渐减小。

阶段 温度 允许下降概率 行为特点
高温期 高 30%-50% 大胆探索,跳出局部极值
中温期 中 10%-20% 逐步收敛,缩小搜索范围
低温期 低 1%-5% 精细搜索,锁定峰值

我记得第一次在项目中用模拟退火时,参数调了整整两天。温度下降太快,算法还是掉进局部极值;下降太慢,对焦时间翻倍。后来我总结出一个经验:温度下降曲线用指数衰减,衰减系数 0.95 左右比较稳妥。

3.4.3 自适应步长爬山法

这个改进解决的是「步长选择」问题。核心思想是:根据清晰度变化率动态调整步长。

// 自适应步长逻辑
function adaptive_step_hill_climbing() {
    step = INITIAL_STEP;
    last_sharpness = get_sharpness(current_pos);
    
    while (step > MIN_STEP) {
        next_sharpness = get_sharpness(current_pos + step);
        delta = next_sharpness - last_sharpness;
        
        if (delta > THRESHOLD_UP) {
            // 清晰度明显上升,大步前进
            step = step * 1.5;
            current_pos += step;
        } else if (delta < THRESHOLD_DOWN) {
            // 清晰度下降,回头并缩小步长
            step = step * 0.5;
            current_pos -= step;
        } else {
            // 变化不大,缩小步长精细搜索
            step = step * 0.7;
        }
        
        last_sharpness = next_sharpness;
    }
}

注意:自适应步长有个陷阱——如果步长放大太快,可能直接跳过峰值区域。我曾经把放大系数设成 2.0,结果算法在峰值附近来回震荡,永远收敛不了。后来我限制步长最大不超过行程的 15%,才稳定下来。

3.5 三种改进算法的对比

我把这三种改进算法在实际项目中跑过很多次,总结了一张对比表:

算法 抗局部极值 收敛速度 计算开销 实现难度
多起点爬山 高 中 高(与起点数成正比) 低
模拟退火爬山 高 慢 中 中
自适应步长爬山 中 快 低 低

我个人习惯的做法是:如果对焦时间要求宽松(比如显微镜、工业检测),用多起点爬山+自适应步长组合;如果要求实时性(比如手机摄像头、无人机),用自适应步长爬山就够了。

最后说一句:爬山算法虽然简单,但用好了效果不输那些花里胡哨的算法。关键是理解它的局限性,然后针对性地做改进。我在项目中见过太多人一上来就上深度学习,结果连基本的爬山都没调明白——嗯,先把基础打扎实了再说。