Optuna Tutorial: Automatic Hyperparameter Optimization

# Tutorial Lengkap Optuna: Optimasi Hyperparameter Otomatis ## Daftar Isi 1. [Pendahuluan](#pendahuluan) 2. [Prasyarat](#prasyarat) 3. [Instalasi dan Setup](#instalasi-dan-setup) 4. [Konsep Dasar: St...

By Ruby Abdullah · · tutorial
OptunaHyperparameterOptimizationAutoMLMachine LearningPython

Complete Optuna Tutorial: Automatic Hyperparameter Optimization

Table of Contents

  • Introduction
  • Prerequisites
  • Installation and Setup
  • Core Concepts: Study and Trial
  • Basic Optimization
  • Samplers and Search Strategies
  • Pruning for Early Stopping
  • Multi-Objective Optimization
  • Integration with ML Frameworks
  • Visualization and Dashboard
  • Distributed Optimization
  • Best Practices
  • Conclusion
  • 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):
    

    nestimators = 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.besttrial.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

  • Start with TPE sampler - it's the most efficient for most use cases
  • Use log scale for learning rates and regularization parameters
  • Enable pruning to save computation on unpromising trials
  • Set reasonable search ranges based on domain knowledge
  • Use enough trials - at least 50-100 for meaningful optimization
  • Save studies to database for persistence and distributed optimization
  • Visualize results to understand parameter interactions
  • Use conditional parameters to avoid invalid combinations
  • Set seeds for reproducibility across samplers
  • Monitor with dashboard for real-time optimization tracking
  • 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.

    Related Articles

    Cleanlab Tutorial: Improving Data Quality for Machine Learning

    Tutorial Cleanlab: Meningkatkan Kualitas Data untuk Machine Learning Kualitas data adalah fondasi dari setiap proyek mac...

    Complete Comet ML Tutorial: MLOps Platform for Experiment Tracking and Model Management

    Tutorial Lengkap Comet ML: Platform MLOps untuk Experiment Tracking dan Model Management Dalam dunia machine learning mo...

    MLX Tutorial: Apple's Machine Learning Framework for Apple Silicon

    Tutorial MLX: Framework Machine Learning Apple untuk Apple Silicon MLX adalah framework machine learning open-source dar...

    SHAP Tutorial: Explainable AI and Model Interpretability

    SHAP - Panduan Praktis Explainable AI dan Interpretabilitas Model Model machine learning makin sering dipakai untuk meng...