Optuna 4 en Python : Optimisation d'Hyperparamètres ML (Guide 2026)
Guide pratique 2026 pour Optuna 4 en Python : samplers TPE et GPSampler, pruners, intégration XGBoost, scikit-learn et PyTorch, persistance et parallélisation, avec quatre exemples complets prêts à copier.
Optuna 4 est un framework Python open source d'optimisation d'hyperparamètres qui utilise des algorithmes bayésiens (TPE, CMA-ES, GPSampler) pour trouver la meilleure configuration de vos modèles ML en 10 à 100 fois moins d'essais qu'une recherche par grille. En 2026, Optuna 4.x est devenu le standard de facto pour tuner XGBoost, LightGBM, scikit-learn et PyTorch, et il a largement remplacé Hyperopt sur la majorité des projets grâce à son API define-by-run, ses pruners agressifs et son dashboard temps réel. Honnêtement, sur ma dernière compétition Kaggle tabulaire, passer de GridSearchCV à Optuna m'a fait gagner deux jours de calcul (et un cran au classement). Ce guide couvre l'installation, chaque sampler, la persistance en base, la parallélisation et quatre exemples complets prêts à copier.
Optuna 4.x expose une API define-by-run où l'espace de recherche est décrit avec trial.suggest_* à l'intérieur d'une fonction objective Python normale (pas de dictionnaire figé).
Le sampler par défaut TPESampler (Tree-structured Parzen Estimator) surpasse Grid Search sur presque tous les benchmarks tabulaires en moins de 50 essais.
Les pruners (MedianPruner, HyperbandPruner) coupent les essais non prometteurs pendant l'entraînement, ce qui économise souvent 40 à 70 % de temps GPU.
Optuna 4 introduit GPSampler stable (processus gaussiens) et améliore le stockage JournalStorage, plus rapide que SQLite pour les études distribuées.
Le Optuna Dashboard permet de suivre les essais en temps réel via une interface web sans code supplémentaire.
Parallélisation triviale : lancez n_jobs=-1 sur une machine ou plusieurs workers pointant sur la même base RDB pour distribuer sur un cluster.
Qu'est-ce qu'Optuna en Python ?
Optuna est une bibliothèque Python d'optimisation d'hyperparamètres développée à l'origine par Preferred Networks et publiée en open source en 2018. En juillet 2026, la version stable est la branche 4.x, qui apporte un sampler basé sur les processus gaussiens (GPSampler), un stockage JournalStorage haute performance, et des améliorations importantes du support Python 3.13 free-threaded.
L'idée clé, c'est l'API define-by-run. Au lieu de déclarer un espace de recherche statique (comme param_grid dans scikit-learn), vous écrivez une fonction Python normale, appelée objective, qui reçoit un objet trial et retourne une métrique à optimiser. Chaque appel à trial.suggest_float, trial.suggest_int ou trial.suggest_categorical échantillonne une valeur d'hyperparamètre. Cette approche permet des espaces conditionnels ("si le modèle est XGBoost, alors chercher max_depth ; si c'est un LightGBM, chercher num_leaves") impossibles à exprimer proprement dans un dictionnaire figé.
Le pipeline typique se compose de trois primitives : un Study qui orchestre l'optimisation, des Trials qui représentent chaque essai, et un Sampler qui décide quels hyperparamètres tester ensuite en fonction des essais passés. Pour des modèles coûteux à entraîner, un Pruner peut interrompre un essai en cours si les métriques intermédiaires suggèrent qu'il ne battra pas le meilleur résultat courant.
Installation et premier exemple
Installez Optuna avec pip (Python 3.9 à 3.13 supportés en 2026) :
Voici l'exemple le plus court possible : optimiser une fonction quadratique. Il vous permet de vérifier que l'installation fonctionne et d'observer le comportement du sampler par défaut.
import optuna
def objective(trial):
x = trial.suggest_float("x", -10.0, 10.0)
y = trial.suggest_int("y", -5, 5)
return (x - 2) ** 2 + (y + 1) ** 2 # minimum en x=2, y=-1
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=50, show_progress_bar=True)
print("Best value :", study.best_value)
print("Best params :", study.best_params)
Après 50 essais, le TPESampler par défaut trouve typiquement x ≈ 2.0 et y = -1 avec une valeur objective proche de 0. Contrairement à une recherche aléatoire pure qui explore uniformément l'espace, TPE construit deux distributions (une pour les "bons" essais et une pour les "mauvais"), puis échantillonne préférentiellement les zones où le ratio est élevé. C'est ce qui explique la convergence rapide sur des problèmes de faible et moyenne dimension.
Samplers : TPE, CMA-ES, GPSampler et Random
Le sampler est le cœur algorithmique d'Optuna. Il détermine la stratégie d'exploration de l'espace de recherche. Chaque sampler a un profil de performance différent selon la dimensionnalité du problème et le coût d'un essai.
TPESampler (défaut) : bon compromis universel, excellent jusqu'à 20-30 hyperparamètres, supporte les variables catégorielles et conditionnelles. Utilisez-le sauf raison contraire.
CmaEsSampler : basé sur CMA-ES (Covariance Matrix Adaptation Evolution Strategy). Puissant pour les problèmes purement continus de dimension 10-50 avec des interactions complexes entre variables. Ignore les variables catégorielles.
GPSampler : processus gaussiens avec noyau Matérn. Stabilisé en Optuna 4.x, il excelle sur les problèmes avec très peu d'essais (moins de 100) et des évaluations coûteuses, typique en deep learning. Coût de calcul du sampler lui-même en O(n³) avec n = nombre d'essais.
RandomSampler : baseline de recherche aléatoire. Utile pour comparer et prouver que votre optimisation bayésienne apporte vraiment de la valeur.
NSGAIISampler et NSGAIIISampler : algorithmes génétiques pour l'optimisation multi-objectifs (voir section dédiée).
from optuna.samplers import TPESampler, CmaEsSampler, GPSampler, RandomSampler
# TPE avec seed reproductible et 20 essais de warmup en random
sampler = TPESampler(seed=42, n_startup_trials=20, multivariate=True)
# GPSampler pour un budget très restreint (< 100 essais)
sampler_gp = GPSampler(seed=42, n_startup_trials=10)
study = optuna.create_study(direction="maximize", sampler=sampler)
Bon, un piège classique : l'option multivariate=True sur TPE modélise les corrélations entre hyperparamètres au lieu de les traiter indépendamment, et c'est presque toujours ce qu'on veut. Le paramètre n_startup_trials contrôle combien d'essais random sont exécutés avant que le sampler bayésien prenne le relais ; augmentez-le (30-50) si votre espace est très large.
Pruners : arrêter tôt les essais non prometteurs
Un pruner permet d'interrompre un essai en cours d'entraînement si les métriques intermédiaires (par exemple la validation loss à l'époque 5) sont clairement pires que celles du meilleur essai courant. Sur un entraînement de réseau de neurones coûteux, l'économie atteint souvent 50 à 70 % du temps GPU. Sérieusement, la première fois que j'ai activé un pruner sur une étude de 200 essais PyTorch, mon RTX 4090 a fini le job avant l'heure du café au lieu d'y passer la nuit.
from optuna.pruners import MedianPruner, HyperbandPruner
pruner = MedianPruner(n_startup_trials=5, n_warmup_steps=10, interval_steps=1)
def objective(trial):
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
dropout = trial.suggest_float("dropout", 0.0, 0.5)
model = build_model(lr, dropout)
for epoch in range(30):
val_loss = train_one_epoch(model)
trial.report(val_loss, step=epoch) # signaler la métrique intermédiaire
if trial.should_prune():
raise optuna.TrialPruned()
return val_loss
study = optuna.create_study(direction="minimize", pruner=pruner)
study.optimize(objective, n_trials=100)
MedianPruner coupe un essai si sa métrique intermédiaire est pire que la médiane de tous les essais précédents à la même étape. Simple et efficace pour la plupart des cas. HyperbandPruner, plus agressif, alloue exponentiellement plus de budget aux essais qui survivent aux premières rondes. Idéal quand vous avez plus de 100 essais à évaluer et un vrai signal d'apprentissage précoce.
Comment optimiser XGBoost avec Optuna ?
XGBoost est l'un des cas d'usage les plus populaires d'Optuna. Combinés, les deux outils atteignent régulièrement les meilleurs scores sur les compétitions Kaggle tabulaires. Voici un exemple complet et reproductible utilisant la version 3.x de XGBoost et l'intégration optuna-integration[xgboost]. Pour un rappel complet sur XGBoost, consultez notre guide complet XGBoost 3.0 en Python.
Sur le dataset California Housing avec 100 essais, cette configuration converge en général vers un RMSE de 0.44 à 0.46, contre 0.49 pour une recherche aléatoire équivalente et 0.52 pour les hyperparamètres par défaut. Le XGBoostPruningCallback coupe automatiquement les essais dont la RMSE de validation est en retard, ce qui divise le temps d'exécution par 2 à 3. Pour approfondir les hyperparamètres eux-mêmes, la doc officielle XGBoost sur les paramètres reste la référence.
Intégration avec scikit-learn Pipelines
Optuna se marie parfaitement avec les Pipelines scikit-learn. Vous pouvez chercher simultanément le préprocesseur, le modèle et ses hyperparamètres. Cette approche est particulièrement puissante quand elle est combinée à notre guide feature engineering avec Pandas et scikit-learn.
import optuna
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, RobustScaler
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer
X, y = load_breast_cancer(return_X_y=True)
def objective(trial):
scaler_name = trial.suggest_categorical("scaler", ["standard", "robust", "none"])
scaler = {"standard": StandardScaler(), "robust": RobustScaler(), "none": "passthrough"}[scaler_name]
model_name = trial.suggest_categorical("model", ["rf", "gb"])
if model_name == "rf":
model = RandomForestClassifier(
n_estimators=trial.suggest_int("rf_n_estimators", 50, 500),
max_depth=trial.suggest_int("rf_max_depth", 3, 20),
min_samples_split=trial.suggest_int("rf_min_samples_split", 2, 20),
random_state=42,
)
else:
model = GradientBoostingClassifier(
n_estimators=trial.suggest_int("gb_n_estimators", 50, 500),
learning_rate=trial.suggest_float("gb_lr", 0.01, 0.3, log=True),
max_depth=trial.suggest_int("gb_max_depth", 2, 8),
random_state=42,
)
pipe = Pipeline([("scaler", scaler), ("clf", model)])
return cross_val_score(pipe, X, y, cv=5, scoring="roc_auc", n_jobs=-1).mean()
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=60)
print("AUC :", round(study.best_value, 4))
print("Config gagnante :", study.best_params)
Notez comment l'API define-by-run permet un espace conditionnel : si model_name == "rf", seuls les hyperparamètres du Random Forest sont échantillonnés. Impossible à faire proprement avec GridSearchCV. Le résultat contient uniquement les paramètres pertinents pour le modèle gagnant.
Optuna avec PyTorch et Deep Learning
Pour les réseaux de neurones, les pruners deviennent essentiels parce que chaque essai coûte cher. Voici un squelette pour PyTorch 2.6 avec Optuna 4.x.
import optuna
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
def build_model(trial, in_dim, out_dim):
n_layers = trial.suggest_int("n_layers", 1, 4)
layers = []
prev = in_dim
for i in range(n_layers):
units = trial.suggest_int(f"units_l{i}", 32, 512, log=True)
dropout = trial.suggest_float(f"dropout_l{i}", 0.0, 0.5)
layers += [nn.Linear(prev, units), nn.ReLU(), nn.Dropout(dropout)]
prev = units
layers.append(nn.Linear(prev, out_dim))
return nn.Sequential(*layers)
def objective(trial):
device = "cuda" if torch.cuda.is_available() else "cpu"
model = build_model(trial, in_dim=784, out_dim=10).to(device)
lr = trial.suggest_float("lr", 1e-5, 1e-2, log=True)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
for epoch in range(20):
model.train()
for xb, yb in train_loader:
optimizer.zero_grad()
loss = criterion(model(xb.to(device)), yb.to(device))
loss.backward()
optimizer.step()
model.eval()
val_acc = evaluate(model, valid_loader, device)
trial.report(val_acc, step=epoch)
if trial.should_prune():
raise optuna.TrialPruned()
return val_acc
study = optuna.create_study(
direction="maximize",
pruner=optuna.pruners.HyperbandPruner(min_resource=3, max_resource=20),
)
study.optimize(objective, n_trials=50, timeout=60 * 60 * 4)
Le paramètre timeout=60*60*4 limite l'étude à 4 heures, ce qui est bien pratique pour un job planifié. HyperbandPruner alloue peu d'époques aux essais initiaux et donne plus de budget uniquement aux configurations survivantes. Résultat : vous explorez massivement plus l'espace que si vous exécutiez 50 essais complets.
Persistance : JournalStorage vs SQLite vs MySQL
Par défaut, un Study Optuna vit uniquement en mémoire, et il est perdu à la fin du script. Pour reprendre une étude, la partager entre workers, ou l'afficher dans le dashboard, il faut un backend de stockage persistant.
Backend
Cas d'usage
Concurrence
Performance
InMemoryStorage
Prototypage, tests unitaires
Aucune
Maximale
SQLite (RDBStorage)
Machine unique, plusieurs workers locaux
Faible (verrou fichier)
Correcte jusqu'à ~10k essais
JournalStorage (fichier)
NFS partagé, cluster HPC
Moyenne
2-5× plus rapide que SQLite
MySQL / PostgreSQL
Production, équipes multiples
Élevée
Excellente à toute échelle
# SQLite : simple, un seul fichier
study = optuna.create_study(
storage="sqlite:///optuna_studies.db",
study_name="xgb_california",
load_if_exists=True, # reprendre si existe déjà
direction="minimize",
)
# JournalStorage : recommandé pour clusters partagés (Optuna 4.x)
from optuna.storages import JournalStorage
from optuna.storages.journal import JournalFileBackend
storage = JournalStorage(JournalFileBackend("./optuna-journal.log"))
study = optuna.create_study(storage=storage, study_name="cluster_run", load_if_exists=True)
Pour la production, préférez PostgreSQL avec un pool de connexions. L'ancien JournalFileStorage reste utilisable, mais JournalFileBackend est le nom stable en Optuna 4.x.
Parallélisation et calcul distribué
Optuna offre deux niveaux de parallélisation. Pour un seul processus multi-thread (utile si votre fonction objective libère le GIL, par exemple en appelant du code C/CUDA) :
study.optimize(objective, n_trials=100, n_jobs=8)
Pour distribuer sur plusieurs machines ou processus, il suffit de lancer plusieurs workers qui pointent tous vers le même stockage RDB ou Journal :
# worker.py, à exécuter sur N nœuds
import optuna
study = optuna.load_study(
study_name="cluster_run",
storage="postgresql://user:pwd@host:5432/optuna",
)
study.optimize(objective, n_trials=50) # chaque worker fait 50 essais
Aucune coordination manuelle nécessaire : les workers piochent les prochains essais dans la file, publient leurs résultats, et le sampler central conditionne les suggestions futures sur l'ensemble des essais accumulés. Cette architecture "trivialement parallèle" est la raison pour laquelle Optuna passe à l'échelle sur des centaines de nœuds.
Optimisation multi-objectifs
En pratique, on veut souvent optimiser deux métriques simultanément. Par exemple, maximiser l'accuracy tout en minimisant la latence d'inférence. Optuna gère cela nativement avec NSGAIISampler et retourne un ensemble de solutions Pareto-optimales.
import optuna
def objective(trial):
max_depth = trial.suggest_int("max_depth", 2, 12)
n_estimators = trial.suggest_int("n_estimators", 50, 800)
accuracy = train_and_score(max_depth, n_estimators)
latency_ms = benchmark_latency(max_depth, n_estimators)
return accuracy, latency_ms
study = optuna.create_study(
directions=["maximize", "minimize"],
sampler=optuna.samplers.NSGAIISampler(seed=42),
)
study.optimize(objective, n_trials=200)
for t in study.best_trials: # front de Pareto
print(f"acc={t.values[0]:.3f}, latence={t.values[1]:.1f} ms, params={t.params}")
Il n'existe pas un unique "meilleur" trial dans le cas multi-objectifs : study.best_trials retourne l'ensemble des configurations non-dominées. À vous de choisir selon vos contraintes métier (par exemple, latence maximale de 50 ms).
Optuna Dashboard et visualisations
Le optuna-dashboard est une application web séparée qui lit directement le stockage d'une étude et affiche en temps réel : historique d'optimisation, importance des hyperparamètres, front de Pareto, coordonnées parallèles, et logs des essais. Lancez-le en une commande.
Ouvrez ensuite http://localhost:8080 dans votre navigateur. Le dashboard rafraîchit automatiquement pendant que votre script d'optimisation tourne, ce qui est vraiment indispensable pour les longues études.
Pour générer des figures dans un notebook Jupyter sans le dashboard, utilisez le module optuna.visualization (backend Plotly) :
from optuna.visualization import (
plot_optimization_history, plot_param_importances,
plot_parallel_coordinate, plot_slice, plot_contour,
)
plot_optimization_history(study).show()
plot_param_importances(study).show() # calcul basé sur fANOVA
plot_parallel_coordinate(study, params=["max_depth", "learning_rate"]).show()
L'importance des paramètres via fANOVA est particulièrement utile : elle vous dit quels hyperparamètres ont réellement influencé la métrique. Souvent, seuls 2 ou 3 dominent, ce qui simplifie les études futures. Pour interpréter les modèles eux-mêmes une fois tunés, complétez avec notre guide SHAP en Python.
Optuna vs GridSearchCV vs Hyperopt vs Ray Tune
Comment choisir entre les principales bibliothèques d'optimisation d'hyperparamètres en Python ? Voici une comparaison synthétique basée sur l'écosystème 2026.
Critère
Optuna 4.x
GridSearchCV
Hyperopt
Ray Tune 2.x
Algorithme par défaut
TPE bayésien
Grille exhaustive
TPE bayésien
ASHA + PBT + BO
API define-by-run
Oui
Non
Partielle
Oui
Pruning intégré
Oui
Non
Non
Oui (ASHA)
Dashboard temps réel
Oui (optuna-dashboard)
Non
Non
Oui (Ray Dashboard)
Parallélisation cluster
Trivial (RDB partagée)
joblib local
MongoDB requis
Ray cluster
Multi-objectifs
Oui (NSGA-II/III)
Non
Non
Oui
Maintenance active en 2026
Très active
Active
Faible
Très active
Cas idéal
ML tabulaire, DL modéré
Espaces très petits
Legacy, à éviter
DL distribué, RL
En résumé : utilisez Optuna pour 95 % des besoins ML en Python en 2026. Passez à Ray Tune uniquement si vous entraînez du deep learning distribué sur plusieurs GPU avec besoin de PBT (Population Based Training) ou de reinforcement learning. Réservez GridSearchCV aux espaces vraiment minuscules (moins de 20 combinaisons). Hyperopt n'est plus maintenu activement et vaut la peine d'être migré vers Optuna.
Questions fréquentes
Optuna est-il compatible avec Python 3.13 free-threaded ?
Oui, Optuna 4.x supporte officiellement Python 3.13, y compris le mode free-threaded (sans GIL). La parallélisation n_jobs=-1 devient réellement CPU-parallèle même pour du code Python pur, ce qui n'était pas possible avant Python 3.13.
Comment reprendre une étude Optuna interrompue ?
Utilisez un stockage persistant (SQLite, JournalStorage ou PostgreSQL) et passez load_if_exists=True à create_study avec le même study_name. Les essais précédents sont conservés et le sampler bayésien conditionne les nouvelles suggestions sur l'historique complet.
Combien d'essais Optuna faut-il pour un bon résultat ?
Règle empirique : 20 à 30 essais par hyperparamètre pour TPE. Pour un modèle XGBoost avec 7 hyperparamètres, visez 100 à 200 essais. Avec un pruner activé, doublez ce budget car les essais coupés coûtent peu de temps réel.
Optuna fonctionne-t-il sur GPU ?
Optuna lui-même est CPU (le sampler est très léger), mais votre fonction objective peut évidemment entraîner sur GPU. Pour du deep learning, un seul GPU par essai est courant ; pour paralléliser, lancez plusieurs workers pointant chacun sur un GPU différent via CUDA_VISIBLE_DEVICES.
Quelle différence entre suggest_float log=True et log=False ?
Avec log=True, l'échantillonnage est uniforme en échelle logarithmique. Utilisez-le pour les hyperparamètres qui varient sur plusieurs ordres de grandeur, typiquement le learning rate (1e-5 à 1e-1) ou les coefficients de régularisation. Pour des valeurs linéaires (dropout entre 0 et 0.5), gardez log=False.
Guide pratique de SHAP 0.46 en Python : valeurs de Shapley, TreeExplainer sur XGBoost et Scikit-Learn, summary_plot vs waterfall, et pièges vécus en production. Code reproductible avec exemples 2026.
Guide pratique XGBoost 3.0 en Python : installation, API scikit-learn, hyperparamètres clés, accélération GPU CUDA, SHAP et comparaison avec LightGBM et CatBoost.
Polars 1.x change la donne en Python data science : moteur Rust, API lazy, multi-thread natif, Apache Arrow. Découvrez comment migrer de Pandas, optimiser vos pipelines et exploiter le streaming sur des milliards de lignes — avec benchmarks 2026 et code prêt à l'emploi.