TAI BUI
Back to curriculum
BuildPython

Hyperparameter Tuning

Hyperparameters are the knobs you set before training starts. Most of them barely move the outcome, so the real skill is finding the one or two that do and spending your search budget there.

TIME: ~90 minutesPREREQUISITES: Phase 2, Lesson 11 (Ensemble Methods)

Learning Objectives

  • Implement grid search, random search, and Bayesian optimization from scratch and compare their sample efficiency
  • Explain why random search outperforms grid search when most hyperparameters have low effective dimensionality
  • Build a Bayesian optimization loop using a surrogate model and acquisition function to guide the search
  • Design a hyperparameter tuning strategy that avoids overfitting the validation set through proper cross-validation

The Problem

Your gradient boosting model has a learning rate, number of trees, max depth, min samples per leaf, subsample ratio, and column sample ratio. That's six hyperparameters. Give each five reasonable values and the grid balloons to 5^6 = 15,625 combinations. At 10 seconds per training run, trying them all costs 43 hours of compute.

Grid search is the obvious approach and the worst one at scale. Random search does better with less compute. Bayesian optimization does better still, because it learns from every evaluation instead of throwing the result away. Knowing which strategy fits your budget, and which hyperparameters actually move the needle, is the difference between an afternoon of tuning and a week of wasted GPU time.

Warm-up0/3
1. A model scores 99% on its training data and 70% on data it has never seen. What does this gap most directly indicate?Warm-up
Two horizontal bars for one model: 99 percent on training data and 70 percent on data it has never seen.
Numbers from the stem.
2. In gradient descent, what is the usual symptom of a step size set far too large?Warm-up
A parabola of loss against a weight w with a start point on its right slope, the update rule w = w - step_size * slope with step size far too large, and an empty loss-over-steps chart marked with a question mark.
Loss bowl is f(w) = w squared, drawn for illustration.
3. A number is drawn uniformly at random between 0.001 and 1.0. About what fraction of draws land below 0.1?Warm-up
A number line from 0.001 to 1.0 with a broken axis, a dashed marker at 0.1, and the prompt: fraction of draws below 0.1, question mark. The axis is not to scale.
Schematic, not to scale.

The Concept

Parameters vs Hyperparameters

Parameters are learned during training (weights, biases, split thresholds). Hyperparameters are set before training starts and control how learning happens.

HyperparameterWhat it controlsTypical range
Learning rateStep size per update0.001 to 1.0
Number of trees/epochsHow long to train10 to 10,000
Max depthModel complexity1 to 30
Regularization (lambda)Overfitting prevention0.0001 to 100
Batch sizeGradient estimation noise16 to 512
Dropout rateFraction of neurons dropped0.0 to 0.5

Grid Search

Grid search evaluates every combination of specified values. It is exhaustive and easy to understand, but scales exponentially with the number of hyperparameters.

Grid for 2 hyperparameters:

  learning_rate: [0.01, 0.1, 1.0]
  max_depth:     [3, 5, 7]

  Evaluations: 3 x 3 = 9 combinations

  (0.01, 3)  (0.01, 5)  (0.01, 7)
  (0.1,  3)  (0.1,  5)  (0.1,  7)
  (1.0,  3)  (1.0,  5)  (1.0,  7)

Grid search has a fundamental flaw: if one hyperparameter matters and the other does not, most evaluations are wasted. You get only 3 unique values of the important parameter from 9 evaluations.

Random Search

Random search samples hyperparameters from distributions instead of a grid. With the same budget of 9 evaluations, you get 9 unique values of each hyperparameter.

Mermaid diagram

Why random beats grid (Bergstra & Bengio, 2012):

  • Most hyperparameters have low effective dimensionality. Only 1-2 of 6 hyperparameters usually matter for a given problem.
  • Grid search wastes evaluations on unimportant dimensions.
  • Random search covers the important dimensions more densely for the same budget.
  • At 60 random trials, you have a 95% chance of finding a point within 5% of the optimum (if one exists in the search space).

Bayesian Optimization

Bayesian optimization uses the results of past evaluations to decide where to search next. Random search can't do that: it doesn't learn that high learning rates cause divergence, or that depth 3 consistently beats depth 10, because every trial starts from scratch with no memory of what came before.

Mermaid diagram

The two key components:

