Hyperband: Efficient Hyperparameter Optimization

Hyperband : Guide Complet — Optimisation d'Hyperparamètres Efficace

Hyperband: Complete Guide for Efficient Hyperparameter Optimization

Summary

Hyperband is a hyperparameter optimization algorithm that dynamically allocates computational resources to the most promising configurations. Unlike an exhaustive grid search or a naive random search, Hyperband eliminates poor configurations early through the successive halving mechanism, and systematically explores different exploration-exploitation tradeoffs via multiple brackets. The result is a theoretically optimal method for a fixed computational budget, capable of achieving performance equivalent to random search with a fraction of the time and resources. This guide covers the mathematical principle, practical intuition, complete Python implementation, and real-world use cases of Hyperband.

Mathematical Principle of Hyperband

Hyperband’s foundation rests on two complementary ideas: successive halving and the multi-bracket strategy.

Successive Halving

Successive halving is an iterative progressive resource allocation process:

  1. Initialization: start with N randomly sampled hyperparameter configurations.
  2. Partial evaluation: each configuration is trained for r epochs (or r arbitrary resource units: number of samples, iterations, etc.).
  3. Selection: rank configurations by their performance on a validation set, and keep only the top ⌈N/η⌉, where η is the reduction factor (typically η = 3).
  4. Reallocation: surviving configurations receive η · r resources for the next iteration.
  5. Repeat: repeat steps 2 to 4 until only one configuration remains.

Mathematically, each iteration i of successive halving has:

  • nᵢ = ⌈N / ηⁱ⌉ configurations
  • rᵢ = r · ηⁱ resources per configuration

The process stops when nᵢ = 1, i.e., after i_max = ⌊log_η(N)⌋ iterations.

The Problem with Successive Halving Alone

Successive halving has a major flaw: it requires knowing in advance the exact number of iterations i_max, which depends on the relationship between the maximum budget R_max and the minimum budget r per configuration. If this estimate is incorrect, the algorithm can either waste resources on poor configurations or too aggressively eliminate promising configurations that didn’t have enough time to reveal their potential.

The Hyperband Solution: Multi-Brackets

Hyperband solves this problem by running successive halving for multiple values of s, where s varies from 0 to s_max:

  • s_max = ⌊log_η(R_max / r_min)⌋, where r_min is the minimum number of resources per configuration.
  • For each value of s, an independent bracket is defined with its own parameters.

In bracket s:

  • The initial number of configurations is n = ⌈(s_max + 1) / (s + 1) · ηˢ⌉
  • The initial budget per configuration is r = R_max · η⁻ˢ
  • The number of successive halving iterations is exactly s

The total budget allocated to each bracket is identical: R_max · (s_max + 1).

Exploration vs. Exploitation

Different values of s correspond to distinct tradeoffs:

  • s = s_max (maximum exploration): a very large number of configurations are evaluated with very few resources each. This is ideal for discovering promising regions of the search space, but at the cost of superficial evaluations.
  • s = 0 (maximum exploitation): very few configurations are evaluated, but each receives the maximum budget R_max. This is the most cautious scenario, close to a full training run but with minimal early elimination.
  • Intermediate s: tradeoff between exploration and exploitation, with a moderate number of configurations and a moderate budget per configuration.

Theoretical Optimality

Hyperband is theoretically optimal for a fixed computational budget. The proof relies on the fact that executing all brackets guarantees that at least one bracket has a number of iterations s adapted to the real “difficulty” of the optimization problem. The overhead compared to the best individual bracket is at most a logarithmic factor O(log(R_max / r_min)), which is negligible in practice.

In summary, Hyperband answers the fundamental question: “How to allocate a limited computational budget between evaluating many configurations superficially and evaluating a few configurations in depth?”. The answer is: don’t choose — do both.

Intuition: Hyperband is a Casting Call

Imagine you are organizing a casting call for a movie and you have 10 hours of audition time. You have 100 potential candidates.

The naive approach (grid search or pure random search) would be to give exactly 6 minutes to each candidate. You’d see everyone, but superficially. Some great actors would need more time to reveal their talent, and you’d miss them.

