超参数调优:在指数级空间里聪明地搜索


文档摘要

超参数调优:在指数级空间里聪明地搜索 本节摘要:超参数(Hyperparameters)是训练开始前你拧的那些旋钮。拧得好与拧得差,是一个平庸模型与一个优秀模型的差别。你的梯度提升模型有学习率、树数、最大深度、叶最小样本数、子采样率、列采样率——六个超参数,每个 5 个合理取值,网格就有 5^6=15,625 种组合,每个训练 10 秒就是 43 小时算力。本节讲透如何聪明地搜索:网格搜索穷举但指数爆炸;随机搜索在多数超参数「有效维度低」时以相同预算覆盖更重要维度;贝叶斯优化用代理模型记住过去的评估、不再浪费算力在明显差的区域,以 25 倍更少评估找到更好配置。我们还会讲学习率调度、早停(Hyperband)、嵌套交叉验证,以及哪些超参数真正重要(学习率永远第一)。

超参数调优:在指数级空间里聪明地搜索

本节摘要:超参数(Hyperparameters)是训练开始前你拧的那些旋钮。拧得好与拧得差,是一个平庸模型与一个优秀模型的差别。你的梯度提升模型有学习率、树数、最大深度、叶最小样本数、子采样率、列采样率——六个超参数,每个 5 个合理取值,网格就有 5^6=15,625 种组合,每个训练 10 秒就是 43 小时算力。本节讲透如何聪明地搜索:网格搜索穷举但指数爆炸;随机搜索在多数超参数「有效维度低」时以相同预算覆盖更重要维度;贝叶斯优化用代理模型记住过去的评估、不再浪费算力在明显差的区域,以 2~5 倍更少评估找到更好配置。我们还会讲学习率调度、早停(Hyperband)、嵌套交叉验证,以及哪些超参数真正重要(学习率永远第一)。

学习目标

阅读完本节,你应当能够:

  1. 从零实现网格搜索、随机搜索、贝叶斯优化,对比它们的样本效率。
  2. 解释为何在「多数超参数有效维度低」时随机搜索胜过网格搜索。
  3. 用代理模型和采集函数构建贝叶斯优化循环来引导搜索。
  4. 设计通过恰当交叉验证避免对验证集过拟合的调参策略。

一、问题与直觉

你的梯度提升模型有学习率、树数、最大深度、叶最小样本数、子采样率、列采样率——六个超参数。每个 5 个合理取值,网格就是 5^6 = 15,625 种组合。每个训练 10 秒,全部试一遍是 43 小时算力。

网格搜索是最直觉的思路,也是规模上来后最糟的思路。随机搜索用更少算力做得更好。贝叶斯优化通过从过去评估中学习,做得更好。知道用哪种策略、哪些超参数真正重要,能省下几天的浪费 GPU 时间。

参数 vs 超参数

参数(Parameter)在训练中学习(权重、偏置、分裂阈值)。超参数在训练开始前设定,控制学习如何发生。

超参数 控制什么 典型范围
学习率 每次更新的步长 0.001 到 1.0
树数 / epoch 数 训多久 10 到 10,000
最大深度 模型复杂度 1 到 30
正则化(lambda) 防过拟合强度 0.0001 到 100
批大小 梯度估计噪声 16 到 512
Dropout 率 失活神经元比例 0.0 到 0.5

网格搜索

网格搜索评估指定取值的每种组合。穷举、好懂,但随超参数数量指数爆炸。

2 个超参数的网格: learning_rate: [0.01, 0.1, 1.0] max_depth: [3, 5, 7] 评估数:3 x 3 = 9 种组合 (0.01, 3) (0.01, 5) (0.01, 7) (0.1, 3) (0.1, 5) (0.1, 7) (1.0, 3) (1.0, 5) (1.0, 7)

网格搜索有根本缺陷:如果一个超参数重要而另一个不重要,多数评估是浪费的。9 次评估里,那个重要参数你只测到 3 个不同值。

