Random Search : Guide complet — Optimisation d’Hyperparamètres en Python
Résumé
Le Random Search (recherche aléatoire) est une méthode d’optimisation d’hyperparamètres qui repose sur l’échantillonnage aléatoire de combinaisons depuis un espace de recherche prédéfini. Contrairement au Grid Search qui explore systématiquement chaque combinaison possible, le Random Search tire au hasard un nombre fixé de configurations et évalue chacune d’elles via une validation croisée. Cette approche, introduite dans le contexte de l’apprentissage automatique par Bergstra et Bengio en 2012, s’avère remarquablement efficace — souvent supérieure au Grid Search — car la plupart des modèles ne dépendent fortement que de quelques hyperparamètres parmi l’ensemble de ceux qui sont tunés.
Dans ce guide complet, nous explorerons le principe mathématique du Random Search, son intuition, son implémentation pratique avec scikit-learn, ses avantages et limites, ainsi que quatre cas d’usage concrets.
Principe mathématique
Échantillonnage dans l’espace des hyperparamètres
Le Random Search définit un espace de recherche (\mathcal{H} = \mathcal{H}_1 \times \mathcal{H}_2 \times \cdots \times \mathcal{H}_k) pour (k) hyperparamètres. À chaque itération (i \in {1, \dots, n}), une configuration (\lambda^{(i)} = (\lambda_1^{(i)}, \dots, \lambda_k^{(i)})) est tirée selon une distribution de probabilité définie sur (\mathcal{H}).
Les distributions les plus courantes sont :
- Distribution uniforme : (\lambda_j \sim \mathcal{U}(a_j, b_j)) pour un hyperparamètre continu borné. Chaque valeur dans l’intervalle ([a_j, b_j]) a la même probabilité d’être sélectionnée.
- Distribution log-uniforme : (\log(\lambda_j) \sim \mathcal{U}(\log(a_j), \log(b_j))). Cette distribution est particulièrement adaptée aux hyperparamètres dont l’impact est multiplicatif (taux d’apprentissage, régularisation), car elle échantillonne de manière équitable à chaque ordre de grandeur.
- Distribution discrète uniforme : (\lambda_j \sim \text{RandInt}(a_j, b_j)) pour un hyperparamètre entier.
Probabilité de converger vers l’optimum
Un résultat théorique fondamental du Random Search concerne la probabilité de trouver une solution (\epsilon)-proche de l’optimum. Soit (\epsilon) la fraction du volume de l’espace de recherche correspondant aux configurations dont la performance est dans les (\epsilon) meilleurs pourcents. Alors, après (n) itérations, la probabilité d’avoir trouvé au moins une telle configuration est :
$$
P(\text{succès}) = 1 – (1 – \epsilon)^n
$$
Par exemple, si (\epsilon = 0,05) (5 % de l’espace contient de très bonnes configurations) et (n = 60) itérations, alors (P = 1 – 0,95^{60} \approx 95,4) %. Avec seulement 60 tirages aléatoires, on a plus de 95 % de chances de tomber sur une excellente configuration.
Le théorème de Bergstra et Bengio (2012)
L’article fondateur de Bergstra et Bengio, « Random Search for Hyper-Parameter Optimization » (Journal of Machine Learning Research, 2012), démontre que le Random Search est plus efficace que le Grid Search lorsque tous les hyperparamètres n’ont pas le même impact sur la performance du modèle — ce qui est le cas pour la grande majorité des problèmes pratiques.
L’intuition clé est la suivante : dans un espace de recherche de grande dimension, Grid Search gaspille énormément d’évaluations en explorant inutilement les hyperparamètres qui ont peu d’importance, tandis que Random Search explore chaque hyperparamètre individuellement de manière exhaustive. Si seuls (d) hyperparamètres parmi (D) sont vraiment influents, Grid Search consacre son budget à (N^D) combinaisons, mais Random Search explore (n) valeurs différentes pour chacun des (D) hyperparamètres, offrant une couverture bien supérieure le long des axes importants.
Intuition : pourquoi le hasard bat la grille
Imaginez que vous cherchez un trésor caché quelque part sur un vaste territoire. Vous savez qu’il se trouve sur les coordonnées ((x, y)) avec (x \in [0, 100]) et (y \in [0, 100]).
Le Grid Search, c’est comme quadriller méthodiquement un petit coin du territoire : vous placez une grille de 10 × 10 cases et fouillez chaque intersection. Le problème ? Ce petit coin ne couvre que 10 % du territoire. Si le trésor est ailleurs, vous ne le trouverez jamais. De plus, vous avez dépensé 100 évaluations pour explorer une zone restreinte.
Le Random Search, c’est comme lancer 60 fléchettes au hasard sur la carte entière. Chaque tir explore l’intégralité du territoire, même si les points sont dispersés. Si le trésor se trouve dans une zone correspondant à 5 % du territoire, vous avez (1 – 0,95^{60} \approx 95) % de chances d’avoir au moins une fléchette dans la bonne zone.
C’est exactement ce qui se passe avec les hyperparamètres : la performance d’un modèle dépend surtout de quelques hyperparamètres clés (comme le learning rate ou la profondeur maximale d’un arbre), tandis que d’autres ont un impact marginal. Le Random Search explore toutes les valeurs possibles de ces hyperparamètres importants, alors que le Grid Search consacre une bonne partie de son budget à varier des hyperparamètres sans conséquence.
Cette analogie explique pourquoi, en pratique, un Random Search avec 50 itérations bat souvent un Grid Search avec 243 combinaisons.
Implémentation Python complète
Installation des dépendances
# Scikit-learn inclut RandomizedSearchCV nativement
# scipy fournit les distributions statistiques
# pip install scikit-learn scipy numpy
Exemple 1 : RandomizedSearchCV sur un Random Forest
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import RandomizedSearchCV, train_test_split
from sklearn.metrics import classification_report
from scipy.stats import randint, uniform
import numpy as np
# Génération d'un jeu de données synthétique
X, y = make_classification(
n_samples=10000, n_features=20, n_informative=15,
n_redundant=5, n_classes=2, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Définition du modèle de base
rf = RandomForestClassifier(random_state=42)
# Espace de recherche des hyperparamètres
param_dist = {
'n_estimators': randint(50, 500), # Nombre d'arbres : 50 à 549
'max_depth': randint(3, 50), # Profondeur max : 3 à 52
'min_samples_split': randint(2, 30), # Échantillon minimum pour split
'min_samples_leaf': randint(1, 20), # Échantillon minimum par feuille
'max_features': uniform(0.1, 0.9), # Fraction de features : 0.1 à 1.0
'bootstrap': [True, False], # Échantillonnage avec/sans remplacement
'criterion': ['gini', 'entropy'] # Critère de division
}
# Recherche aléatoire avec validation croisée
random_search = RandomizedSearchCV(
estimator=rf,
param_distributions=param_dist,
n_iter=100, # 100 configurations testées
cv=5, # Validation croisée 5 plis
scoring='accuracy', # Métrique d'évaluation
n_jobs=-1, # Utilisation de tous les cœurs CPU
random_state=42,
verbose=1,
return_train_score=True
)
random_search.fit(X_train, y_train)
# Résultats
print(f"Meilleur score : {random_search.best_score_:.4f}")
print(f"Meilleurs hyperparamètres : {random_search.best_params_}")
# Évaluation sur l'ensemble de test
y_pred = random_search.predict(X_test)
print(classification_report(y_test, y_pred))
Exemple 2 : Comparaison Grid Search vs Random Search
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
import time
# Grille paramétrique pour le Grid Search (plus restrictive)
param_grid = {
'n_estimators': [100, 200, 300, 400],
'max_depth': [10, 20, 30, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4],
'criterion': ['gini', 'entropy']
}
# Grid Search : 4 × 4 × 3 × 3 × 2 = 288 combinaisons
# Comparaison directe
t0 = time.time()
grid_search = GridSearchCV(
estimator=rf, param_grid=param_grid,
cv=3, scoring='accuracy', n_jobs=-1
)
grid_search.fit(X_train, y_train)
t_grid = time.time() - t0
t0 = time.time()
rand_search = RandomizedSearchCV(
estimator=rf, param_distributions=param_dist,
n_iter=50, cv=3, scoring='accuracy', n_jobs=-1, random_state=42
)
rand_search.fit(X_train, y_train)
t_rand = time.time() - t0
print(f"Grid Search — Score: {grid_search.best_score_:.4f}, Temps: {t_grid:.1f}s, Combinaisons: {grid_search.n_iter_}")
print(f"Random Search — Score: {rand_search.best_score_:.4f}, Temps: {t_rand:.1f}s, Combinaisons: {rand_search.n_iter_}")
Dans de nombreux cas pratiques, le Random Search atteint un score comparable (voire supérieur) au Grid Search en une fraction du temps, car il explore un espace de recherche beaucoup plus large avec le même budget computationnel.
Exemple 3 : Distributions scipy avancées pour le Deep Learning
from scipy.stats import loguniform
from sklearn.neural_network import MLPClassifier
# Distribution log-uniforme pour le taux d'apprentissage
# Échantillonne de manière égale sur chaque ordre de grandeur
# loguniform(rvs) tire dans [1e-5, 1] de manière log-uniforme
param_dist_nn = {
'hidden_layer_sizes': [
(50,), (100,), (50, 25), (100, 50), (200, 100, 50)
],
'alpha': loguniform(1e-5, 1e-1), # Régularisation L2
'learning_rate_init': loguniform(1e-4, 1e-1), # Taux d'apprentissage
'batch_size': randint(32, 256), # Taille du lot
'activation': ['relu', 'tanh', 'logistic'],
'solver': ['adam', 'sgd'],
'max_iter': [300, 500, 1000]
}
mlp = MLPClassifier(random_state=42)
random_search_nn = RandomizedSearchCV(
estimator=mlp,
param_distributions=param_dist_nn,
n_iter=80,
cv=3,
scoring='accuracy',
n_jobs=-1,
random_state=42
)
random_search_nn.fit(X_train, y_train)
print(f"Meilleurs params MLP : {random_search_nn.best_params_}")
La distribution log-uniforme est ici essentielle : le taux d’apprentissage optimal se situe généralement entre (10^{-5}) et (10^{-1}), et la log-uniforme garantit qu’on explore également les ordres (10^{-5}), (10^{-4}), (10^{-3}), (10^{-2}) et (10^{-1}), plutôt que de se concentrer uniquement sur les grandes valeurs comme le ferait une distribution uniforme classique.
Paramètres clés de RandomizedSearchCV
| Paramètre | Type | Description |
|---|---|---|
param_distributions |
dict | Dictionnaire associant chaque nom d’hyperparamètre à une distribution (scipy.stats) ou une liste de valeurs |
n_iter |
int | Nombre de configurations aléatoires à évaluer. C’est le budget principal : plus il est élevé, meilleures sont les chances de trouver l’optimum |
cv |
int ou splitter | Stratégie de validation croisée. Par défaut 5. Peut être un entier (nombre de plis), un objet CV splitter, ou un itérable de splits train/test |
scoring |
str ou callable | Métrique d’optimisation : ‘accuracy’, ‘f1’, ‘roc_auc’, ‘neg_mean_squared_error’, etc. Pour la régression, on utilise souvent ‘neg_mean_squared_error’ |
n_jobs |
int | Nombre de jobs parallèles. -1 utilise tous les cœurs disponibles |
random_state |
int | Graine aléatoire pour la reproductibilité des tirages |
refit |
bool ou str | Si True (défaut), le meilleur modèle est réentraîné sur l’ensemble des données d’entraînement après la recherche |
verbose |
int | Niveau de verbosité. 1 affiche une ligne par itération |
return_train_score |
bool | Si True, inclut les scores d’entraînement dans cv_results_ |
error_score |
float ou ‘raise’ | Valeur attribuée en cas d’erreur d’ajustement. Par défaut np.nan |
Choix du nombre d’itérations (n_iter)
Le choix de n_iter est un arbitrage entre qualité de la solution et coût computationnel :
- 10-20 itérations : exploration rapide, utile pour un premier aperçu
- 50-100 itérations : bon compromis, recommandé pour la plupart des problèmes
- 200-500 itérations : recherche approfondie, pour les compétitions ou les projets critiques
- 1000+ itérations : overkill pour la plupart des cas, envisager plutôt des méthodes plus sophistiquées (Bayesian Optimization)
La règle empirique de Bergstra suggère que n_iter = 60 est souvent suffisant pour surpasser un Grid Search exhaustif dans un espace de 5-7 hyperparamètres.
Avantages et limites
Avantages
- Efficacité computationnelle supérieure : Pour un même budget de (n) évaluations, le Random Search explore un espace de recherche bien plus large qu’un Grid Search. Sur un espace à 6 hyperparamètres avec 5 valeurs chacun, Grid Search nécessite (5^6 = 15\,625) évaluations, tandis que Random Search testera (n = 100) configurations tirées dans des distributions continues — chaque hyperparamètre aura potentiellement 100 valeurs distinctes, contre seulement 5 pour Grid Search.
- Support des distributions continues : Contrairement au Grid Search qui se limite à des grilles discrètes, Random Search permet d’échantillonner selon des distributions réalistes (log-uniforme pour les taux d’apprentissage, exponentielle pour la régularisation), ce qui reflète mieux la nature des hyperparamètres.
- Indépendance des hyperparamètres au lancement : Le fait de générer toutes les configurations à l’avance permet une parallélisation triviale : chaque configuration peut être évaluée indépendamment, sur des machines différentes, sans communication intermédiaire.
- Simplicité d’implémentation : Aucune logique complexe n’est requise. L’algorithme ne nécessite pas de modèle de substitution, de fonction d’acquisition, ou de mise à jour itérative d’une distribution de probabilité comme le ferait un optimiseur bayésien.
-
Reproductibilité parfaite : Avec une graine aléatoire fixe (
random_state), les résultats sont strictement reproductibles, ce qui est crucial pour la validation scientifique et le débogage.
Limites
- Aucune exploitation de l’information acquise : Le Random Search ne tire aucune leçon des évaluations précédentes. Chaque configuration est tirée indépendamment, sans tenir compte des performances observées. Un optimiseur bayésien (Bayesian Optimization), en revanche, construit un modèle probabiliste de la fonction objectif et guide les tirages futurs vers les zones prometteuses.
- Pas de convergence garantie vers le global optimum : La probabilité de trouver l’optimum exact est nulle pour des espaces continus. On ne garantit qu’une solution (\epsilon)-proche avec une certaine probabilité.
-
Budget fixe a priori : Le nombre d’itérations
n_iterdoit être fixé avant le lancement. On ne peut pas adapter dynamiquement le budget en fonction de la convergence observée. - Peut manquer des interactions entre hyperparamètres : Le tirage uniforme assume une indépendance entre les hyperparamètres. Si la performance optimale nécessite une combinaison spécifique de deux hyperparamètres corrélés, le Random Search peut mettre beaucoup de temps à la découvrir par pur hasard.
4 Cas d’usage pratiques
Cas 1 : Tuning d’un modèle de classification sur données déséquilibrées
from sklearn.datasets import make_classification
from sklearn.model_selection import RandomizedSearchCV
from sklearn.ensemble import GradientBoostingClassifier
from scipy.stats import randint, loguniform
# Jeu de données déséquilibré
X, y = make_classification(
n_samples=5000, n_features=30,
weights=[0.9, 0.1], random_state=42 # 90/10
)
param_dist = {
'n_estimators': randint(100, 500),
'learning_rate': loguniform(0.001, 0.3),
'max_depth': randint(3, 10),
'subsample': uniform(0.6, 0.4),
'min_samples_leaf': randint(5, 50)
}
search = RandomizedSearchCV(
GradientBoostingClassifier(random_state=42),
param_dist, n_iter=60, cv=3,
scoring='f1_macro', random_state=42, n_jobs=-1
)
search.fit(X, y)
print(f"Meilleur F1 macro : {search.best_score_:.4f}")
L’utilisation du scoring f1_macro est ici cruciale : sur un jeu de données déséquilibré, l’accuracy serait un piège (un modèle qui prédit toujours la classe majoritaire atteindrait 90 % d’accuracy). Le F1 macro pénalise équitablement chaque classe.
Cas 2 : Optimisation d’un pipeline complet avec prétraitement
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.linear_model import Ridge
pipe = Pipeline([
('poly', PolynomialFeatures()),
('scaler', StandardScaler()),
('ridge', Ridge())
])
param_dist_pipe = {
'poly__degree': randint(1, 5),
'poly__interaction_only': [True, False],
'ridge__alpha': loguniform(1e-3, 1e3),
'ridge__solver': ['auto', 'svd', 'cholesky', 'lsqr', 'saga']
}
search_pipe = RandomizedSearchCV(
pipe, param_dist_pipe,
n_iter=40, cv=5,
scoring='neg_mean_squared_error',
random_state=42
)
search_pipe.fit(X_train, y_train)
Le Random Search fonctionne parfaitement avec les pipelines scikit-learn, permettant de tuner simultanément les étapes de prétraitement et le modèle final.
Cas 3 : Sélection de modèle comparative
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from scipy.stats import randint, loguniform, uniform
# On peut comparer plusieurs algorithmes avec le même Random Search
models = {
'rf': (RandomForestClassifier(random_state=42), {
'n_estimators': randint(50, 500),
'max_depth': randint(3, 30),
'max_features': uniform(0.1, 0.9)
}),
'gb': (GradientBoostingClassifier(random_state=42), {
'n_estimators': randint(50, 300),
'learning_rate': loguniform(0.001, 0.3),
'max_depth': randint(2, 8)
}),
'svm': (SVC(), {
'C': loguniform(0.01, 100),
'gamma': loguniform(0.001, 0.1),
'kernel': ['rbf', 'poly', 'sigmoid']
})
}
results = {}
for name, (model, params) in models.items():
search = RandomizedSearchCV(
model, params, n_iter=30, cv=5,
scoring='accuracy', random_state=42, n_jobs=-1
)
search.fit(X_train, y_train)
results[name] = search.best_score_
print(f"{name} : {search.best_score_:.4f} -> {search.best_params_}")
Cas 4 : Recherche en deux passes (coarse-to-fine)
Une stratégie courante consiste à exécuter le Random Search en deux phases :
# Passe 1 : recherche large pour identifier les ordres de grandeur
param_dist_coarse = {
'n_estimators': randint(10, 1000),
'max_depth': randint(2, 100),
'learning_rate': loguniform(1e-5, 1.0),
'min_samples_split': randint(2, 100)
}
search_coarse = RandomizedSearchCV(
GradientBoostingClassifier(random_state=42),
param_dist_coarse, n_iter=100, cv=3,
scoring='roc_auc', random_state=42, n_jobs=-1
)
search_coarse.fit(X_train, y_train)
coarse_best = search_coarse.best_params_
# Passe 2 : raffinement autour des meilleures valeurs trouvées
learning_rate_center = coarse_best['learning_rate']
param_dist_fine = {
'n_estimators': randint(max(10, coarse_best['n_estimators']-100),
coarse_best['n_estimators']+100),
'max_depth': randint(max(2, coarse_best['max_depth']-10),
coarse_best['max_depth']+10),
'learning_rate': loguniform(learning_rate_center/10, learning_rate_center*10),
'min_samples_split': randint(max(2, coarse_best['min_samples_split']-10),
coarse_best['min_samples_split']+10)
}
search_fine = RandomizedSearchCV(
GradientBoostingClassifier(random_state=42),
param_dist_fine, n_iter=80, cv=5,
scoring='roc_auc', random_state=42, n_jobs=-1
)
search_fine.fit(X_train, y_train)
print(f"Score raffiné : {search_fine.best_score_:.4f}")
Cette approche combine la couverture globale de la première passe avec la précision locale de la seconde, offrant un excellent compromis entre exploration et exploitation.
Voir aussi
- Trouvez le Plus Petit Multiple en Python : Guide Complet et Astuces d’Optimisation
- Implémentez l’algorithme de hachage SHA-1 en Python : Guide étape par étape

