Aller au contenu principal

Module 6 — Réglage automatique des hyperparamètres

Les modules 4 et 5 ont livré des tâches d'entraînement qui prennent 3 à 6 minutes et exposent une métrique validation:auc. Ce module confie à SageMaker le soin d'essayer méthodiquement des combinaisons d'hyperparamètres pour maximiser cette métrique — l'équivalent de GridSearchCV de scikit-learn, mais parallélisé, bayésien et facturable à la minute.

Le problème que le tuner résout

Sur le fil rouge, XGBoost expose une demi-douzaine d'hyperparamètres influents : max_depth, eta, subsample, min_child_weight, gamma, num_round. Chacun vaut typiquement dix valeurs raisonnables. Explorer la grille complète coûterait 10⁶ tâches, soit 5 000 heures d'instance à 0,46 $/h — infaisable.

Les bonnes stratégies évitent cette explosion :

StratégiePrincipeQuand
GridToutes les combinaisons d'une grillePetites dimensions (≤ 3 hyperparamètres)
RandomTirage aléatoire dans les plagesDéfaut simple, très bon rapport qualité/prix
BayesianUtilise les résultats passés pour guider les prochainsLe meilleur quand chaque tâche est coûteuse
HyperbandAlloue plus de temps aux configurations prometteusesRéseaux profonds avec courbes d'apprentissage

Sur XGBoost où chaque tâche prend 3 minutes, Bayesian gagne face à Random en environ 30 % d'appels en moins pour atteindre la même AUC. C'est celle que le fil rouge utilise.

Un job de réglage, ligne par ligne

from sagemaker.tuner import HyperparameterTuner, IntegerParameter, ContinuousParameter

plages = {
"num_round": IntegerParameter(50, 300),
"max_depth": IntegerParameter(3, 10),
"eta": ContinuousParameter(0.01, 0.3, scaling_type="Logarithmic"),
"subsample": ContinuousParameter(0.5, 1.0),
"min_child_weight": IntegerParameter(1, 10),
"gamma": ContinuousParameter(0.0, 5.0),
}

tuner = HyperparameterTuner(
estimator=estimateur, # de module 4
objective_metric_name="validation:auc",
objective_type="Maximize",
hyperparameter_ranges=plages,
metric_definitions=[
{"Name": "validation:auc", "Regex": r"validation-auc:(\S+)"},
],
max_jobs=60, # 60 tâches au total
max_parallel_jobs=6, # 6 en parallèle
strategy="Bayesian",
early_stopping_type="Auto",
base_tuning_job_name="resiliation-xgb-tune",
)

tuner.fit({
"train": "s3://resiliation-donnees/prepare/v3/train.parquet",
"validation": "s3://resiliation-donnees/prepare/v3/valid.parquet",
})

Quatre décisions structurent ce code.

Les plages : Integer pour les valeurs entières, Continuous pour les réelles, Categorical pour les listes fixes. Sur eta, l'échelle Logarithmic est décisive : eta a un effet perceptif exponentiel, aller de 0,01 à 0,3 en linéaire concentrerait l'exploration sur les valeurs élevées.

max_jobs=60, max_parallel_jobs=6 : 60 tâches valent typiquement 100 % du gain accessible ; au-delà, le rendement décroche. Le parallélisme accélère l'exécution mais dégrade légèrement la stratégie bayésienne, qui a moins d'informations passées pour choisir. Un ratio de 10:1 (60 tâches, 6 en parallèle) est un bon compromis.

objective_metric_name="validation:auc" : doit correspondre exactement à un metric_definitions, sinon le tuner échoue silencieusement en ne recevant aucune valeur et déclare toutes les tâches équivalentes.

early_stopping_type="Auto" : SageMaker arrête une tâche dès qu'il devient clair qu'elle ne battra pas le meilleur résultat courant. Sur XGBoost avec 60 tâches, l'économie observée est de 25 à 40 % du temps total.

Le coût, chiffré

60 tâches × 4 minutes en moyenne × ml.m5.2xlarge (0,46 $/h) = 1,84 $ en instances à la demande. En Spot avec point de reprise : environ 0,55 $. La lecture n'est pas triviale : ces montants sont la facture du réglage, à ne pas confondre avec le prix d'une tâche unique. Le facteur 60 se paie une fois par cycle d'ingénierie ; c'est bon marché pour un gain d'AUC souvent supérieur au demi-point.