随机搜索

随机搜索从分布里采样超参数,而不是用网格。同样的 9 次评估预算,每个超参数你都能测到 9 个不同值。

随机胜网格的原因(Bergstra & Bengio, 2012):

  • 多数超参数有效维度低。6 个超参数里通常只有 1~2 个对给定问题重要。
  • 网格在不重要维度上浪费评估。
  • 随机搜索在相同预算下更密集地覆盖重要维度。
  • 60 次随机试验,你有 95% 概率找到搜索空间内(若存在)最优点的 5% 邻域内一个点。

贝叶斯优化

随机搜索忽略结果。它不会学到「高学习率导致发散」或「深度 3 持续优于深度 10」。贝叶斯优化用过去的评估来决定下一步搜哪里。

两个关键组件:

代理模型(Surrogate Model):一个评估成本低的模型(通常高斯过程,Gaussian Process),近似那个昂贵的真实目标函数。它在搜索空间任意点既给出预测又给出不确定性估计。

采集函数(Acquisition Function):通过权衡开发(exploitation,在已知好点附近搜)和探索(exploration,在不确定性高的地方搜)来决定下一步评估哪里。常见选择:

  • 期望改进(Expected Improvement, EI):在这个点上我们期望比当前最佳改进多少?
  • 置信上界(Upper Confidence Bound, UCB):预测值加上不确定性的倍数。UCB 高意味着要么有前途要么未被探索。
  • 改进概率(Probability of Improvement, PI):这个点打败当前最佳的概率多大?

贝叶斯优化通常比随机搜索用 2~5 倍更少的评估找到更好超参数。拟合代理模型的开销相比训练真实模型可忽略。

早停(Early Stopping)

不是每次训练都需要跑完。如果一个配置在 10 个 epoch 后明显很差,就停掉它转向下一个。这是超参数搜索语境下的早停。

策略:

  • 基于耐心(Patience-based):若验证损失连续 N 个 epoch 无改善就停。
  • 中位数剪枝(Median Pruning):若某试验的中间结果比同步骤已完成试验的中位数还差就停。
  • Hyperband:给很多配置小预算,再逐步给最好的那些加预算。

Hyperband 特别有效。它启动 81 个配置各跑 1 个 epoch,保留前 1/3 给它们 3 个 epoch,再保留前 1/3……这样比把所有配置跑满预算快 10~50 倍找到好配置。

学习率调度器

学习率几乎永远是最重要的超参数。与其保持固定,调度器会在训练中调整它。

调度器 公式 何时用
阶梯衰减 每 N 个 epoch 乘 0.1 经典 CNN 训练
余弦退火 lr * 0.5 * (1 + cos(pi * t / T)) 现代默认
预热+衰减 线性升再余弦降 Transformer
单周期 一个周期内先升后降 快速收敛
平台衰减 指标停滞时降一档 安全默认

超参数重要性

不是所有超参数同等重要。随机森林(Probst 等,2019)和梯度提升的研究显示一致规律:

高重要性:

  • 学习率(永远第一个调)
  • 估计器数 / epoch 数(用早停代替调)
  • 正则化强度

中等重要性:

  • 最大深度 / 层数
  • 叶最小样本数 / 权重衰减
  • 子采样率

低重要性:

  • 最大特征数(随机森林)
  • 具体激活函数选择
  • 批大小(合理范围内)

先调重要的,其余留默认。

实用策略

具体流程:

  1. 从库默认值开始。它们由经验丰富的从业者选定,通常已经到 80% 水准。
  2. 粗随机搜索。宽范围,20~50 次试验,用早停快速杀掉差的运行。
  3. 分析结果。哪些超参数与性能相关?缩窄搜索空间。
  4. 精细搜索。在缩窄空间里贝叶斯优化或聚焦随机搜索,50~100 次试验。
  5. 用找到的最佳超参数在全部训练数据上重训。

