Complete Optuna Tutorial: Automatic Hyperparameter Optimization
Table of Contents
Introduction
Hyperparameter tuning is one of the most time-consuming tasks in machine learning. Optuna is an automatic hyperparameter optimization framework that uses efficient search algorithms to find optimal configurations significantly faster than grid or random search.
In this tutorial, you'll learn how to use Optuna for everything from basic optimization to distributed multi-objective tuning across ML frameworks.
Prerequisites
- Python 3.8+
- Basic understanding of machine learning
- Familiarity with scikit-learn or PyTorch
Installation and Setup
pip install optuna optuna-dashboard plotly scikit-learn xgboost lightgbm torch
Optional: for distributed optimization
pip install optuna[rdb] psycopg2-binary
Core Concepts: Study and Trial
Optuna revolves around two main concepts:
- Study: An optimization session consisting of multiple trials
- Trial: A single evaluation of the objective function with specific hyperparameters
import optuna
Define objective function
def objective(trial):
# Suggest hyperparameters
x = trial.suggestfloat("x", -10, 10)
y = trial.suggestfloat("y", -10, 10)
# Return value to minimize
return (x - 2) 2 + (y + 3) 2
Create study and optimize
study = optuna.createstudy(direction="minimize")
study.optimize(objective, ntrials=100)
print(f"Best value: {study.bestvalue:.4f}")
print(f"Best params: {study.bestparams}")
Parameter Suggestion Methods
def objective(trial):
# Integer parameter
nlayers = trial.suggestint("nlayers", 1, 5)
# Float parameter
learningrate = trial.suggestfloat("lr", 1e-5, 1e-1, log=True)
# Categorical parameter
optimizer = trial.suggestcategorical("optimizer", ["adam", "sgd", "rmsprop"])
# Discrete float
dropout = trial.suggestfloat("dropout", 0.1, 0.5, step=0.1)
# Conditional parameters
if optimizer == "sgd":
momentum = trial.suggestfloat("momentum", 0.0, 0.99)
return trainmodel(nlayers, learningrate, optimizer, dropout)
Basic Optimization with Scikit-Learn
import optuna
from sklearn.ensemble import RandomForestClassifier
from sklearn.modelselection import crossvalscore
from sklearn.datasets import loadbreastcancer
X, y = loadbreastcancer(returnXy=True)
def objective(trial):
params = {
"nestimators": trial.suggestint("nestimators", 50, 500),
"maxdepth": trial.suggestint("maxdepth", 3, 20),
"minsamplessplit": trial.suggestint("minsamplessplit", 2, 20),
"minsamplesleaf": trial.suggestint("minsamplesleaf", 1, 10),
"maxfeatures": trial.suggestcategorical("maxfeatures", ["sqrt", "log2", None]),
"criterion": trial.suggestcategorical("criterion", ["gini", "entropy"]),
}
clf = RandomForestClassifier(params, randomstate=42, njobs=-1)
scores = crossvalscore(clf, X, y, cv=5, scoring="accuracy")
return scores.mean()
study = optuna.createstudy(direction="maximize")
study.optimize(objective, ntrials=200, showprogressbar=True)
print(f"Best accuracy: {study.bestvalue:.4f}")
print(f"Best params: {study.bestparams}")
Samplers and Search Strategies
# TPE Sampler (default) - Tree-structured Parzen Estimator
from optuna.samplers import TPESampler
study = optuna.createstudy(sampler=TPESampler(seed=42))
CMA-ES Sampler - for continuous parameters
from optuna.samplers import CmaEsSampler
study = optuna.createstudy(sampler=CmaEsSampler(seed=42))
Grid Sampler - exhaustive search
from optuna.samplers import GridSampler
searchspace = {
"lr": [1e-4, 1e-3, 1e-2],
"nlayers": [1, 2, 3, 4],
}
study = optuna.createstudy(sampler=GridSampler(searchspace))
Random Sampler - baseline comparison
from optuna.samplers import RandomSampler
study = optuna.createstudy(sampler=RandomSampler(seed=42))
QMC Sampler - Quasi-Monte Carlo for better coverage
from optuna.samplers import QMCSampler
study = optuna.createstudy(sampler=QMCSampler(seed=42))
Sampler Comparison
import time
samplers = {
"TPE": TPESampler(seed=42),
"CMA-ES": CmaEsSampler(seed=42),
"Random": RandomSampler(seed=42),
}
results = {}
for name, sampler in samplers.items():
study = optuna.createstudy(direction="minimize", sampler=sampler)
start = time.time()
study.optimize(objective, ntrials=100, showprogressbar=False)
elapsed = time.time() - start
results[name] = {
"bestvalue": study.bestvalue,
"time": elapsed
}
print(f"{name}: best={study.bestvalue:.4f}, time={elapsed:.1f}s")
Pruning for Early Stopping
Pruning stops unpromising trials early, saving compute time.
from optuna.pruners import MedianPruner, HyperbandPruner, SuccessiveHalvingPruner
Median Pruner - prune if below median of previous trials
study = optuna.createstudy(
direction="maximize",
pruner=MedianPruner(nstartuptrials=5, nwarmupsteps=10)
)
Hyperband Pruner - aggressive early stopping
study = optuna.createstudy(
direction="maximize",
pruner=HyperbandPruner(minresource=1, maxresource=100, reductionfactor=3)
)
Pruning with PyTorch Training
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
def objective(trial):
lr = trial.suggestfloat("lr", 1e-5, 1e-1, log=True)
nunits = trial.suggestint("nunits", 32, 256)
dropout = trial.suggestfloat("dropout", 0.1, 0.5)
model = nn.Sequential(
nn.Linear(30, nunits),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(nunits, nunits),
nn.ReLU(),
nn.Dropout(dropout),
nn.Linear(nunits, 1),
nn.Sigmoid()
)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.BCELoss()
for epoch in range(100):
model.train()
for batchX, batchy in trainloader:
optimizer.zerograd()
output = model(batchX).squeeze()
loss = criterion(output, batchy)
loss.backward()
optimizer.step()
# Evaluate and report for pruning
model.eval()
with torch.nograd():
valoutput = model(Xvaltensor).squeeze()
valacc = ((valoutput > 0.5) == yvaltensor).float().mean().item()
# Report intermediate value
trial.report(valacc, epoch)
# Check if trial should be pruned
if trial.shouldprune():
raise optuna.TrialPruned()
return valacc
study = optuna.createstudy(
direction="maximize",
pruner=HyperbandPruner(minresource=5, maxresource=100)
)
study.optimize(objective, ntrials=50)
Multi-Objective Optimization
Optimize multiple objectives simultaneously (e.g., accuracy vs inference time).
def multiobjective(trial):
n
estimators = trial.suggestint("nestimators", 10, 500)
maxdepth = trial.suggestint("maxdepth", 2, 20)
clf = RandomForestClassifier(
nestimators=nestimators,
maxdepth=maxdepth,
randomstate=42
)
# Objective 1: Accuracy
scores = crossvalscore(clf, X, y, cv=3, scoring="accuracy")
accuracy = scores.mean()
# Objective 2: Training time (lower is better)
import time
start = time.time()
clf.fit(X, y)
traintime = time.time() - start
return accuracy, traintime
study = optuna.createstudy(
directions=["maximize", "minimize"],
studyname="multi-objective-rf"
)
study.optimize(multiobjective, ntrials=100)
Get Pareto front
paretotrials = study.besttrials
print(f"Number of Pareto-optimal solutions: {len(paretotrials)}")
for trial in paretotrials:
print(f" Accuracy: {trial.values[0]:.4f}, Time: {trial.values[1]:.4f}s")
Integration with ML Frameworks
XGBoost Integration
import xgboost as xgb
def xgbobjective(trial):
params = {
"objective": "binary:logistic",
"evalmetric": "logloss",
"nestimators": trial.suggestint("nestimators", 100, 1000),
"maxdepth": trial.suggestint("maxdepth", 3, 12),
"learningrate": trial.suggestfloat("learningrate", 1e-3, 0.3, log=True),
"subsample": trial.suggestfloat("subsample", 0.5, 1.0),
"colsamplebytree": trial.suggestfloat("colsamplebytree", 0.5, 1.0),
"regalpha": trial.suggestfloat("regalpha", 1e-8, 10.0, log=True),
"reglambda": trial.suggestfloat("reglambda", 1e-8, 10.0, log=True),
"minchildweight": trial.suggestint("minchildweight", 1, 10),
}
model = xgb.XGBClassifier(params, randomstate=42, uselabelencoder=False)
scores = crossvalscore(model, X, y, cv=5, scoring="accuracy")
return scores.mean()
study = optuna.createstudy(direction="maximize")
study.optimize(xgbobjective, ntrials=100)
PyTorch Lightning Integration
from optuna.integration import PyTorchLightningPruningCallback
def plobjective(trial):
lr = trial.suggestfloat("lr", 1e-5, 1e-2, log=True)
nlayers = trial.suggestint("nlayers", 1, 4)
hiddensize = trial.suggestint("hiddensize", 32, 256)
model = LightningModel(nlayers=nlayers, hiddensize=hiddensize, lr=lr)
trainer = pl.Trainer(
maxepochs=50,
callbacks=[PyTorchLightningPruningCallback(trial, monitor="valacc")],
enableprogressbar=False,
)
trainer.fit(model, trainloader, valloader)
return trainer.callbackmetrics["valacc"].item()
Visualization and Dashboard
from optuna.visualization import (
plotoptimizationhistory,
plotparamimportances,
plotparallelcoordinate,
plotslice,
plotcontour,
)
Optimization history
fig = plotoptimizationhistory(study)
fig.show()
Parameter importance
fig = plotparamimportances(study)
fig.show()
Parallel coordinate plot
fig = plotparallelcoordinate(study)
fig.show()
Slice plot - individual parameter effects
fig = plotslice(study)
fig.show()
Contour plot - parameter interactions
fig = plotcontour(study, params=["lr", "nlayers"])
fig.show()
Optuna Dashboard
# Launch web dashboard
pip install optuna-dashboard
optuna-dashboard sqlite:///optunastudy.db --port 8080
# Save study to database for dashboard
study = optuna.createstudy(
studyname="my-experiment",
storage="sqlite:///optunastudy.db",
direction="maximize",
loadifexists=True
)
Distributed Optimization
# Use PostgreSQL for distributed optimization
storage = "postgresql://user:password@localhost:5432/optunadb"
Worker 1
study = optuna.createstudy(
studyname="distributed-study",
storage=storage,
direction="maximize",
loadifexists=True
)
study.optimize(objective, ntrials=50)
Worker 2 (run on another machine)
study = optuna.loadstudy(
studyname="distributed-study",
storage=storage
)
study.optimize(objective, ntrials=50)
Custom Callbacks
def loggingcallback(study, trial):
if study.best
trial.number == trial.number:
print(f"New best trial {trial.number}: value={trial.value:.4f}")
print(f" Params: {trial.params}")
study.optimize(objective, ntrials=100, callbacks=[loggingcallback])
Best Practices
Conclusion
Optuna provides a powerful, flexible framework for hyperparameter optimization. Its efficient samplers, pruning strategies, and multi-objective support make it the go-to choice for ML practitioners. Combined with its visualization tools and distributed capabilities, Optuna can significantly accelerate your model development workflow.