Successive halving is like proceeding by progressive elimination: start with 100 candidates, watch each of them for 30 seconds (very quick, eliminate the obvious ones), keep the 20 best. Then watch those 20 for 2 minutes, keep the 5 best. Finally, listen to those 5 for 10 minutes each for the final choice. In total, very little time wasted on mediocre candidates, and lots of time on the best ones.

Hyperband is even smarter: instead of doing this casting once with a single pace, do it multiple times with different durations:

  • First pass: watch 80 candidates for 30 seconds → 20 pass → 5 pass → listen to the 5 for 10 minutes.
  • Second pass: watch 30 candidates for 2 minutes → 10 pass → 3 pass → listen to the 3 for 6 minutes.
  • Third pass: watch 10 candidates for 6 minutes → 3 pass → listen to the 3 for 10 minutes.

You don’t spend 10 minutes with each candidate — that would waste time. And instead of doing the casting only once, Hyperband does it multiple times with different durations to make sure not to miss an atypical profile. This is exactly what the algorithm does with hyperparameters: it doesn’t bet everything on a single allocation scenario.

Python Implementation

Basic Hyperband with Successive Halving

import numpy as np
import math
import random

class Hyperband:
    """
    Complete implementation of the Hyperband algorithm for
    hyperparameter optimization.
    """

    def __init__(self, objective_func, max_resource, eta=3, min_resource=1):
        """
        Initializes Hyperband.

        Args:
            objective_func: Function to optimize. Must accept
                           (config, resource) and return a score.
            max_resource (int): Maximum budget per configuration (R_max).
            eta (int): Reduction factor (η). Default 3.
            min_resource (int): Minimum budget per configuration (r_min).
        """
        self.objective_func = objective_func
        self.max_resource = max_resource
        self.eta = eta
        self.min_resource = min_resource
        self.s_max = int(math.floor(math.log(max_resource / min_resource, eta)))
        self.B = (self.s_max + 1) * max_resource  # Total budget per bracket
        self.best_config = None
        self.best_score = float('-inf')
        self.history = []  # History of all evaluations

    def _get_n(self, s):
        """Initial number of configurations for bracket s."""
        return int(math.ceil((self.s_max + 1) / (s + 1) * (self.eta ** s)))

    def _get_r(self, s):
        """Initial budget per configuration for bracket s."""
        return self.max_resource * (self.eta ** (-s))

    def _successive_halving(self, n, r, s):
        """
        Runs successive halving with n initial configurations,
        initial budget r, and s iterations.
        """
        configurations = []
        for _ in range(n):
            config = self._sample_configuration()
            configurations.append(config)

        for i in range(s + 1):
            # Number of configurations at this iteration
            n_i = int(math.ceil(n * (self.eta ** (-i))))
            # Budget per configuration at this iteration
            r_i = int(math.ceil(r * (self.eta ** i)))
            r_i = min(r_i, self.max_resource)

            # Evaluate all remaining configurations
            scores = []
            for config in configurations[:n_i]:
                score = self.objective_func(config, r_i)
                scores.append((config, score, r_i))
                self.history.append({
                    'config': config,
                    'resource': r_i,
                    'score': score,
                    'bracket_s': s,
                    'iteration_i': i
                })

            # Sort by descending score
            scores.sort(key=lambda x: x[1], reverse=True)

            # Update global best score
            for config, score, res in scores:
                if score > self.best_score:
                    self.best_score = score
                    self.best_config = config.copy()

            # Keep top 1/eta for next iteration
            n_next = int(math.ceil(n_i / self.eta))
            configurations = [s[0] for s in scores[:n_next]]

            # Last iteration: found the best of this bracket
            if n_next <= 1:
                break

        return self.best_config, self.best_score

    def _sample_configuration(self):
        """Samples a random hyperparameter configuration."""
        return {
            'learning_rate': 10 ** random.uniform(-5, -1),
            'batch_size': 2 ** random.randint(3, 8),
            'hidden_units': 2 ** random.randint(4, 10),
            'dropout': random.uniform(0.0, 0.7),
            'l2_reg': 10 ** random.uniform(-6, -1)
        }

    def run(self):
        """
        Runs Hyperband: launches one successive halving for
        each bracket s = s_max, s_max-1, ..., 0.
        """
        print(f"Hyperband: s_max = {self.s_max}, B = {self.B}, η = {self.eta}")
        print(f"Max budget per config: R_max = {self.max_resource}")
        print("=" * 60)

        for s in range(self.s_max, -1, -1):
            n = self._get_n(s)
            r = self._get_r(s)
            print(f"\n🔹 Bracket s={s}: n={n} configs, r={r} initial resources")

            best_config, best_score = self._successive_halving(n, r, s)

            print(f"   Best bracket score: {best_score:.4f}")
            print(f"   Config: LR={best_config['learning_rate']:.2e}, "
                  f"BS={best_config['batch_size']}, HU={best_config['hidden_units']}")

        print("\n" + "=" * 60)
        print(f"✅ Best overall configuration:")
        print(f"   Score: {self.best_score:.4f}")
        for k, v in self.best_config.items():
            if isinstance(v, float):
                print(f"   {k}: {v:.4e}")
            else:
                print(f"   {k}: {v}")

        return self.best_config, self.best_score

