Validation and Hyperparameter Search

Optuna

Optuna runs the whole tuning loop for you — suggesting settings, remembering every result, and abandoning hopeless trials early — behind a three-line API.

On this page 5
  1. Why this had to be invented
  2. How it works
  3. A real example you have seen
  4. Remember this
  5. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

Optuna is a Python library that runs your tuning experiments for you.

It proposes the next settings, records every result, and cancels trials that are going nowhere.

Think of a meticulous lab assistant with a logbook. You describe the experiment once. The assistant then runs it again and again. Each next mixture is chosen from everything in the logbook. Every outcome is written down. The burner goes off early whenever a batch is visibly ruined.

You already know the assistant's two skills from earlier lessons: choosing informed next trials is Bayesian optimisation, and cancelling doomed trials early is successive halving. Optuna packages both behind a tiny interface.

Why this had to be invented

By 2018, the ideas of smart tuning were settled and the pain had moved to plumbing. Teams were hand-writing loops to sample settings and track histories in spreadsheets. They wrote their own code to resume crashed searches and spread work across machines. The same scaffolding was rebuilt at every company, with the same bugs.

Optuna's design insight was the define-by-run interface: you do not declare a search space in a config file. You write an ordinary Python function that trains your model, and inside it you ask for values — "give me a learning rate between these bounds". The search space is whatever your code asks for, so loops, conditions and branching spaces come free.

How it works

You write one function, called the objective: settings in, score out. Optuna calls it repeatedly.

you write:   objective(trial) → score

optuna:      trial 1   settings A   → 0.84      ┐
             trial 2   settings B   → 0.79      │ every result
             trial 3   settings C   → 0.91      │ goes in the
             trial 4   near C's recipe → 0.92   │ logbook
             trial 5   looking bad halfway → cancelled
             ...
             best after N trials: settings, score, full history

Early trials are near-random. Later trials cluster around what the logbook says works — the borewell-drilling behaviour, automated.

A real example you have seen

Optuna came out of Preferred Networks, a Japanese ML lab. It became the default tuner across the Kaggle community and many industry teams. Most tabular competition write-ups since 2020 mention it by name. If a model at your workplace was tuned recently, there is a fair chance this exact library ran the loop overnight.

Remember this

  • Optuna = smart suggestions + a logbook of every trial + early cancelling, in one library.
  • You write one objective function; the search space lives inside your code.
  • The same ideas as the last two lessons — packaged so you actually use them.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install optuna

A light install: the wheel is under half a megabyte, plus a few small dependencies (SQLAlchemy for storage among them). Outputs verified with optuna 4.9.0 and scikit-learn 1.7.2 on CPU; the script runs in a few seconds.

The three-lesson rematch

Same dataset and pipeline as the grid search and random search lessons, same budget of 80 model fits (16 trials × 5 folds). Grid scored 0.916, random 0.926.

optuna_demo.py
import optuna
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

optuna.logging.set_verbosity(optuna.logging.WARNING)   # keep the console quiet

X, y = make_classification(n_samples=500, n_features=10, n_informative=5,
                           flip_y=0.05, random_state=4)

def objective(trial):
    pipe = Pipeline([
        ("scale", StandardScaler()),
        ("svm", SVC(C=trial.suggest_float("C", 1e-2, 1e3, log=True),
                    gamma=trial.suggest_float("gamma", 1e-4, 1e1, log=True))),
    ])
    return cross_val_score(pipe, X, y, cv=5).mean()

study = optuna.create_study(direction="maximize",
                            sampler=optuna.samplers.TPESampler(seed=2))
study.optimize(objective, n_trials=16)

print("trials run:", len(study.trials))
print("best settings:", {k: round(v, 4) for k, v in study.best_params.items()})
print("best CV accuracy:", round(study.best_value, 3))
Output
trials run: 16
best settings: {'C': 32.3871, 'gamma': 0.0309}
best CV accuracy: 0.928

The scoreboard at equal budget: grid 0.916, random 0.926, Optuna 0.928. An honest reading: on a small, cheap problem the smart methods win by whiskers — their advantage compounds as trials get expensive and spaces grow. What you always gain is the workflow: history, resumability, and pruning.

The walkthrough

trial.suggest_float("C", 1e-2, 1e3, log=True) is the define-by-run move: a request for a value, made mid-code. log=True gives the log-uniform sampling that scale-type settings need — the same lesson as loguniform. Integers use suggest_int, choices use suggest_categorical, and an if on one suggestion can gate another — conditional spaces with no special syntax.

TPESampler(seed=2) — Optuna's default smart sampler. Its first ~10 trials are random startup; after that it proposes settings resembling past winners. The seed makes the run reproducible.

Pruning is one integration away. For iterative models, report progress inside the objective — trial.report(score, step) then if trial.should_prune(): raise optuna.TrialPruned() — and add pruner=optuna.pruners.MedianPruner() to the study. Trials below the running median get cancelled mid-training: successive halving's economics per trial. Note pruning needs stepwise scores, so it pairs with epoch-style training loops rather than a single opaque cross_val_score call.

