2023年Q3,我负责一个信贷违约预测模型。数据量50万行,特征120维,用XGBoost。初始参数跑AUC=0.72,业务要求0.78+。
我按老办法:网格搜索。参数空间:max_depth [3,5,7,9,11],learning_rate [0.01,0.05,0.1,0.2],n_estimators [100,200,300,500],subsample [0.6,0.8,1.0],colsample_bytree [0.6,0.8,1.0]。组合数:5×4×4×3×3=720组。
每组训练+验证平均耗时45秒(单卡V100)。720×45=32400秒=9小时。跑完一轮9小时,AUC最高0.761,没达标。
我调整参数范围再跑,又是9小时。连续3天,每天跑2轮,总共54小时。最后AUC=0.773,离目标差0.007。人快废了。
后来换成贝叶斯优化,同样的硬件,同样的数据,3小时找到参数,AUC=0.782。直接达标。
这篇文章就是那次经历的系统总结。
原理:穷举所有参数组合。假设参数空间有N个离散值,组合数=∏(每个参数取值数)。
优点:实现简单,结果可复现。
缺点:维度灾难。参数每增加一个维度,组合数指数增长。连续参数必须离散化,可能错过最优值。
实测数据(50万行数据,XGBoost,V100):
原理:从参数空间中随机采样N组,取最优。Bergstra & Bengio (2012) 证明随机搜索在低维空间效率高于网格搜索。
优点:采样次数固定,可控制时间。
缺点:随机性导致结果不稳定,可能漏掉最优区域。
实测数据(同样数据,采样200次):
原理:用概率代理模型(高斯过程)拟合目标函数,通过采集函数(EI/PI/UCB)选择下一个评估点。核心思想:在探索(exploration)和利用(exploitation)之间平衡。
优点:迭代次数少,适合高维、连续参数空间,能处理噪声。
缺点:实现复杂,对初始点敏感,高斯过程计算复杂度O(n³)。
实测数据(同样数据,迭代100次):
版本:Python 3.10.12, XGBoost 2.0.3, scikit-optimize 0.9.0, numpy 1.24.3, pandas 2.0.3
pip install xgboost==2.0.3 scikit-optimize==0.9.0 numpy==1.24.3 pandas==2.0.3 scikit-learn==1.3.0
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import roc_auc_score
# 模拟数据生成(实际场景替换为真实数据)
np.random.seed(42)
n_samples = 500000
n_features = 120
X = np.random.randn(n_samples, n_features)
# 构造非线性关系
y = (np.sin(X[:, 0]) + np.cos(X[:, 1]) + X[:, 2]**2 +
np.random.randn(n_samples) * 0.5) > 0
y = y.astype(int)
# 划分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"训练集大小: {X_train.shape}")
print(f"验证集大小: {X_val.shape}")
print(f"正样本比例: {y.mean():.3f}")
import xgboost as xgb
from sklearn.model_selection import ParameterGrid
import time
# 定义参数网格
param_grid = {
'max_depth': [3, 5, 7, 9, 11],
'learning_rate': [0.01, 0.05, 0.1, 0.2],
'n_estimators': [100, 200, 300, 500],
'subsample': [0.6, 0.8, 1.0],
'colsample_bytree': [0.6, 0.8, 1.0]
}
# 生成所有组合
all_params = list(ParameterGrid(param_grid))
print(f"总参数组合数: {len(all_params)}")
best_auc = 0
best_params = None
start_time = time.time()
for i, params in enumerate(all_params):
# 训练模型
model = xgb.XGBClassifier(
**params,
random_state=42,
n_jobs=-1,
verbosity=0
)
model.fit(X_train, y_train)
# 验证
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
if auc > best_auc:
best_auc = auc
best_params = params
if (i + 1) % 50 == 0:
elapsed = time.time() - start_time
print(f"已完成 {i+1}/{len(all_params)},耗时 {elapsed:.1f}s,当前最佳AUC: {best_auc:.4f}")
total_time = time.time() - start_time
print(f"\n网格搜索完成")
print(f"总耗时: {total_time:.1f}s ({total_time/3600:.2f}h)")
print(f"最佳AUC: {best_auc:.4f}")
print(f"最佳参数: {best_params}")
import random
import time
# 定义参数空间(连续值)
param_space_random = {
'max_depth': (3, 11),
'learning_rate': (0.01, 0.2),
'n_estimators': (100, 500),
'subsample': (0.6, 1.0),
'colsample_bytree': (0.6, 1.0)
}
n_iterations = 200
best_auc = 0
best_params = None
start_time = time.time()
for i in range(n_iterations):
# 随机采样参数
params = {
'max_depth': random.randint(3, 11),
'learning_rate': random.uniform(0.01, 0.2),
'n_estimators': random.randint(100, 500),
'subsample': random.uniform(0.6, 1.0),
'colsample_bytree': random.uniform(0.6, 1.0)
}
model = xgb.XGBClassifier(
**params,
random_state=42,
n_jobs=-1,
verbosity=0
)
model.fit(X_train, y_train)
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
if auc > best_auc:
best_auc = auc
best_params = params
if (i + 1) % 50 == 0:
elapsed = time.time() - start_time
print(f"已完成 {i+1}/{n_iterations},耗时 {elapsed:.1f}s,当前最佳AUC: {best_auc:.4f}")
total_time = time.time() - start_time
print(f"\n随机搜索完成")
print(f"总耗时: {total_time:.1f}s ({total_time/3600:.2f}h)")
print(f"最佳AUC: {best_auc:.4f}")
print(f"最佳参数: {best_params}")
from skopt import gp_minimize
from skopt.space import Integer, Real
from skopt.utils import use_named_args
import time
# 定义参数空间
param_space_bayes = [
Integer(3, 11, name='max_depth'),
Real(0.01, 0.2, name='learning_rate'),
Integer(100, 500, name='n_estimators'),
Real(0.6, 1.0, name='subsample'),
Real(0.6, 1.0, name='colsample_bytree')
]
# 目标函数:返回负AUC(gp_minimize默认最小化)
@use_named_args(param_space_bayes)
def objective(**params):
model = xgb.XGBClassifier(
**params,
random_state=42,
n_jobs=-1,
verbosity=0
)
model.fit(X_train, y_train)
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
return -auc # 返回负值
# 执行贝叶斯优化
n_calls = 100 # 总评估次数
n_initial_points = 10 # 初始随机点
start_time = time.time()
result = gp_minimize(
func=objective,
dimensions=param_space_bayes,
n_calls=n_calls,
n_initial_points=n_initial_points,
random_state=42,
acq_func='EI', # Expected Improvement
xi=0.01, # 探索-利用平衡参数
kappa=1.96 # UCB参数(当acq_func='UCB'时使用)
)
total_time = time.time() - start_time
# 提取最佳参数
best_params_bayes = {
'max_depth': result.x[0],
'learning_rate': result.x[1],
'n_estimators': result.x[2],
'subsample': result.x[3],
'colsample_bytree': result.x[4]
}
best_auc_bayes = -result.fun
print(f"\n贝叶斯优化完成")
print(f"总耗时: {total_time:.1f}s ({total_time/3600:.2f}h)")
print(f"最佳AUC: {best_auc_bayes:.4f}")
print(f"最佳参数: {best_params_bayes}")
print(f"评估次数: {len(result.func_vals)}")
# 绘制收敛曲线(可选)
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(-result.func_vals, 'b-', alpha=0.7)
plt.xlabel('Iteration')
plt.ylabel('AUC')
plt.title('Bayesian Optimization Convergence')
plt.grid(True, alpha=0.3)
plt.show()
# 汇总结果
results = {
'Grid Search': {'AUC': 0.761, 'Time(h)': 9.0, 'Iterations': 720},
'Random Search': {'AUC': 0.775, 'Time(h)': 2.5, 'Iterations': 200},
'Bayesian Optimization': {'AUC': 0.782, 'Time(h)': 3.0, 'Iterations': 100}
}
import pandas as pd
df_results = pd.DataFrame(results).T
print(df_results)
# 效率对比
print("\n效率对比(每单位时间获得的AUC提升):")
baseline_auc = 0.72
for method, data in results.items():
improvement = (data['AUC'] - baseline_auc) * 100
efficiency = improvement / data['Time(h)']
print(f"{method}: AUC提升 {improvement:.2f}%,每小时提升 {efficiency:.2f}%")
贝叶斯优化在前20次迭代中AUC从0.72快速提升到0.775,之后缓慢提升到0.782。随机搜索前50次迭代波动较大,AUC在0.74-0.77之间震荡。网格搜索由于是穷举,没有收敛曲线概念,但720次中前100次已经覆盖大部分区域。
5次重复实验(不同随机种子):
| 方法 | 平均AUC | 标准差 | 最好AUC | 最差AUC |
|---|---|---|---|---|
| 网格搜索 | 0.761 | 0.000 | 0.761 | 0.761 |
| 随机搜索 | 0.773 | 0.008 | 0.781 | 0.765 |
| 贝叶斯优化 | 0.781 | 0.003 | 0.784 | 0.778 |
贝叶斯优化标准差0.003,远低于随机搜索的0.008。网格搜索没有方差因为它是确定性的。
| 方法 | GPU利用率 | 峰值内存 | CPU利用率 |
|---|---|---|---|
| 网格搜索 | 65% | 16GB | 85% |
| 随机搜索 | 45% | 12GB | 70% |
| 贝叶斯优化 | 55% | 14GB | 78% |
网格搜索GPU利用率最高因为连续训练,但内存也最高因为要缓存中间结果。贝叶斯优化在资源消耗和效果之间取得平衡。
问题:第一次用贝叶斯优化,n_initial_points=3,结果前3个点都在一个区域,高斯过程模型偏差严重,后续迭代一直在这个区域探索,最终AUC只有0.745。
解决:n_initial_points至少设为参数维度的2倍。5个参数至少10个初始点。用拉丁超立方采样(LHS)替代随机采样,覆盖更均匀。
from skopt.sampler import Lhs
from skopt.space import Space
space = Space(param_space_bayes)
lhs = Lhs(criterion="maximin", iterations=1000)
initial_points = lhs.generate(space.dimensions, 10)
# 然后传给gp_minimize的x0参数
问题:acq_func='UCB'时kappa=0.1,导致完全偏向利用(exploitation),模型只在已知最优区域附近搜索,错过了更好的参数组合。
解决:kappa默认1.96(对应95%置信区间),对于探索需求强的场景可以设到2.5-3.0。EI函数的xi参数控制探索程度,默认0.01,可以设到0.05-0.1。
# 探索性更强的配置
result = gp_minimize(
objective,
dimensions=param_space_bayes,
n_calls=100,
n_initial_points=10,
acq_func='EI',
xi=0.05, # 增加探索
random_state=42
)
问题:某些参数组合(如learning_rate=0.2, n_estimators=500)导致模型过拟合,验证集AUC=1.0,但测试集AUC=0.65。更严重的是,某些参数导致训练崩溃返回NaN。
解决:在目标函数中加入异常处理,返回一个惩罚值。
@use_named_args(param_space_bayes)
def objective_safe(**params):
try:
model = xgb.XGBClassifier(
**params,
random_state=42,
n_jobs=-1,
verbosity=0
)
model.fit(X_train, y_train)
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
if np.isnan(auc) or np.isinf(auc):
return 1.0 # 返回最差值
return -auc
except Exception as e:
print(f"参数 {params} 出错: {e}")
return 1.0 # 返回最差值
问题:max_depth设为Integer(1, 50),learning_rate设为Real(0.0001, 1.0)。结果贝叶斯优化花了很多时间在max_depth>30的区域探索,这些区域模型严重过拟合,AUC反而低。
解决:根据经验缩小参数范围。先用少量随机搜索(50次)确定大致范围,再用贝叶斯优化精细搜索。
# 两阶段调参
# 第一阶段:宽范围随机搜索
# 第二阶段:窄范围贝叶斯优化
param_space_phase2 = [
Integer(3, 10, name='max_depth'), # 缩小范围
Real(0.01, 0.15, name='learning_rate'),
Integer(100, 400, name='n_estimators'),
Real(0.7, 1.0, name='subsample'),
Real(0.7, 1.0, name='colsample_bytree')
]
问题:没有设置early_stopping_rounds,每次训练都跑满n_estimators=500轮,耗时是必要时间的3-5倍。
解决:在XGBoost中设置early_stopping_rounds,并传入评估集。
@use_named_args(param_space_bayes)
def objective_with_early_stop(**params):
model = xgb.XGBClassifier(
**params,
random_state=42,
n_jobs=-1,
verbosity=0,
early_stopping_rounds=50,
eval_metric='auc'
)
model.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
verbose=False
)
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
return -auc
问题:实际业务中不仅要AUC高,还要推理速度快(特征数少)。单目标贝叶斯优化只优化AUC,找到的参数可能特征维度很高。
解决:用多目标贝叶斯优化(如ParEGO、EHVI),或者将推理时间作为惩罚项加入目标函数。
# 多目标:AUC + 特征数惩罚
def objective_multi(**params):
model = xgb.XGBClassifier(**params, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)
y_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred)
# 惩罚特征数(假设特征重要性)
feature_importance = model.feature_importances_
n_features_used = np.sum(feature_importance > 0.01)
penalty = n_features_used / X_train.shape[1] * 0.05 # 5%惩罚
return -(auc - penalty)
贝叶斯优化不是万能药,但在以下场景效果显著:
如果参数维度<3,网格搜索或随机搜索更简单。如果参数维度>20,考虑先做特征选择或降维。
最后,别迷信默认参数。scikit-optimize的默认配置适合通用场景,但针对你的数据和模型,一定要调采集函数参数和初始点策略。我踩过的坑,你大概率也会遇到。
专注技术分享与实战