大数跨境

导师:“为什么训练集效果很好,验证集一塌糊涂?”我:“因为你优化了参数,却没认真优化超参数”导师:“这是核心!”

导师:“为什么训练集效果很好,验证集一塌糊涂?”我:“因为你优化了参数,却没认真优化超参数”导师:“这是核心!” 机器学习和人工智能AI
2026-08-06
0

哈喽,大家好~

今儿和大家来聊聊超参数优化的核心逻辑。

我们在训练机器学习模型时,通常要做两类“调节”:

  1. 模型参数,例如神经网络的权重,这些参数是通过训练数据自动学习得到的。
  2. 超参数,例如学习率、正则化强度、网络层宽度、dropout 比例、优化器种类、batch size 等。这些不是网络直接学到的,需要人为设定或通过外部搜索得到。

为什么要调超参?

因为超参数决定训练过程和模型能学到什么。

好的超参数组合能让模型更快收敛、泛化更好;坏的超参数可能导致训练不稳定、欠拟合或过拟合。

超参数优化的核心任务,可以用数学语言描述为一个“二层优化问题”:

  • 内层(训练层):给定超参数  ,训练得到模型参数  ,通常通过最小化训练损失:
  • 外层(验证层):选择使验证(或交叉验证)损失最小的超参数:

换句话说,我们在寻找一组超参数  ,使得在这个   下训练出来的模型在验证集上表现最好。

关键表达

参数更新(梯度下降):

其中   是学习率(一个关键超参)。

超参优化的元目标(外层):

贝叶斯优化中的预期改进一种采集函数:

其中   是当前最好的目标值(例如验证精度)。

实战案例

我们构造一个分类任务,多维输入,使用一个简单 MLP。

定义 PyTorch 模型与训练/评估函数。用 Optuna 做超参数优化(使用 TPE 算法),超参包括学习率、权重衰减、隐藏层维度、dropout、batch size、优化器类型等。

用 Grid Search 在两个关键超参(lr, weight_decay)上做网格评估,用以绘制热力图(可视化验证性能在这两个超参上的响应面)。

import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader, random_split
import optuna
import matplotlib.pyplot as plt
import seaborn as sns
import os

RANDOM_SEED = 42
torch.manual_seed(RANDOM_SEED)
np.random.seed(RANDOM_SEED)
random.seed(RANDOM_SEED)

plt.rcParams["figure.figsize"] = (86)

# 1) 数据集:有两个簇和一些噪声,输入维度为 12(模拟高维输入)
def make_synthetic(n_samples=2000, input_dim=12, n_classes=2):
    X = []
    y = []
    for i in range(n_samples):
        cls = np.random.randint(0, n_classes)
        center = np.zeros(input_dim)
        # 每类在某些维度有偏移
        if cls == 0:
            center[:4] += 1.5
            center[4:8] -= 0.5
        else:
            center[:4] -= 1.0
            center[4:8] += 1.0
        x = center + 0.8 * np.random.randn(input_dim)
        X.append(x)
        y.append(cls)
    X = np.array(X, dtype=np.float32)
    y = np.array(y, dtype=np.int64)
    return X, y

X, y = make_synthetic(n_samples=2000, input_dim=12, n_classes=2)
# 转为 PyTorch TensorDataset
dataset = TensorDataset(torch.from_numpy(X), torch.from_numpy(y))
# 分割为 train / val / test
n_train = int(0.7 * len(dataset))
n_val = int(0.15 * len(dataset))
n_test = len(dataset) - n_train - n_val
train_set, val_set, test_set = random_split(dataset, [n_train, n_val, n_test], generator=torch.Generator().manual_seed(RANDOM_SEED))

# 2) 定义一个可配置的 MLP(超参数:hidden_dim, dropout)
class MLP(nn.Module):
    def __init__(self, input_dim=12, hidden_dim=64, dropout=0.2):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, hidden_dim//2),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim//22)
        )
    def forward(self, x):
        return self.net(x)

