第4章:网格搜索算法——全网格、稀疏网格与自适应网格

网格搜索,说白了就是暴力调参。你给出一组参数候选值,它把所有组合都试一遍。听起来很笨,对吧?但在我多年的实战经验里,这招往往是最稳的。尤其是当你对模型参数空间还不太熟悉的时候,网格搜索能帮你快速建立直觉。

4.1 全网格搜索:最笨但最可靠的方法

全网格搜索,就是穷举所有参数组合。假设你有两个参数:learning_rate 取 [0.01, 0.1, 1.0],max_depth 取 [3, 5, 7]。那么总共要跑 3×3 = 9 组实验。

我刚开始做调参时,总觉得这方法太原始。后来在一个风控模型项目里,我用随机搜索试了200次,结果还不如全网格搜索的50次效果好。为什么?因为随机搜索可能会漏掉参数空间里的「好区域」。全网格虽然慢,但它不会漏。

核心公式: 组合数 = 参数1候选数 × 参数2候选数 × ... × 参数n候选数

举个例子:

from sklearn.model_selection import GridSearchCV

param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7, 10],
    'min_samples_split': [2, 5, 10]
}

# 组合数 = 3 × 4 × 3 = 36 组
grid_search = GridSearchCV(
    estimator=RandomForestClassifier(),
    param_grid=param_grid,
    cv=5,  # 5折交叉验证
    scoring='accuracy'
)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.4f}")

嗯,这里要注意:cv=5 意味着每组参数要训练5次。所以实际训练次数是 36 × 5 = 180 次。如果你的数据集有10万条,每次训练要30秒...那你就得等一个半小时。

4.2 稀疏网格:用更少的组合覆盖更大的空间

全网格搜索的问题很明显:参数一多,组合数爆炸。我记得有一次做深度学习调参,参数有6个,每个取5个值,组合数直接到了 5⁶ = 15625 组。按每组训练2分钟算,得跑21天。

稀疏网格的思路是:不取所有组合,而是只取一部分。比如每个参数只取3个值,但让这些值在空间里分布得更「稀疏」一些。

我的经验: 稀疏网格适合参数范围很大的情况。比如 learning_rate 从 0.0001 到 1.0,你可以在对数尺度上均匀取点:0.0001, 0.001, 0.01, 0.1, 1.0。这样5个点就能覆盖4个数量级。
import numpy as np

# 对数尺度稀疏网格
param_grid_sparse = {
    'learning_rate': np.logspace(-4, 0, 5),  # [0.0001, 0.001, 0.01, 0.1, 1.0]
    'batch_size': [16, 32, 64, 128],
    'epochs': [10, 20, 50]
}

# 组合数 = 5 × 4 × 3 = 60 组
# 比全网格(比如每个参数取10个值)少了100倍

你想想看,稀疏网格的本质是什么?是用先验知识来减少搜索空间。如果你知道某个参数对结果不敏感,那就少取几个值。如果你知道某个参数在某个区间内效果最好,那就多取几个值。

4.3 自适应网格:让搜索变得更聪明

自适应网格,说白了就是「先粗搜,再细搜」。我习惯分两步走:

  1. 粗搜阶段: 用稀疏网格快速定位「好区域」
  2. 细搜阶段: 在好区域周围加密网格

举个例子,假设我们要调 n_estimatorsmax_depth

# 第一步:粗搜
coarse_grid = {
    'n_estimators': [10, 50, 100, 200, 500],
    'max_depth': [3, 5, 7, 10, 15]
}
# 25组实验

# 假设粗搜发现最佳点在 n_estimators=100, max_depth=7 附近

# 第二步:细搜
fine_grid = {
    'n_estimators': [80, 90, 100, 110, 120],
    'max_depth': [5, 6, 7, 8, 9]
}
# 又是25组实验

# 总共50组,比全网格的25×25=625组少了12倍
我曾经踩过的坑: 自适应网格有个陷阱——如果粗搜的网格太稀疏,可能会漏掉真正的最优区域。比如最优点在 n_estimators=150 附近,但粗搜只取了 100 和 200,那细搜就会在 100 附近加密,永远找不到 150。所以粗搜的步长不能太大,至少要保证「好区域」不会被跳过。

4.4 维度灾难:网格搜索的致命弱点

维度灾难,是网格搜索最大的敌人。随着参数数量的增加,搜索空间呈指数级增长。我画个图你就明白了:

维度灾难示意图:网格数量随维度增长 参数维度数 1 2 3 4 5 10⁰ 10² 10⁴ 10⁶ 10⁵ 组合 (每个维度10个值) 5⁵ = 3125 组合 (每个维度5个值) 10个值/维度 5个值/维度

