关注

Optuna 完全指南:从手动调参到自动化贝叶斯优化,一次搞定

目录


第一部分:基础篇

1.1 什么是超参数调优?为什么需要 Optuna?

在机器学习中,模型有两类参数:

  • 模型参数:训练过程中自动学习得到的,如神经网络的权重。

  • 超参数:训练前需要人为设定的,如学习率、树的深度、正则化系数。

超参数调优的目标是找到一组超参数,使模型在验证集上的性能最好。

传统调优方法各有痛点:

方法原理缺点
手动调参凭经验试耗时、依赖经验、易陷入局部最优
网格搜索(Grid Search)列举所有组合维度灾难,参数稍多就爆炸
随机搜索(Random Search)随机采样组合效率优于网格,但没有学习能力

Optuna 应运而生:

  • 使用贝叶斯优化(具体是 TPE 算法)智能选择下一组参数

  • 支持剪枝:提前停止没希望的试验

  • 轻量、易用、与主流框架无缝集成

1.2 安装

 pip install optuna
 # 可选:可视化仪表板
 pip install optuna-dashboard
 # 如需保存图表为图片,还需安装 kaleido
 pip install -U kaleido

1.3 一个最简单的例子

 import optuna
 ​
 def objective(trial):
     x = trial.suggest_float('x', -10, 10)
     return (x - 2) ** 2
 ​
 study = optuna.create_study(direction='minimize')
 study.optimize(objective, n_trials=100)
 ​
 print(study.best_params)  # {'x': 2.001} 附近
 print(study.best_value)   # 接近 0

这个例子演示了 Optuna 的核心流程:

  1. 定义目标函数(objective)—— 告诉 Optuna 要优化什么

  2. 创建 Study —— 管理整个调优过程

  3. 运行 optimize —— 开始搜索

1.4 一个深度学习的例子

 import optuna
 import torch
 import torch.nn as nn
 import torch.optim as optim
 import torch.nn.functional as F
 from torch.utils.data import DataLoader, Subset
 from torchvision import datasets, transforms
 import numpy as np
 from optuna.visualization import (
     plot_optimization_history,  # 优化历史曲线
     plot_param_importances,      # 超参数重要性
     plot_intermediate_values,    # 学习曲线/中间值趋势
     plot_parallel_coordinate,    # 高维平行坐标图
     plot_contour,                # 等高线图(参数交互)
     plot_slice,                  # 切片图(单参数影响)
     plot_edf,                    # 经验分布函数图(目标值分布)
     plot_timeline,               # 试验时间线
 )
 ​
 # ------- 1. 数据加载(固定 batch_size 暂时) -------
 transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
 train_dataset = datasets.FashionMNIST('./data', train=True, download=True, transform=transform)
 val_dataset = datasets.FashionMNIST('./data', train=False, download=True, transform=transform)
 ​
 # ------- 2. 定义动态模型(层数与大小可变) -------
 class DynamicNet(nn.Module):
     def __init__(self, trial):
         super().__init__()
         n_layers = trial.suggest_int('n_layers', 1, 4)
         hidden_size = trial.suggest_int('hidden_size', 32, 256, log=True)
         dropout = trial.suggest_float('dropout', 0.1, 0.5)
         layers = []
         in_features = 28*28
         for _ in range(n_layers):
             layers.append(nn.Linear(in_features, hidden_size))
             layers.append(nn.ReLU())
             layers.append(nn.Dropout(dropout))
             in_features = hidden_size
         layers.append(nn.Linear(in_features, 10))
         self.net = nn.Sequential(*layers)
 ​
     def forward(self, x):
         return self.net(x.view(x.size(0), -1))
 ​
 # ------- 3. 目标函数(含中间值上报与剪枝) -------
 def objective(trial):
     lr = trial.suggest_float('lr', 1e-4, 1e-2, log=True)
     batch_size = trial.suggest_categorical('batch_size', [32, 64, 128])
     train_loader = DataLoader(train_dataset, batch_size, shuffle=True)
     val_loader = DataLoader(val_dataset, batch_size, shuffle=False)
 ​
     model = DynamicNet(trial)
     optimizer = optim.Adam(model.parameters(), lr=lr)
     device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
     model.to(device)
 ​
     n_epochs = 10
     for epoch in range(n_epochs):
         model.train()
         for data, target in train_loader:
             data, target = data.to(device), target.to(device)
             optimizer.zero_grad()
             output = model(data)
             loss = F.cross_entropy(output, target)
             loss.backward()
             optimizer.step()
 ​
         # 验证并上报中间值(用于剪枝)
         model.eval()
         correct = 0
         with torch.no_grad():
             for data, target in val_loader:
                 data, target = data.to(device), target.to(device)
                 output = model(data)
                 pred = output.argmax(dim=1, keepdim=True)
                 correct += pred.eq(target.view_as(pred)).sum().item()
         acc = correct / len(val_loader.dataset)
         trial.report(acc, epoch)
         if trial.should_prune():
             raise optuna.TrialPruned()
     return acc
 ​
 # ------- 4. 运行优化(注意:此处n_trials较小,仅作演示) -------
 study = optuna.create_study(direction='maximize', pruner=optuna.pruners.MedianPruner())
 study.optimize(objective, n_trials=30)
 ​
 # ========== 5. 生成并保存所有图表 ==========
 fig1 = plot_optimization_history(study)                 # 优化历史: 收敛性/趋势
 fig1.write_image("optimization_history.png")            # 保存为高清图片
 ​
 fig2 = plot_param_importances(study)                   # 参数重要性: 聚焦关键参数
 fig2.write_image("param_importances.png")
 ​
 fig3 = plot_intermediate_values(study)                 # 中间值: 观察训练曲线
 fig3.write_image("intermediate_values.png")
 ​
 fig4 = plot_parallel_coordinate(study)                 # 高维: 好/坏参数分群
 fig4.write_image("parallel_coordinate.png")
 ​
 fig5 = plot_contour(study, params=['n_layers', 'hidden_size']) # 等高线: 交互效应
 fig5.write_image("contour.png")
 ​
 fig6 = plot_slice(study, params=['lr', 'dropout'])      # 切片: 单变量影响分布
 fig6.write_image("slice.png")
 ​
 fig7 = plot_edf(study)                                  # EDF: 目标值分布对比
 fig7.write_image("edf.png")
 ​
 fig8 = plot_timeline(study)                             # 时间线: 耗时分析
 fig8.write_image("timeline.png")
 ​
 print(f"Best value: {study.best_value:.4f} | Params: {study.best_params}")