Surrogate model: A cheap-to-evaluate model (usually a Gaussian process) that approximates the expensive objective function. It gives both a prediction and an uncertainty estimate at any point in the search space.

Acquisition function: Decides where to evaluate next by balancing exploitation (search near known good points) and exploration (search where uncertainty is high). Common choices:

  • Expected Improvement (EI): How much improvement over the current best do we expect at this point?
  • Upper Confidence Bound (UCB): Prediction plus a multiple of uncertainty. Higher UCB means either promising or unexplored.
  • Probability of Improvement (PI): What is the probability this point beats the current best?

Bayesian optimization typically finds better hyperparameters than random search with 2-5x fewer evaluations. The overhead of fitting the surrogate model is negligible compared to training the actual model.

Early Stopping

Early stopping, in the context of hyperparameter search, means not letting every training run finish: if a configuration is clearly bad after 10 epochs, kill it and move on.

Strategies:

  • Patience-based: Stop if validation loss has not improved for N consecutive epochs
  • Median pruning: Stop if the trial's intermediate result is worse than the median of completed trials at the same step
  • Hyperband: Allocate small budgets to many configurations, then progressively increase budget for the best ones

Hyperband is particularly effective. It starts 81 configurations with 1 epoch each, keeps the top third, gives them 3 epochs, keeps the top third, and so on. This finds good configurations 10-50x faster than evaluating all configs for the full budget.

Learning Rate Schedulers

A learning rate scheduler adjusts the learning rate during training instead of holding it fixed. That matters because the learning rate is almost always the single most important hyperparameter you'll tune.

SchedulerFormulaWhen to use
Step decayMultiply by 0.1 every N epochsClassic CNN training
Cosine annealinglr * 0.5 * (1 + cos(pi * t / T))Modern default
Warmup + decayLinear increase then cosine decayTransformers
One-cycleIncrease then decrease over one cycleFast convergence
Reduce on plateauReduce by factor when metric stallsSafe default

Hyperparameter Importance

Not all hyperparameters matter equally. Research on random forests (Probst et al., 2019) and gradient boosting shows consistent patterns:

High importance:

  • Learning rate (always tune first)
  • Number of estimators / epochs (use early stopping instead of tuning)
  • Regularization strength

Medium importance:

  • Max depth / number of layers
  • Min samples per leaf / weight decay
  • Subsample ratio

Low importance:

  • Max features (for random forests)
  • Specific activation function choice
  • Batch size (within reasonable range)

Tune the important ones first, leave the rest at defaults.

Practical Strategy

Mermaid diagram

The concrete workflow:

  1. Start with library defaults. They are chosen by experienced practitioners and are often 80% of the way there.
  2. Coarse random search. Wide ranges, 20-50 trials. Use early stopping to kill bad runs fast.
  3. Analyze results. Which hyperparameters correlate with performance? Narrow the search space.
  4. Fine search. Bayesian optimization or focused random search in the narrowed space. 50-100 trials.
  5. Retrain on all training data with the best hyperparameters found.

Cross-Validation Integration

Nested cross-validation guards against a real risk: tuning hyperparameters against a single validation split can hand you hyperparameters that just overfit that one fold. It fixes this with two loops:

  • Outer loop (evaluation): splits data into train+val and test. Reports unbiased performance.
  • Inner loop (tuning): splits train+val into train and val. Finds best hyperparameters.

Mermaid diagram

Each outer fold finds its own best hyperparameters independently. The outer scores are an unbiased estimate of generalization performance.

With sklearn:

python
from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.ensemble import GradientBoostingRegressor
 
inner_cv = GridSearchCV(
    GradientBoostingRegressor(),
    param_grid={
        "learning_rate": [0.01, 0.05, 0.1],
        "max_depth": [2, 3, 5],
        "n_estimators": [50, 100, 200],
    },
    cv=5,
    scoring="neg_mean_squared_error",
)
 
outer_scores = cross_val_score(
    inner_cv, X, y, cv=5, scoring="neg_mean_squared_error"
)
 
print(f"Nested CV MSE: {-outer_scores.mean():.4f} +/- {outer_scores.std():.4f}")

This is expensive (5 outer folds x 5 inner folds x 27 grid points = 675 model fits), but it gives you a trustworthy performance estimate. Use it when reporting final results in papers, or when the stakes of the decision are high.

Practical Tips