交叉验证集成

在单一验证切分上调超参有风险——最佳超参可能过拟合到那个特定验证折。嵌套交叉验证(Nested Cross-Validation)用两个循环解决:

  • 外层循环(评估):把数据切成 训练+验证 和 测试。报告无偏性能。
  • 内层循环(调参):把 训练+验证 切成 训练 和 验证。找最佳超参。

每个外层折独立找自己的最佳超参。外层分数是泛化性能的无偏估计。

⚠️ 嵌套 CV 很贵(5 外层 x 5 内层 x 27 网格点 = 675 次模型拟合),但给你可信的性能估计。在论文报告最终结果、或决策利害攸关时用它。

实用贴士

先调学习率。 它对基于梯度的方法永远是最重要的超参数。一个坏学习率让其他一切都不重要。把其他超参固定在默认值,先扫学习率。

学习率和正则化用对数均匀分布。 0.001 和 0.01 的差别与 0.1 和 1.0 的差别一样大。线性搜索在大值端浪费预算。

用早停代替调 n_estimators 对提升和神经网络,把 n_estimators 或 epoch 设高,让早停决定何时停。这样从搜索里去掉一个超参数。

预算分配。 把调参预算的 60% 花在最重要的前 2 个超参数上,剩下 40% 花在其他一切。前 2 个贡献了大部分性能变化。

尺度很重要。 批大小绝不在对数尺度上搜(16、32、64 就好)。学习率永远在对数尺度上搜。让搜索分布匹配超参数影响模型的方式。

模型类型 顶级超参数 推荐搜索 预算
随机森林 n_estimators, max_depth, min_samples_leaf 随机搜索,50 次 低(训练快)
梯度提升 learning_rate, n_estimators, max_depth 贝叶斯,100 次 + 早停
神经网络 learning_rate, weight_decay, batch_size 贝叶斯或随机,100+ 次 高(训练慢)
SVM C, gamma(RBF 核) 对数尺度网格,25~50 次 低(2 参数)
Lasso/Ridge alpha 一维对数搜索,20 次 极低
XGBoost learning_rate, max_depth, subsample, colsample 贝叶斯,100~200 次 + 早停

拿不准时:随机搜索,试验数至少是超参数数的 2 倍(如 6 个超参数至少 12+ 次试验)。你会惊讶于 50 次随机搜索多频繁地击败精心设计的网格搜索。

二、从零实现

code/tuning.py 从零实现网格搜索、随机搜索和一个简化的贝叶斯优化器。

第 1 步:从零实现网格搜索

def grid_search(model_fn, param_grid, X_train, y_train, X_val, y_val): keys = list(param_grid.keys()) values = list(param_grid.values()) best_score = -float("inf") best_params = None n_evals = 0 for combo in itertools.product(*values): params = dict(zip(keys, combo)) model = model_fn(**params) model.fit(X_train, y_train) score = evaluate(model, X_val, y_val) n_evals += 1 if score > best_score: best_score = score best_params = params return best_params, best_score, n_evals

第 2 步:从零实现随机搜索

def random_search(model_fn, param_distributions, X_train, y_train, X_val, y_val, n_iter=50, seed=42): rng = np.random.RandomState(seed) best_score = -float("inf") best_params = None for _ in range(n_iter): params = {k: sample(v, rng) for k, v in param_distributions.items()} model = model_fn(**params) model.fit(X_train, y_train) score = evaluate(model, X_val, y_val) if score > best_score: best_score = score best_params = params return best_params, best_score, n_iter

第 3 步:贝叶斯优化(简化版)

核心思想:用高斯过程拟合已观测的 (超参数, 分数) 对,再用采集函数决定下一步看哪里。