第二部分:核心概念与基本用法

2.1 Study 和 Trial

  • Study:相当于一次完整的调优实验。它包含:

    • 所有已完成的 Trials

    • 最佳参数记录

    • 优化方向(最大化或最小化)

  • Trial:一次独立的超参数组合执行。每个 Trial 会调用一次 objective 函数。

2.2 定义搜索空间:suggest 系列 API

在 objective 函数内部,通过 trial.suggest_xxx 定义超参数及其范围。

API说明示例
suggest_int(name, low, high)整数类型trial.suggest_int('n_estimators', 50, 500)
suggest_int(..., step=10)整数步长trial.suggest_int('n_estimators', 100, 1000, step=50)
suggest_float(name, low, high)浮点数类型trial.suggest_float('lr', 1e-5, 1e-1)
suggest_float(..., log=True)对数尺度(适用于跨越多个数量级)trial.suggest_float('lr', 1e-5, 1e-1, log=True)
suggest_categorical(name, choices)类别类型trial.suggest_categorical('optimizer', ['SGD', 'Adam', 'RMSprop'])
suggest_uniform() (已废弃)建议用 suggest_float

常见错误:把 suggest_float 的范围设反(low > high)会导致 ValueError

2.3 一个完整的机器学习调优示例(scikit-learn + XGBoost)

 import optuna
 import xgboost as xgb
 from sklearn.datasets import load_breast_cancer
 from sklearn.model_selection import train_test_split
 from sklearn.metrics import accuracy_score
 ​
 data = load_breast_cancer()
 X_train, X_val, y_train, y_val = train_test_split(
     data.data, data.target, test_size=0.2, random_state=42
 )
 ​
 def objective(trial):
     params = {
         'max_depth': trial.suggest_int('max_depth', 3, 10),
         'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
         'n_estimators': trial.suggest_int('n_estimators', 100, 1000, step=50),
         'subsample': trial.suggest_float('subsample', 0.5, 1.0),
         'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
         'gamma': trial.suggest_float('gamma', 0, 5),
         'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True),
         'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True),
     }
     
     model = xgb.XGBClassifier(**params, random_state=42, eval_metric='logloss')
     model.fit(X_train, y_train)
     y_pred = model.predict(X_val)
     return accuracy_score(y_val, y_pred)
 ​
 study = optuna.create_study(direction='maximize')
 study.optimize(objective, n_trials=50)
 ​
 print("Best accuracy:", study.best_value)
 print("Best params:", study.best_params)

2.4 条件搜索空间(Conditional Search Space)—— 深度学习完整示例