Start with the learning rate. It is always the most important hyperparameter for gradient-based methods. A bad learning rate makes everything else irrelevant. Fix other hyperparameters at defaults and sweep learning rate first.

Use log-uniform distributions for learning rate and regularization. The difference between 0.001 and 0.01 matters as much as the difference between 0.1 and 1.0. Searching linearly wastes budget on the large end.

Use early stopping instead of tuning n_estimators. For boosting and neural networks, set n_estimators or epochs high and let early stopping decide when to stop. This removes one hyperparameter from the search.

Budget allocation. Put 60% of your tuning budget on the top 2 most important hyperparameters and the remaining 40% on everything else. Those top 2 account for most of the performance variation.

Scale matters. Never search batch size on a log scale (16, 32, 64 are fine). Always search learning rate on a log scale. Match the search distribution to how the hyperparameter affects the model.

Model TypeTop HyperparametersRecommended SearchBudget
Random Forestn_estimators, max_depth, min_samples_leafRandom search, 50 trialsLow (fast training)
Gradient Boostinglearning_rate, n_estimators, max_depthBayesian, 100 trials + early stoppingMedium
Neural Networklearning_rate, weight_decay, batch_sizeBayesian or random, 100+ trialsHigh (slow training)
SVMC, gamma (RBF kernel)Grid on log scale, 25-50 trialsLow (2 params)
Lasso/Ridgealpha1D search on log scale, 20 trialsVery low
XGBoostlearning_rate, max_depth, subsample, colsampleBayesian, 100-200 trials + early stoppingMedium

When in doubt: random search with 2x the number of hyperparameters as trials (e.g., 6 hyperparameters = 12+ trials minimum). Random search with just 50 trials beats a carefully designed grid search more often than it should.

