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

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

Hyperband : Guide Complet pour l’Optimisation Efficace des Hyperparamètres

Résumé

Hyperband est un algorithme d’optimisation d’hyperparamètres qui alloue dynamiquement les ressources computationnelles aux configurations les plus prometteuses. Contrairement à une recherche par grille exhaustive ou à une recherche aléatoire naïve, Hyperband élimine précocément les mauvaises configurations grâce au mécanisme de successive halving, et explore systématiquement différents compromis entre exploration et exploitation via plusieurs brackets. Le résultat est une méthode théoriquement optimale pour un budget computationnel fixé, capable d’atteindre des performances équivalentes à la recherche aléatoire avec une fraction du temps et des ressources. Ce guide couvre le principe mathématique, l’intuition pratique, l’implémentation Python complète et les cas d’usage réels d’Hyperband.

Principe Mathématique d’Hyperband

Le fondement d’Hyperband repose sur deux idées complémentaires : le successive halving et la stratégie multi-brackets.

Le Successive Halving

Le successive halving est un processus itératif d’allocation progressive de ressources :

  1. Initialisation : on démarre avec N configurations d’hyperparamètres tirées aléatoirement.
  2. Évaluation partielle : chaque configuration est entraînée pendant r epochs (ou r unités de ressource quelconque : nombre d’échantillons, itérations, etc.).
  3. Sélection : on classe les configurations selon leur performance sur un jeu de validation, et on ne conserve que le top ⌈N/η⌉, où η est le facteur de réduction (typiquement η = 3).
  4. Réallocation : les configurations survivantes reçoivent η · r ressources pour l’itération suivante.
  5. Répétition : on répète les étapes 2 à 4 jusqu’à ce qu’il ne reste plus qu’une seule configuration.

Mathématiquement, chaque itération i du successive halving possède :

  • nᵢ = ⌈N / ηⁱ⌉ configurations
  • rᵢ = r · ηⁱ ressources par configuration

Le processus s’arrête lorsque nᵢ = 1, c’est-à-dire après i_max = ⌊log_η(N)⌋ itérations.

Le problème du Successive Halving seul

Le successive halving présente un défaut majeur : il nécessite de connaître à l’avance le nombre exact d’itérations i_max, qui dépend de la relation entre le budget maximal R_max et le budget minimal r par configuration. Si cette estimation est incorrecte, l’algorithme peut soit gaspiller des ressources sur de mauvaises configurations, soit éliminer trop agressivement des configurations prometteuses qui n’avaient pas assez de temps pour révéler leur potentiel.

La solution Hyperband : les Multi-Brackets

Hyperband résout ce problème en exécutant le successive halving pour plusieurs valeurs de s, où s varie de 0 à s_max :

  • s_max = ⌊log_η(R_max / r_min)⌋, où r_min est le nombre minimal de ressources par configuration.
  • Pour chaque valeur de s, on définit un bracket indépendant avec ses propres paramètres.

Dans le bracket d’indice s :

  • Le nombre initial de configurations est n = ⌈(s_max + 1) / (s + 1) · ηˢ⌉
  • Le budget initial par configuration est r = R_max · η⁻ˢ
  • Le nombre d’itérations du successive halving est exactement s

Le budget total alloué à chaque bracket est identique : R_max · (s_max + 1).

Exploration versus Exploitation

Les différentes valeurs de s correspondent à des compromis distincts :

  • s = s_max (exploration maximale) : on évalue un très grand nombre de configurations avec très peu de ressources chacune. C’est idéal pour découvrir des régions prometteuses de l’espace de recherche, mais au prix d’évaluations superficielles.
  • s = 0 (exploitation maximale) : on évalue très peu de configurations, mais chacune reçoit le budget maximal R_max. C’est le scénario le plus prudent, proche d’un entraînement complet mais avec élimination précoce minimale.
  • s intermédiaire : compromis entre exploration et exploitation, avec un nombre modéré de configurations et un budget modéré par configuration.

Optimalité Théorique