条件搜索空间允许超参数之间存在依赖关系。例如,选择优化器后再决定其专有参数。下面是一个完整的 PyTorch 深度学习示例,动态选择优化器及其超参数。

 import optuna
 import torch
 import torch.nn as nn
 import torch.optim as optim
 import torch.nn.functional as F
 from torch.utils.data import DataLoader
 from torchvision import datasets, transforms
 ​
 # 简单的 CNN 模型
 class SimpleCNN(nn.Module):
     def __init__(self, dropout_rate):
         super().__init__()
         self.conv1 = nn.Conv2d(1, 32, 3, 1)
         self.conv2 = nn.Conv2d(32, 64, 3, 1)
         self.dropout = nn.Dropout(dropout_rate)
         self.fc1 = nn.Linear(9216, 128)
         self.fc2 = nn.Linear(128, 10)
 ​
     def forward(self, x):
         x = self.conv1(x)
         x = F.relu(x)
         x = self.conv2(x)
         x = F.relu(x)
         x = F.max_pool2d(x, 2)
         x = torch.flatten(x, 1)
         x = self.dropout(x)
         x = self.fc1(x)
         x = F.relu(x)
         x = self.fc2(x)
         return x
 ​
 def get_data_loaders(batch_size):
     transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
     train = datasets.MNIST('./data', train=True, download=True, transform=transform)
     test = datasets.MNIST('./data', train=False, transform=transform)
     return DataLoader(train, batch_size, shuffle=True), DataLoader(test, batch_size)
 ​
 def objective(trial):
     # 固定参数
     dropout = trial.suggest_float('dropout', 0.1, 0.5)
     batch_size = trial.suggest_categorical('batch_size', [64, 128, 256])
     train_loader, test_loader = get_data_loaders(batch_size)
 ​
     # 条件 1:优化器类型
     optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'SGD'])
     lr = trial.suggest_float('lr', 1e-4, 1e-2, log=True)
 ​
     model = SimpleCNN(dropout)
     device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
     model.to(device)
 ​
     # 根据优化器类型,定义不同的参数
     if optimizer_name == 'Adam':
         beta1 = trial.suggest_float('beta1', 0.8, 0.999)
         beta2 = trial.suggest_float('beta2', 0.9, 0.9999)
         optimizer = optim.Adam(model.parameters(), lr=lr, betas=(beta1, beta2))
     else:  # SGD
         momentum = trial.suggest_float('momentum', 0.0, 0.99)
         weight_decay = trial.suggest_float('weight_decay', 0.0, 1e-4)
         optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum, weight_decay=weight_decay)
 ​
     # 条件 2:学习率调度器(可选)
     use_scheduler = trial.suggest_categorical('use_scheduler', [True, False])
     if use_scheduler:
         scheduler_name = trial.suggest_categorical('scheduler', ['StepLR', 'CosineAnnealing'])
         if scheduler_name == 'StepLR':
             step_size = trial.suggest_int('step_size', 3, 10)
             scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=step_size)
         else:
             T_max = trial.suggest_int('T_max', 10, 20)
             scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=T_max)
 ​
     # 训练循环(剪枝部分见后)
     n_epochs = 10
     for epoch in range(n_epochs):
         model.train()
         for data, target in train_loader:
             data, target = data.to(device), target.to(device)
             optimizer.zero_grad()
             output = model(data)
             loss = F.cross_entropy(output, target)
             loss.backward()
             optimizer.step()
 ​
         # 验证
         model.eval()
         correct = 0
         with torch.no_grad():
             for data, target in test_loader:
                 data, target = data.to(device), target.to(device)
                 output = model(data)
                 pred = output.argmax(1)
                 correct += (pred == target).sum().item()
         accuracy = correct / len(test_loader.dataset)
 ​
         # 报告中间值
         trial.report(accuracy, epoch)
         if trial.should_prune():
             raise optuna.TrialPruned()
 ​
         if use_scheduler:
             scheduler.step()
 ​
     return accuracy
 ​
 # 运行优化
 study = optuna.create_study(direction='maximize', pruner=optuna.pruners.MedianPruner())
 study.optimize(objective, n_trials=30)
 print(study.best_params)

关键点

  • optimizer_name 决定后续采样 beta1/beta2 还是 momentum/weight_decay

  • use_scheduler 决定是否添加调度器及其类型。

  • Optuna 自动处理依赖关系,不会在无效组合上浪费试验。


第三部分:运行优化与结果分析

3.1 create_study 的重要参数(含 storage 完整用法)

 study = optuna.create_study(
     study_name='my_study',           # 研究名称(持久化必需)
     storage='sqlite:///example.db',  # 数据库地址
     direction='maximize',
     sampler=TPESampler(seed=42),
     pruner=MedianPruner(),
     load_if_exists=True
 )
storage='sqlite:///example.db' 完整用法与深度学习例子

storage='sqlite:///example.db' 的含义:

  • sqlite:/// 表示使用 SQLite 数据库引擎。SQLite 是 Python 自带的轻量级嵌入式数据库,无需安装、无需配置、无需启动服务进程

  • example.db 只是一个普通的本地文件名(相对路径,保存在当前工作目录)。当你第一次运行调优脚本时,Optuna 会自动创建这个文件,并初始化所需的表结构。你不需要做任何手动建库操作。