Check: The Concept0/24
1. What is a hyperparameter, and how does it differ from a parameter?Define
The call GBMForTuning with n_estimators 3, learning_rate 0.1 and max_depth 2, above the first fitted tree: a root split on x0 at 0.098, two child splits at -1.041 and 1.106, and four leaf values.
Real run of the lesson's tuning.py on its make_data (seed 42), first tree only.
2. What does grid search do?Define
Two value lists, learning_rate 0.01, 0.1, 1.0 and max_depth 3, 5, 7, feed a box called grid search that produces an unknown set of evaluations.
Value lists from the lesson example.
3. What does random search do differently from grid search?Define
Two scatter panels over learning_rate and max_depth, each with a budget of 9 trials. The grid search panel shows nine points in a 3 by 3 pattern; the random search panel is empty with a question mark.
Grid values as in the lesson example (3 x 3).
4. What sets Bayesian optimization apart from random search?Define
A table of five finished trials with learning_rate, max_depth and score, and two dashed boxes asking where trial 6 comes from under random search and under Bayesian optimization.
Scores computed from the lesson's synthetic_objective on five random draws (numpy seed 42).
5. In Bayesian optimization, what is the surrogate model?Define
A loop of boxes: pairs of parameters and scores seen so far flow into a dashed box with a question mark, then to the acquisition function, then to the next parameters to try, then to an expensive train-and-score step that feeds back into the pairs.
Loop drawn from the lesson's description.
6. In Bayesian optimization, what does the acquisition function balance when it chooses the next point?Define
Top chart: three evaluated points, a blue prediction line and a shaded uncertainty band over a scaled hyperparameter axis. Bottom chart: the expected improvement curve computed from it, with a dashed line at its maximum.
Numpy surrogate with the lesson's RBF kernel form; length scale 0.18 chosen for the picture (the lesson's code uses 1.0). Toy objective.
7. In a hyperparameter search, what does early stopping do?Define
Five validation-loss curves over 10 epochs, one per configuration: run 1 falls fastest to the lowest value, run 2 flattens, run 3 stays high and flat, run 4 falls slowly, run 5 slowly rises. A dashed line marks epoch 10.
Illustrative curves, not measured.
8. What does Hyperband do with its budget across many hyperparameter configurations?Define
A 9 by 9 grid of 81 dots for configurations feeds a box labeled Hyperband, which leads to three rounds. Round 1 reads 81 configs with 1 epoch each; rounds 2 and 3 show question marks for the counts and epochs.
Round 1 as in the lesson; later rounds left open.
9. What is a learning rate scheduler?Define
A flow from epoch t into a dashed box with a question mark, then to the learning rate for that epoch, then to the weight update, with a three-line training loop whose lr line reads question marks.
Typed sketch of a training loop.
10. What is nested cross-validation?Define
A row of five folds with the last one marked as validation, and below it a dashed box labeled nested cross-validation with a question mark.
Setup sketch.
11. You're tuning a model with 6 hyperparameters and a budget of only 30 evaluations. Why would random search likely outperform grid search under the same budget?Explain
Six chips naming boosting hyperparameters, a card saying the budget is 30 evaluations, and a note that two values for each of the six would already make 64 combinations.
Hyperparameter names from the lesson's opening example.
12. A transformer run needs a learning rate that rises linearly at first and then decays along a cosine curve. Which scheduler is that?Explain
Learning rate against training step: a straight rise to a peak followed by a smooth cosine-shaped fall to zero.
Shape as described in the question; step counts not given.
13. With a small tuning budget, which hyperparameters can usually stay at their defaults because they matter least?Explain
A table of six hyperparameters from the lesson with what each controls and an empty question mark in a column for how much it matters.
Names and descriptions from the lesson's hyperparameter table.
14. You want an unbiased estimate of generalization performance after tuning hyperparameters, without letting that tuning leak into the final score. In nested cross-validation, what do the outer loop and inner loop each do?Explain
A large outer-loop rectangle containing an inner-loop rectangle, each with a dashed does-question-mark placeholder, and a note that the inner loop runs inside every pass of the outer loop.
Structure sketch.
15. Why can Bayesian optimization reach good settings in fewer evaluations than random search?Explain
A heat map of the synthetic objective over learning_rate on a log axis and max_depth, dark for low scores and orange for high, with ten white dots marking where random search sampled.
Objective from the lesson's Step 4; ten random draws (numpy seed 42).
16. Why sample the learning rate log-uniformly instead of linearly between 0.001 and 1.0?Explain
Two code snippets drawing a learning rate between 0.001 and 1.0, one with a plain uniform draw and one with the exponential of a uniform draw between the logs of the bounds.
Code from tuning.py's sample_param, shortened.
17. Grid search over 4 hyperparameters with 5 values each requires how many evaluations?Apply
Four columns, one per hyperparameter, each listing five values, with a dashed box asking how many evaluations a grid search needs.
Setup from the stem.
18. Hyperband starts 81 configurations with 1 epoch each and keeps the top third. How many configurations get 3 epochs in the next round?Apply
A 9 by 9 grid of 81 dots, 1 epoch each, an arrow through a box labeled keep the top third, and a dashed result box reading question mark configs, 3 epochs.
Setup from the stem.
19. A nested setup uses 3 outer folds, 4 inner folds and a grid of 27 points. How many model fits does it need?Apply
Three outer-fold boxes, each with four inner-fold tiles, below them a box for a grid of 27 points and a dashed box asking for the number of model fits.
Counts from the stem.
20. Only learning rate matters. A 3 x 3 grid and 9 random trials both use a budget of 9. How many distinct learning rates does each try?Apply
Two empty scatter panels, a 3 by 3 grid and 9 random trials, with learning_rate on the x axis marked as the one that matters and the prompt: distinct learning rates tried, question mark.
Setup from the stem.
21. A random forest with 3 important hyperparameters trains in seconds. Which tuning plan fits best?Design
A card describing a random forest with 3 important hyperparameters named n_estimators, max_depth and min_samples_leaf, training in seconds, and a tuning plan left as a question mark.
Hyperparameter names from the lesson's model-type table.
22. A neural network takes an hour per run and you can afford roughly 100 runs. Which design fits?Design
A 5 by 20 grid of 100 squares, each one run of one hour, with a design prompt left as a question mark.
Setup from the stem.
23. You must tune only the alpha of a ridge regression, and each fit is very fast. Which plan fits?Design
The ridge regression objective with a penalty alpha, a chip reading alpha equals question mark, notes that alpha is the only setting and each fit is fast, and a tuning plan left open.
Standard ridge objective.
24. You are tuning a new boosting model with unknown good ranges and a medium budget. Which workflow follows the lesson?Design
Five dashed boxes in a row labeled step 1 to step 5 with question marks, joined by arrows.
Setup sketch.

Build It

Step 1: Grid Search from Scratch

The code in code/tuning.py implements grid search, random search, and a simple Bayesian optimizer from scratch.

