目录
第一部分:基础篇
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 的核心流程:
-
定义目标函数(objective)—— 告诉 Optuna 要优化什么
-
创建 Study —— 管理整个调优过程
-
运行 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 采样器的工作原理
-
初始阶段:随机采样若干组参数,得到它们的目标值。
-
划分好坏:根据目标值排序,把历史试验分成两组:好组(比如前 25% 的目标值)和差组(后 75%)。
-
密度估计:分别对两组参数建立概率密度模型(Parzen 估计)。
-
选择新参数:计算每个参数点在“好密度”与“差密度”之间的比值,比值越大说明该点越可能出好结果。选择比值最大的参数点作为下一组试验。
这个过程反复迭代,搜索空间逐渐聚焦到高潜力区域。
4.3 常用采样器对比
| 采样器 | 核心算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| TPESampler (默认) | 贝叶斯优化 (TPE) | 通用单目标,混合参数类型 | 平衡探索与利用,效率高 | 对超参数尺度不敏感吗?实际上还好 |
| RandomSampler | 纯随机 | 基线对比,简单测试 | 简单、可复现 | 没有学习能力,效率低 |
| CmaEsSampler | 协方差矩阵自适应进化策略 | 连续参数空间(所有参数都是数值型) | 在连续优化问题上非常强大 | 不支持类别参数 |
| GridSampler | 网格搜索 | 极小规模搜索空间 | 保证覆盖所有组合 | 维度爆炸,不推荐 |
| NSGAIISampler | NSGA-II 遗传算法 | 多目标优化 | 经典多目标算法 | 仅多目标 |
| MOTPESampler | TPE 多目标扩展 | 多目标优化 | 基于概率模型 |
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,它表现稳定。 -
做消融实验:对比
TPESamplervsRandomSampler,验证智能采样的价值。 -
所有超参数都是连续浮点数:可以尝试
CmaEsSampler,有时收敛更快。 -
多目标问题:使用
NSGAIISampler或MOTPESampler。
第五部分:深入原理 — 剪枝(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 进程,从数据库中取任务并将结果写回。
实现步骤:
-
安装数据库驱动(以 MySQL 为例)
pip install pymysql
-
创建共享数据库和 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" )
-
在每个 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 最佳实践
-
固定随机种子:确保结果可复现。
sampler = TPESampler(seed=42) study = optuna.create_study(sampler=sampler)
-
使用日志尺度:对于学习率、正则化系数等跨数量级的参数,设置
log=True。lr = trial.suggest_float('lr', 1e-5, 1e-1, log=True) -
设置超时或最大试验次数:防止无限运行。
study.optimize(objective, n_trials=200, timeout=7200)
-
先粗搜,再细搜:
-
第一轮:较少的试验次数(如 50),宽阔的搜索范围。
-
第二轮:在第一轮最佳点附近缩小范围,增加试验次数(如 100)。
-
-
持久化 study:防止意外中断丢失结果。
study = optuna.create_study(storage='sqlite:///study.db', load_if_exists=True)
-
诊断采样效率:如果参数重要性图中所有参数重要性相近,说明搜索空间可能太大或指标过于随机。
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