Hyperband est théoriquement optimal pour un budget computationnel fixé. La preuve repose sur le fait que l’exécution de tous les brackets garantit qu’au moins un bracket possède un nombre d’itérations s adapté à la « difficulté » réelle du problème d’optimisation. Le surcoût par rapport au meilleur bracket individuel est au plus un facteur logarithmique O(log(R_max / r_min)), ce qui est négligeable en pratique.

En résumé, Hyperband répond à la question fondamentale : « Comment allouer un budget computationnel limité entre l’évaluation de nombreuses configurations superficielles et l’évaluation approfondie de quelques configurations ? ». La réponse est : ne choisissez pas — faites les deux.

Intuition : Hyperband, c’est un Casting

Imaginez que vous organisez un casting pour un film et que vous disposez de 10 heures de temps d’audition. Vous avez 100 candidats potentiels.

L’approche naïve (recherche par grille ou aléatoire pure) serait de donner exactement 6 minutes à chaque candidat. Vous verriez tout le monde, mais superficiellement. Certains grands acteurs auraient besoin de plus de temps pour révéler leur talent, et vous les rateriez.

Le successive halving, c’est comme procéder par élimination progressive : on commence avec 100 candidats, on les regarde 30 secondes chacun (très rapide, on élimine les évidents), on garde les 20 meilleurs. Ensuite, on regarde ces 20 pendant 2 minutes, on garde les 5 meilleurs. Enfin, on écoute ces 5 pendant 10 minutes chacun pour le choix final. Au total, très peu de temps gaspillé sur les candidats médiocres, et beaucoup de temps sur les meilleurs.

Hyperband, c’est encore plus malin : au lieu de faire ce casting une seule fois avec un seul rythme, on le fait plusieurs fois avec des durées différentes :

  • Premier passage : on regarde 80 candidats pendant 30 secondes → 20 passent → 5 passent → on écoute les 5 pendant 10 minutes.
  • Deuxième passage : on regarde 30 candidats pendant 2 minutes → 10 passent → 3 passent → on écoute les 3 pendant 6 minutes.
  • Troisième passage : on regarde 10 candidats pendant 6 minutes → 3 passent → on écoute les 3 pendant 10 minutes.

On ne passe pas 10 minutes avec chaque candidat — ça gaspillerait du temps. Et au lieu de faire le casting une seule fois, Hyperband le fait plusieurs fois avec des durées différentes pour être sûr de ne pas passer à côté d’un profil atypique. C’est exactement ce que fait l’algorithme avec les hyperparamètres : il ne mise pas tout sur un seul scénario d’allocation.

Implémentation Python

Hyperband de Base avec Successive Halving

import numpy as np
import math
import random

class Hyperband:
    """
    Implémentation complète de l'algorithme Hyperband pour
    l'optimisation d'hyperparamètres.
    """

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

        Args:
            objective_func: Fonction à optimiser. Doit accepter
                           (config, resource) et retourner un score.
            max_resource (int): Budget maximal par configuration (R_max).
            eta (int): Facteur de réduction (η). Par défaut 3.
            min_resource (int): Budget minimal par 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  # Budget total par bracket
        self.best_config = None
        self.best_score = float('-inf')
        self.history = []  # Historique de toutes les évaluations

    def _get_n(self, s):
        """Nombre initial de configurations pour le bracket s."""
        return int(math.ceil((self.s_max + 1) / (s + 1) * (self.eta ** s)))

    def _get_r(self, s):
        """Budget initial par configuration pour le bracket s."""
        return self.max_resource * (self.eta ** (-s))

    def _successive_halving(self, n, r, s):
        """
        Exécute un successive halving avec n configurations initiales,
        un budget initial r, et s itérations.
        """
        configurations = []
        for _ in range(n):
            config = self._sample_configuration()
            configurations.append(config)

        for i in range(s + 1):
            # Nombre de configurations à cette itération
            n_i = int(math.ceil(n * (self.eta ** (-i))))
            # Budget par configuration à cette itération
            r_i = int(math.ceil(r * (self.eta ** i)))
            r_i = min(r_i, self.max_resource)

            # Évaluer toutes les configurations restantes
            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
                })

            # Trier par score décroissant
            scores.sort(key=lambda x: x[1], reverse=True)

            # Mettre à jour le meilleur score global
            for config, score, res in scores:
                if score > self.best_score:
                    self.best_score = score
                    self.best_config = config.copy()

            # Garder le top 1/eta pour l'itération suivante
            n_next = int(math.ceil(n_i / self.eta))
            configurations = [s[0] for s in scores[:n_next]]

            # Dernière itération : on a trouvé le meilleur de ce bracket
            if n_next <= 1:
                break

        return self.best_config, self.best_score

    def _sample_configuration(self):
        """Échantillonne une configuration aléatoire d'hyperparamètres."""
        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):
        """
        Exécute Hyperband : lance un successive halving pour
        chaque bracket s = s_max, s_max-1, ..., 0.
        """
        print(f"Hyperband: s_max = {self.s_max}, B = {self.B}, η = {self.eta}")
        print(f"Budget max par 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} ressources initiales")

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

            print(f"   Meilleur score du bracket: {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"✅ Meilleure configuration globale:")
        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

Fonction Objective Synthétique pour le Testing

def synthetic_objective(config, resource):
    """
    Fonction objective synthétique simulant la performance
    d'un modèle en fonction de ses hyperparamètres et du budget.

    Cette fonction imite le comportement réel d'un modèle :
    - Le learning rate optimal est autour de 0.001
    - Plus de ressources = meilleure performance (rendements décroissants)
    - Le dropout et la régularisation ont des zones optimales
    """
    lr = config['learning_rate']
    bs = config['batch_size']
    hu = config['hidden_units']
    dropout = config['dropout']
    l2 = config['l2_reg']

    # Score de base basé sur la qualité intrinsèque de la config
    lr_score = math.exp(-0.5 * ((math.log10(lr) + 3) ** 2))  # Optimal autour de 1e-3
    bs_score = math.exp(-0.5 * ((math.log2(bs) - 6) ** 2) / 8)  # Optimal autour de 64
    hu_score = min(1.0, hu / 512)  # Plus c'est grand, mieux c'est (saturant)
    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)

    # Effet de la ressource : rendements décroissants
    resource_factor = 1 - math.exp(-0.01 * resource)

    # Bruit aléatoire (simule la variance stochastique)
    noise = random.gauss(0, 0.01)

    return intrinsic_quality * resource_factor + noise


# ── Exécution d'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 d'évaluations: {len(hb.history)}")
    print(f"📈 Score final: {best_score:.4f}")

Comparaison avec la Recherche Aléatoire

def random_search_comparison(objective_func, max_resource, n_evaluations):
    """
    Recherche aléatoire naïve : n_evaluations configurations,
    chacune entraînée avec le budget maximal.
    """
    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__":
    # Budget total équivalent pour comparaison loyale
    total_budget_hb = 100 * (3 + 1)  # B pour 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"Évaluations totales: {n_hb}")

    print(f"\n🔴 === RANDOM SEARCH ===")
    random.seed(42)
    # Même nombre total d'évaluations qu'Hyperband pour comparaison équitable
    rs_config, rs_score = random_search_comparison(
        synthetic_objective, max_resource=100, n_evaluations=n_hb
    )
    print(f"Meilleur score random search: {rs_score:.4f}")

    print(f"\n📊 Résumé:")
    print(f"   Hyperband: {hb.best_score:.4f}")
    print(f"   Random Search: {rs_score:.4f}")
    print(f"   Écart: {hb.best_score - rs_score:+.4f}")

Intégration avec Optuna (Hyperband Pruner)

import optuna
from optuna.pruners import HyperbandPruner