python
def grid_search(model_fn, param_grid, X_train, y_train, X_val, y_val):
    keys = list(param_grid.keys())
    values = list(param_grid.values())
    best_score = -float("inf")
    best_params = None
    n_evals = 0
 
    for combo in itertools.product(*values):
        params = dict(zip(keys, combo))
        model = model_fn(**params)
        model.fit(X_train, y_train)
        score = evaluate(model, X_val, y_val)
        n_evals += 1
 
        if score > best_score:
            best_score = score
            best_params = params
 
    return best_params, best_score, n_evals

Step 2: Random Search from Scratch

python
def random_search(model_fn, param_distributions, X_train, y_train,
                  X_val, y_val, n_iter=50, seed=42):
    rng = np.random.RandomState(seed)
    best_score = -float("inf")
    best_params = None
 
    for _ in range(n_iter):
        params = {k: sample(v, rng) for k, v in param_distributions.items()}
        model = model_fn(**params)
        model.fit(X_train, y_train)
        score = evaluate(model, X_val, y_val)
 
        if score > best_score:
            best_score = score
            best_params = params
 
    return best_params, best_score, n_iter

Step 3: Bayesian Optimization (Simplified)

The core idea: fit a Gaussian process to observed (hyperparameter, score) pairs, then use an acquisition function to decide where to look next.

python
class SimpleBayesianOptimizer:
    def __init__(self, search_space, n_initial=5):
        self.search_space = search_space
        self.n_initial = n_initial
        self.X_observed = []
        self.y_observed = []
 
    def _kernel(self, x1, x2, length_scale=1.0):
        dists = np.sum((x1[:, None, :] - x2[None, :, :]) ** 2, axis=2)
        return np.exp(-0.5 * dists / length_scale ** 2)
 
    def _fit_gp(self, X_new):
        X_obs = np.array(self.X_observed)
        y_obs = np.array(self.y_observed)
        y_mean = y_obs.mean()
        y_centered = y_obs - y_mean
 
        K = self._kernel(X_obs, X_obs) + 1e-4 * np.eye(len(X_obs))
        K_star = self._kernel(X_new, X_obs)
 
        L = np.linalg.cholesky(K)
        alpha = np.linalg.solve(L.T, np.linalg.solve(L, y_centered))
        mu = K_star @ alpha + y_mean
 
        v = np.linalg.solve(L, K_star.T)
        var = 1.0 - np.sum(v ** 2, axis=0)
        var = np.maximum(var, 1e-6)
 
        return mu, var
 
    def _expected_improvement(self, mu, var, best_y):
        sigma = np.sqrt(var)
        z = (mu - best_y) / (sigma + 1e-10)
        ei = sigma * (z * norm_cdf(z) + norm_pdf(z))
        return ei
 
    def suggest(self):
        if len(self.X_observed) < self.n_initial:
            return sample_random(self.search_space)
 
        candidates = [sample_random(self.search_space) for _ in range(500)]
        X_cand = np.array([to_vector(c) for c in candidates])
        mu, var = self._fit_gp(X_cand)
        ei = self._expected_improvement(mu, var, max(self.y_observed))
        return candidates[np.argmax(ei)]
 
    def observe(self, params, score):
        self.X_observed.append(to_vector(params))
        self.y_observed.append(score)

The GP surrogate gives two things at each candidate point: a predicted score (mu) and an uncertainty (var). Expected Improvement balances these: it favors points where the model predicts high scores OR where uncertainty is high. Early on, most points have high uncertainty so the optimizer explores. Later, it focuses on the most promising region.

Step 4: Compare All Methods

Run all three methods on the same synthetic objective and compare them directly. This version calls each optimizer against a plain objective function instead of training a model, so the API looks a little different from the model-based implementations above:

python
def synthetic_objective(params):
    lr = params["learning_rate"]
    depth = params["max_depth"]
    return -(np.log10(lr) + 2) ** 2 - (depth - 4) ** 2 + 10
 
param_grid = {
    "learning_rate": [0.001, 0.01, 0.1, 1.0],
    "max_depth": [2, 3, 4, 5, 6, 7, 8],
}
 
grid_best = None
grid_score = -float("inf")
grid_history = []
for combo in itertools.product(*param_grid.values()):
    params = dict(zip(param_grid.keys(), combo))
    score = synthetic_objective(params)
    grid_history.append((params, score))
    if score > grid_score:
        grid_score = score
        grid_best = params
 