Resumable, shareable studies. optuna.create_study(storage="sqlite:///tuning.db", study_name="svm", load_if_exists=True) persists every trial. A crashed overnight search resumes where it stopped; several machines pointing at one database parallelise the same study.

Common mistakes

No seed on the sampler. Rerun the script, get different trials, best score wobbles — and yesterday's result cannot be reproduced. Seed the sampler for experiments; drop the seed only for long production searches where extra diversity helps.

Judging TPE after ten trials. The first ~10 trials are deliberate random startup; stopping there means you paid TPE's overhead for random search's answer. Give it 30–100 trials to earn its keep.

Optimising a metric you do not care about. The objective returns one number and Optuna will maximise exactly that. Accuracy on imbalanced data is the classic wrong number — decide the metric before the search, not after.

Reporting best_value as final performance. Sixteen attempts at the same validation folds — the winner is flattered, as in every search method. Confirm the chosen settings once on an untouched test set.

Try it yourself

Add a third suggestion — kernel=trial.suggest_categorical("kernel", ["rbf", "poly"]) — and, when the kernel is polynomial, a conditional degree=trial.suggest_int("degree", 2, 4). The branching space needs zero configuration. Then bump n_trials to 60 and watch study.best_value — where does the improvement flatten?

What to learn next

Researcher — Mathematics and papers.

Design contributions

Akiba, Sano, Yanase, Ohta and Koyama (2019), Optuna: a next-generation hyperparameter optimization framework (KDD), name three design goals: define-by-run search-space construction (by analogy to define-by-run autodiff frameworks), efficient sampling and pruning as co-equal primitives, and easy scalability through storage-backed studies. The architectural separation matters: samplers decide what to try; pruners decide how long to run it — orthogonal axes matching the model-based versus bandit-based split of the two previous lessons.

The default sampler: TPE

The tree-structured Parzen estimator (Bergstra et al., 2011) models the configuration densities $\ell(x)$ — fitted to trials in the best $\gamma$ quantile — and $g(x)$, fitted to the rest, then proposes $x$ maximising $\ell(x)/g(x)$, which is monotone in expected improvement under the quantile construction. The tree structure mirrors define-by-run's conditional branches: each suggest call is a node whose density is estimated where the branch was active. Multivariate TPE (multivariate=True) models joint densities to capture parameter interactions the independent default misses.

Other bundled samplers: RandomSampler (the honest baseline), CmaEsSampler — CMA-ES (Hansen and Ostermeier, 2001) for continuous, non-conditional spaces — QMCSampler (Sobol' low-discrepancy startup), GPSampler (GP-based BO), and NSGAIISampler (Deb et al., 2002) as the multi-objective default.

Pruners as bandit schedulers

MedianPruner cancels a trial whose intermediate value trails the median of completed trials at the same step — an aggressive approximation of successive halving. SuccessiveHalvingPruner implements ASHA-style asynchronous promotion (Li et al., 2020); HyperbandPruner adds the bracket structure over minimum resources (Li et al., 2018). The pruning contract requires that intermediate metrics be comparable across trials at equal steps — the same rankability assumption analysed in the Hyperband lesson, inherited unchanged.

create_study(directions=["maximize", "minimize"]) runs NSGA-II by default, returning a Pareto front (study.best_trials) — the standard formulation for accuracy-versus-latency trades in deployment-bound models. Constraints enter via constraints_func on supported samplers, implementing feasibility-aware selection.

Ecosystem position

Peer frameworks: Ray Tune (distributed-first, scheduler-rich), Hyperopt (TPE's original home, now largely dormant), SMAC3 (random-forest surrogates, strong on algorithm configuration), Ax/BoTorch (GP-based, research-grade acquisition functions). Optuna's niche is the pragmatic middle: minimal API, storage-backed durability, first-class pruning, and integrations (LightGBM callbacks, PyTorch, sklearn) that wire trial.report into existing training loops. Benchmark caution transfers verbatim from earlier lessons: at large budgets on cheap objectives, tuned random search remains within noise of sophisticated samplers (Li and Talwalkar, 2020) — the framework's durable value is the workflow, with sampling gains concentrated in expensive, low-budget regimes.

What to learn next

What to learn next

These follow on from what you just read.

  • Validation and Hyperparameter Search

    Learning curves

    A learning curve plots model score against training-set size, and its shape answers the most expensive question in ML — will more data help?

  • Validation and Hyperparameter Search

    AIC and BIC

    AIC and BIC score a model as fit minus a fee for complexity, so you can compare candidates from a single fit each — no refitting, no folds, and a different answer from each of the two.

  • Calibration and Uncertainty

    ROC vs precision-recall curves

    ROC curves judge a ranking against all the negatives while precision-recall curves judge what the alarms contain — on rare positives the two tell very different stories.