def optuna_objective(trial):
    """Fonction objective pour Optuna avec élagage Hyperband."""
    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)

    # Budget maximal en « époques » (pruner décidera quand arrêter)
    max_epochs = 100
    best_intermediate_score = 0.0

    for epoch in range(1, max_epochs + 1):
        # Simuler l'entraînement epoch par epoch
        resource = epoch
        score = synthetic_objective({
            'learning_rate': lr,
            'batch_size': batch_size,
            'hidden_units': hidden_units,
            'dropout': dropout,
            'l2_reg': l2_reg
        }, resource)

        # Signaler la performance intermédiaire au pruner Hyperband
        trial.report(score, epoch)

        # Le pruner décide si on continue ou on arrête tôt
        if trial.should_prune():
            # Early stopping : cette configuration est éliminée
            raise optuna.TrialPruned()

        if score > best_intermediate_score:
            best_intermediate_score = score

    return best_intermediate_score


if __name__ == "__main__":
    # Configuration du pruner Hyperband dans Optuna
    pruner = HyperbandPruner(
        max_resource=100,       # R_max : nombre maximal d'époques
        min_resource=1,         # r_min : nombre minimal d'époques
        reduction_factor=3      # η : facteur de réduction
    )

    study = optuna.create_study(
        direction="maximize",
        pruner=pruner,
        sampler=optuna.samplers.TPESampler()  # TPE pour l'échantillonnage
    )

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

    print(f"\n🏆 Meilleure trouvée par Optuna + Hyperband:")
    print(f"   Score: {study.best_value:.4f}")
    print(f"   Params: {study.best_params}")
    print(f"   Nombre de trials complets: {len(study.trials)}")
    print(f"   Trials élagués (pruned): "
          f"{sum(1 for t in study.trials if t.state.name == 'PRUNED')}")

Hyperparamètres d’Hyperband

La configuration d’Hyperband influence directement le compromis entre vitesse et qualité des résultats :

max_resource (R_max)

Le budget maximal attribuable à une seule configuration. Pour un modèle de deep learning, c’est typiquement le nombre maximal d’époques. Pour un algorithme comme Random Forest, ce pourrait être le nombre maximal d’arbres. Règle pratique : fixez R_max à la valeur que vous seriez prêt à allouer à une configuration dans un entraînement individuel. Plus R_max est élevé, plus le nombre de brackets (s_max) augmente, ce qui accroît le coût total mais améliore la qualité.

reduction_factor (η)

Le facteur par lequel on divise le nombre de configurations et on multiplie les ressources à chaque itération du successive halving. Valeur typique : 3. Avec η = 3, on garde le tiers des configurations et on triple les ressources. η = 2 est plus conservateur (on garde la moitié), η = 4 est plus agressif (on ne garde que le quart). Un η plus petit explore plus finement mais coûte plus cher ; un η plus grand élimine plus vite au risque de perdre des bonnes configurations.

eta

Identique à reduction_factor. Dans certaines implémentations, ce paramètre est nommé eta plutôt que reduction_factor. Les deux désignent le même concept : le facteur de réduction η du successive halving.

min_resource (r_min)

Le budget minimal pour évaluer une configuration lors de la première itération d’un bracket. Pour un réseau de neurones, cela correspond au nombre minimal d’époques avant de pouvoir juger de la qualité d’une configuration. Règle pratique : r_min doit être suffisamment grand pour que la performance soit informative (au moins 1-5 époques pour un réseau de neurones), mais suffisamment petit pour permettre l’évaluation de nombreuses configurations dans les brackets d’exploration.

Avantages et Limites d’Hyperband

Avantages

  • Efficacité computationnelle exceptionnelle : Hyperband atteint des résultats comparables à la recherche aléatoire en utilisant typiquement 5 à 20 fois moins de ressources computationnelles. Le early stopping agressif des mauvaises configurations est la clé de cette efficacité.
  • Garanties théoriques solides : l’algorithme est prouvement optimal (à un facteur logarithmique près) pour tout budget computationnel fixé. Ce n’est pas une heuristique ad hoc, c’est un algorithme avec des bornes de régret.
  • Peu d’hyperparamètres à configurer : seulement η (réduction), R_max (budget maximal) et r_min (budget minimal). Contrairement à l’optimisation bayésienne, il n’y a pas de noyau à choisir ni de modèle probabiliste à ajuster.
  • Agnostique au modèle : Hyperband fonctionne avec n’importe quel type de modèle — réseaux de neurones, forêts aléatoires, SVM, XGBoost — tant qu’on peut évaluer la performance à différentes étapes de l’entraînement.
  • Parallélisation naturelle : toutes les configurations d’une même itération peuvent être évaluées en parallèle sans aucune communication entre elles. C’est idéal pour les clusters et les GPU multiples.
  • Intégration facile : disponible nativement dans Optuna, Keras Tuner, et Ray Tune. L’intégration se fait en quelques lignes de code.