param_dist = {
    "learning_rate": ("log_float", 0.001, 1.0),
    "max_depth": ("int", 2, 8),
}
 
rand_best = None
rand_score = -float("inf")
rand_history = []
rng = np.random.RandomState(42)
for _ in range(28):
    params = {k: sample(v, rng) for k, v in param_dist.items()}
    score = synthetic_objective(params)
    rand_history.append((params, score))
    if score > rand_score:
        rand_score = score
        rand_best = params
 
optimizer = SimpleBayesianOptimizer(param_dist, n_initial=5)
bayes_history = []
for _ in range(28):
    params = optimizer.suggest()
    score = synthetic_objective(params)
    optimizer.observe(params, score)
    bayes_history.append((params, score))
bayes_score = max(s for _, s in bayes_history)
 
print(f"{'Method':<20} {'Best Score':>12} {'Evaluations':>12}")
print("-" * 50)
print(f"{'Grid Search':<20} {grid_score:>12.4f} {len(grid_history):>12}")
print(f"{'Random Search':<20} {rand_score:>12.4f} {len(rand_history):>12}")
print(f"{'Bayesian Opt':<20} {bayes_score:>12.4f} {len(bayes_history):>12}")

With the same budget, Bayesian optimization usually finds the best score fastest because it does not waste evaluations in clearly bad regions. Random search covers more ground than grid search. Grid search only wins when you have very few hyperparameters and can afford to be exhaustive.

Check: Build It0/6
1. In SimpleBayesianOptimizer, why does the search mostly explore in its early iterations?Explain
A bar chart of the number of observations growing with each of 28 iterations, above a card saying the search mostly explores early on and that the reason is a question mark.
Bars grow by one observation per iteration. Setup from the stem.
2. In Step 4, param_grid has 4 learning rates and 7 depths. How many evaluations does the grid loop make?Apply
The lesson's param_grid with four learning rates and seven depths, the itertools.product loop that appends to grid_history, and a final line asking for len(grid_history).
Code from the lesson's Step 4, shortened.
3. What does synthetic_objective return for learning_rate=0.01 and max_depth=4?Apply
The lesson's synthetic_objective function and a call with learning_rate 0.01 and max_depth 4, whose return value is a question mark.
Code from the lesson's Step 4.
4. What does synthetic_objective return for learning_rate=0.1 and max_depth=6?Apply
A four-row worksheet for synthetic_objective with learning_rate 0.1 and max_depth 6, listing each step of the formula with a question mark for its value.
Worksheet; values left blank.
5. What two quantities does _fit_gp return for each candidate point in SimpleBayesianOptimizer?Apply
The first lines of _fit_gp from the lesson: the observed data, the centered scores, the kernel matrices K and K_star, the Cholesky factor L and alpha, then dots and a return of two question marks.
Code from the lesson, middle lines elided.
6. SimpleBayesianOptimizer has n_initial=5 and has observed 3 points. What does suggest() return?Apply
The constructor line with n_initial=5, a table of three observed points, and a call to suggest() whose return value is a question mark.
Observed rows computed with numpy seed 42 and the Step 4 objective.

Use It

Optuna in Practice

Optuna is the recommended library for serious hyperparameter tuning. It supports pruning, distributed search, and visualization out of the box.

python
import optuna
 
def objective(trial):
    lr = trial.suggest_float("learning_rate", 1e-4, 1e-1, log=True)
    n_est = trial.suggest_int("n_estimators", 50, 500)
    max_depth = trial.suggest_int("max_depth", 2, 10)
 
    model = GradientBoostingRegressor(
        learning_rate=lr,
        n_estimators=n_est,
        max_depth=max_depth,
    )
    model.fit(X_train, y_train)
    return mean_squared_error(y_val, model.predict(X_val))
 
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
 
print(f"Best params: {study.best_params}")
print(f"Best MSE: {study.best_value:.4f}")

Key Optuna features:

  • suggest_float(..., log=True) for parameters best searched on log scale (learning rate, regularization)
  • suggest_int for integer parameters
  • suggest_categorical for discrete choices
  • Built-in MedianPruner for early stopping of bad trials
  • study.trials_dataframe() for analysis

Optuna with Pruning

Pruning stops unpromising trials early, saving massive compute. Here is the pattern:

python
import optuna
from sklearn.model_selection import cross_val_score
 
def objective(trial):
    params = {
        "learning_rate": trial.suggest_float("lr", 1e-4, 0.5, log=True),
        "max_depth": trial.suggest_int("max_depth", 2, 10),
        "n_estimators": trial.suggest_int("n_estimators", 50, 500),
        "subsample": trial.suggest_float("subsample", 0.5, 1.0),
    }
 
    model = GradientBoostingRegressor(**params)
    scores = cross_val_score(model, X_train, y_train, cv=3,
                             scoring="neg_mean_squared_error")
    mean_score = -scores.mean()
 
    trial.report(mean_score, step=0)
    if trial.should_prune():
        raise optuna.TrialPruned()
 
    return mean_score
 
pruner = optuna.pruners.MedianPruner(n_startup_trials=10, n_warmup_steps=5)
study = optuna.create_study(direction="minimize", pruner=pruner)
study.optimize(objective, n_trials=200)

The MedianPruner stops a trial if its intermediate value is worse than the median of all completed trials at the same step. Pruning requires calling trial.report() to report intermediate metrics and trial.should_prune() to check whether the trial should be stopped. The n_startup_trials=10 ensures at least 10 trials complete fully before pruning kicks in. This typically saves 40-60% of total compute.

sklearn's Built-in Tuners

For quick experiments, sklearn provides GridSearchCV, RandomizedSearchCV, and HalvingRandomSearchCV:

python
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import loguniform, randint
 
param_dist = {
    "learning_rate": loguniform(1e-4, 0.5),
    "max_depth": randint(2, 10),
    "n_estimators": randint(50, 500),
}
 
search = RandomizedSearchCV(
    GradientBoostingRegressor(),
    param_dist,
    n_iter=100,
    cv=5,
    scoring="neg_mean_squared_error",
    random_state=42,
    n_jobs=-1,
)
search.fit(X_train, y_train)
print(f"Best params: {search.best_params_}")
print(f"Best CV MSE: {-search.best_score_:.4f}")

Use loguniform from scipy for learning rate and regularization. Use randint for integer hyperparameters. The n_jobs=-1 flag parallelizes across all CPU cores.

Common Mistakes in Hyperparameter Tuning

Data leakage through preprocessing. If you fit a scaler on the full dataset before cross-validation, information from the validation fold leaks into training. Always put preprocessing inside a Pipeline so it is fit only on the training fold.

Overfitting to the validation set. Running thousands of trials effectively trains on the validation set. Use nested cross-validation for final performance estimates, or hold out a separate test set that you never touch during tuning.

Searching too narrow a range. If your best value sits at the boundary of your search space, the true optimum is probably outside it. Check whether the best parameters land at the edges before you trust the result.

Ignoring interaction effects. Learning rate and number of estimators interact strongly in boosting. A low learning rate needs more estimators. Tuning them independently gives worse results than tuning them together.

Not using early stopping for iterative models. For gradient boosting and neural networks, set n_estimators or epochs to a high value and use early stopping. This is strictly better than tuning the number of iterations as a hyperparameter.