Synthetic Objective Function for Testing

def synthetic_objective(config, resource):
    """
    Synthetic objective function simulating model performance
    based on its hyperparameters and budget.

    This function mimics real model behavior:
    - Optimal learning rate is around 0.001
    - More resources = better performance (diminishing returns)
    - Dropout and regularization have optimal zones
    """
    lr = config['learning_rate']
    bs = config['batch_size']
    hu = config['hidden_units']
    dropout = config['dropout']
    l2 = config['l2_reg']

    # Base score based on intrinsic config quality
    lr_score = math.exp(-0.5 * ((math.log10(lr) + 3) ** 2))  # Optimal around 1e-3
    bs_score = math.exp(-0.5 * ((math.log2(bs) - 6) ** 2) / 8)  # Optimal around 64
    hu_score = min(1.0, hu / 512)  # Bigger is better (saturating)
    dropout_score = math.exp(-0.5 * ((dropout - 0.3) ** 2) / 0.05)
    l2_score = math.exp(-0.5 * ((math.log10(l2) + 4) ** 2) / 2)

    intrinsic_quality = (0.35 * lr_score + 0.15 * bs_score +
                         0.20 * hu_score + 0.15 * dropout_score +
                         0.15 * l2_score)

    # Resource effect: diminishing returns
    resource_factor = 1 - math.exp(-0.01 * resource)

    # Random noise (simulates stochastic variance)
    noise = random.gauss(0, 0.01)

    return intrinsic_quality * resource_factor + noise


# ── Running Hyperband ──────────────────────────────────
if __name__ == "__main__":
    random.seed(42)
    np.random.seed(42)

    hb = Hyperband(
        objective_func=synthetic_objective,
        max_resource=100,
        eta=3,
        min_resource=1
    )

    best_config, best_score = hb.run()
    print(f"\n📊 Total evaluations: {len(hb.history)}")
    print(f"📈 Final score: {best_score:.4f}")

Comparison with Random Search

def random_search_comparison(objective_func, max_resource, n_evaluations):
    """
    Naive random search: n_evaluations configurations,
    each trained with the maximum budget.
    """
    best_score = float('-inf')
    best_config = None

    for _ in range(n_evaluations):
        config = {
            'learning_rate': 10 ** random.uniform(-5, -1),
            'batch_size': 2 ** random.randint(3, 8),
            'hidden_units': 2 ** random.randint(4, 10),
            'dropout': random.uniform(0.0, 0.7),
            'l2_reg': 10 ** random.uniform(-6, -1)
        }
        score = objective_func(config, max_resource)
        if score > best_score:
            best_score = score
            best_config = config

    return best_config, best_score