# 3) 训练与评估函数(返回 validation accuracy)
def train_and_eval(params, return_curves=False):
    # params 包含:lr, weight_decay, hidden_dim, dropout, batch_size, optimizer_name, epochs
    device = torch.device("cuda"if torch.cuda.is_available() else"cpu")
    model = MLP(input_dim=12, hidden_dim=params['hidden_dim'], dropout=params['dropout']).to(device)
    criterion = nn.CrossEntropyLoss()
    if params['optimizer_name'] == 'adam':
        optimizer = optim.Adam(model.parameters(), lr=params['lr'], weight_decay=params['weight_decay'])
    else:
        optimizer = optim.SGD(model.parameters(), lr=params['lr'], momentum=0.9, weight_decay=params['weight_decay'])
    train_loader = DataLoader(train_set, batch_size=params['batch_size'], shuffle=True)
    val_loader = DataLoader(val_set, batch_size=256, shuffle=False)

    epochs = params.get('epochs'30)
    train_losses, val_losses, val_accs = [], [], []
    for epoch in range(epochs):
        model.train()
        running_loss = 0.0
        for xb, yb in train_loader:
            xb, yb = xb.to(device), yb.to(device)
            optimizer.zero_grad()
            logits = model(xb)
            loss = criterion(logits, yb)
            loss.backward()
            optimizer.step()
            running_loss += loss.item() * xb.size(0)
        train_losses.append(running_loss / len(train_loader.dataset))

        # eval on val
        model.eval()
        total, correct = 00
        val_loss = 0.0
        with torch.no_grad():
            for xb, yb in val_loader:
                xb, yb = xb.to(device), yb.to(device)
                logits = model(xb)
                loss = criterion(logits, yb)
                val_loss += loss.item() * xb.size(0)
                preds = logits.argmax(dim=1)
                correct += (preds == yb).sum().item()
                total += xb.size(0)
        val_losses.append(val_loss / len(val_loader.dataset))
        val_accs.append(correct / total)
    best_val_acc = max(val_accs)
    if return_curves:
        return best_val_acc, (train_losses, val_losses, val_accs)
    return best_val_acc

# 4) 使用 Optuna 做超参数搜索(TPE)
def objective(trial):
    params = {
        'lr': trial.suggest_loguniform('lr'1e-41e-1),
        'weight_decay': trial.suggest_loguniform('weight_decay'1e-61e-2),
        'hidden_dim': trial.suggest_categorical('hidden_dim', [3264128]),
        'dropout': trial.suggest_uniform('dropout'0.00.5),
        'batch_size': trial.suggest_categorical('batch_size', [3264128]),
        'optimizer_name': trial.suggest_categorical('optimizer_name', ['adam''sgd']),
        'epochs'30
    }
    val_acc = train_and_eval(params)
    # Optuna 最大化目标(默认是 minimization,所以我们返回负的错误率,于是也可以直接设置 study=maximize)
    return val_acc

# 运行 Optuna 搜索
import optuna
study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler(seed=RANDOM_SEED))
study.optimize(objective, n_trials=50, n_jobs=1)

print("Best trial:")
print(study.best_trial.params)
print("Best value (val acc):", study.best_value)

# 5) 获取最优 trial 的训练曲线(重跑并获取曲线)
best_params = study.best_trial.params
best_params['epochs'] = 60# 多训练一些 epoch 以观察学习曲线
best_params['batch_size'] = int(best_params['batch_size'])
best_acc, curves = train_and_eval(best_params, return_curves=True)
train_losses, val_losses, val_accs = curves

# 6) Grid search(lr x weight_decay)用于绘制热力图(较粗网格)
lrs = np.logspace(-4-110)
wds = np.logspace(-6-210)
grid_results = np.zeros((len(wds), len(lrs)))
for i, wd in enumerate(wds):
    for j, lr in enumerate(lrs):
        params = {
            'lr': float(lr),
            'weight_decay': float(wd),
            'hidden_dim': best_params['hidden_dim'],
            'dropout': best_params['dropout'],
            'batch_size': best_params['batch_size'],
            'optimizer_name': best_params['optimizer_name'],
            'epochs'20
        }
        acc = train_and_eval(params)
        grid_results[i, j] = acc

# 7) 超参重要性
try:
    imp = optuna.importance.get_param_importances(study)
except Exception as e:
    # 如果缺少依赖,fallback:根据 trials 做简单粗略重要性(方差解释)
    imp = {}
    trials = study.trials
    # 统计每个参数与目标的皮尔逊相关(数值化 categorical)
    import pandas as pd
    df = pd.DataFrame([dict(t.params, value=t.value) for t in trials])
    for col in df.columns:
        if col == 'value'continue
        try:
            corr = df[col].astype(float).corr(df['value'])
            imp[col] = abs(corr)
        except:
            imp[col] = 0.0

# 8) 可视化

# 图 1:最优 trial 的训练/验证损失与验证精度(学习曲线)
plt.figure(figsize=(10,6))
epochs = np.arange(1, len(train_losses)+1)
plt.plot(epochs, train_losses, label='train loss', color='#FF6F61', linewidth=2)
plt.plot(epochs, val_losses, label='val loss', color='#6B5B95', linewidth=2)
plt.plot(epochs, val_accs, label='val acc', color='#009B77', linewidth=2)
plt.xlabel('epoch')
plt.title('Best Trial Learning Curves (loss & val acc)')
plt.legend()
plt.grid(alpha=0.3)
plt.show()

