Skip to content

调参与超参数优化

本页速览 调参是模型落地的必经之路,也是最容易浪费算力与时间的地方。本文讲解调参的正确顺序、各模型关键超参数、网格/随机/贝叶斯三种搜索方法的对比与代码实战,并给出深度学习调参与实验纪律的完整方法论。

调参与超参数优化 ​

你花在调参上的每一分钟,都应该先回答一个问题:这个参数真的值得调吗?

超参数优化(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. 基线模型已经跑通:简单的逻辑回归或单棵决策树已经有了合理的表现,你知道"无脑下限"是多少;
  2. 数据与特征已冻结:不再频繁增删特征——否则每次改特征,之前调参的结果全部作废;
  3. 评估协议已固定:切分方式、评估指标、交叉验证折数都已定死,详见从零搭一套模型评估;
  4. 目标明确:是精度优先(竞赛、离线任务)还是延迟/资源优先(线上推理),这决定了搜索空间的方向。

在满足这些之前调参,你只是在给一个还在摇晃的画架裱画。

二、超参数全景 ​

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.0GBDT 系列常用,随机森林默认
colsample_bytree每棵树用到的特征比例同上0.6–1.0XGBoost 独有,类似 max_features
lambda / reg_lambda叶子权重的 L2 正则越大越防过拟合1–100树多、深度大时建议打开

树模型的调参顺序(省时间的黄金顺序)

  1. 先固定学习率(默认 0.1)与足够多的树,把 max_depth、min_child_weight 调到一个"欠拟合/过拟合边界";
  2. 再调采样类参数(subsample、colsample_bytree、max_features);
  3. 最后调正则(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–10 是纯 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-2Adam 配套的隐藏正则旋钮
优化器更新规则(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_decaywarmup/scheduler 用默认高:lr↔batch↔wd 互相影响

这张表对应的选择逻辑是:耦合越低的模型越适合手动+网格,耦合越高的越适合贝叶斯优化。下面进入搜索方法。

三、搜索方法对比 ​

1. 手动调参 ​

靠经验与直觉逐次修改参数。优点是可控、能积累领域直觉、几乎零成本;缺点是依赖经验、难以复现、容易陷入局部。手动调参真正成立的前提是"你熟悉这个模型的超参数地形"——比如你知道逻辑回归的 C 用对数刻度、树模型过拟合先动 min_child_weight。

对每个超参数给定一组候选值,穷举它们的笛卡尔积。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 个)、且你对取值空间有把握的场景。

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

决策规则(按这个顺序选):

  1. 单次训练几秒以内、参数 ≤3 个 → 网格搜索;
  2. 参数 3–6 个、预算中等 → 随机搜索(永远比网格划算);
  3. 单次训练几分钟到几小时、参数 ≥5 个、强耦合 → 贝叶斯优化(Optuna);
  4. 无论哪种,能早停就早停,把省下的预算乘以十倍搜索次数。

四、代码实战 ​

下面用同一份数据(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 optuna
python
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 还是手动,还取决于团队是否理解搜索空间的定义——错误的空间定义(范围太窄、缺失关键参数)会让任何搜索方法白跑。工具与框架的选择逻辑见框架与工具怎么选。

八、延伸阅读 ​

参考资料 ​