对比其他数据库:

  • 如果使用 MySQL 或 PostgreSQL(如 mysql://user:pass@host/db_name),则需要你先安装数据库软件、启动服务、手动创建数据库(CREATE DATABASE ...),然后 Optuna 才能连接并创建表。

  • 而 SQLite 完全免运维,直接写文件即可。

作用:将 Study 的所有试验数据保存到 SQLite 数据库文件中,避免因程序中断而丢失进度,支持后续继续优化或单独分析。

基本操作

 # 创建并持久化 Study(如果已存在同名,则加载)
 study = optuna.create_study(
     study_name='mnist_tuning',
     storage='sqlite:///mnist_optuna.db',
     load_if_exists=True,
     direction='maximize'
 )
 study.optimize(objective, n_trials=50)   # 运行 50 次试验

跨会话恢复:再次运行完全相同代码,Optuna 会自动加载已有 Study,继续新增试验。

 # 第二天继续调优
 study = optuna.create_study(
     study_name='mnist_tuning',
     storage='sqlite:///mnist_optuna.db',
     load_if_exists=True,
     direction='maximize'
 )
 study.optimize(objective, n_trials=50)   # 在原有 50 次基础上再跑 50 次

单独加载 Study 进行分析

 # 在另一个脚本或 Jupyter 中
 study = optuna.load_study(
     study_name='mnist_tuning',
     storage='sqlite:///mnist_optuna.db'
 )
 print(f"已完成试验数: {len(study.trials)}")
 print(f"最佳准确率: {study.best_value:.4f}")
 print(f"最佳参数: {study.best_params}")

命令行管理

 # 创建 Study(不运行优化)
 optuna create-study --study-name mnist_tuning --storage sqlite:///mnist_optuna.db --direction maximize
 ​
 # 删除 Study
 optuna delete-study --study-name mnist_tuning --storage sqlite:///mnist_optuna.db
 ​
 # 启动 Dashboard(交互式可视化)
 optuna-dashboard sqlite:///mnist_optuna.db

深度学习应用提示:对于耗时数小时甚至数天的深度学习调优,持久化是必备操作。即使训练中途断电,也能无缝恢复。

注意:SQLite 仅适用于单机、非并发写入的场景。分布式并行优化需使用 MySQL/PostgreSQL 等支持高并发的数据库。

3.2 optimize 的重要参数

 study.optimize(
     objective, 
     n_trials=100,           # 试验次数
     timeout=3600,           # 超时限制(秒)
     n_jobs=4,               # 多线程并行
     catch=(ValueError,),    # 忽略指定异常
     callbacks=[callback_func]
 )

3.3 结果分析及可视化完整用法

Optuna 提供强大的可视化模块,帮助理解参数与目标值的关系。以下是一个完整的深度学习示例(MNIST + CNN),展示所有常用图表。

 import optuna
 from optuna.visualization import (
     plot_optimization_history,
     plot_param_importances,
     plot_intermediate_values,
     plot_parallel_coordinate,
     plot_contour,
     plot_slice,
     plot_edf,
     plot_timeline,
 )
 ​
 # 假设之前已经运行过 study(使用上面的条件搜索空间示例)
 # 现在加载或直接使用现有 study
 study = optuna.load_study(study_name='mnist_tuning', storage='sqlite:///mnist_optuna.db')
 ​
 # 1. 优化历史曲线:观察收敛情况
 fig1 = plot_optimization_history(study)
 fig1.write_image("optimization_history.png")   # 保存图片(需 kaleido)
 fig1.show()
 ​
 # 2. 参数重要性:哪些超参数影响最大
 fig2 = plot_param_importances(study)
 fig2.write_image("param_importances.png")
 fig2.show()
 ​
 # 3. 中间值曲线:查看每个 trial 的验证准确率随 epoch 变化(需剪枝时报告中间值)
 fig3 = plot_intermediate_values(study)
 fig3.write_image("intermediate_values.png")
 fig3.show()
 ​
 # 4. 平行坐标图:高维参数空间与目标值的关系
 fig4 = plot_parallel_coordinate(study)
 fig4.write_image("parallel_coordinate.png")
 fig4.show()
 ​
 # 5. 等高线图:两个参数的交互效应
 fig5 = plot_contour(study, params=['lr', 'dropout'])
 fig5.write_image("contour.png")
 fig5.show()
 ​
 # 6. 切片图:单个参数对目标值的影响
 fig6 = plot_slice(study, params=['batch_size', 'optimizer'])
 fig6.write_image("slice.png")
 fig6.show()
 ​
 # 7. EDF 图:目标值的经验分布函数(比较不同采样策略或剪枝效果)
 fig7 = plot_edf(study)
 fig7.write_image("edf.png")
 fig7.show()
 ​
 # 8. 时间线图:每个 trial 的起止时间与状态
 fig8 = plot_timeline(study)
 fig8.write_image("timeline.png")
 fig8.show()

可视化解读速查表

图表作用关注点
优化历史收敛性曲线是否趋于平稳,最佳值是否还在上升
参数重要性聚焦关键参数重要性低的参数可固定或缩小范围
中间值曲线训练过程诊断被剪枝的 trial 曲线会中途断裂
平行坐标图高维参数组合模式好 trial 通常集中在哪些参数区间
等高线图两参数联合影响色深区域为最优组合
切片图单参数影响分布是否存在明显的最优区间
EDF 图整体试验质量曲线越陡峭,结果越集中
时间线图执行效率检查耗时异常或状态失败的 trial

Matplotlib 后端:如果无法安装 Plotly 或需要静态图片,可以使用 Matplotlib 后端,只需将导入路径改为:

 from optuna.visualization.matplotlib import plot_optimization_history, ...

第四部分:深入原理 — 采样器(Sampler)

4.1 采样器是做什么的?

采样器负责决定下一组超参数尝试什么值。 它根据所有历史 trial 的结果,智能地推测哪些区域可能更好。

Optuna 默认使用 TPE(Tree-structured Parzen Estimator) 采样器,这是一种贝叶斯优化方法。

4.2 TPE 采样器的工作原理

  1. 初始阶段:随机采样若干组参数,得到它们的目标值。

  2. 划分好坏:根据目标值排序,把历史试验分成两组:好组(比如前 25% 的目标值)和差组(后 75%)。

  3. 密度估计:分别对两组参数建立概率密度模型(Parzen 估计)。

  4. 选择新参数:计算每个参数点在“好密度”与“差密度”之间的比值,比值越大说明该点越可能出好结果。选择比值最大的参数点作为下一组试验。

这个过程反复迭代,搜索空间逐渐聚焦到高潜力区域。

4.3 常用采样器对比

采样器核心算法适用场景优点缺点
TPESampler (默认)贝叶斯优化 (TPE)通用单目标,混合参数类型平衡探索与利用,效率高对超参数尺度不敏感吗?实际上还好
RandomSampler纯随机基线对比,简单测试简单、可复现没有学习能力,效率低
CmaEsSampler协方差矩阵自适应进化策略连续参数空间(所有参数都是数值型)在连续优化问题上非常强大不支持类别参数
GridSampler网格搜索极小规模搜索空间保证覆盖所有组合维度爆炸,不推荐
NSGAIISamplerNSGA-II 遗传算法多目标优化经典多目标算法仅多目标
MOTPESamplerTPE 多目标扩展多目标优化基于概率模型

4.4 采样器的使用示例

 # 默认 TPE
 study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=42))
 ​
 # 随机采样(作为性能下限参考)
 study = optuna.create_study(sampler=optuna.samplers.RandomSampler(seed=42))
 ​
 # CMA-ES(所有参数最好都是连续浮点数)
 study = optuna.create_study(sampler=optuna.samplers.CmaEsSampler(seed=42))
 ​
 # 网格采样(需要预先定义搜索空间字典)
 search_space = {'x': [-10, 0, 10], 'y': [0, 1]}
 study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))