if __name__ == "__main__":
    # Equivalent total budget for fair comparison
    total_budget_hb = 100 * (3 + 1)  # B for s_max=3, η=3

    print("🔵 === HYPERBAND ===")
    random.seed(42)
    hb = Hyperband(synthetic_objective, max_resource=100, eta=3)
    hb.run()
    n_hb = len(hb.history)
    print(f"Total evaluations: {n_hb}")

    print(f"\n🔴 === RANDOM SEARCH ===")
    random.seed(42)
    # Same total number of evaluations as Hyperband for fair comparison
    rs_config, rs_score = random_search_comparison(
        synthetic_objective, max_resource=100, n_evaluations=n_hb
    )
    print(f"Best random search score: {rs_score:.4f}")

    print(f"\n📊 Summary:")
    print(f"   Hyperband: {hb.best_score:.4f}")
    print(f"   Random Search: {rs_score:.4f}")
    print(f"   Difference: {hb.best_score - rs_score:+.4f}")

Integration with Optuna (Hyperband Pruner)

import optuna
from optuna.pruners import HyperbandPruner


def optuna_objective(trial):
    """Objective function for Optuna with Hyperband pruning."""
    lr = trial.suggest_float("learning_rate", 1e-5, 1e-1, log=True)
    batch_size = trial.suggest_int("batch_size", 8, 256, log=True)
    hidden_units = trial.suggest_int("hidden_units", 16, 1024, log=True)
    dropout = trial.suggest_float("dropout", 0.0, 0.7)
    l2_reg = trial.suggest_float("l2_reg", 1e-6, 1e-1, log=True)

    # Maximum budget in "epochs" (pruner will decide when to stop)
    max_epochs = 100
    best_intermediate_score = 0.0

    for epoch in range(1, max_epochs + 1):
        # Simulate training epoch by epoch
        resource = epoch
        score = synthetic_objective({
            'learning_rate': lr,
            'batch_size': batch_size,
            'hidden_units': hidden_units,
            'dropout': dropout,
            'l2_reg': l2_reg
        }, resource)

        # Report intermediate performance to the Hyperband pruner
        trial.report(score, epoch)

        # The pruner decides whether to continue or stop early
        if trial.should_prune():
            # Early stopping: this configuration is eliminated
            raise optuna.TrialPruned()

        if score > best_intermediate_score:
            best_intermediate_score = score

    return best_intermediate_score


if __name__ == "__main__":
    # Configure the Hyperband pruner in Optuna
    pruner = HyperbandPruner(
        max_resource=100,       # R_max: maximum number of epochs
        min_resource=1,         # r_min: minimum number of epochs
        reduction_factor=3      # η: reduction factor
    )

    study = optuna.create_study(
        direction="maximize",
        pruner=pruner,
        sampler=optuna.samplers.TPESampler()  # TPE for sampling
    )

    study.optimize(optuna_objective, n_trials=200, show_progress_bar=True)

    print(f"\n🏆 Best found by Optuna + Hyperband:")
    print(f"   Score: {study.best_value:.4f}")
    print(f"   Params: {study.best_params}")
    print(f"   Number of complete trials: {len(study.trials)}")
    print(f"   Pruned trials: "
          f"{sum(1 for t in study.trials if t.state.name == 'PRUNED')}")

Hyperparameters of Hyperband

The configuration of Hyperband directly influences the tradeoff between speed and result quality:

max_resource (R_max)

The maximum budget attributable to a single configuration. For a deep learning model, this is typically the maximum number of epochs. For an algorithm like Random Forest, it could be the maximum number of trees. Practical rule: set R_max to the value you would be willing to allocate to a configuration in an individual training run. The higher R_max, the more brackets (s_max) increase, which raises the total cost but improves quality.

reduction_factor (η)

The factor by which the number of configurations is divided and resources are multiplied at each successive halving iteration. Typical value: 3. With η = 3, one-third of configurations are kept and resources are tripled. η = 2 is more conservative (half are kept), η = 4 is more aggressive (only one-quarter are kept). A smaller η explores more finely but costs more; a larger η eliminates faster at the risk of losing good configurations.

eta