Check: Use It0/10
1. Why set n_estimators high and use early stopping instead of searching over n_estimators?Explain
Training loss on a log axis falling steadily as trees are added from 1 to 300, with a note that the validation loss is unknown.
Real GBMForTuning run from tuning.py (learning_rate 0.3, max_depth 5, make_data seed 42).
2. Why should a scaler live inside a Pipeline when you cross-validate hyperparameters?Explain
A row of five folds with fold 5 marked as the validation fold in this round, and a dashed box reading StandardScaler fit on, question mark.
Setup sketch.
3. In Optuna's MedianPruner, what determines whether a trial gets pruned early?Explain
A MedianPruner configuration line with n_startup_trials 10, a strip of ten blue dots for the first intermediate values of finished trials on an axis from 3 to 9, and a note that trial 11 reports its first value, unknown.
Values from a real optuna run with an objective written for this picture.
4. According to the lesson's list of common hyperparameter tuning mistakes, what problem arises from tuning learning rate and number of estimators independently in boosting?Explain
Two cards: tuning learning rate and number of estimators one at a time in two steps, and tuning the pair together as one search space; each ends in a dashed result question mark.
Setup sketch.
5. A MedianPruner has n_startup_trials=10. Trial 4 reports an intermediate value worse than the median of the completed trials. What happens?Apply
A row of twelve numbered trial tiles with trial 4 outlined in red, a note that trial 4 reports a value worse than the median of completed trials, and a dashed box asking what happens to trial 4.
Setup from the stem.
6. You tune hyperparameters using the test set and report the best test performance. What is wrong with this approach?Analyze
Four configurations are trained on the train set and each is scored on the test set; the best test score is then reported.
Flow from the stem.
7. Your best learning rate is 0.5, the top edge of a search range of 1e-4 to 0.5. What should you conclude?Analyze
A log axis of learning rate from 1e-5 to 10 with a bar for the search range 1e-4 to 0.5 and an orange dot at its right edge labeled best found 0.5.
Setup from the stem.
8. After thousands of trials on one validation split, validation error is excellent but error on fresh data is much worse. What is the most likely cause?Analyze
Trial chips 1, 2, 3 and N all scored on the same validation split, and two bars: a short bar for error on the validation split and a tall bar for error on fresh data.
Bar heights illustrative; the stem gives no values.
9. A boosting pipeline grid-searches n_estimators over 100, 200 and 300, tripling its cost. What is the better design?Analyze
A grid with three rows for n_estimators 100, 200 and 300 and columns for combinations A, B, C and more of the other settings, each cell one model fit.
Setup from the stem; the other settings are not specified.
10. Your objective trains and scores a full model, then returns the score without calling trial.report() or trial.should_prune(). What does a MedianPruner do?Analyze
An Optuna objective that suggests a learning rate, fits a model and returns the validation error, followed by a study created with a MedianPruner and optimized for 100 trials.
Typed sketch based on the lesson's Optuna code.
Author's Note

The tool people reach for first is almost always the wrong one for their problem. I've watched teams spend a week wiring up Optuna with a Gaussian process pruner to tune a random forest that trains in four seconds, when fifty lines of random search would have found the same answer before the Optuna config was even finished. Bayesian optimization earns its overhead when training itself is expensive: an hour per run, a GPU cluster, a search space nobody has mapped before. Below that, it's just extra machinery.

The mistake that actually burns money is a different one: tuning n_estimators or epochs by hand instead of turning on early stopping. I still see grid search running over number of trees in production pipelines, paying for compute the model would have told you to stop spending if you'd just let validation loss flatten out on its own. Fix that habit before you touch anything fancier.

Exercises

  1. Run grid search and random search with the same total budget (e.g., 50 evaluations). Compare the best scores found. Run the experiment 10 times with different seeds. How often does random search win?

  2. Implement Hyperband from scratch. Start with 81 configurations, each trained for 1 epoch. Keep the top 1/3 at each round and triple their budget. Compare total compute (sum of all epochs across all configs) to running 81 configs for the full budget.

  3. Add a learning rate scheduler (cosine annealing) to the gradient boosting implementation from Lesson 11. Does it help compared to a fixed learning rate?

  4. Use Optuna to tune a RandomForestClassifier on a real dataset (e.g., sklearn's breast cancer dataset). Use optuna.visualization.plot_param_importances(study) to see which hyperparameters matter most. Does it match the importance ranking from this lesson?

  5. Implement a simple acquisition function (Expected Improvement) and demonstrate exploration vs exploitation. Plot the surrogate model's mean and uncertainty, and show where EI chooses to evaluate next.

Key Terms

TermWhat people sayWhat it actually means
Hyperparameter"A setting you choose"A value set before training that controls the learning process, not learned from data
Grid search"Try every combination"Exhaustive search over a specified parameter grid. Exponential cost.
Random search"Just sample randomly"Sample hyperparameters from distributions. Covers important dimensions better than grid search.
Bayesian optimization"Smart search"Uses a surrogate model of the objective to decide where to evaluate next, balancing exploration and exploitation
Surrogate model"A cheap approximation"A model (usually Gaussian process) that approximates the expensive objective function from observed evaluations
Acquisition function"Where to look next"Scores candidate points by balancing expected improvement with uncertainty. EI and UCB are common choices.
Early stopping"Stop wasting time"Terminate training early when validation performance stops improving
Hyperband"Tournament bracket for configs"Adaptive resource allocation: start many configs with small budgets, keep the best and increase their budgets
Learning rate scheduler"Change lr during training"A function that adjusts the learning rate over the course of training for better convergence

Further Reading

Code

Ships With