4.5 如何选择采样器?

  • 新手或通用任务:使用默认的 TPESampler,它表现稳定。

  • 做消融实验:对比 TPESampler vs RandomSampler,验证智能采样的价值。

  • 所有超参数都是连续浮点数:可以尝试 CmaEsSampler,有时收敛更快。

  • 多目标问题:使用 NSGAIISamplerMOTPESampler


第五部分:深入原理 — 剪枝(Pruning)—— 深度学习完整示例

剪枝能提前终止劣质试验,极大节省算力。本节给出常用剪枝器在深度学习中的完整实战示例

5.1 剪枝工作原理回顾

  • 在训练循环中调用 trial.report(value, step) 报告中间指标(如验证准确率)。

  • 调用 trial.should_prune() 检查是否应终止。

  • 若应终止,抛出 optuna.TrialPruned 异常。

5.2 深度学习目标函数模板(支持剪枝)

 def objective(trial):
     # 超参数定义略
     model = create_model(trial)
     optimizer = create_optimizer(trial, model)
     train_loader, val_loader = get_data(batch_size)
     
     for epoch in range(1, max_epochs+1):
         train_one_epoch(model, train_loader, optimizer)
         val_acc = validate(model, val_loader)
         
         trial.report(val_acc, epoch)          # 报告当前 epoch 的准确率
         if trial.should_prune():
             raise optuna.TrialPruned()        # 提前终止
     return val_acc

5.3 常用剪枝器完整用法(深度学习场景)

(1) MedianPruner — 稳妥首选
 from optuna.pruners import MedianPruner
 ​
 pruner = MedianPruner(
     n_startup_trials=5,    # 前5个 trial 不剪枝,积累历史
     n_warmup_steps=3,      # 每个 trial 的前3个 epoch 不剪枝(模型预热)
     interval_steps=1       # 每1个 epoch 检查一次
 )
 study = optuna.create_study(direction='maximize', pruner=pruner)
(2) HyperbandPruner — 自动强力型

Hyperband 通过多阶段资源分配自动淘汰劣质试验,不需要人工设定预热步数。

 from optuna.pruners import HyperbandPruner
 ​
 pruner = HyperbandPruner(
     min_resource=1,        # 每个 trial 最少资源(epoch 数)
     max_resource=20,       # 最多资源(应与 max_epochs 一致)
     reduction_factor=3     # 每阶段淘汰比例(保留 1/3)
 )
 study = optuna.create_study(direction='maximize', pruner=pruner)
(3) PatientPruner — 防止误杀(震荡曲线专用)

当验证准确率震荡时,普通剪枝器可能过早剪掉潜力股。PatientPruner 允许连续若干次不提升才剪枝。

 from optuna.pruners import MedianPruner, PatientPruner
 ​
 base_pruner = MedianPruner(n_startup_trials=5, n_warmup_steps=3)
 pruner = PatientPruner(base_pruner, patience=3)   # 连续3次未提升才剪
 study = optuna.create_study(direction='maximize', pruner=pruner)
(4) PercentilePruner — 灵活分位数
 from optuna.pruners import PercentilePruner
 ​
 # 只保留排名前 30% 的试验(百分位数 70 以上)
 pruner = PercentilePruner(percentile=70.0, n_startup_trials=5)
(5) ThresholdPruner — 硬阈值
 from optuna.pruners import ThresholdPruner
 ​
 # 对于最大化准确率,若准确率低于 0.5 则剪枝
 pruner = ThresholdPruner(lower=0.5)   # lower 是下界,高于此值保留

5.4 深度学习完整实战:整合剪枝器的调优脚本

以下代码在 MNIST 数据集上训练一个 CNN,使用 HyperbandPruner 进行剪枝。

 import optuna
 import torch
 import torch.nn as nn
 import torch.optim as optim
 from torchvision import datasets, transforms
 from torch.utils.data import DataLoader
 ​
 # 定义模型
 class Net(nn.Module):
     def __init__(self, dropout):
         super().__init__()
         self.conv1 = nn.Conv2d(1, 32, 3)
         self.conv2 = nn.Conv2d(32, 64, 3)
         self.fc1 = nn.Linear(64*12*12, 128)
         self.fc2 = nn.Linear(128, 10)
         self.dropout = nn.Dropout(dropout)
 ​
     def forward(self, x):
         x = torch.relu(self.conv1(x))
         x = torch.relu(self.conv2(x))
         x = x.view(x.size(0), -1)
         x = self.dropout(x)
         x = torch.relu(self.fc1(x))
         x = self.fc2(x)
         return x
 ​
 def get_data(batch_size):
     transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
     train = datasets.MNIST('./data', train=True, download=True, transform=transform)
     test = datasets.MNIST('./data', train=False, transform=transform)
     return DataLoader(train, batch_size, shuffle=True), DataLoader(test, batch_size)
 ​
 def objective(trial):
     lr = trial.suggest_float('lr', 1e-4, 1e-2, log=True)
     batch_size = trial.suggest_categorical('batch_size', [64, 128])
     dropout = trial.suggest_float('dropout', 0.1, 0.5)
     
     train_loader, test_loader = get_data(batch_size)
     model = Net(dropout)
     optimizer = optim.Adam(model.parameters(), lr=lr)
     device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
     model.to(device)
 ​
     max_epochs = 10
     for epoch in range(1, max_epochs+1):
         model.train()
         for data, target in train_loader:
             data, target = data.to(device), target.to(device)
             optimizer.zero_grad()
             output = model(data)
             loss = nn.functional.cross_entropy(output, target)
             loss.backward()
             optimizer.step()
 ​
         # 验证
         model.eval()
         correct = 0
         with torch.no_grad():
             for data, target in test_loader:
                 data, target = data.to(device), target.to(device)
                 output = model(data)
                 pred = output.argmax(1)
                 correct += (pred == target).sum().item()
         acc = correct / len(test_loader.dataset)
 ​
         trial.report(acc, epoch)
         if trial.should_prune():
             raise optuna.TrialPruned()
     return acc
 ​
 # 使用 HyperbandPruner
 pruner = optuna.pruners.HyperbandPruner(min_resource=1, max_resource=10, reduction_factor=3)
 study = optuna.create_study(direction='maximize', pruner=pruner, storage='sqlite:///mnist_hyperband.db', load_if_exists=True)
 study.optimize(objective, n_trials=50)
 ​
 print("Best accuracy:", study.best_value)
 print("Best params:", study.best_params)
 ​
 # 查看剪枝统计
 pruned = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]
 complete = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
 print(f"完成: {len(complete)} 个, 剪枝: {len(pruned)} 个")

5.5 剪枝器选择指南