Identical to reduction_factor. In some implementations, this parameter is named eta rather than reduction_factor. Both refer to the same concept: the reduction factor η of successive halving.

min_resource (r_min)

The minimum budget for evaluating a configuration during the first iteration of a bracket. For a neural network, this corresponds to the minimum number of epochs before being able to judge the quality of a configuration. Practical rule: r_min should be large enough for performance to be informative (at least 1-5 epochs for a neural network), but small enough to allow evaluation of many configurations in the exploration brackets.

Advantages and Limitations of Hyperband

Advantages

  • Exceptional computational efficiency: Hyperband achieves results comparable to random search while typically using 5 to 20 times fewer computational resources. Aggressive early stopping of poor configurations is the key to this efficiency.
  • Strong theoretical guarantees: the algorithm is provably optimal (to within a logarithmic factor) for any fixed computational budget. It’s not an ad hoc heuristic; it’s an algorithm with regret bounds.
  • Few hyperparameters to configure: only η (reduction), R_max (maximum budget), and r_min (minimum budget). Unlike Bayesian optimization, there is no kernel to choose and no probabilistic model to tune.
  • Model-agnostic: Hyperband works with any type of model — neural networks, random forests, SVMs, XGBoost — as long as performance can be evaluated at different stages of training.
  • Natural parallelization: all configurations in the same iteration can be evaluated in parallel without any communication between them. This is ideal for clusters and multiple GPUs.
  • Easy integration: natively available in Optuna, Keras Tuner, and Ray Tune. Integration takes just a few lines of code.

Limitations

  • No cross-bracket learning: each bracket is independent. Information gained in one bracket is not transferred to others. Bayesian optimization excels precisely on this point by building a global surrogate model.
  • Purely random configuration sampling: unlike Bayesian methods, Hyperband does not guide the sampling of configurations toward promising regions. It relies entirely on successive halving filtering, not on learning the objective surface.
  • Dependence on early correlation: successive halving assumes that performance after few resources correlates with final performance. If a model needs a long time to show its potential (e.g., a slowly converging model), it risks being eliminated prematurely.
  • Higher total budget than a single bracket: by running all brackets, Hyperband spends approximately s_max + 1 times the budget of a single bracket. This overhead is the price of theoretical robustness.
  • Less efficient than BOHB in some cases: BOHB (Bayesian Optimization + Hyperband) combines the strengths of Hyperband and Bayesian optimization, replacing random sampling with a probabilistic model. BOHB is generally superior when the budget is sufficient.

4 Concrete Use Cases of Hyperband

Case 1 — Tuning a Deep Neural Network

The classic use case for Hyperband. You are training a neural network with millions of parameters and need to optimize the learning rate, batch size, number of layers, hidden layer size, dropout rate, and regularization. A full training run takes hours — impossible to test systematically. Hyperband allows evaluating hundreds of configurations by quickly eliminating unpromising combinations, spending time only on architectures that truly seem to work.

Case 2 — Model Architecture Search (Simplified NAS)

When looking for which architecture to use — number of layers, activation type, presence of batch normalization — each evaluation involves a full training run. Hyperband allows testing many architectures with a reduced budget per initial architecture, then concentrating resources on the most promising architectures. This is the approach underlying many efficient Neural Architecture Search methods.

Case 3 — Tabular Model Optimization with XGBoost/LightGBM

For tabular data, XGBoost and LightGBM have many hyperparameters: learning rate, tree depth, number of estimators, regularization parameters, subsampling. LightGBM natively integrates Hyperband support for its training. One can thus explore hundreds of hyperparameter combinations in parallel, with automatic early stopping of suboptimal configurations after a few boosting iterations.

Case 4 — Production Tuning with Constrained Budget

In a production context where compute time is billed (cloud GPU, managed services), every minute of training has a direct cost. Hyperband allows setting an absolute maximum budget and obtaining the best possible hyperparameters within that budget. This is particularly relevant for companies that need to optimize their models regularly (weekly retraining, A/B testing of versions) without exploding their cloud bill.

See Also

About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →