3. 随机搜索算法:纯随机采样、均匀分布与对数分布、随机搜索的优缺点、实战:参数随机采样
说到超参数调优,很多人第一反应就是网格搜索。嗯,网格搜索确实很直观——把所有参数组合都试一遍,总能找到最好的。但我在实际项目中遇到过一个问题:当参数维度超过3个时,网格搜索的计算量会爆炸式增长。你想想看,每个参数哪怕只取10个值,5个参数就是10万次实验,这谁受得了?
这时候,随机搜索就派上用场了。说白了,它就是在参数空间里随机撒点,然后评估这些点的效果。听起来很简单对吧?但这里面的门道可不少。
3.1 纯随机采样:最朴素的思路
纯随机采样,就是完全靠运气。每个参数在它的取值范围内随机选一个值,组合起来就是一组参数。我刚开始做调优时,觉得这方法太不靠谱了——随机碰运气能有什么好结果?
但后来我发现,随机搜索其实有个很妙的性质:它不会像网格搜索那样,在某个维度上密集采样而忽略其他维度。举个例子,假设有两个参数,一个很重要,一个几乎没影响。网格搜索会在两个维度上都均匀采样,浪费大量计算资源在无关参数上。而随机搜索呢?它天然地会在重要参数上探索更多不同的值。
核心观点:随机搜索在低维空间(<5维)上效果不如网格搜索,但在高维空间(≥5维)上,随机搜索往往能更快找到好的参数组合。
3.2 均匀分布与对数分布:采样策略的选择
随机搜索不是简单地「随机」就完事了。采样分布的选择,直接影响搜索效果。我个人习惯把参数分为两类:
- 线性参数:比如学习率、批量大小,用均匀分布采样
- 指数参数:比如正则化系数、衰减率,用对数分布采样
为什么会这样?你想想看,学习率从0.1调到0.2,变化幅度是0.1;但从0.01调到0.02,变化幅度只有0.01。如果用均匀分布,小数值区域会被严重忽略。而用对数分布,就能保证在不同数量级上都有足够的采样点。
实战技巧:我一般用np.logspace或np.loguniform来实现对数分布采样。比如学习率范围[1e-5, 1e-1],用对数分布能均匀覆盖10^-5到10^-1这个区间。
3.3 随机搜索的优缺点
做了这么多年调优,我总结了一下随机搜索的优缺点:
| 优点 | 缺点 |
|---|---|
| 实现简单,几行代码就能跑 | 无法利用历史信息,每次采样独立 |
| 高维空间下效率优于网格搜索 | 低维空间下不如网格搜索稳定 |
| 天然支持并行计算 | 无法保证找到全局最优 |
| 对参数分布不敏感 | 需要大量采样才能覆盖好区域 |
我曾经在一个项目里,用随机搜索调优一个8维参数空间。网格搜索需要10^8次实验,而随机搜索只用了500次就找到了比人工调参好30%的结果。嗯,这就是随机搜索的魅力所在。
3.4 实战:参数随机采样
好了,理论说完了,咱们直接上代码。下面是一个完整的随机搜索示例:
import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor
# 定义参数空间
param_space = {
'n_estimators': (10, 500), # 均匀分布
'max_depth': (3, 30), # 均匀分布
'min_samples_split': (2, 20), # 均匀分布
'learning_rate': (1e-4, 1e-1), # 对数分布
'reg_alpha': (1e-6, 1e-2) # 对数分布
}
def random_sample(param_space, n_samples=100):
"""随机采样n组参数"""
samples = []
for _ in range(n_samples):
params = {}
for name, (low, high) in param_space.items():
if name in ['learning_rate', 'reg_alpha']:
# 对数分布采样
log_low = np.log10(low)
log_high = np.log10(high)
params[name] = 10 ** np.random.uniform(log_low, log_high)
else:
# 均匀分布采样
params[name] = np.random.randint(low, high + 1)
samples.append(params)
return samples
# 生成100组随机参数
random_params = random_sample(param_space, n_samples=100)
# 评估每组参数
best_score = -np.inf
best_params = None
for i, params in enumerate(random_params):
model = RandomForestRegressor(**params, random_state=42)
scores = cross_val_score(model, X_train, y_train, cv=5, scoring='r2')
mean_score = np.mean(scores)
if mean_score > best_score:
best_score = mean_score
best_params = params
if (i + 1) % 20 == 0:
print(f'已完成 {i+1}/100 组参数评估')
print(f'最佳R²分数: {best_score:.4f}')
print(f'最佳参数: {best_params}')
注意:随机搜索的结果具有随机性。同一份代码跑两次,结果可能不同。我建议固定随机种子(np.random.seed(42)),或者多跑几次取平均。
这段代码里,我用了两种采样策略:均匀分布用于整数参数,对数分布用于连续参数。你可能会问,为什么不用np.random.uniform直接采样?因为对于学习率这种跨越几个数量级的参数,均匀分布会让小数值区域几乎采不到点。对数分布能保证每个数量级都有足够的采样。
另外,我习惯在评估过程中打印进度。这看起来是个小细节,但在实际项目中,几百组参数可能要跑好几个小时。没有进度提示,你根本不知道程序是在正常运行还是卡住了。
最后说一句,随机搜索虽然简单,但别小看它。在很多实际场景中,随机搜索配合适当的后处理(比如局部优化),效果甚至能媲美贝叶斯优化。嗯,这就是我为什么一直强调「先跑随机搜索,再考虑更复杂的方法」。