场景推荐剪枝器理由
训练轮数固定,希望简单剪枝MedianPruner简单有效
训练资源多变,希望自动调度HyperbandPruner自动分配资源
loss 曲线震荡严重PatientPruner + MedianPruner给予耐心
你对指标有明确硬性要求ThresholdPruner快刀斩乱麻

第六部分:并行化与分布式

6.1 为什么需要并行?

  • 单次试验本身可能就是多线程/GPU 加速的,但多个试验之间是串行的,浪费计算资源。

  • 多核 CPU 或多机集群可以让多个试验同时运行,总耗时大幅下降。

6.2 并行化的三种方式

方式一:多线程(n_jobs)

适用:目标函数是 I/O 密集型(如从磁盘加载数据)或不占用太多 CPU 计算时。Python GIL 限制,不适合计算密集型任务。

 study.optimize(objective, n_trials=50, n_jobs=4)
方式二:多进程(ProcessPoolExecutor)—— 单机推荐

适用:计算密集型任务,充分利用多核 CPU。

 import concurrent.futures
 ​
 with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
     study.optimize(objective, n_trials=50, executor=executor)

注意:目标函数必须是可 pickle 的(即定义在模块顶层,不能是 lambda 或嵌套函数依赖外部变量),否则会序列化失败。

方式三:分布式(多机器)

适用:多台服务器共同优化同一个研究,或者想利用 Kubernetes、Slurm 等集群调度。

原理:使用一个共享的 RDB 后端(如 MySQL、PostgreSQL)存储所有 trial 记录。每个 worker 运行一个独立的 Optuna 进程,从数据库中取任务并将结果写回。

实现步骤

  1. 安装数据库驱动(以 MySQL 为例)

     pip install pymysql
  2. 创建共享数据库和 study

     # 用命令行创建(也可以在 Python 中创建)
     optuna create-study --study-name "distributed_study" \
         --storage "mysql+pymysql://user:password@host/db_name" \
         --direction maximize

    或者在 Python 中创建:

     optuna.create_study(
         study_name="distributed_study",
         storage="mysql+pymysql://user:password@host/db_name",
         load_if_exists=True,
         direction="maximize"
     )
  3. 在每个 worker 上运行优化

     study = optuna.load_study(
         study_name="distributed_study",
         storage="mysql+pymysql://user:password@host/db_name"
     )
     study.optimize(objective, n_trials=100)  # 每个 worker 跑100次
    • 每个 worker 可以跑不同数量的 trials,它们会合并到同一个 study 中。

    • Worker 之间通过数据库的锁机制避免重复试验。

注意事项

  • 数据库必须支持并发读写,建议使用 MySQL/PostgreSQL,SQLite 不支持高并发分布式。

  • 每个 worker 需要能够访问相同的代码和数据环境。


第七部分:高级特性

7.1 多目标优化(Multi-objective Optimization)

有些时候我们需要同时优化多个可能冲突的目标,例如:

  • 最大化准确率,同时最小化模型推理时间

  • 最小化损失,同时最小化模型参数量

Optuna 支持多目标优化(需要 Optuna >= 2.0)。

 def multi_objective(trial):
     # 超参数定义...
     accuracy = ...
     inference_time = ...
     return accuracy, inference_time   # 返回一个元组
 ​
 # 创建 study 时指定 directions 列表
 study = optuna.create_study(directions=['maximize', 'minimize'])
 study.optimize(multi_objective, n_trials=100)
 ​
 # 获取帕累托前沿
 pareto_trials = study.best_trials  # 所有非支配的 trials

可视化多目标结果:

 from optuna.visualization import plot_pareto_front
 plot_pareto_front(study).show()

7.2 集成 MLflow / TensorBoard 进行实验跟踪

与 MLflow 集成

 import optuna
 from optuna.integration.mlflow import MLflowCallback
 ​
 mlflow_cb = MLflowCallback(
     tracking_uri="./mlruns",
     metric_name="accuracy",
 )
 ​
 study = optuna.create_study()
 study.optimize(objective, n_trials=100, callbacks=[mlflow_cb])

与 TensorBoard 集成

 from optuna.integration.tensorboard import TensorBoardCallback
 ​
 tb_cb = TensorBoardCallback(metric_name="loss", log_dir="./logs")
 study.optimize(objective, n_trials=100, callbacks=[tb_cb])

7.3 自定义采样器

如果需要实现自己的采样逻辑,可以继承 BaseSampler

 from optuna.samplers import BaseSampler
 ​
 class MySampler(BaseSampler):
     def __init__(self, seed=42):
         self._rng = np.random.RandomState(seed)
     
     def sample_relative(self, study, trial, search_space):
         # 返回相对采样参数(可选)
         return {}
     
     def sample_independent(self, study, trial, param_name, param_distribution):
         # 对每个独立参数采样
         if param_distribution.__class__ is optuna.distributions.FloatDistribution:
             return self._rng.uniform(param_distribution.low, param_distribution.high)
         # ... 处理整数、类别等

