3. 爬山搜索算法:从原理到实战改进
爬山算法,说白了就是「往高处走」。在自动对焦里,我们把图像清晰度评价函数看作一座山,山顶就是最清晰的位置。算法从当前点出发,每次朝清晰度值更高的方向移动一步,直到找不到更高的点为止。
听起来很简单对吧?但我在实际项目中踩过不少坑。今天咱们就把爬山算法的底裤扒干净,看看它到底怎么工作,又该怎么改进。
3.1 爬山算法的基本原理
想象你蒙着眼站在一座山上,想走到山顶。你只能用手杖探探周围的地面——哪边高就往哪边挪一步。这就是爬山算法的核心思想。
核心流程:
- 从当前位置出发,计算当前清晰度值
- 朝某个方向移动一步,计算新位置的清晰度
- 如果新位置更清晰,继续朝这个方向走
- 如果新位置更模糊,换个方向试试
- 重复直到找不到更清晰的位置
我习惯把爬山算法分成两种模式:
| 模式 | 策略 | 适用场景 |
|---|---|---|
| 最速爬山 | 比较左右两个方向,选更优的那个 | 清晰度曲线比较平滑时 |
| 简单爬山 | 固定一个方向,走到不能走再换 | 实时性要求高、计算资源有限时 |
嗯,这里要注意:最速爬山虽然每一步都选最优方向,但计算量翻倍。我在做手机摄像头对焦时,就因为这个原因选了简单爬山——每帧图像的处理时间只有几毫秒,经不起折腾。
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 三种改进算法的对比
我把这三种改进算法在实际项目中跑过很多次,总结了一张对比表:
| 算法 | 抗局部极值 | 收敛速度 | 计算开销 | 实现难度 |
|---|---|---|---|---|
| 多起点爬山 | 高 | 中 | 高(与起点数成正比) | 低 |
| 模拟退火爬山 | 高 | 慢 | 中 | 中 |
| 自适应步长爬山 | 中 | 快 | 低 | 低 |
我个人习惯的做法是:如果对焦时间要求宽松(比如显微镜、工业检测),用多起点爬山+自适应步长组合;如果要求实时性(比如手机摄像头、无人机),用自适应步长爬山就够了。
最后说一句:爬山算法虽然简单,但用好了效果不输那些花里胡哨的算法。关键是理解它的局限性,然后针对性地做改进。我在项目中见过太多人一上来就上深度学习,结果连基本的爬山都没调明白——嗯,先把基础打扎实了再说。