# 图 2:lr x weight_decay 热力图
plt.figure(figsize=(10,8))
# 使用 seaborn heatmap,行对应 weight_decay(从小到大)
ax = sns.heatmap(grid_results, xticklabels=[f"{v:.1e}"for v in lrs], yticklabels=[f"{v:.1e}"for v in wds],
                 cmap="YlOrRd", annot=True, fmt=".3f")
plt.xlabel("learning rate")
plt.ylabel("weight decay")
plt.title("Validation Acc over lr and weight_decay (grid search)")
plt.show()

# 图 3:超参重要性条形图
plt.figure(figsize=(8,5))
items = sorted(imp.items(), key=lambda x: x[1], reverse=True)
names = [k for k,_ in items]
scores = [v for _,v in items]
palette = sns.color_palette("bright", len(names))
sns.barplot(x=scores, y=names, palette=palette)
plt.title("Hyperparameter Importance (approx.)")
plt.xlabel("importance score")
plt.show()

# 图 4:Trials 散点(lr vs val acc),颜色表示 hidden_dim,点大小表示 batch_size;并显示优化路径(按 trial number 连线)
trials = study.trials
lr_vals = [t.params['lr'for t in trials]
acc_vals = [t.value for t in trials]
# color by hidden_dim
hidden_dims = [int(t.params['hidden_dim']) for t in trials]
batch_sizes = [int(t.params['batch_size']) for t in trials]
trial_nums = np.arange(len(trials))

plt.figure(figsize=(10,6))
sc = plt.scatter(lr_vals, acc_vals, c=hidden_dims, s=np.array(batch_sizes), cmap='viridis', alpha=0.85, edgecolor='k', linewidth=0.5)
for i in range(len(trials)-1):
    plt.plot([lr_vals[i], lr_vals[i+1]], [acc_vals[i], acc_vals[i+1]], color='gray', alpha=0.3, linewidth=0.8)
plt.xscale('log')
plt.colorbar(sc, label='hidden_dim')
plt.xlabel('learning rate (log scale)')
plt.ylabel('validation accuracy')
plt.title('Trials: lr vs val acc (color=hidden_dim, size=batch_size)')
plt.show()

学习曲线:

横轴为 epoch,包含三条曲线:训练损失、验证损失和验证精度。

  • 如果训练损失持续下降且验证损失也下降,说明模型在学到有用的模式,未过拟合。
  • 若训练损失下降但验证损失上升,说明开始过拟合。
  • 若训练与验证损失都很高,可能是欠拟合(模型容量不足或学习率问题)。

lr × weight_decay 热力图:

行是不同的 weight_decay(从上到下通常是从小到大),列是不同的学习率(从小到大),格子中的颜色与数字表示验证精度。

  • 这张图直观展示在 lr 与 weight_decay 两个关键超参上的性能响应面。
  • 你可能看到:当 lr 太大(右侧),模型表现变差甚至不稳定;当 lr 太小(左侧),训练过慢,精度也受限。
  • weight_decay(L2 正则)太小可能欠拟合/过拟合迹象不明显,太大会使模型欠拟合。
  • 从热力图可以直接选出一个“高性能区间”供后续更精细搜索或微调使用。

超参重要性条形图:

显示 Optuna(或简单相关替代)评估的各个超参对目标(验证精度)的“重要性分数”。

  • 分数高的超参表示该参数对结果影响大,值得优先调优或更细致地搜索。
  • 分数低的超参可以固定一个合理值以减少搜索空间(节省计算资源)。

Trials 散点:

横轴是 lr(对数尺度),纵轴是验证精度;点颜色表示 hidden_dim,点大小表示 batch_size;并用线将 trial 依次连起来,展示 Optuna 搜索的“轨迹”。

  • 这个图帮助理解搜索过程是否“集中”:是否早期样本分散,后期聚焦到高性能区域(理想情况)。
  • 如果连线在探索过程中跳来跳去,说明优化器在不停尝试不同区域;如果逐渐集中说明贝叶斯优化找到了表现好的方向。
  • 颜色/大小的编码还帮你看出某些超参(如 hidden_dim 或 batch_size)如何与性能关联。

总结

超参数优化是一个“有成本的搜索”问题:每次评估都要训练模型(代价可能很高)。

因此正确的策略应该综合“预算/时间/性能需求”来选择方法:

  • 少量超参、且你可以并行评估很多候选:随机搜索 + Hyperband(或 ASHA)是不错的选择。
  • 预算有限、想尽快找到较好解:贝叶斯优化(如 Optuna 的 TPE)通常效果好。
  • 若训练非常昂贵,建议使用多级筛选(先用小模型或少量 epoch 快速筛,再用更大的资源精调)。

最后

宝子们,快来领取【点击👉】16大块的内容,124个算法问题的总结,完整的机器学习小册,免费领取~
干脆利落,干货满满!~

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 381
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读5.0k
粉丝0
内容381