Limites

  • Pas d’apprentissage inter-brackets : chaque bracket est indépendant. Les informations acquises dans un bracket ne sont pas transférées aux autres. L’optimisation bayésienne excelle précisément sur ce point en construisant un modèle de substitution global.
  • Tirage purement aléatoire des configurations : contrairement aux méthodes bayésiennes, Hyperband ne guide pas l’échantillonnage des configurations vers les régions prometteuses. Il repose entièrement sur le filtrage par successive halving, pas sur l’apprentissage de la surface d’objectif.
  • Dépendance à la corrélation précoce : le successive halving suppose que la performance après peu de ressources est corrélée avec la performance finale. Si un modèle a besoin de beaucoup de temps pour montrer son potentiel (par exemple, un modèle qui converge lentement), il risque d’être éliminé prématurément.
  • Budget total plus élevé qu’un seul bracket : en exécutant tous les brackets, Hyperband dépense environ s_max + 1 fois le budget d’un seul bracket. Ce surcoût est le prix de la robustesse théorique.
  • Moins efficace que BOHB dans certains cas : BOHB (Bayesian Optimization + Hyperband) combine les forces d’Hyperband et de l’optimisation bayésienne, remplaçant le tirage aléatoire par un modèle probabiliste. BOHB est généralement supérieur lorsque le budget est suffisant.

4 Cas d’Usage Concrets d’Hyperband

Cas 1 — Réglage d’un Réseau de Neurones Profond

Le cas d’usage classique d’Hyperband. Vous entraînez un réseau de neurones avec des millions de paramètres et devez optimiser le learning rate, la taille du batch, le nombre de couches, la taille des couches cachées, le taux de dropout et la régularisation. Un entraînement complet prend des heures — impossible de tester systématiquement. Hyperband permet d’évaluer des centaines de configurations en éliminant rapidement les combinaisons non prometteuses, ne dépensant du temps que pour les architectures qui semblent réellement fonctionner.

Cas 2 — Recherche d’Architecture de Modèle (NAS simplifié)

Quand on cherche quelle architecture utiliser — nombre de couches, type d’activation, présence de normalisation par lots — chaque évaluation implique un entraînement complet. Hyperband permet de tester de nombreuses architectures avec un budget réduit par architecture initiale, puis de concentrer les ressources sur les architectures les plus prometteuses. C’est l’approche sous-jacente à de nombreuses méthodes de Neural Architecture Search efficaces.

Cas 3 — Optimisation de Modèle Tabulaire avec XGBoost/LightGBM

Pour les données tabulaires, XGBoost et LightGBM possèdent de nombreux hyperparamètres : le taux d’apprentissage, la profondeur des arbres, le nombre d’estimateurs, les paramètres de régularisation, le subsampling. LightGBM intègre nativement le support d’Hyperband pour son entraînement. On peut ainsi explorer des centaines de combinaisons d’hyperparamètres en parallèle, avec arrêt automatique des configurations sous-optimales après quelques itérations de boosting.

Cas 4 — Réglage en Production avec Budget Contraint

Dans un contexte de production où le temps de calcul est facturé (cloud GPU, services managés), chaque minute d’entraînement a un coût direct. Hyperband permet de fixer un budget maximum absolu et d’obtenir les meilleures hyperparamètres possibles dans ce budget. C’est particulièrement pertinent pour les entreprises qui doivent optimiser leurs modèles régulièrement (ré-entraînement hebdomadaire, A/B testing de versions) sans exploser leur facture cloud.

Voir Aussi

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.