看到没?维度从1涨到5,组合数从10涨到10万。这就是维度灾难。我见过有人调参时列了10个参数,每个取10个值,组合数 10¹⁰ = 100亿。就算你有1000台机器并行跑,也得跑几年。

维度灾难的数学本质: 在d维空间中,要维持相同的采样密度,需要的样本数随d呈指数增长。换句话说,高维空间是「稀疏」的——大部分区域都是空的。

4.5 实战:网格搜索调参全流程

好了,理论说完了。咱们来一个完整的实战案例。我用XGBoost调参,目标是最大化AUC。

import xgboost as xgb
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.metrics import roc_auc_score
import numpy as np

# 加载数据(假设你已经有了)
# X, y = load_your_data()
# X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 第一步:粗搜
print("=== 第一阶段:粗搜 ===")
param_grid_coarse = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.3]
}

xgb_model = xgb.XGBClassifier(
    objective='binary:logistic',
    eval_metric='auc',
    use_label_encoder=False
)

grid_coarse = GridSearchCV(
    estimator=xgb_model,
    param_grid=param_grid_coarse,
    cv=3,
    scoring='roc_auc',
    n_jobs=-1,
    verbose=1
)
grid_coarse.fit(X_train, y_train)

print(f"粗搜最佳参数: {grid_coarse.best_params_}")
print(f"粗搜最佳AUC: {grid_coarse.best_score_:.4f}")

# 第二步:在粗搜结果周围细搜
print("\n=== 第二阶段:细搜 ===")
best_params = grid_coarse.best_params_

# 在最佳点周围加密
param_grid_fine = {
    'n_estimators': [
        best_params['n_estimators'] - 20,
        best_params['n_estimators'] - 10,
        best_params['n_estimators'],
        best_params['n_estimators'] + 10,
        best_params['n_estimators'] + 20
    ],
    'max_depth': [
        best_params['max_depth'] - 1,
        best_params['max_depth'],
        best_params['max_depth'] + 1
    ],
    'learning_rate': [
        best_params['learning_rate'] * 0.5,
        best_params['learning_rate'],
        best_params['learning_rate'] * 2
    ]
}

# 过滤掉无效值
param_grid_fine['n_estimators'] = [
    x for x in param_grid_fine['n_estimators'] if x > 0
]
param_grid_fine['max_depth'] = [
    x for x in param_grid_fine['max_depth'] if x > 0
]

grid_fine = GridSearchCV(
    estimator=xgb_model,
    param_grid=param_grid_fine,
    cv=5,  # 细搜用更多折数
    scoring='roc_auc',
    n_jobs=-1,
    verbose=1
)
grid_fine.fit(X_train, y_train)

print(f"细搜最佳参数: {grid_fine.best_params_}")
print(f"细搜最佳AUC: {grid_fine.best_score_:.4f}")

# 第三步:用最佳参数在测试集上评估
final_model = xgb.XGBClassifier(
    **grid_fine.best_params_,
    objective='binary:logistic',
    eval_metric='auc',
    use_label_encoder=False
)
final_model.fit(X_train, y_train)
y_pred = final_model.predict_proba(X_test)[:, 1]
test_auc = roc_auc_score(y_test, y_pred)
print(f"\n测试集AUC: {test_auc:.4f}")
我的习惯: 粗搜用3折交叉验证,细搜用5折。粗搜速度快,细搜精度高。另外,n_jobs=-1 可以并行跑所有组合,能省不少时间。

4.6 网格搜索的实用技巧总结

场景 推荐方法 说明
参数少(≤3个) 全网格搜索 穷举所有组合,结果最可靠
参数中等(4-6个) 自适应网格 先粗搜再细搜,平衡效率与精度
参数多(≥7个) 别用网格搜索 改用随机搜索或贝叶斯优化
参数范围跨度大 对数尺度网格 np.logspace 生成候选值
计算资源有限 稀疏网格 减少每个参数的候选数
我曾经犯过的错: 有一次我为了省时间,把交叉验证折数从5降到了2。结果选出来的参数在测试集上表现很差。后来发现是因为数据分布不均匀,2折的验证结果波动太大。所以,交叉验证折数不能太少,至少3折,最好5折。

最后说一句:网格搜索不是万能的,但它是你理解参数空间的最好工具。先用网格搜索跑几轮,你就能知道哪些参数重要、哪些参数不敏感、最优区域大概在哪里。有了这些直觉,再用更高级的调参方法,效果会好很多。


公众号:蓝海资料掘金营,微信deep3321