外观
调参与超参数优化
你花在调参上的每一分钟,都应该先回答一个问题:这个参数真的值得调吗?
超参数优化(hyperparameter optimization, HPO)是机器学习工程里最矛盾的一环:它被说得无比重要——"调参是门手艺""高手和新手的差距就在这"——可现实是,项目里 90% 的调参工作在浪费算力,而真正决定模型上限的往往是数据、特征和模型选择。本文的目标不是教你背参数,而是帮你建立一套"该调什么、什么时候调、用什么方法调、怎么保证调得对"的完整方法论。
先看一个反直觉的事实:一份流传很广的 Kaggle 经验总结里,顶尖选手花在特征工程和数据清洗上的时间,通常远多于花在调参上的时间;而他们调参时也几乎不用暴力网格搜索,而是"少量参数、快速验证、按经验收窄"。这不是因为他们参数背得熟,而是因为他们把调参放对了位置。
全文结构:先讲调参观(什么时候才该调参),再给超参数全景地图(各模型到底有哪些关键旋钮),然后对比四种搜索方法,接着是 sklearn 与 Optuna 的完整代码实战,再深入深度学习调参,最后是一套保证实验可信的调参纪律。
一、调参观:先数据后模型再超参
1. 为什么"一上来就调参"是最大的浪费
新手拿到一个建模任务,最常见的路径是:装好 XGBoost → 直接 GridSearchCV 跑一晚上 → 盯着 best_params_ 兴奋半天 → 提交。这个流程的问题不在工具,而在顺序:超参数是模型的最后一个旋钮,在它之前还有四层问题没有排查。
┌─────────────────────────────────────────────────────────────┐
│ 问题排查顺序(自上而下,越靠上越优先) │
├─────────────────────────────────────────────────────────────┤
│ ① 数据层 样本够吗?标签对吗?有泄露吗?分布正常吗? │
│ ② 特征层 有没有可用特征没用上?缺失/异常值处理了吗? │
│ ③ 模型层 模型族选对了吗?偏差是欠拟合还是过拟合? │
│ ④ 超参层 在①~③确定后,才轮到超参数 │
└─────────────────────────────────────────────────────────────┘为什么是这个顺序?因为上层问题会污染下层问题的信号。如果数据里有泄露(比如切分前做了去重),你在超参数上看到的所有"最优值"都是虚的——模型只是在背答案,而你还在煞费苦心地给这个"背答案"调最合适的口型。数据泄露、时间泄漏、训练/线上不一致这些致命陷阱的完整清单,见常见陷阱与反模式;而一个项目的完整流程框架,见从零构建一个 ML 项目。
别在错误的地基上调参
如果你发现:训练集指标很高、验证集却很差——先别怀疑超参数,先怀疑过拟合与数据泄露;如果训练集指标本身就低——也别急着调参,先检查特征和模型是否匹配问题。超参数只能在"地基稳了"之后放大你的收益,地基歪了它只会放大错误。
2. 先确定"调参能解决什么"
超参数能影响的,只有一件事:偏差-方差权衡的具体落点。模型是欠拟合(高偏差)还是过拟合(高方差),决定了你该拧哪个方向:
| 症状 | 诊断 | 调参方向 |
|---|---|---|
| 训练集、验证集都不及格 | 欠拟合 / 高偏差 | 增大容量:加深/加树、减小正则化、降低 min_samples_leaf |
| 训练集满分、验证集很差 | 过拟合 / 高方差 | 增强正则:加 dropout/weight_decay、加大 min_child_weight、减小 max_depth |
| 训练集、验证集都接近理论下限 | 已到模型上限 | 别再调参,回数据与特征层面找增量 |
这个判断方法也叫"偏差-方差分解法",是模型评估与验证一文里最实用的工程技巧。做任何调参之前,先画出训练误差与验证误差随容量变化的曲线——你会立刻知道自己站在曲线的哪一段。
3. 什么时候才真的轮到超参数
对照下面清单,全部满足才进入调参阶段:
- 基线模型已经跑通:简单的逻辑回归或单棵决策树已经有了合理的表现,你知道"无脑下限"是多少;
- 数据与特征已冻结:不再频繁增删特征——否则每次改特征,之前调参的结果全部作废;
- 评估协议已固定:切分方式、评估指标、交叉验证折数都已定死,详见从零搭一套模型评估;
- 目标明确:是精度优先(竞赛、离线任务)还是延迟/资源优先(线上推理),这决定了搜索空间的方向。
在满足这些之前调参,你只是在给一个还在摇晃的画架裱画。
二、超参数全景
1. 参数 vs 超参数
先分清两个概念:**参数(parameters)**是模型从数据里学出来的,比如线性回归的系数 w、b,神经网络的权重——它们的取值由训练算法决定,不需要你干预;超参数(hyperparameters)是你在训练之前就设定好的旋钮,比如树的数量、学习率、正则化强度——数据不会自动告诉你它们该取多少,这恰恰是调参要解决的问题。
参数: fit(X, y) 之后才存在 → 由优化算法确定
超参数: fit(X, y) 之前就存在 → 由"调参者"确定一个实用规则:超参数越少、越不敏感,模型越容易用;超参数之间的相互作用越强,越需要系统性搜索。随机森林几乎只有 n_estimators 一个"随便给都行"的参数,所以它成为 Kaggle 万能基线;而深度学习有七八个相互耦合的旋钮,所以它需要专门的调参技术。
2. 树模型与集成模型
树模型家族(决策树、随机森林、XGBoost、LightGBM、CatBoost)是目前表格数据上的事实标准,详细机制见树模型与集成学习。它们的超参数可以分为四组:结构(控制单棵树长多深)、集成(控制有多少棵树、怎么采样)、正则(防过拟合)、训练(学习率等)。
| 超参数 | 含义 | 作用方向 | 常见范围 | 调参提示 |
|---|---|---|---|---|
n_estimators / num_boost_round | 树的数量 | 越多偏差越小,但收益递减、耗时线性增长 | 100–2000 | 后调;配合早停比堆数量划算 |
max_depth | 单棵树最大深度 | 越深拟合越强、越易过拟合 | 3–15 | 控制过拟合的头号旋钮 |
learning_rate / eta | 每棵树贡献的步长 | 越小越稳、需要更多树 | 0.01–0.3 | 与树数量联动:lr 减半,树常需加倍 |
min_samples_split / min_child_weight | 分裂所需最少样本/权重 | 越大越防过拟合 | 2–20 / 1–10 | 过拟合时优先加大 |
min_samples_leaf | 叶子最少样本数 | 越大树越平滑 | 1–20 | 与小样本数据集关系密切 |
max_features | 每次分裂考虑的特征数 | 越小方差越小 | sqrt 或 0.3–0.7 | 特征很多时调小有奇效 |
subsample / bagging_fraction | 每棵树采样的样本比例 | 越小越防过拟合 | 0.6–1.0 | GBDT 系列常用,随机森林默认 |
colsample_bytree | 每棵树用到的特征比例 | 同上 | 0.6–1.0 | XGBoost 独有,类似 max_features |
lambda / reg_lambda | 叶子权重的 L2 正则 | 越大越防过拟合 | 1–100 | 树多、深度大时建议打开 |
树模型的调参顺序(省时间的黄金顺序)
- 先固定学习率(默认 0.1)与足够多的树,把
max_depth、min_child_weight调到一个"欠拟合/过拟合边界"; - 再调采样类参数(subsample、colsample_bytree、max_features);
- 最后调正则(lambda)并配合 early stopping 找最优树数量。 这条顺序把相互耦合的参数解耦开,每一步只动一两个旋钮。
3. 线性模型与正则化
线性模型(线性回归、Ridge、Lasso、ElasticNet、逻辑回归、线性 SVM)的超参数少而精,几乎全部围绕"正则化强度"。这些模型的原理与正则化机制见过拟合与正则化。
| 超参数 | 含义 | 常见范围 | 说明 |
|---|---|---|---|
C(逻辑回归、LinearSVC) | 正则化强度的倒数:C 越大惩罚越弱 | 1e-4–1e4(对数网格) | 最容易踩的坑:C 与"强度"方向相反 |
alpha(Ridge/Lasso/ElasticNet) | 正则化强度:越大惩罚越强 | 1e-4–10 | 与 C 方向相反,注意区分 |
l1_ratio(ElasticNet) | L1 与 L2 的混合比例 | 0–1 | 0 是纯 Ridge,1 是纯 Lasso |
penalty / solver | 正则项类型与求解算法 | 见 sklearn 文档 | 一般先定 penalty,再按数据规模选 solver |
fit_intercept / tol | 是否拟合截距 / 收敛阈值 | — | 通常保持默认即可 |
线性模型的经验:用对数网格(np.logspace(-4, 4, 20))扫 C/alpha,画一条"C vs 交叉验证得分"的曲线。如果曲线两端都在上升,说明搜索范围不够,往外扩;如果中间有个平台,说明模型对正则强度不敏感,取平台中间的保守值即可——不必追求精确最优。
4. 神经网络
神经网络(MLP、CNN、Transformer)是超参数耦合最严重的模型族,机制见深度学习基础。它们的超参数可以分成三层:容量层(多大)、优化层(怎么学)、正则层(怎么防过拟合)。
| 超参数 | 含义 | 作用方向 | 常见默认 | 说明 |
|---|---|---|---|---|
learning_rate | 每步更新幅度 | 过大发散、过小收敛极慢 | Adam: 1e-3 / SGD: 1e-2 | 最重要,优先调它 |
batch_size | 每个批次样本数 | 越大梯度越稳、越吃内存 | 32–256 | 与学习率强耦合,见第五部分 |
| 层数与每层宽度 | 模型容量 | 越大拟合越强、越易过拟合 | 从 2 层 64 宽起步 | 小模型起步,不够再加 |
dropout | 随机丢弃神经元的比例 | 越大越防过拟合 | 0.2–0.5 | 过拟合时从 0.3 开始调 |
weight_decay | 权重的 L2 正则强度 | 越大越防过拟合 | 1e-6–1e-2 | Adam 配套的隐藏正则旋钮 |
| 优化器 | 更新规则(Adam/SGD+momentum) | — | Adam | 先定优化器再调 lr,不要反复横跳 |
warmup_steps | 学习率预热的步数 | 防止大 lr 初期震荡 | 总步数的 5%–10% | 大 batch + 大 lr 时必需 |
scheduler | 学习率随训练衰减的策略 | — | cosine / step decay | 决定收敛质量的关键 |
神经网络的"默认值陷阱"
神经网络调参最大的误区是把超参数当独立变量一个个调。lr、batch_size、weight_decay 是强耦合的——单独把 lr 调大 10 倍,很可能只是把模型从"欠拟合"推到"发散",看起来像 lr 的问题,其实是没同步调整 batch_size 和 weight_decay。这也是为什么深度学习调参通常交给自动化工具(见第四部分的 Optuna),而不是手动一个个试。
5. 超参数全景总表
把三类模型的"先调谁、后调谁"并排放在一起,方便对照:
| 模型族 | 优先调(收益大) | 其次调 | 基本不用调 | 参数间的耦合 |
|---|---|---|---|---|
| 树/集成 | max_depth、min_child_weight、learning_rate | 采样类、正则类 | n_estimators(配早停) | 中:lr 与树数联动 |
| 线性 | C / alpha(对数网格) | l1_ratio、solver | 其余默认 | 低:几乎独立 |
| 神经网络 | learning_rate、batch_size | 容量、dropout、weight_decay | warmup/scheduler 用默认 | 高:lr↔batch↔wd 互相影响 |
这张表对应的选择逻辑是:耦合越低的模型越适合手动+网格,耦合越高的越适合贝叶斯优化。下面进入搜索方法。
三、搜索方法对比
1. 手动调参
靠经验与直觉逐次修改参数。优点是可控、能积累领域直觉、几乎零成本;缺点是依赖经验、难以复现、容易陷入局部。手动调参真正成立的前提是"你熟悉这个模型的超参数地形"——比如你知道逻辑回归的 C 用对数刻度、树模型过拟合先动 min_child_weight。
2. 网格搜索(Grid Search)
对每个超参数给定一组候选值,穷举它们的笛卡尔积。scikit-learn 的 GridSearchCV 是标准实现,内部自动做交叉验证。
python
param_grid = {
"n_estimators": [100, 300],
"max_depth": [5, 10],
"min_samples_split": [2, 5],
} # 2 × 2 × 2 = 8 组参数,× 5 折 = 40 次完整训练优点:实现简单、结果可复现、天然支持并行;缺点:组合数随参数个数指数爆炸(维度诅咒)——6 个参数各 10 个候选就是 100 万次训练,绝大多数预算浪费在没意义的区域。它只适合参数少(≤2–3 个)、且你对取值空间有把握的场景。
3. 随机搜索(Random Search)
Bergstra 与 Bengio 在 2012 年那篇经典论文中证明了反直觉的结论:同样的预算下,从分布里随机采样 60 组,几乎总是优于穷举同样规模的网格。原因很简单:超参数里通常只有两三个真正起决定性作用,随机采样让每个参数都能覆盖更广的值域,而网格搜索里大部分组合是在"没用的维度"上重复。
python
from scipy.stats import randint, uniform
param_dist = {
"n_estimators": randint(50, 500), # 整数的均匀分布
"max_depth": randint(3, 20),
"max_features": uniform(0.1, 0.9), # 连续值的均匀分布
}优点:实现与网格几乎一样简单、不受维度诅咒、效率显著高于网格;缺点:不利用历史实验信息,纯靠概率覆盖。它是"预算有限 + 不追求极端最优"时的默认选择。
4. 贝叶斯优化(Bayesian Optimization)
核心思想是用前几次实验的结果,指导下一次实验该试哪。具体做法:用一个廉价代理模型(通常是高斯过程或树形结构代理模型,如 TPE)拟合"超参数 → 验证得分"的黑盒函数,再用采集函数(acquisition function)在"开发(已知好的区域)"和"探索(没试过的区域)"之间做权衡,选下一个最有潜力的点。Optuna、Hyperopt、SMAC 都是它的工程实现。
贝叶斯优化循环:
① 用已有观测 (参数→得分) 拟合代理模型
② 采集函数选下一个候选点(平衡开发与探索)
③ 用候选点真实训练并评估,得到新观测
④ 回到 ①,直到预算耗尽优点:同等预算下效率最高,尤其适合单次训练很贵(深度学习、大集成)的场景;缺点:存在顺序探索的串行开销(不过 Optuna 支持多并行 trial)、实现和理解门槛更高、对参数空间的定义更敏感。
5. 早停式调参(Early Stopping 与迭代式方法)
深度学习与 GBDT 特有的一类方法:不完整训练每组参数,而是在训练中途判断好坏就提前终止。深度学习里是监控验证损失、连续 N 个 epoch 不改善就停;XGBoost/LightGBM 则用 early_stopping_rounds 在树数量上做同样的截断。它本身不是一种"搜索算法",而是所有搜索算法的加速器——让每组候选都便宜十倍,搜索就能多跑十倍。
6. 对比总表与选择决策
| 方法 | 原理 | 需要的试验次数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 手动调参 | 经验+直觉 | 最少 | 可控、积累直觉 | 依赖经验、难复现 | 熟悉模型、参数极少 |
| 网格搜索 | 笛卡尔积穷举 | 组合数×折数 | 简单、可并行、可复现 | 维度诅咒、浪费预算 | ≤2–3 个参数、离散取值 |
| 随机搜索 | 从分布随机采样 | 与网格相当 | 覆盖更广、实现简单 | 不利用历史信息 | 3–6 个参数、含连续参数 |
| 贝叶斯优化 | 代理模型+采集函数 | 少(几十~几百) | 预算效率最高 | 串行开销、实现复杂 | 训练昂贵、参数多、强耦合 |
| 早停式 | 训练中途截断 | 最少 | 单组成本最低 | 依赖动态评估协议 | 深度学习 / GBDT |
决策规则(按这个顺序选):
- 单次训练几秒以内、参数 ≤3 个 → 网格搜索;
- 参数 3–6 个、预算中等 → 随机搜索(永远比网格划算);
- 单次训练几分钟到几小时、参数 ≥5 个、强耦合 → 贝叶斯优化(Optuna);
- 无论哪种,能早停就早停,把省下的预算乘以十倍搜索次数。
四、代码实战
下面用同一份数据(scikit-learn 自带的手写数字)完整跑通三种方法,保证可以复制运行。先做数据准备。
1. 数据准备与基线
python
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
# ── ① 数据:内置数据集,避免数据准备干扰调参本身 ──
X, y = load_digits(return_X_y=True) # 1797 张 8×8 手写数字图
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}, 验证集: {X_val.shape}")
# ── ② 基线:用一组"肯定能跑"的默认参数,先知道下限 ──
baseline = RandomForestClassifier(n_estimators=100, random_state=42)
baseline.fit(X_train, y_train)
print(f"基线验证集准确率: {accuracy_score(y_val, baseline.predict(X_val)):.4f}")注意这里刻意保留了 X_val 不用:交叉验证内部自己再切分,X_val 是全部调参结束后最后一次做独立评估用的——这是防止"调参调到验证集上"的关键隔离,详见第六部分。
2. 网格搜索:GridSearchCV
python
from sklearn.model_selection import GridSearchCV
param_grid = {
"n_estimators": [100, 300], # 2
"max_depth": [5, 10, None], # 3
"min_samples_split": [2, 5], # 2
} # 共 12 组 × 5 折 = 60 次训练
grid = GridSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_grid=param_grid,
cv=5, # 5 折交叉验证
scoring="accuracy",
n_jobs=-1, # 全部并行
verbose=1,
)
grid.fit(X_train, y_train)
print("最优参数:", grid.best_params_)
print("CV 最优得分: %.4f" % grid.best_score_)
print("验证集得分(独立评估): %.4f" % grid.score(X_val, y_val))三个输出各自的意义:best_params_ 是调参结论;best_score_ 是交叉验证里的平均得分(调参的决策依据);grid.score(X_val, y_val) 是对从未参与过任何决策的独立数据的最终检验。
3. 随机搜索:RandomizedSearchCV
python
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
param_dist = {
"n_estimators": randint(50, 500),
"max_depth": randint(3, 20),
"min_samples_split": randint(2, 20),
"max_features": uniform(0.1, 1.0), # 特征比例,0.1~1.0
}
random_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_distributions=param_dist,
n_iter=30, # 只试 30 组,而不是 450×17×18 的组合空间
cv=5,
scoring="accuracy",
n_jobs=-1,
random_state=42, # 固定随机种子,保证可复现
)
random_search.fit(X_train, y_train)
print("最优参数:", random_search.best_params_)
print("CV 最优得分: %.4f" % random_search.best_score_)对照随机搜索的 30 组 × 5 折 = 150 次训练:它在高维空间里的"实际覆盖率"远高于刚才 12 组的网格搜索,这就是 2012 年那篇论文的结论在工程上的直接体现。
4. 嵌套交叉验证:防止"调参本身过拟合"
上面的流程有个隐患:你在 X_train 上既选了超参数又估了性能。如果调参循环跑得足够久,最好的那组参数很可能是因为运气好——这叫"调参过拟合",模型在 X_val 上的分数会虚高。严肃的评估用嵌套交叉验证(nested CV):外层折负责估性能,内层折负责选超参,两层严格隔离。详细原理见模型评估与验证与从零搭一套模型评估。
python
from sklearn.model_selection import cross_val_score, KFold
# 外层 3 折:每折在内部重新做一次随机搜索,然后评估
outer_scores = []
outer_cv = KFold(n_splits=3, shuffle=True, random_state=42)
for train_idx, test_idx in outer_cv.split(X):
X_outer_train, X_outer_test = X[train_idx], X[test_idx]
y_outer_train, y_outer_test = y[train_idx], y[test_idx]
# 内层:只用外层训练集做搜索
inner = RandomizedSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_distributions=param_dist,
n_iter=15, cv=3, scoring="accuracy", n_jobs=-1, random_state=42,
)
inner.fit(X_outer_train, y_outer_train)
outer_scores.append(accuracy_score(y_outer_test, inner.predict(X_outer_test)))
print("嵌套交叉验证得分: ", np.round(outer_scores, 4))
print("无偏性能估计: %.4f (±%.4f)" % (np.mean(outer_scores), np.std(outer_scores)))嵌套 CV 得到的分数才是"如果我用这套调参流程,在新数据上大概能到多少"的无偏估计。代价是计算量成倍,所以最终上线前做一次即可,日常迭代不用。
5. Optuna 贝叶斯优化
当单次训练变贵(深度学习、大模型、大数据集)时,网格和随机都不划算,上 Optuna。它用 TPE(Tree-structured Parzen Estimator)作为代理模型,支持剪枝(pruning,即第三部分说的早停式截断)、并行 trial 与可视化。
bash
pip install optunapython
import optuna
from sklearn.model_selection import cross_val_score
def objective(trial):
# 在 Optuna 建议空间中声明每个超参数
params = {
"n_estimators": trial.suggest_int("n_estimators", 100, 600, step=50),
"max_depth": trial.suggest_int("max_depth", 3, 18),
"min_samples_split": trial.suggest_int("min_samples_split", 2, 20),
"max_features": trial.suggest_float("max_features", 0.1, 1.0),
"criterion": trial.suggest_categorical("criterion", ["gini", "entropy"]),
}
clf = RandomForestClassifier(**params, random_state=42, n_jobs=-1)
# 一次 trial = 一次 5 折交叉验证的平均分
return cross_val_score(clf, X_train, y_train, cv=5,
scoring="accuracy", n_jobs=-1).mean()
study = optuna.create_study(direction="maximize", study_name="rf_digits")
study.optimize(objective, n_trials=50) # 50 次试验,自动学习
print("最优超参数:", study.best_params)
print("最优得分: %.4f" % study.best_value)注意 objective 里只用了 X_train——和前面一样,X_val 被留作最终评估。跑完后可以用 optuna.visualization.plot_param_importances(study) 看每个超参数的重要性排序(基于排列重要性),这会直接告诉你"下一步该不该继续深挖某个参数"——这是贝叶斯优化对比暴力搜索的隐藏收益:它同时告诉你哪些旋钮根本不用再碰。
关于数据规模的一个提醒
上面三份代码在 1797 样本的手写数字上都是秒级完成,方便你复现。真实项目中,单次训练越贵,越要把预算往"少而精"倾斜:先用随机搜索或手动扫一遍粗粒度空间,锁定 2–3 个敏感参数,再让 Optuna 在这几个参数上深挖——不要一上来就让 Optuna 替你扫 8 个参数。
五、深度学习调参
深度学习的调参哲学和传统机器学习完全不同:参数多、耦合强、单次训练贵,几乎不能手动穷举。下面是按重要性排序的调参策略,优化原理的背景见优化与梯度下降,网络结构背景见深度学习基础。
1. 学习率永远第一
在深度学习里,学习率(learning rate, lr)对最终性能的影响远大于其他任何超参数:lr 太大,损失震荡不收敛;lr 太小,训练陷入停滞还以为是容量不够。它的最优区间通常跨越 2–3 个数量级,所以必须用对数刻度扫。
python
# 学习率扫描:每种 lr 只训 2~3 个 epoch,看验证损失的趋势即可
lr_candidates = [1e-4, 3e-4, 1e-3, 3e-3, 1e-2, 3e-2, 1e-1]
for lr in lr_candidates:
model = SimpleNet().to(device)
opt = torch.optim.Adam(model.parameters(), lr=lr)
for _ in range(3):
train_one_epoch(model, opt, train_loader)
val_loss = evaluate(model, val_loader)
print(f"lr={lr:6.0e} val_loss={val_loss:.4f}")判断标准:验证损失稳步下降 → lr 可行;震荡不降 → lr 偏大;几乎不动 → lr 偏小。快速扫描后选定最佳 lr,再进入下一步。
学习率的具体数值参考
- SGD + momentum:常用 0.1 / 0.01;
- Adam:常用 1e-3 / 3e-4;
- 大规模预训练 / 大 batch:配合 warmup 可用到 1e-2 甚至更高。 这只是起跑线,扫描永远比记忆数值可靠。
2. warmup:训练初期的安全带
学习率在训练初期就冲得很高,会导致损失在起步阶段剧烈震荡,甚至把参数推到回不来的地方。**warmup(预热)**让学习率从接近 0 线性爬升到目标值,再开始正常训练,是"大 lr + 大 batch"组合的标准配套:
python
total_steps = n_epochs * len(train_loader)
warmup_steps = int(0.05 * total_steps) # 前 5% 的步数做预热
def lr_lambda(step):
if step < warmup_steps: # 线性升到 1.0
return step / max(1, warmup_steps)
progress = (step - warmup_steps) / max(1, total_steps - warmup_steps)
return 0.5 * (1 + np.cos(np.pi * progress)) # 之后 cosine 退火到 0
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)3. cosine 退火:让学习率"下班"
光有恒定 lr 还不够:训练后期学习率不变,容易在损失曲面的鞍点附近徘徊。cosine 退火让学习率按余弦曲线从峰值平滑降到 0,让模型在后半程做精细收敛。上面的 lr_lambda 已经演示了 warmup + cosine 的完整组合,这是当前预训练与微调任务的事实标配(Loshchilov & Hutter, 2017)。
学习率曲线(一次完整训练)
lr ┤
│ ╭───────────╮
│ ╱ ╲
│ ╱ cosine ╲
│ ╱──╱ ╲
│ ╱ warmup ╲
└──────────────────────────→ 步数
0 ↑5% ↑100%4. batch size 与学习率的关系
这是深度学习调参里最实用的一个规律:batch size 增大到 k 倍,学习率通常也放大到 k 倍(线性缩放规则),来自 Facebook AI 的经典工作 Goyal et al., 2017:
假设梯度是随机噪声加真梯度的近似。batch 越大,梯度噪声越小、每一步越"准"。为了在相同步数内走完相同距离,lr 应随 batch 同步放大。标准做法:batch 翻倍 → lr 翻倍;lr 翻倍时,warmup 步数也要翻倍。
两个实用推论:
- 想用大 batch 加速训练? 同步放大 lr + warmup,通常能在几乎不损精度的情况下把训练时间砍半;
- 直觉对比:小 batch 是"每天问十个人"的噪声学习,大 batch 是"每天问一万人"的稳定学习——后者每问一步更贵(更慢),但步的方向更准,所以可以迈更大的步子(更大的 lr)。
5. 深度学习调参的其余要点
- dropout 与 weight_decay:过拟合时先调 weight_decay(如 1e-4 起步)再调 dropout(0.3 附近起步),两者往往只需其一;
- 优化器优先级:先用 Adam 快速拿到合理结果,若精度不够再换 SGD + momentum + cosine,通常能再涨几个点;
- 一次只动一组耦合参数:lr、batch、weight_decay 要视为一个整体,详见第六部分;
- 该用自动化时就交给工具:深度学习场景下,把
objective改成"lr + batch_size + weight_decay + 层数"让 Optuna 跑,比自己手动试快一个数量级。
六、调参纪律
方法再先进,如果实验流程不可信,一切等于零。以下是五条铁律,违反任意一条都可能让你的"最优参数"是幻觉。
1. 一次只改一个变量(或明确解耦的小组)
改两个参数得到的好结果,你无法知道是哪个起作用。虽然这在现代工作流里常常让位于自动化搜索,但人工迭代时这条纪律依然成立。如果必须同时改,请在实验记录里明确写出"本次同时改了 A 和 B,因为 C 原因",并确保记录完整到可以事后复盘。
2. 每次实验都必须可复现、可追溯
调参本质是"假设-检验"的循环,没有记录就没有科学。一个最小实验记录至少包含:
| 字段 | 说明 | 示例 |
|---|---|---|
| 实验 ID | 唯一编号 | exp-2026-06-18-rf-depth |
| 参数变更 | 与上一版的差异 | max_depth: 5 → 10 |
| 数据与切分 | 数据版本、随机种子 | digits v1, seed=42 |
| 评估协议 | 折数、指标 | 5-fold, accuracy |
| 结果 | CV 分数 ± 标准差 | 0.9712 ± 0.004 |
| 结论 | 该不该保留这次变更 | 保留,进入下一轮 |
一条偷懒但管用的规则
把所有实验的变更与结果追加写进同一个 CSV/Notion 表。一周之后,你复盘时最值钱的不是"哪组参数最好",而是"我试过哪条路是死的"——后者能避免你下周再走一遍。
3. 决策永远用验证集,不用测试集
这是机器学习领域最经典、也最常被破坏的纪律。数据应分成三份:
全量数据
├── 训练集 train → fit 模型
├── 验证集 val → 选择超参数(允许反复使用)
└── 测试集 test → 最终一次性评估(用完即弃)超参数的每次调整都基于验证集,测试集只在全部调参结束后用一次。理由很直白:只要你对测试集看过两次以上,你就开始隐性地把它"记住"了——测试集的意义就没了。正确与错误的评估协议细节,见模型评估与验证和从零搭一套模型评估。
4. 防止调参本身过拟合验证集
调参循环本身也是一种学习——它在"学习"验证集。搜索跑得越久、trial 越多,越可能选中一组只在验证集上运气好的参数。防线有三道:
- 尽早用随机/贝叶斯而非穷举,减少"赌博式"的采样次数;
- 关键决策用嵌套交叉验证(第四部分演示过)得到无偏估计;
- 给最终验证集得分设一个"不信任系数":它通常比你嵌套 CV 的平均分略高,别把高出的一点点当真。
5. 每轮调参之前先问"收益是否值得"
最后也是最重要的纪律:用时钟而不是用指标来评估调参。花 8 小时把准确率从 0.970 提到 0.972,在线上业务里几乎毫无意义;而同样的 8 小时用来补一个被忽略的特征、修一个数据管道 bug,可能带来 5 个点的提升。调参是对齐问题的最后一步微调,不是问题本身。数据与特征才是大头——这也是第一部分的"调参观"想让你记住的第一原则。
七、权衡与取舍
调参里每一组选择背后都是成本与收益的权衡,值得明确说出来:
- 算力预算 vs 精度收益:调参存在明显的收益递减。画一条"搜索预算 → 验证得分"曲线,你会看到它迅速变平。理性做法是设定一个"足够好"的阈值,达到即停,把剩余算力留给其他环节。
- 自动化 vs 可解释:Optuna 能自动找到好参数,但你失去的是"为什么这个值好"的理解。对小团队和长期项目,值得在自动化搜索之外保留人工抽查——了解参数地形,下次起步会快得多。
- 调参 vs 特征工程:同样的预算,修一个特征往往比把某个超参数从 0.971 调到 0.972 收益大一个数量级。特征工程的重要性不亚于调参——本站核心知识模块有专门章节展开。
- 简单模型 + 调参 vs 复杂模型 + 默认值:一个调好了的随机森林,经常胜过用默认参数硬跑的 XGBoost;一个调好的 3 层 MLP,胜过不调参的 ResNet 18。超参数与模型复杂度的匹配,比复杂度本身更重要。
- 搜索强度 vs 过拟合验证集:搜索越用力,验证集上的分数越虚。这个矛盾没有免费午餐,只能靠嵌套 CV 与"测试集只用一次"来管理。
- 工具选型 vs 团队能力:是上 Optuna 还是手动,还取决于团队是否理解搜索空间的定义——错误的空间定义(范围太窄、缺失关键参数)会让任何搜索方法白跑。工具与框架的选择逻辑见框架与工具怎么选。
八、延伸阅读
- 调参的前提是理解评估——模型评估与验证、从零搭一套模型评估
- 调参要防的坑(数据泄露、过拟合验证集)——常见陷阱与反模式
- 正则化超参数背后的机制——过拟合与正则化
- 树模型超参数背后的原理——树模型与集成学习
- 深度学习调参的理论基础——优化与梯度下降、深度学习基础
- 完整项目流程中的调参定位——从零构建一个 ML 项目
- 术语速查——资源:术语表
参考资料
- scikit-learn: Tuning the hyper-parameters of an estimator(GridSearchCV / RandomizedSearchCV 官方文档)
- Optuna 官方文档与教程
- Bergstra & Bengio. Random Search for Hyper-Parameter Optimization (JMLR 2012) —— "随机搜索优于网格搜索"的经典证明
- Snoek, Larochelle, Adams. Practical Bayesian Optimization of Machine Learning Algorithms (NeurIPS 2012) —— 贝叶斯优化用于超参数调优的开山之作
- Shahriari et al. Taking the Human Out of the Loop: A Review of Bayesian Optimization (Proceedings of the IEEE 2016) —— 贝叶斯优化的权威综述
- Akiba et al. Optuna: A Next-generation Hyperparameter Optimization Framework (KDD 2019) —— Optuna 框架原论文
- Loshchilov & Hutter. SGDR: Stochastic Gradient Descent with Warm Restarts (ICLR 2017) —— cosine 学习率退火
- Goyal et al. Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour (2017) —— batch size 与学习率的线性缩放规则
- Smith. A Disciplined Approach to Neural Network Hyper-parameters: Part 1 – Learning Rate, Batch Size, Momentum, and Weight Decay (2018) —— 深度学习超参数之间耦合关系的系统实验