class SimpleBayesianOptimizer: def __init__(self, search_space, n_initial=5): self.search_space = search_space self.n_initial = n_initial self.X_observed = [] self.y_observed = [] def _kernel(self, x1, x2, length_scale=1.0): dists = np.sum((x1[:, None, :] - x2[None, :, :]) ** 2, axis=2) return np.exp(-0.5 * dists / length_scale ** 2) def _fit_gp(self, X_new): X_obs = np.array(self.X_observed) y_obs = np.array(self.y_observed) y_mean = y_obs.mean() y_centered = y_obs - y_mean K = self._kernel(X_obs, X_obs) + 1e-4 * np.eye(len(X_obs)) K_star = self._kernel(X_new, X_obs) L = np.linalg.cholesky(K) alpha = np.linalg.solve(L.T, np.linalg.solve(L, y_centered)) mu = K_star @ alpha + y_mean v = np.linalg.solve(L, K_star.T) var = 1.0 - np.sum(v ** 2, axis=0) var = np.maximum(var, 1e-6) return mu, var def _expected_improvement(self, mu, var, best_y): sigma = np.sqrt(var) z = (mu - best_y) / (sigma + 1e-10) ei = sigma * (z * norm_cdf(z) + norm_pdf(z)) return ei def suggest(self): if len(self.X_observed) < self.n_initial: return sample_random(self.search_space) candidates = [sample_random(self.search_space) for _ in range(500)] X_cand = np.array([to_vector(c) for c in candidates]) mu, var = self._fit_gp(X_cand) ei = self._expected_improvement(mu, var, max(self.y_observed)) return candidates[np.argmax(ei)] def observe(self, params, score): self.X_observed.append(to_vector(params)) self.y_observed.append(score)

GP 代理在每点给出两样东西:预测分数(mu)和不确定性(var)。期望改进( Expected Improvement)权衡两者:它偏爱模型预测高分不确定性高的点。早期多数点不确定性高,优化器探索;后期聚焦最有前途的区域。

第 4 步:三种方法对比

在同一合成目标上跑三种方法对比。这里用一个简化的包装器,直接对一个目标函数调用各优化器(无模型训练):

def synthetic_objective(params): lr = params["learning_rate"] depth = params["max_depth"] return -(np.log10(lr) + 2) ** 2 - (depth - 4) ** 2 + 10 param_grid = { "learning_rate": [0.001, 0.01, 0.1, 1.0], "max_depth": [2, 3, 4, 5, 6, 7, 8], } grid_best = None grid_score = -float("inf") grid_history = [] for combo in itertools.product(*param_grid.values()): params = dict(zip(param_grid.keys(), combo)) score = synthetic_objective(params) grid_history.append((params, score)) if score > grid_score: grid_score = score grid_best = params param_dist = { "learning_rate": ("log_float", 0.001, 1.0), "max_depth": ("int", 2, 8), } rand_best = None rand_score = -float("inf") rand_history = [] rng = np.random.RandomState(42) for _ in range(28): params = {k: sample(v, rng) for k, v in param_dist.items()} score = synthetic_objective(params) rand_history.append((params, score)) if score > rand_score: rand_score = score rand_best = params optimizer = SimpleBayesianOptimizer(param_dist, n_initial=5) bayes_history = [] for _ in range(28): params = optimizer.suggest() score = synthetic_objective(params) optimizer.observe(params, score) bayes_history.append((params, score)) bayes_score = max(s for _, s in bayes_history) print(f"{'方法':<20} {'最佳分数':>12} {'评估数':>12}") print("-" * 50) print(f"{'网格搜索':<20} {grid_score:>12.4f} {len(grid_history):>12}") print(f"{'随机搜索':<20} {rand_score:>12.4f} {len(rand_history):>12}") print(f"{'贝叶斯优化':<20} {bayes_score:>12.4f} {len(bayes_history):>12}")

相同预算下,贝叶斯优化通常最快找到最佳分数,因为它不在明显差的区域浪费评估。随机搜索比网格搜索覆盖更广。网格搜索只在超参数很少、能负担穷举时才赢。

三、框架对比

Optuna 实战