Sur le fil rouge, un premier réglage part de la ligne de base eta=0,1, max_depth=5, num_round=200 (AUC = 0,802 en validation) et remonte à eta=0,073, max_depth=6, num_round=140 (AUC = 0,821). Ce demi-point d'AUC vaut environ 200 clients sauvés par mois selon la matrice de coût métier ; le retour sur les 1,84 $ est infini à l'échelle mensuelle.

Lire les résultats sans se noyer

Après la fin, deux vues suffisent :

resultats = tuner.analytics().dataframe()   # une ligne par tâche
resultats = resultats.sort_values("FinalObjectiveValue", ascending=False)
print(resultats[["max_depth", "eta", "num_round", "FinalObjectiveValue"]].head(10))

meilleure = tuner.best_training_job()
print("Meilleure tâche :", meilleure)

La console SageMaker affiche également un graphique interactif : une projection en deux dimensions (eta en abscisse, max_depth en ordonnée, FinalObjectiveValue en couleur) permet de voir immédiatement les régions denses — là où le tuner a concentré son effort après quelques itérations. Une zone dense et brillante indique un optimum robuste ; un optimum isolé au milieu d'un plateau sombre est suspect (surajustement du tuner à un tirage particulier).

Redémarrer un job pour affiner

Une fois la région prometteuse identifiée, on peut relancer un tuner avec des plages resserrées autour de l'optimum trouvé. Cette approche à deux passes coûte 25 % plus cher qu'un tuner unique bien dimensionné, mais donne souvent un ou deux dixièmes de point d'AUC de plus :

plages_2 = {
"num_round": IntegerParameter(100, 180),
"max_depth": IntegerParameter(5, 7),
"eta": ContinuousParameter(0.05, 0.10, scaling_type="Logarithmic"),
}

Sur des systèmes matures, cette itération manuelle est remplacée par un tuner à budget adaptatif géré par le pipeline (module 9), qui décide seul de l'affinage.

Le piège du calibrage sur la validation

Le tuner optimise sur validation. Une AUC de 0,821 en validation n'est pas l'AUC attendue en production ; c'est une borne supérieure biaisée, parce que l'on a fait 60 essais sur cette même partition. Le vrai chiffre attendu est celui du fichier test.parquet, jamais vu par le tuner.

Sur le fil rouge, test donne 0,814 — un demi-point de moins que la validation, mais toujours au-dessus de la ligne de base à 0,802. Le trou entre validation et test est la surestimation induite par le tuner ; sans partition de test, on la découvre en production, ce qui est trop tard.

Deux plages qui rendent le réglage stérile

Fixer max_depth=10 alors que le bruit du jeu et la régularisation ne le supportent pas produit un surajustement systématique : toutes les tâches remontent une AUC train très haute et une AUC validation médiocre. Le tuner « bat » pourtant la ligne de base à chaque essai, sans améliorer la production. Autre piège : eta linéaire avec plage 0,001 à 0,3 — le tuner passe 55 essais dans la moitié droite parce que le tirage est équidistant, et rate les optima autour de 0,03. L'échelle Logarithmic est le seul remède. Ces deux erreurs sont explicitement thématisées dans la banque d'examen.

Enchaîner avec les modules suivants

tuner.best_estimator() renvoie un estimateur configuré avec les meilleurs hyperparamètres et pointant sur le meilleur model.tar.gz. Cet objet se déploie tel quel :

meilleur_estimateur = tuner.best_estimator()
point_de_terminaison = meilleur_estimateur.deploy( # module 7
initial_instance_count=1,
instance_type="ml.m5.large",
)

Ce lien direct entre tuner.best_estimator() et deploy() est ce qui rend la chaîne SageMaker cohérente : le meilleur modèle du réglage devient sans étape supplémentaire le modèle servi.

En résumé

  • Le réglage automatique parallélise et guide l'exploration des hyperparamètres ; Bayesian gagne 30 % d'appels sur Random quand chaque tâche coûte cher.
  • Les plages utilisent Integer, Continuous ou Categorical ; l'échelle Logarithmic est indispensable pour des paramètres à effet exponentiel comme eta.
  • La métrique objectif doit correspondre à un metric_definitions déjà déclaré ; sans cela, le tuner échoue en silence.
  • early_stopping_type="Auto" économise 25 à 40 % du temps ; un jeu de test distinct est le seul garde-fou contre la surestimation induite par le réglage sur validation.

Module suivant : servir le meilleur modèle en temps réel avec un point de terminaison HTTPS, puis en variante sans serveur qui ne facture que lorsqu'une requête arrive.