Optuna : Guide Complet — Optimisation d’Hyperparamètres
Résumé
Optuna est un framework open-source d’optimisation d’hyperparamètres développé par Preferred Networks, une société japonaise spécialisée dans l’intelligence artificielle. Depuis sa publication initiale en 2019, Optuna s’est imposé comme l’un des outils les plus populaires et les plus puissants pour automatiser la recherche des meilleurs hyperparamètres dans les modèles d’apprentissage automatique.
Contrairement aux approches classiques comme la recherche en grille (grid search) ou la recherche aléatoire (random search), Optuna utilise un échantillonneur bayésien sophistiqué appelé TPE (Tree-structured Parzen Estimator) qui apprend de chaque essai précédent pour orienter les propositions suivantes vers les régions les plus prometteuses de l’espace de recherche. Cette approche rend le processus d’optimisation considérablement plus efficace, en particulier lorsque l’espace des hyperparamètres est vaste et que l’évaluation de chaque configuration est coûteuse en temps de calcul.
La particularité distinctive d’Optuna est son API define-by-run, qui permet de définir dynamiquement l’espace de recherche à l’intérieur même de la fonction objectif. Cette approche contraste avec les frameworks statiques où l’espace de recherche doit être défini à l’avance, offrant ainsi une flexibilité bien supérieure pour les modèles complexes possédant des hyperparamètres conditionnels.
Principe mathématique
TPE (Tree-structured Parzen Estimator)
Le cœur algorithmique d’Optuna repose sur le Tree-structured Parzen Estimator, une méthode d’optimisation bayésienne qui modélise la probabilité conditionnelle p(x|y) — c’est-à-dire la probabilité d’un hyperparamètre x étant donné une performance y observée.
Au lieu de modéliser directement p(y|x) comme le ferait une méthode bayésienne classique avec des processus gaussiens, le TPE adopte une approche duale en construisant deux distributions de densité distinctes :
- l(x) (lower) : la distribution de densité des hyperparamètres ayant conduit aux meilleures performances, celles situées au-dessus d’un seuil quantile y* (généralement le top 10 à 25 % des essais).
- g(x) : la distribution de densité des hyperparamètres ayant conduit aux autres performances, celles situées en dessous du seuil y*.
Le ratio d’acquisition s’exprime alors comme l’Espérance d’Amélioration (EI, Expected Improvement), qui est proportionnelle au rapport de ces deux densités :
$$EI(x) \propto \frac{l(x)}{g(x)}$$
Intuitivement, ce ratio mesure à quel point un hyperparamètre x est « typique » des bonnes performances par rapport aux performances médiocres. Plus ce ratio est élevé, plus il est probable que x conduise à une bonne performance. L’algorithme maximise donc ce ratio d’acquisition pour choisir les prochaines configurations à évaluer.
Chaque densité est modélisée comme un mélange de distributions gaussiennes (pour les hyperparamètres continus) ou catégorielles (pour les hyperparamètres discrets), avec des noyaux centrés sur les valeurs observées lors des essais précédents. Cette modélisation permet au TPE de capturer des structures complexes et multimodales dans l’espace des hyperparamètres.
Pruning : Median Pruner
L’un des atouts majeurs d’Optuna est son système d’élagage automatique (pruning). Le Median Pruner est l’implémentation par défaut : à chaque étape intermédiaire d’un essai (trial), il compare la performance actuelle avec la médiane des performances intermédiaires des essais précédents au même stade. Si la performance du trial courant est inférieure à cette médiane, le trial est arrêté prématurément, libérant ainsi des ressources de calcul pour explorer des configurations plus prometteuses.
Formellement, un trial t est élagué à l’étape intermédiaire s si :
$$v(t, s) < \text{median}({v(t’, s) \mid t’ \in \text{trials précédents}})$$
où v(t, s) représente la valeur intermédiaire du trial t à l’étape s. Ce mécanisme peut réduire considérablement le temps total d’optimisation, souvent de 30 à 50 %, en abandonnant rapidement les configurations qui s’avèrent médiocres.
Sampling : Define-by-Run
L’API define-by-run d’Optuna permet de définir l’espace de recherche de manière dynamique, à l’intérieur de la fonction objectif elle-même. Cela signifie que les hyperparamètres peuvent être conditionnés par les valeurs d’autres hyperparamètres, créant ainsi un arbre de recherche structurellement dépendant. Par exemple, si l’hyperparamètre kernel d’un SVM prend la valeur 'rbf', alors l’hyperparamètre gamma devient pertinent ; si kernel prend la valeur 'linear', alors gamma n’a plus de sens. Cette expressivité est naturelle avec Optuna et représente un avantage significatif par rapport aux frameworks à définition statique.
Intuition
Imaginez qu’Optuna soit un sommelier expert qui doit dénicher les meilleurs vins d’un immense caveau contenant des milliers de bouteilles. Au lieu de goûter chaque bouteille jusqu’à la dernière goutte (ce que ferait une recherche en grille naïve), notre sommelier développe une stratégie bien plus intelligente :
Dès les premières gorgées, il identifie les bouteilles qui montrent des signes de médiocrité — un nez fermé, une acidité déséquilibrée, un manque de longueur en bouche. Il les abandonne immédiatement (pruning), économisant ainsi son temps et son palais. Puis, il analyse les caractéristiques communes des bouteilles prometteuses — peut-être que les vins de la vallée du Rhône avec un élevage de 18 mois et un cépage dominant de Syrah obtiennent systématiquement de meilleurs scores. Il concentre alors ses dégustations sur les régions qui partagent ces attributs (TPE sampling), tout en explorant occasionnellement des pistes inattendues pour ne rien manquer.
Après chaque dégustation, le sommelier affine sa compréhension de ce qui fait un grand vin. Il apprend de chaque expérience, corrige ses intuitions, et devient progressivement plus précis dans ses sélections. C’est exactement ce que fait Optuna à chaque trial : il construit une compréhension de plus en plus fine de l’espace des hyperparamètres, guidant la recherche vers les configurations optimales avec une efficacité remarquable.
Implémentation Python
Intégration avec scikit-learn : Random Forest
Voici un exemple complet d’optimisation d’un Random Forest avec Optuna, incluant le pruning avec rapport intermédiaire et la visualisation des résultats :
import optuna
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import StratifiedKFold
# Chargement des données
X, y = load_breast_cancer(return_X_y=True)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
def objective(trial):
# Définition dynamique des hyperparamètres
n_estimators = trial.suggest_int('n_estimators', 50, 500, step=50)
max_depth = trial.suggest_int('max_depth', 3, 30)
min_samples_split = trial.suggest_int('min_samples_split', 2, 20)
min_samples_leaf = trial.suggest_int('min_samples_leaf', 1, 10)
max_features = trial.suggest_categorical('max_features', ['sqrt', 'log2', None])
bootstrap = trial.suggest_categorical('bootstrap', [True, False])
if bootstrap:
max_samples = trial.suggest_float('max_samples', 0.5, 1.0)
else:
max_samples = None
modèle = RandomForestClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
min_samples_split=min_samples_split,
min_samples_leaf=min_samples_leaf,
max_features=max_features,
bootstrap=bootstrap,
max_samples=max_samples,
random_state=42,
n_jobs=-1
)
# Validation croisée avec rapport intermédiaire pour le pruning
scores = []
for fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
modèle.fit(X_train, y_train)
score = modèle.score(X_val, y_val)
scores.append(score)
# Rapport intermédiaire pour le pruning
trial.report(score, step=fold)
if trial.should_prune():
raise optuna.TrialPruned()
return float(np.mean(scores))
# Création de l'étude avec pruner et sampler
étude = optuna.create_study(
direction='maximize',
sampler=optuna.samplers.TPESampler(seed=42),
pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=2)
)
# Lancement de l'optimisation
étude.optimize(objective, n_trials=100, timeout=600, show_progress_bar=True)
# Affichage des résultats
print(f"Meilleur score : {étude.best_value:.4f}")
print(f"Meilleurs hyperparamètres : {étude.best_params}")
Visualisation avec optuna.visualization
Optuna offre un écosystème complet de visualisation intégré pour analyser les résultats de l’étude :
import optuna.visualization as vis
# Tracé de l'historique d'optimisation
fig_history = vis.plot_optimization_history(étude)
fig_history.show()
# Carte de contour des hyperparamètres principaux
fig_contour = vis.plot_contour(
étude,
params=['n_estimators', 'max_depth', 'min_samples_split']
)
fig_contour.show()
# Importance des hyperparamètres
fig_importance = vis.plot_param_importances(étude)
fig_importance.show()
# Front de pareto (pour l'optimisation multi-objectifs)
# fig_pareto = vis.plot_pareto_front(étude_multi_objectifs)
# fig_pareto.show()
# Tracé en tranches (slice plot)
fig_slice = vis.plot_slice(étude)
fig_slice.show()
# Tracé de l'évolution intermédiaire
fig_inter = vis.plot_intermediate_values(étude)
fig_inter.show()
# Diagramme de rang (EDF - Empirical Distribution Function)
fig_edf = vis.plot_edf(étude)
fig_edf.show()
Intégration avec PyTorch
Optuna s’intègre parfaitement avec PyTorch grâce à son système de callback et de rapport intermédiaire :
import torch
import torch.nn as nn
import torch.optim as optim
import optuna
class SimpleNet(nn.Module):
def __init__(self, input_dim, hidden_dim, dropout_rate, num_layers):
super().__init__()
layers = []
dims = [input_dim] + [hidden_dim] * num_layers + [1]
for i in range(len(dims) - 1):
layers.append(nn.Linear(dims[i], dims[i+1]))
if i < num_layers:
layers.append(nn.ReLU())
layers.append(nn.Dropout(dropout_rate))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x).squeeze(-1)
def objectif_pytorch(trial):
hidden_dim = trial.suggest_int('hidden_dim', 32, 256, log=True)
dropout_rate = trial.suggest_float('dropout_rate', 0.1, 0.5)
num_layers = trial.suggest_int('num_layers', 1, 4)
lr = trial.suggest_float('lr', 1e-5, 1e-2, log=True)
batch_size = trial.suggest_categorical('batch_size', [16, 32, 64, 128])
weight_decay = trial.suggest_float('weight_decay', 1e-6, 1e-3, log=True)
modèle = SimpleNet(input_dim=10, hidden_dim=hidden_dim,
dropout_rate=dropout_rate, num_layers=num_layers)
optimiseur = optim.Adam(modèle.parameters(), lr=lr, weight_decay=weight_decay)
critère = nn.MSELoss()
for epoch in range(50):
# Entraînement sur un batch
# ... boucle d'entraînement ...
val_loss = 0.0 # Calculer la perte de validation
trial.report(val_loss, epoch)
if trial.should_prune():
raise optuna.TrialPruned()
return val_loss
Intégration avec Keras / TensorFlow
import optuna
import tensorflow as tf
from tensorflow import keras
def callback_keras(study, trial):
"""Callback pour le pruning avec Keras."""
if trial.should_prune():
raise optuna.TrialPruned()
def objectif_keras(trial):
units = trial.suggest_int('units', 32, 256, log=True)
dropout = trial.suggest_float('dropout', 0.1, 0.5)
lr = trial.suggest_float('lr', 1e-4, 1e-2, log=True)
optimizer_name = trial.suggest_categorical('optimizer', ['adam', 'rmsprop', 'sgd'])
if optimizer_name == 'adam':
optimizer = keras.optimizers.Adam(learning_rate=lr)
elif optimizer_name == 'rmsprop':
optimizer = keras.optimizers.RMSprop(learning_rate=lr)
else:
momentum = trial.suggest_float('momentum', 0.8, 0.99)
optimizer = keras.optimizers.SGD(learning_rate=lr, momentum=momentum)
modèle = keras.Sequential([
keras.layers.Dense(units, activation='relu', input_shape=(20,)),
keras.layers.Dropout(dropout),
keras.layers.Dense(units // 2, activation='relu'),
keras.layers.Dropout(dropout / 2),
keras.layers.Dense(1, activation='sigmoid')
])
modèle.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy'])
modèle.fit(X_train, y_train, epochs=50, batch_size=32,
validation_data=(X_val, y_val), verbose=0,
callbacks=[optuna.integration.TFKerasPruningCallback(trial, 'val_accuracy')])
return modèle.evaluate(X_val, y_val, verbose=0)[1]
étude_keras = optuna.create_study(direction='maximize',
pruner=optuna.pruners.MedianPruner())
étude_keras.optimize(objectif_keras, n_trials=50)
Hyperparamètres de configuration d’Optuna
La configuration d’une étude Optuna repose sur plusieurs paramètres clés qui déterminent le comportement et l’efficacité de l’optimisation :
| Paramètre | Description | Valeurs typiques |
|---|---|---|
n_trials |
Nombre total d’essais à exécuter | 50 à 500 (dépend du budget) |
timeout |
Durée maximale en secondes | 300 à 3600 (5min à 1h) |
sampler |
Algorithme d’échantillonnage | TPESampler, CmaEsSampler, RandomSampler |
pruner |
Stratégie d’élagage | MedianPruner, HyperbandPruner, PatientPruner |
direction |
Sens d’optimisation | 'maximize' ou 'minimize' |
n_startup_trials |
Essais aléatoires initiaux avant TPE | 5 à 20 |
n_warmup_steps |
Étapes avant que le pruner n’agisse | 0 à 5 |
Choix du sampler
Le choix de l’échantillonneur influence significativement les performances :
- TPESampler (défaut) : excellent rapport qualité/prix, adapté à la majorité des cas. Fonctionne bien avec les espaces mixtes (continus, entiers, catégoriels).
- CmaEsSampler : basé sur l’algorithme CMA-ES (Covariance Matrix Adaptation Evolution Strategy), particulièrement performant pour les espaces continus de dimension modérée (jusqu’à ~50 dimensions).
- RandomSampler : recherche purement aléatoire, utile comme baseline ou pour les premières explorations rapides.
- NSGAIISampler : conçu pour l’optimisation multi-objectifs, utilise un algorithme génétique non dominé.
Choix du pruner
- MedianPruner : élagage basé sur la médiane, robuste et simple. Bon choix par défaut.
- HyperbandPruner : variante de l’algorithme Hyperband, particulièrement efficace pour les modèles itératifs (réseaux de neurones, gradient boosting).
- PatientPruner : plus tolérant, ne prune que si la performance est significativement en dessous de la référence. Utile quand les performances fluctuent.
- ThresholdPruner : prune si la performance dépasse un seuil fixe prédéfini.
- PercentilePruner : variante du MedianPruner utilisant un percentile configurable.
Avantages et limites d’Optuna
Avantages
- Efficacité supérieure : Le TPE Sampler converge nettement plus vite que la recherche en grille et la recherche aléatoire, en particulier dans les espaces de grande dimension. Les benchmarks publiés montrent souvent des améliorations de 2 à 10 fois par rapport aux méthodes aléatoires.
- API intuitive et flexible : L’approche define-by-run est naturelle à utiliser et permet des espaces de recherche conditionnels complexes sans configuration supplémentaire. La syntaxe Python est élégante et expressive.
- Pruning intégré : L’élagage automatique des essais médiocres économise un temps de calcul considérable, souvent de 30 à 50 % du budget total. Le système est flexible avec plusieurs stratégies au choix.
-
Visualisation riche : L’écosystème de visualisation intégré (
optuna.visualization) offre des outils puissants pour analyser les résultats, comprendre l’importance des hyperparamètres et diagnostiquer les problèmes de recherche. - Intégrations étendues : Optuna s’intègre nativement avec scikit-learn, PyTorch, TensorFlow/Keras, XGBoost, LightGBM, CatBoost, Hugging Face Transformers, MLflow et bien d’autres bibliothèques.
- Persistance et reprise : Les études peuvent être sauvegardées dans des bases de données SQLite, PostgreSQL ou MySQL, permettant de reprendre une optimisation après une interruption ou de partager les résultats entre plusieurs machines.
- Optimisation multi-objectifs : Optuna supporte nativement l’optimisation avec plusieurs objectifs simultanés grâce à l’algorithme NSGA-II, produisant un front de Pareto de solutions optimales.
- Communauté active : Développé par Preferred Networks avec une communauté open-source importante, Optuna bénéficie de mises à jour régulières et d’une documentation de qualité.
Limites
- Courbe d’apprentissage : Bien que l’API soit intuitive, la maîtrise des différents samplers, pruners et des paramètres avancés nécessite un investissement temps non négligeable.
- Temps de calcul initial : Les 10 à 20 premiers essais sont aléatoires et ne bénéficient pas encore de l’intelligence bayésienne. Pour des modèles extrêmement coûteux, même ces essais initiaux peuvent représenter un budget significatif.
- Pas de parallélisme distribué natif aussi performant : Bien qu’Optuna supporte le parallélisme, la gestion de la concurrence peut devenir complexe à grande échelle, nécessitant une base de données externe pour synchroniser les trials entre workers.
-
Sensibilité aux hyperparamètres du sampler : Le TPE lui-même possède des hyperparamètres (
n_startup_trials,gamma,weights) qui influencent ses performances. Un mauvais réglage peut réduire l’efficacité. - Moins adapté aux espaces très vastes : Pour des espaces de recherche dépassant 100 dimensions, les méthodes bayésiennes comme le TPE peuvent souffrir du fléau de la dimension et perdre en efficacité.
Cas d’usage
1. Optimisation de modèles de deep learning
Le cas d’usage le plus classique d’Optuna est l’optimisation des hyperparamètres d’un réseau de neurones profond : taux d’apprentissage, taille des couches cachées, taux de dropout, taille de batch, poids de régularisation L2, et choix de l’optimiseur. Avec le pruning intégré, Optuna peut réduire de moitié le temps nécessaire pour trouver la meilleure architecture.
2. Sélection et réglage de modèles pour la compétition Kaggle
Dans le contexte compétitif de Kaggle, chaque fraction de point d’accuracy compte. Optuna permet d’optimiser simultanément plusieurs algorithmes (XGBoost, LightGBM, CatBoost) avec leurs hyperparamètres spécifiques, en utilisant le pruning pour éliminer rapidement les configurations sous-performantes et se concentrer sur les approches les plus prometteuses. L’optimisation multi-objectifs permet également de trouver le meilleur compromis entre précision et temps d’inférence.
3. Réglage de pipelines scikit-learn complexes
Pour les pipelines de machine learning traditionnels combinant prétraitement (imputation, encodage, normalisation) et modélisation, Optuna permet d’optimiser l’ensemble du pipeline de bout en bout. La nature define-by-run permet de choisir dynamiquement le type de prétraitement en fonction des données, puis d’ajuster les hyperparamètres du modèle en conséquence, créant des espaces de recherche conditionnels riches et expressifs.
4. Optimisation multi-objectifs pour le déploiement en production
En production, on ne cherche pas seulement la meilleure précision, mais le meilleur compromis entre précision, latence d’inférence, taille du modèle et consommation mémoire. Optuna supporte nativement l’optimisation multi-objectifs avec NSGA-II, produisant un front de Pareto de solutions qui permettent aux ingénieurs de choisir le modèle le plus adapté aux contraintes de production spécifiques.
Voir aussi
- Maîtriser la Géométrie avec Python : Création et Analyse de Triangles Concaves
- Exploration des Polygones Polaires avec Python : Guide Complet et Astuces pour Développeurs