Optuna 是严肃调参的推荐库,开箱支持剪枝、分布式搜索、可视化。

import optuna def objective(trial): lr = trial.suggest_float("learning_rate", 1e-4, 1e-1, log=True) n_est = trial.suggest_int("n_estimators", 50, 500) max_depth = trial.suggest_int("max_depth", 2, 10) model = GradientBoostingRegressor( learning_rate=lr, n_estimators=n_est, max_depth=max_depth, ) model.fit(X_train, y_train) return mean_squared_error(y_val, model.predict(X_val)) study = optuna.create_study(direction="minimize") study.optimize(objective, n_trials=100) print(f"最佳参数: {study.best_params}") print(f"最佳 MSE: {study.best_value:.4f}")

Optuna 关键特性:

  • suggest_float(..., log=True) 用于最好在对数尺度搜的参数(学习率、正则化)。
  • suggest_int 用于整数参数。
  • suggest_categorical 用于离散选择。
  • 内置 MedianPruner 给坏试验早停。
  • study.trials_dataframe() 用于分析。

Optuna 配合剪枝

剪枝早停没前途的试验,省下大量算力:

import optuna from sklearn.model_selection import cross_val_score def objective(trial): params = { "learning_rate": trial.suggest_float("lr", 1e-4, 0.5, log=True), "max_depth": trial.suggest_int("max_depth", 2, 10), "n_estimators": trial.suggest_int("n_estimators", 50, 500), "subsample": trial.suggest_float("subsample", 0.5, 1.0), } model = GradientBoostingRegressor(**params) scores = cross_val_score(model, X_train, y_train, cv=3, scoring="neg_mean_squared_error") mean_score = -scores.mean() trial.report(mean_score, step=0) if trial.should_prune(): raise optuna.TrialPruned() return mean_score pruner = optuna.pruners.MedianPruner(n_startup_trials=10, n_warmup_steps=5) study = optuna.create_study(direction="minimize", pruner=pruner) study.optimize(objective, n_trials=200)

MedianPruner 在某试验的中间值比同步骤所有已完成试验的中位数还差时停掉它。剪枝要求调 trial.report() 报告中间指标、调 trial.should_prune() 检查是否该停。n_startup_trials=10 确保至少 10 个试验完整跑完再启用剪枝。这通常省 40~60% 总算力。

sklearn 内置调参器

快速实验时,sklearn 提供 GridSearchCVRandomizedSearchCVHalvingRandomSearchCV:

from sklearn.model_selection import RandomizedSearchCV from scipy.stats import loguniform, randint param_dist = { "learning_rate": loguniform(1e-4, 0.5), "max_depth": randint(2, 10), "n_estimators": randint(50, 500), } search = RandomizedSearchCV( GradientBoostingRegressor(), param_dist, n_iter=100, cv=5, scoring="neg_mean_squared_error", random_state=42, n_jobs=-1, ) search.fit(X_train, y_train) print(f"最佳参数: {search.best_params_}") print(f"最佳 CV MSE: {-search.best_score_:.4f}")

用 scipy 的 loguniform 给学习率和正则化,用 randint 给整数超参。n_jobs=-1 跨所有 CPU 核并行。

嵌套交叉验证

from sklearn.model_selection import cross_val_score, GridSearchCV from sklearn.ensemble import GradientBoostingRegressor inner_cv = GridSearchCV( GradientBoostingRegressor(), param_grid={ "learning_rate": [0.01, 0.05, 0.1], "max_depth": [2, 3, 5], "n_estimators": [50, 100, 200], }, cv=5, scoring="neg_mean_squared_error", ) outer_scores = cross_val_score( inner_cv, X, y, cv=5, scoring="neg_mean_squared_error" ) print(f"嵌套 CV MSE: {-outer_scores.mean():.4f} +/- {outer_scores.std():.4f}")
维度 手写实现 Optuna / sklearn
灵活 可看清 GP、采集函数内部 封装好,默认合理
算力利用 无剪枝 内置剪枝、并行
适用 理解原理 生产调参

