TP4. XGBoost : arrêt anticipé et ensemble de validation¶
Durée : 1 h 45.
Objectifs¶
- Comprendre pourquoi un troisième ensemble, la validation, complète le couple entraînement/test des TP précédents.
- Mettre en œuvre l'arrêt anticipé (early stopping) d'XGBoost avec l'API actuelle.
- Relier le réglage du
learning_rateau nombre d'arbres réellement utilisés avant l'arrêt. - Lire une courbe d'apprentissage pour diagnostiquer sous-apprentissage et surapprentissage.
Prérequis¶
Le TP3 (classification, arbres, forêts) et le TP5 (validation croisée) : ce TP les combine sur XGBoost.
Ressources¶
Étape 1. Pourquoi un troisième ensemble (15 min)¶
Le TP3 séparait les données en entraînement et test. Ici, un troisième ensemble s'intercale : la validation. L'entraînement s'arrête dès que la performance sur la validation cesse de s'améliorer, un mécanisme qu'on ne peut pas piloter avec le seul jeu de test, réservé à l'évaluation finale, jamais consultée pendant l'entraînement.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
donnees = load_breast_cancer()
X, y = donnees.data, donnees.target
X_train_complet, X_test, y_train_complet, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
X_train, X_val, y_train, y_val = train_test_split(
X_train_complet, y_train_complet, test_size=0.2, random_state=42, stratify=y_train_complet
)
print(X_train.shape, X_val.shape, X_test.shape)
Sur les 569 tumeurs du TP3, ce double partage donne 364 exemples d'entraînement, 91
de validation, 114 de test : la validation ampute l'entraînement d'un cinquième
supplémentaire, un coût réel qu'il faut mettre en balance avec ce qu'elle apporte.
Étape 2. L'arrêt anticipé, avec l'API actuelle (25 min)¶
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score
modele = XGBClassifier(
n_estimators=1000,
learning_rate=0.05,
max_depth=4,
subsample=0.8,
colsample_bytree=0.8,
random_state=42,
eval_metric="logloss",
early_stopping_rounds=20,
)
modele.fit(
X_train, y_train,
eval_set=[(X_train, y_train), (X_val, y_val)],
verbose=False,
)
predictions = modele.predict(X_test)
print("Exactitude test :", round(accuracy_score(y_test, predictions), 3))
print("Meilleure itération :", modele.best_iteration)
L'IA vous le donne en trois secondes
Beaucoup de tutoriels, et donc d'assistants entraînés dessus, écrivent
eval_metric="logloss" et early_stopping_rounds=20 comme arguments de .fit()
plutôt que du constructeur XGBClassifier(...). Testez :
modele_ancien_style = XGBClassifier(n_estimators=200, learning_rate=0.1, max_depth=4, random_state=42)
try:
modele_ancien_style.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
eval_metric="logloss",
early_stopping_rounds=20,
verbose=False,
)
except TypeError as erreur:
print("Erreur :", erreur)
Depuis XGBoost 2.0, eval_metric et early_stopping_rounds se déclarent dans le
constructeur, pas dans .fit() : l'ancien style lève une TypeError explicite. C'est
l'exemple même d'un changement d'API qui rend caduque une grande partie des exemples
trouvés en ligne, y compris dans des cours récents, sans que le message d'erreur ne
laisse deviner la cause si on ne connaît pas l'historique de la bibliothèque.
best_iteration (99) indique l'arbre où la validation était la meilleure, sur les
1000 arbres autorisés : l'entraînement s'est arrêté vingt itérations plus tard sans
amélioration, à l'itération 119, et XGBoost a conservé le modèle à l'itération 99, pas
le dernier.
Exercice 2.1 : n_estimators ne veut pas dire nombre d'arbres réellement utilisés
Affichez modele.n_estimators (le paramètre que vous avez fixé) et comparez-le à
modele.best_iteration + 1 (le nombre d'arbres réellement conservés). Que se
passerait-il si vous fixiez n_estimators=50 au lieu de 1000 ?
Résultat attendu : n_estimators vaut 1000, best_iteration + 1 vaut 100 :
l'arrêt anticipé a stoppé l'entraînement bien avant la limite fixée. Avec
n_estimators=50, l'entraînement s'arrêterait à 50 arbres sans jamais déclencher le
critère d'arrêt anticipé, qui a besoin de marge pour observer une dégradation.
Corrigé
print("n_estimators (plafond fixé) :", modele.n_estimators)
print("arbres réellement utilisés :", modele.best_iteration + 1)
n_estimators est un plafond, pas un objectif : avec l'arrêt anticipé, il doit
être fixé large (ici 1000) pour ne jamais être le facteur limitant. C'est
early_stopping_rounds qui décide réellement quand s'arrêter.
Étape 3. learning_rate et le nombre d'arbres réellement utilisés (25 min)¶
for taux in (0.2, 0.1, 0.05, 0.03, 0.01):
m = XGBClassifier(
n_estimators=1000, learning_rate=taux, max_depth=4, subsample=0.8, colsample_bytree=0.8,
random_state=42, eval_metric="logloss", early_stopping_rounds=20,
)
m.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_val, y_val)], verbose=False)
exactitude = accuracy_score(y_test, m.predict(X_test))
print(f"learning_rate={taux} : arbres utilisés={m.best_iteration + 1}, exactitude test={round(exactitude, 3)}")
Exercice 3.1 : la relation entre taux et nombre d'arbres
D'après la sortie ci-dessus, la relation entre learning_rate et le nombre d'arbres
utilisés avant arrêt est-elle croissante ou décroissante ? Est-elle exactement
proportionnelle (diviser le taux par deux double-t-il le nombre d'arbres) ?
Résultat attendu : décroissante et nette : 41 arbres à 0.2, 61 à 0.1,
100 à 0.05, 209 à 0.03, 428 à 0.01. La relation est croissante quand le
taux diminue, mais pas exactement proportionnelle (passer de 0.1 à 0.05, un
facteur 2, ne fait pas exactement doubler le nombre d'arbres). L'exactitude sur le
test, elle, reste dans une fourchette étroite (0.939 à 0.947) sur toute la plage :
ici, le choix du taux affecte surtout le coût d'entraînement, peu la qualité
finale.
Corrigé
Il n'y a pas de code supplémentaire, l'exercice porte sur la lecture des
résultats déjà produits. Retenez la leçon pratique : quand plusieurs réglages de
learning_rate donnent une qualité comparable, préférez le plus grand des taux
qui fonctionne, il coûte moins d'itérations et donc moins de temps de calcul,
pour un résultat final équivalent.
Étape 4. Lire une courbe d'apprentissage (20 min)¶
import matplotlib.pyplot as plt
modele.fit(X_train, y_train, eval_set=[(X_train, y_train), (X_val, y_val)], verbose=False)
resultats = modele.evals_result()
plt.figure(figsize=(8, 5))
plt.plot(resultats["validation_0"]["logloss"], label="Entraînement")
plt.plot(resultats["validation_1"]["logloss"], label="Validation")
plt.axvline(modele.best_iteration, color="red", linestyle="--", label="Meilleure itération")
plt.xlabel("Nombre d'arbres")
plt.ylabel("Logloss")
plt.legend()
plt.tight_layout()
plt.show()
Exercice 4.1 : diagnostiquer sur la courbe
Comparez la logloss d'entraînement et de validation aux dernières itérations (au delà de la ligne rouge). Laquelle continue de baisser, laquelle stagne ou remonte ? Que cela signifie-t-il ?
Résultat attendu : la logloss d'entraînement continue de baisser après la meilleure itération (le modèle continue de mieux coller aux données qu'il a déjà vues), tandis que la logloss de validation stagne ou remonte légèrement : c'est exactement le signal du surapprentissage, déjà rencontré au TP3 avec l'arbre non limité, mais ici observable en continu sur une courbe plutôt qu'en un seul point.
Corrigé
entrainement = resultats["validation_0"]["logloss"]
validation = resultats["validation_1"]["logloss"]
i = modele.best_iteration
print("Logloss entraînement, à la meilleure itération puis 20 plus tard :",
round(entrainement[i], 4), "->", round(entrainement[-1], 4))
print("Logloss validation, à la meilleure itération puis 20 plus tard :",
round(validation[i], 4), "->", round(validation[-1], 4))
C'est précisément ce que l'arrêt anticipé automatise : plutôt que d'inspecter
cette courbe à l'œil après coup, XGBoost surveille la logloss de validation en
continu et arrête dès qu'elle ne s'améliore plus pendant early_stopping_rounds
itérations consécutives.
Étape 5. Avec ou sans ensemble de validation (15 min)¶
modele_sans_validation = XGBClassifier(
n_estimators=100, learning_rate=0.05, max_depth=4, subsample=0.8, colsample_bytree=0.8,
random_state=42,
)
modele_sans_validation.fit(X_train_complet, y_train_complet)
exactitude_sans = accuracy_score(y_test, modele_sans_validation.predict(X_test))
print("Sans validation, 100 arbres fixes :", round(exactitude_sans, 3))
print("Avec arrêt anticipé (étape 2) :", round(accuracy_score(y_test, predictions), 3))
Exercice 5.1 : ce que coûte, et ce que rapporte, l'ensemble de validation
Le modèle sans validation utilise X_train_complet (455 exemples), le modèle avec
arrêt anticipé n'utilise que X_train (364 exemples, un cinquième de moins). Sur ce
jeu de données précis, l'écart d'exactitude entre les deux est-il important ? Dans
quel cas un praticien choisirait-il quand même l'arrêt anticipé ?
Résultat attendu : un écart faible ou nul sur ce jeu de 569 lignes (les deux
approches tournent autour de 0.94), parce que le nombre d'arbres (100) était déjà
raisonnable par construction. L'arrêt anticipé apporte surtout sa valeur quand on ne
connaît pas à l'avance le bon nombre d'arbres : il l'estime automatiquement,
plutôt que de forcer à le deviner ou à le rechercher par validation croisée complète,
plus coûteuse en temps de calcul.
Corrigé
Pas de code supplémentaire : la comparaison est déjà dans le bloc précédent.
Retenez le compromis : l'arrêt anticipé économise du temps de recherche
d'hyperparamètre (pas besoin de tester n_estimators un par un comme au TP5), au
prix d'un ensemble d'entraînement plus petit. Sur un grand jeu de données, ce coût
devient négligeable ; sur un petit jeu, il peut peser.
Pour aller plus loin¶
early_stopping_roundsse règle habituellement entre 10 et 50 : trop petit, l'arrêt se déclenche sur une fluctuation normale du bruit ; trop grand, il perd son intérêt.- Sur des classes déséquilibrées,
eval_metric="auc"ou"aucpr"informe souvent mieux que"logloss"ou"error". - Combiner arrêt anticipé et
RandomizedSearchCV(recherche d'autres hyperparamètres,max_depth,subsample) va plus loin que réglerlearning_rateseul comme dans ce TP.
Ce qu'il faut retenir¶
Un ensemble de validation, distinct du test, permet à XGBoost de surveiller sa propre
progression et de s'arrêter dès qu'il cesse de s'améliorer, sans jamais consulter le jeu
de test réservé à l'évaluation finale. n_estimators doit être fixé large : c'est
early_stopping_rounds qui décide réellement combien d'arbres seront utilisés,
consultable après coup dans best_iteration. Un learning_rate plus petit demande plus
d'arbres avant l'arrêt, pour une qualité finale souvent comparable. Et depuis XGBoost 2.0,
eval_metric et early_stopping_rounds sont des paramètres du modèle, pas de .fit(),
contrairement à ce que montrent encore beaucoup d'exemples en circulation.
Auto-évaluation¶
Avant de continuer, vous devez pouvoir, sans regarder le corrigé :
- expliquer pourquoi un ensemble de validation est distinct du jeu de test ;
- configurer
eval_metricetearly_stopping_roundsavec l'API XGBoost actuelle ; - expliquer la différence entre
n_estimatorsetbest_iteration + 1; - lire une courbe entraînement/validation et y repérer un signe de surapprentissage ;
- relier qualitativement
learning_rateau nombre d'arbres utilisés avant arrêt.
Pas encore de QCM pour ce TP
Contrairement au module IA générative, ce TP n'a pas de QCM d'auto-évaluation prêt à
l'emploi : les TP d'IA prédictive n'en ont jamais eu. En créer un est possible avec le
skill qcm-generator, mais reste à faire.