7.4 自定义剪枝器

继承 BasePruner

 from optuna.pruners import BasePruner
 ​
 class MyPruner(BasePruner):
     def prune(self, study, trial):
         # 获取当前 trial 的所有中间值
         intermediate_values = trial.intermediate_values
         if not intermediate_values:
             return False
         # 自定义剪枝逻辑:比如最后3个step的值都在上升
         steps = sorted(intermediate_values.keys())
         if len(steps) >= 3:
             last_three = [intermediate_values[s] for s in steps[-3:]]
             if all(last_three[i] > last_three[i+1] for i in range(2)):
                 return True
         return False

7.5 缓存目标函数结果(避免重复计算)

如果某些超参数组合已经尝试过,可以直接返回缓存值:

 from functools import lru_cache
 ​
 @lru_cache(maxsize=None)
 def cached_objective(x, y):
     # 注意:参数必须是可哈希的
     return (x - 2) ** 2 + (y + 1) ** 2
 ​
 def objective(trial):
     x = trial.suggest_float('x', -10, 10)
     y = trial.suggest_float('y', -10, 10)
     return cached_objective(x, y)

第八部分:最佳实践与常见陷阱

8.1 最佳实践

  1. 固定随机种子:确保结果可复现。

     sampler = TPESampler(seed=42)
     study = optuna.create_study(sampler=sampler)
  2. 使用日志尺度:对于学习率、正则化系数等跨数量级的参数,设置 log=True

     lr = trial.suggest_float('lr', 1e-5, 1e-1, log=True)
  3. 设置超时或最大试验次数:防止无限运行。

     study.optimize(objective, n_trials=200, timeout=7200)
  4. 先粗搜,再细搜

    • 第一轮:较少的试验次数(如 50),宽阔的搜索范围。

    • 第二轮:在第一轮最佳点附近缩小范围,增加试验次数(如 100)。

  5. 持久化 study:防止意外中断丢失结果。

     study = optuna.create_study(storage='sqlite:///study.db', load_if_exists=True)
  6. 诊断采样效率:如果参数重要性图中所有参数重要性相近,说明搜索空间可能太大或指标过于随机。

8.2 常见陷阱与解决方案

陷阱现象解决方案
参数范围过大优化过程像随机搜索根据先验知识缩小范围
目标函数不稳定(噪声大)最佳值反复横跳增加重复试验取平均,或使用 MedianPruner 增加鲁棒性
忘记设置 direction误解最大化/最小化明确设置 direction='maximize''minimize'
Trial 被频繁剪枝但后期可能变好模型需要热身增大 n_warmup_steps 或使用 PatientPruner
分布式下 trial 重复数据库锁配置问题使用支持行级锁的数据库(如 PostgreSQL),避免 SQLite
使用 suggest_float 时的 step 参数采样不够细腻若需要离散步长,用 step;否则去掉 step 让采样连续

8.3 调试技巧

  • 打印每个 trial 的参数:使用回调函数

     def print_callback(study, trial):
         print(f"Trial {trial.number}: {trial.params} -> {trial.value}")
     ​
     study.optimize(objective, n_trials=10, callbacks=[print_callback])
  • 捕获异常继续运行

     study.optimize(objective, n_trials=50, catch=(ValueError, RuntimeError))
  • 查看被剪枝的 trials

     pruned_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.PRUNED]
     complete_trials = [t for t in study.trials if t.state == optuna.trial.TrialState.COMPLETE]
     print(f"Pruned: {len(pruned_trials)}, Complete: {len(complete_trials)}")

第九部分:总结

Optuna 是一个功能强大、设计优雅的超参数优化框架。通过本指南,你已经掌握:

  • 基础:Study、Trial、objective 函数的基本用法

  • 搜索空间:suggest API 的各种类型及条件参数

  • 采样器:TPE 原理及不同采样器的适用场景

  • 剪枝:为什么需要、如何实现以及各种剪枝器的区别(MedianPruner, HyperbandPruner, PatientPruner, PercentilePruner, ThresholdPruner)

  • 并行化:从多线程到多进程再到分布式集群

  • 可视化:8 种图表(优化历史、参数重要性、中间值、平行坐标、等高线、切片、EDF、时间线)及其使用

  • 持久化:SQLite 数据库的创建、加载、跨会话恢复及 Dashboard 交互

  • 高级特性:多目标优化、与 MLflow/TensorBoard 集成、自定义扩展

学习 Optuna 最好的方式是动手实践。从一个简单的 scikit-learn 模型开始,逐步加入剪枝、并行化,再到深度学习框架(PyTorch/TensorFlow)的集成。当你掌握 Optuna 之后,超参数调优将不再是痛苦的手工劳动,而变成可复现、可并行的自动化流程。

Happy Tuning! 🚀

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/m0_69757158/article/details/160481806

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--