常见错误

预处理导致数据泄漏。 若在交叉验证前对全数据拟合 scaler,验证折的信息就泄漏进训练。永远把预处理放进 Pipeline,让它只在训练折上拟合。

对验证集过拟合。 跑几千次试验实际上等于在验证集上训练。最终性能估计用嵌套交叉验证,或留一个调参时从不碰的独立测试集。

搜索范围太窄。 若你的最佳值落在搜索空间边界,你搜得还不够广。最优值可能在范围外。永远检查最佳参数是否在边缘。

忽略交互效应。 学习率和估计器数在提升里强交互。低学习率需要更多估计器。独立调它们不如一起调。

对迭代模型不用早停。 对梯度提升和神经网络,把 n_estimators 或 epoch 设高用早停。这严格优于把迭代数当超参数调。

四、可复用产物

本节产出 outputs/prompt-hyperparameter-tuner.md——一个帮你设计调参策略的提示词。喂给它模型类型、数据规模、可用算力,它会给出该先调哪些超参数、用什么搜索策略、起始范围、预算分配建议。

Python 代码(code/tuning.py)是独立的从零实现,可与 Optuna/sklearn 直接对照。

五、练习

  1. 网格 vs 随机:用相同总预算(如 50 次评估)跑网格搜索和随机搜索,对比找到的最佳分数。换不同种子跑 10 次,随机搜索多频繁地赢?

  2. 从零实现 Hyperband:启动 81 个配置各训练 1 个 epoch,每轮保留前 1/3 并把它们的预算翻三倍。对比总算力(所有配置所有 epoch 之和)与把 81 个配置跑满预算的算力。

  3. 加学习率调度:给第 11 节的梯度提升实现加余弦退火调度,对比固定学习率是否有帮助。

  4. Optuna 调随机森林:用 Optuna 在真实数据集(如 sklearn 乳腺癌数据集)上调 RandomForestClassifier,用 optuna.visualization.plot_param_importances(study) 看哪些超参数最重要。是否与本节的重要性排序吻合?

  5. 实现采集函数:实现期望改进(EI),演示探索 vs 开发。画出代理模型的均值和不确定性,展示 EI 选择在哪里评估。

本节要点回顾

  1. 参数 vs 超参数:参数训练中学习(权重),超参数训练前设定并控制学习过程(学习率、深度、正则化)。
  2. 网格搜索指数爆炸:6 个超参各 5 值就是 15,625 种组合;若一个重要一个不重要,多数评估浪费在不重要维度。
  3. 随机搜索胜在有效维度低:相同预算下每个超参都能测到更多不同值,60 次随机试验有 95% 概率找到最优点 5% 邻域。
  4. 贝叶斯优化用过去评估:代理模型(高斯过程)近似昂贵目标函数,采集函数(EI/UCB/PI)权衡开发与探索,2~5 倍更少评估找到更好配置。
  5. 早停省算力:Hyperband 给很多配置小预算再逐步加码,比跑满快 1050 倍;Optuna 的 MedianPruner 省 4060% 算力。
  6. 学习率永远第一:坏学习率让其他一切都不重要;学习率和正则化用对数均匀分布搜。
  7. 用早停代替调 n_estimators:对提升和神经网络,迭代数设高让早停决定何时停。
  8. 超参数重要性分档:高(学习率、正则化)、中(深度、子采样)、低(最大特征数、激活函数、批大小);先调高重要性档。
  9. 嵌套交叉验证给无偏估计:外层评估、内层调参,虽贵但可信;在论文报告或决策利害攸关时用。
  10. 五大常见错误:预处理泄漏、对验证集过拟合、搜索范围太窄、忽略交互效应、迭代模型不用早停。

下一节,我们讲机器学习流水线——如何把填补、缩放、编码、建模打包成一个可复现、防数据泄漏的单一对象。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U