Aller au contenu
English

Régler les hyperparamètres

  • Leçon 19
  • 65 min
  • Module 6 : Régler et finaliser le modèle
Leçon 19 sur 27 · Apprentissage automatique : du problème à un modèle fiableRégler les hyperparamètres

Ce que vous saurez faire

Choisissez les réglages du modèle à partir de résultats, sans deviner ni utiliser les données de test. Gardez ensuite le réglage le plus simple qui égale le meilleur. Un score « meilleur » dont le gain est inférieur au bruit n’est pas meilleur.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient toutes les cellules de code, les tâches et les vérifications de cette leçon, avec les consignes. Il exécute aussi la recherche aléatoire et la simulation du réglage sur les données de test. Ses cellules de configuration installent pandas et scikit-learn, puis téléchargent les données d’exercice et ticket_model.py. Il fonctionne donc sans le projet local. Chaque recherche prend environ 15 secondes.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 65 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour le travail local; le carnet n’en a pas besoin)

Testé sous macOS avec Python 3.14.6, pandas 3.0.6 et scikit-learn 1.9.1. Les commandes Windows ne sont pas testées. Le carnet a été testé dans Jupyter.

Vous préparez un gâteau avec une nouvelle recette. La recette indique quels ingrédients mélanger, et le mélange devient le gâteau. Mais deux nombres ne sont pas dans le bol : la température du four et le temps de cuisson. Le gâteau ne les choisit pas et ne les apprend pas pendant la cuisson. Vous les réglez avant de commencer.

L’emballage indique « 180 °C pendant 40 minutes ». C’est un point de départ raisonnable pour un four typique, pas une promesse pour le vôtre. Si votre four chauffe trop, les bords du gâteau brûlent. Si vous le sortez trop tôt, le centre reste cru. Une pâtissière attentive essaie donc quelques réglages, goûte chaque gâteau et garde le réglage qui fonctionne. Elle le note aussi pour ne pas avoir à deviner la prochaine fois.

Un modèle d’apprentissage automatique possède aussi ces deux types de nombres. Vous les avez rencontrés dans Contrôler la complexité. Vous y avez choisi un réglage, la profondeur d’un arbre, par validation croisée. Cette leçon explique l’idée au complet, puis choisit plusieurs réglages à la fois, de façon honnête.

Quand vous appelez fit(), le modèle apprend des nombres à partir des billets de soutien d’entraînement. Ces nombres appris sont les paramètres du modèle. Pour la régression logistique, ce sont les coefficients (un poids par colonne préparée) et l’ordonnée à l’origine. Pour un arbre de décision, ce sont les questions à chaque division et les valeurs dans les feuilles. Personne ne les saisit. Les données les déterminent.

D’autres nombres doivent être fixés avant le début de fit(). Ce sont les hyperparamètres. Le préfixe « hyper » signifie « au-dessus » : ils se situent au-dessus des paramètres et contrôlent leur apprentissage. Le C de la régression logistique en est un. Il règle la force avec laquelle l’ajustement maintient les coefficients petits, comme vous l’avez vu dans Contrôler la complexité. La profondeur maximale d’un arbre en est un autre. Le nombre d’arbres d’une forêt et le taux d’apprentissage du gradient boosting (amplification par gradient) en sont aussi.

Vous pouvez voir les deux types dans le modèle final. Comme dans chaque carnet de ce cours, pd désigne pandas et np désigne NumPy. Les cellules de configuration du carnet les importent.

from ticket_model import FEATURES, TARGET, build_pipeline, load
train, valid = load("train"), load("valid")
pipeline = build_pipeline().fit(train[FEATURES], train[TARGET])
model = pipeline.named_steps["model"]
print("parameters learned by fit():", model.coef_.size, "coefficients and 1 intercept")
print("hyperparameters chosen before:", {name: model.get_params()[name] for name in ("C", "max_iter", "class_weight")})

La sortie :

Sortie
parameters learned by fit(): 28 coefficients and 1 intercept
hyperparameters chosen before: {'C': 1.0, 'max_iter': 1000, 'class_weight': None}

Les 11 caractéristiques deviennent 28 colonnes après la préparation. Chaque catégorie obtient sa propre colonne. L’outil d’imputation ajoute une colonne pour chaque variable numérique qui contient des valeurs manquantes. L’ajustement a donc appris 28 coefficients. Par exemple, team_account a obtenu −1,02 et prior_escalations_90d a obtenu +0,43. Les hyperparamètres ne viennent pas du tout des données. C = 1.0 est la valeur par défaut de scikit-learn, et max_iter = 1000 est celle que définit ticket_model.py.

Deux entrées alimentent l'ajustement : les hyperparamètres, que vous choisissez avant l'ajustement (C = 1, max_iter = 1000), et train.csv avec 16 939 billets. L'ajustement produit les paramètres, qu'il apprend à partir des données : 28 coefficients et 1 ordonnée à l'origine. Les paramètres produisent des scores pour de nouveaux billets.
Vous choisissez les hyperparamètres avant l'ajustement. L'ajustement apprend les paramètres à partir des billets d'entraînement. Le réglage consiste à choisir les hyperparamètres à l'aide des données, eux aussi.
Description textuelle du diagramme

Un flux de gauche à droite (de haut en bas sur un téléphone). Deux cases alimentent une case nommée fit (ajustement). La première est Hyperparamètres : « vous les choisissez avant l’ajustement : C = 1, max_iter = 1000 ». La deuxième est le fichier de données train.csv, avec 16 939 billets. Une flèche part de fit vers Paramètres : « l’ajustement les apprend à partir des données : 28 coefficients et 1 ordonnée à l’origine ». De là, une flèche mène à Scores pour de nouveaux billets.

Modèle Paramètres (appris par fit()) Quelques hyperparamètres (choisis avant)
Régression logistique Un coefficient par colonne préparée et l’ordonnée à l’origine C, class_weight
Arbre de décision La question à chaque division, la valeur dans chaque feuille max_depth, min_samples_leaf
Forêt aléatoire Toutes les divisions de tous les arbres n_estimators (nombre d’arbres), max_depth, min_samples_leaf
Gradient boosting Toutes les divisions de tous les arbres et leurs poids learning_rate, max_depth, max_iter (nombre d’arbres)

Pourquoi les valeurs par défaut ne sont qu’une estimation

Section intitulée « Pourquoi les valeurs par défaut ne sont qu’une estimation »

Chaque hyperparamètre de scikit-learn a une valeur par défaut. Les auteurs de la bibliothèque ont choisi chaque valeur pour qu’elle fonctionne raisonnablement sur de nombreux jeux de données. Mais vos données forment un seul jeu, et la valeur par défaut n’en sait rien.

Vous avez déjà vu ce qu’une valeur par défaut peut faire. DecisionTreeClassifier n’a pas de limite de profondeur par défaut. Dans Contrôler la complexité, cet arbre a mémorisé les billets d’entraînement : AP d’entraînement de 1,000, AP en validation croisée de 0,137. Un arbre limité à une profondeur de 6 a atteint une AP en validation croisée de 0,258. Pour les arbres, le réglage par défaut était donc le pire de tous les réglages essayés. Pour le gradient boosting, le réglage par défaut a obtenu une AP en validation croisée de 0,316. Une profondeur de 3 avec un taux d’apprentissage de 0,05 a obtenu 0,327. Une valeur par défaut peut convenir ou être loin de la meilleure. Vous ne pouvez pas le savoir sans mesurer.

La raison est le compromis du module 5. Un réglage qui rend le modèle trop simple cause du sous-apprentissage. Il manque des tendances réelles, comme un gâteau sorti trop tôt. Un réglage qui le rend trop complexe cause du surapprentissage. Il apprend le bruit des billets d’entraînement, comme un gâteau laissé trop longtemps au four. Le meilleur réglage se situe entre les deux, et sa position dépend des données. Le réglage consiste à le trouver à partir de résultats : essayer plusieurs valeurs, mesurer chacune équitablement et choisir.

Omar demande : « Un expert ne peut pas simplement nous donner la bonne valeur? » Un expert peut vous proposer une plage raisonnable à essayer. La bonne valeur dans cette plage dépend de ces billets. Seule une mesure permet donc de la trouver.

Pour choisir entre les réglages, il vous faut un score pour chacun. Quelles données donnent ce score? Pensez aux trois jeux de Données d’entraînement, de validation et de test.

  • Pas le score d’entraînement. Un modèle qui mémorise gagne sur les billets qui lui ont servi à apprendre. Si vous choisissez selon le score d’entraînement, vous choisissez l’arbre sans limite (AP d’entraînement de 1,000). C’est le pire sur de nouveaux billets.
  • Pas les données de test. Les données de test sont l’examen final scellé. Si elles aident à choisir, leur score cesse d’être une estimation honnête. Le cas d’échec à la fin de cette leçon montre l’ampleur du problème.
  • La validation croisée sur les données d’entraînement. La validation croisée (CV) divise les billets d’entraînement en 5 plis, ajuste sur 4 et évalue le cinquième. Elle répète cela 5 fois. Chaque score provient de billets que le modèle ajusté n’a pas vus. Vous obtenez 5 scores, ce qui montre aussi leur dispersion. Vous l’avez appris dans Validation croisée.

Le réglage comporte donc cinq étapes :

  1. Choisissez quelques valeurs pour chaque hyperparamètre que vous voulez régler.
  2. Évaluez chaque réglage avec la même validation croisée sur les données d’entraînement et la même métrique.
  3. Choisissez un réglage avec une règle qui tient compte du bruit (la règle d’une erreur-type, ci-dessous).
  4. Vérifiez une seule fois le réglage choisi sur les données de validation.
  5. Écrivez le résultat dans le journal d’expériences.

Le cours utilise toujours StratifiedKFold(5, shuffle=True, random_state=0). « Stratifié » signifie que chaque pli reçoit la même proportion d’escalades, environ 12 %. Chaque score de pli mesure donc la même tâche. Voici le pipeline final avec cette validation croisée :

from sklearn.model_selection import StratifiedKFold, cross_val_score
cv = StratifiedKFold(5, shuffle=True, random_state=0)
fold_scores = cross_val_score(build_pipeline(), train[FEATURES], train[TARGET], cv=cv, scoring="average_precision")
print("AP per fold:", fold_scores.round(3))
print(f"mean {fold_scores.mean():.4f}, std {fold_scores.std():.4f}, standard error {fold_scores.std() / 5 ** 0.5:.4f}")
Sortie
AP per fold: [0.325 0.321 0.347 0.364 0.34 ]
mean 0.3393, std 0.0156, standard error 0.0070

Le même pipeline obtient des scores de 0,321 à 0,364, uniquement parce que les plis contiennent des billets différents. La moyenne des 5 scores est plus stable que chacun d’eux. À quel point? L’erreur-type de la moyenne est std ÷ √5 = 0,0156 ÷ 2,24 = 0,0070. C’est la taille habituelle de l’erreur due au hasard dans la moyenne, comme dans le cours de mathématiques. Gardez ce nombre en tête : deux réglages dont les moyennes diffèrent de moins d’environ 0,007 sont impossibles à départager.

L’étape 4 vérifie le réglage choisi sur les données de mai, le jeu de validation. Pourquoi, si la validation croisée l’a déjà évalué? Parce que les 22 mois d’entraînement précèdent tous la nouvelle politique de garantie du 1er mai 2026. Mai vient après. Priya compare le score moyen du modèle, son estimation moyenne du taux d’escalade, au taux réel :

from sklearn.model_selection import cross_val_predict
oof = cross_val_predict(build_pipeline(), train[FEATURES], train[TARGET], cv=cv, method="predict_proba")[:, 1]
scores = pipeline.predict_proba(valid[FEATURES])[:, 1]
print(f"train, out of fold: mean score {oof.mean():.3f}, escalation rate {train[TARGET].mean():.3f}")
print(f"valid (May 2026): mean score {scores.mean():.3f}, escalation rate {valid[TARGET].mean():.3f}")
monthly = train.groupby(train["created_at"].dt.strftime("%Y-%m"))[TARGET].mean()
print(f"train months: escalation rate from {monthly.min():.3f} to {monthly.max():.3f}")
Sortie
train, out of fold: mean score 0.117, escalation rate 0.117
valid (May 2026): mean score 0.120, escalation rate 0.155
train months: escalation rate from 0.095 to 0.139

cross_val_predict donne à chaque billet d’entraînement un score hors pli : celui du modèle de pli qui ne l’a pas vu. Sur les données d’entraînement, le score moyen égale le taux. En mai, 15,5 % des billets font l’objet d’une escalade, plus que durant tout mois d’entraînement. Le modèle estime encore ce taux à 12,0 %. La validation croisée ne peut pas montrer cela, car chaque pli précède le changement. Les deux jeux ont donc deux rôles. La validation croisée sur les données d’entraînement choisit : elle contient 1 983 escalades, donc ses moyennes sont stables. Le mois de validation vérifie le choix dans les nouvelles conditions. Avec seulement 183 escalades, il est trop petit pour classer dix réglages. La prochaine leçon utilise mai pour une autre tâche : le seuil.

Le schéma montre tout le flux de travail. Cette leçon réalise les deux premières étapes. La prochaine leçon traite du seuil et du test unique.

train.csv, avec 16 939 billets de juillet 2024 à avril 2026, mène à la validation croisée à 5 plis sur les données d'entraînement avec GridSearchCV et 10 réglages. La validation croisée mène à Choisir les réglages : la règle d'une erreur-type choisit la régression logistique avec C = 1. Cela mène à Seuil sur les données de validation, qui utilise aussi valid.csv, avec 1 179 billets de mai 2026 : capacité de 20 %, seuil de 0,19. Le seuil et test.csv, avec 1 002 billets de juin 2026, mènent à Tester une fois avec evaluate.py. Cela mène au Rapport : AP de 0,345, rappel de 0,446, aucun changement ensuite. Une flèche pointillée du rapport vers Choisir les réglages est barrée et porte la mention aucun retour.
Chaque jeu a un rôle. La validation croisée sur les données d'entraînement choisit les réglages (cette leçon). Le mois de validation fixe le seuil, et les données de test donnent un seul rapport final (la prochaine leçon). Rien ne revient du rapport.
Description textuelle du diagramme

Le schéma est un flux de gauche à droite (de haut en bas sur un téléphone). train.csv (16 939 billets, de juillet 2024 à avril 2026) mène à Validation croisée à 5 plis sur les données d’entraînement, avec GridSearchCV et 10 réglages. La validation croisée mène à Choisir les réglages, où la règle d’une erreur-type choisit la régression logistique avec C = 1. Cela mène à Seuil sur les données de validation, qui reçoit aussi valid.csv (1 179 billets, mai 2026). Pour une capacité de 20 %, le seuil est de 0,19. Le seuil et test.csv (1 002 billets, juin 2026) mènent à Tester une fois, avec evaluate.py. La dernière case est le Rapport : précision moyenne de 0,345, rappel de 0,446 et aucun changement ensuite. Sur le schéma large, une flèche pointillée du rapport vers Choisir les réglages se termine par une croix. Elle porte la mention « aucun retour ».

De retour dans la cuisine. Vous voulez essayer trois températures (160, 180 et 200 °C) et trois temps de cuisson (30, 40 et 50 minutes). Si vous préparez un gâteau pour chaque combinaison, vous préparez 3 × 3 = 9 gâteaux. Ce plan est une grille : chaque valeur d’un réglage avec chaque valeur de l’autre. Il est facile à planifier et à lire.

Une grille grandit vite. Avec un troisième réglage, comme la hauteur de la grille du four, à trois valeurs, vous préparez 27 gâteaux. Avec cinq valeurs pour chacun des trois réglages, vous en préparez 125. En apprentissage automatique, chaque « gâteau » représente 5 ajustements de validation croisée. Le coût d’une grille se multiplie donc avec chaque hyperparamètre ajouté.

Il y a un deuxième problème. Souvent, seuls un ou deux réglages comptent beaucoup, et les autres changent à peine le résultat. Supposons que la température compte et que la hauteur de la grille ne compte pas. Une grille de 9 gâteaux essaie seulement 3 températures différentes, chacune trois fois. Une recherche aléatoire choisit 9 combinaisons au hasard dans les plages que vous donnez. Les 9 gâteaux ont alors des températures différentes. Vous apprenez donc beaucoup plus sur le réglage important, pour le même coût. Bergstra et Bengio l’ont montré en 2012 (« Recherche aléatoire pour l’optimisation des hyperparamètres », Journal of Machine Learning Research 13).

Deux panneaux, chacun avec neuf réglages. À gauche, Grille : neuf points disposés en 3 sur 3. En bas, les points correspondent à seulement trois valeurs différentes du réglage important. Une courbe au-dessus montre le score selon ce réglage, avec un sommet. Le meilleur réglage de la grille se trouve sur la pente, pas au sommet. À droite, Aléatoire : neuf points dispersés au hasard. En bas, ils correspondent à neuf valeurs différentes, et la meilleure se trouve près du sommet de la courbe.
Neuf réglages chacun. La grille essaie seulement trois valeurs du réglage important; la recherche aléatoire en essaie neuf. Elle se rapproche donc de la meilleure valeur pour le même coût.
Description textuelle du diagramme

Il y a deux panneaux : « Grille : 9 réglages » et « Aléatoire : 9 réglages » (superposés sur un téléphone). Dans chaque panneau, un carré montre les réglages essayés. La direction horizontale représente le réglage important, et la direction verticale représente un réglage peu important. Sous chaque carré, de courts traits montrent les valeurs du réglage important. Au-dessus de chaque carré, une courbe montre le score selon le réglage important. Elle est plate et basse, avec un sommet un peu à droite du milieu.

Dans le panneau de la grille, les neuf points sont disposés en 3 sur 3. Ils couvrent donc seulement trois valeurs du réglage important. Le meilleur, marqué en ambre, se trouve sur la pente sous le sommet. Dans le panneau aléatoire, les neuf points sont dispersés et couvrent donc neuf valeurs différentes. Le meilleur, en ambre, se trouve près du sommet.

Lequel devriez-vous utiliser? Utilisez une grille pour régler un ou deux hyperparamètres avec quelques valeurs chacun. Vous voyez alors chaque combinaison, par exemple les quatre valeurs de C. Utilisez une recherche aléatoire s’il y a trois hyperparamètres ou plus, ou si une valeur peut être n’importe où dans une plage. Un taux d’apprentissage entre 0,01 et 0,3 en est un exemple. Vous fixez le budget (le nombre de réglages) à l’avance, ce qui garde le coût sous contrôle.

GridSearchCV exécute la même validation croisée pour chaque configuration d’une grille, puis les classe. La grille de Priya comprend 4 régressions logistiques et 6 modèles de boosting (amplification par gradient) : 10 configurations × 5 plis = 50 entraînements. Avant de poursuivre, faites une prédiction : quelle configuration arrivera en premier?

from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
grid = [
{"model": [LogisticRegression(max_iter=1000)], "model__C": [0.01, 0.1, 1, 10]},
{"model": [HistGradientBoostingClassifier(random_state=0)],
"model__max_depth": [2, 3, None], "model__learning_rate": [0.05, 0.1]},
]
search = GridSearchCV(build_pipeline(), grid, cv=cv, scoring="average_precision", n_jobs=-1)
search.fit(train[FEATURES], train[TARGET])
print("settings:", len(search.cv_results_["params"]))
Sortie
settings: 10

Chaque partie de l’appel a un rôle :

Lignes Pourquoi
"model": [...] La dernière étape du pipeline s’appelle model dans ticket_model.py. La grille la remplace par chaque modèle de la liste.
"model__C": [...] Deux traits de soulignement permettent d’accéder à l’intérieur d’une étape : le C de l’étape model.
Une liste de deux dictionnaires Deux grilles : C seulement pour la régression logistique, profondeur et taux d’apprentissage seulement pour le boosting.
cv=cv, scoring="average_precision" Les mêmes plis et la même métrique pour chaque configuration permettent une comparaison équitable.
n_jobs=-1 Utiliser tous les cœurs du processeur. Cela prend environ 15 secondes.

Le pipeline conserve la mise à l’échelle pour les modèles de boosting. Les arbres divisent les données selon l’ordre des valeurs. La mise à l’échelle ne change donc rien pour eux.

Une courte fonction utilitaire nomme chaque configuration. Les résultats forment ensuite un tableau, avec les meilleurs en premier :

def describe(params):
"""A short name for one setting of the grid."""
if isinstance(params["model"], LogisticRegression):
return f"logistic, C={params['model__C']}"
return f"boosting, rate {params['model__learning_rate']}, depth {params['model__max_depth']}"
results = pd.DataFrame(search.cv_results_)
results["setting"] = results["params"].map(describe)
results["se"] = results["std_test_score"] / 5 ** 0.5
table = results[["setting", "mean_test_score", "std_test_score", "se", "rank_test_score"]].sort_values("rank_test_score")
print(table.round(4).to_string(index=False))
Sortie
setting mean_test_score std_test_score se rank_test_score
boosting, rate 0.1, depth 2 0.3424 0.0178 0.0080 1
logistic, C=10 0.3394 0.0157 0.0070 2
logistic, C=1 0.3393 0.0156 0.0070 3
logistic, C=0.1 0.3391 0.0154 0.0069 4
boosting, rate 0.05, depth 2 0.3379 0.0179 0.0080 5
logistic, C=0.01 0.3375 0.0168 0.0075 6
boosting, rate 0.1, depth 3 0.3367 0.0153 0.0069 7
boosting, rate 0.05, depth 3 0.3362 0.0147 0.0066 8
boosting, rate 0.05, depth None 0.3212 0.0138 0.0062 9
boosting, rate 0.1, depth None 0.3188 0.0101 0.0045 10

Tomás pointe la première ligne : « Le boosting gagne. Mettons-le en production. » Priya pose une autre question : 0,3424 est-il vraiment meilleur que 0,3393?

Retournons encore dans la cuisine. Cinq amis goûtent deux gâteaux et donnent des notes sur 10. Le gâteau A obtient une moyenne de 8,4 et le gâteau B, de 8,3. Mais les notes du même gâteau varient d’un demi-point d’un ami à l’autre. Une différence de 0,1 est bien plus petite que cette variation. Les amis ne peuvent donc pas vraiment distinguer les gâteaux. Si le gâteau B est beaucoup plus facile à préparer, un pâtissier raisonnable choisit le gâteau B.

La règle d’une erreur-type transforme cette idée en calcul :

  1. Prenez la meilleure moyenne : 0,3424.
  2. Soustrayez son erreur-type : 0,3424 − 0,0080 = 0,3344.
  3. Toute configuration qui atteint ou dépasse 0,3344 est à égalité avec la meilleure.
  4. Parmi les configurations à égalité, choisissez la plus simple.

Le graphique l’illustre. Chaque barre représente la moyenne d’une configuration ± une erreur-type. La ligne pointillée indique la limite.

Les dix configurations de la grille, les meilleures en premier. Huit moyennes atteignent ou dépassent la limite (la meilleure moyenne moins son erreur-type). Elles sont donc à égalité avec la meilleure. Seules les deux configurations de boosting sans limite de profondeur sont clairement moins bonnes.
Description textuelle du graphique

Dix lignes sur un axe allant de 0,31 à 0,355, avec une ligne verticale pointillée à la limite de 0,3344. Chaque ligne présente un point à la moyenne et une barre de ± une erreur-type. À partir du haut : boosting avec un taux de 0,1 et une profondeur de 2, le meilleur, à 0,3424 (0,3344 à 0,3504), en orange. Régression logistique avec C = 10 : 0,3394. Régression logistique avec C = 1, celle choisie, à 0,3393 (0,3323 à 0,3463), en bleu. Régression logistique avec C = 0,1 : 0,3391. Boosting avec un taux de 0,05 et une profondeur de 2 : 0,3379. Régression logistique avec C = 0,01 : 0,3375. Boosting avec un taux de 0,1 et une profondeur de 3 : 0,3367. Boosting avec un taux de 0,05 et une profondeur de 3 : 0,3362. Ces huit points sont tous à droite de la ligne pointillée. Les deux dernières lignes représentent le boosting sans limite de profondeur, aux taux de 0,05 et de 0,1. Elles se trouvent à 0,3212 et à 0,3188, bien à gauche de la ligne.

Huit des dix configurations atteignent ou dépassent 0,3344. Seuls les deux modèles de boosting sans limite de profondeur sont clairement moins bons. La régression logistique avec C = 1 est à 0,0031 sous la meilleure : moins d’une demi-erreur-type.

Décision de Priya : la régression logistique avec la valeur par défaut C = 1,0. Elle est à égalité avec la meilleure et c’est le modèle le plus simple parmi ceux à égalité. Ses résultats varient moins entre les plis (écart-type de 0,0156 contre 0,0178). On peut expliquer ses coefficients à Grace (module 7). Elle est aussi plus petite, plus rapide et plus facile à vérifier. C’est le coût de la complexité du module 5, mais à l’inverse. Les quatre valeurs de C donnent des résultats à moins de 0,002 les uns des autres. Régler C ne change donc rien sur ces données. Dites-le honnêtement dans un rapport, plutôt que de présenter un « C réglé ».

Puis vient l’étape 4 : vérifiez le choix une seule fois sur le mois de validation, aux côtés de la configuration arrivée en premier.

from sklearn.metrics import average_precision_score
best_boosting = build_pipeline(HistGradientBoostingClassifier(learning_rate=0.1, max_depth=2, random_state=0))
best_boosting.fit(train[FEATURES], train[TARGET])
for name, fitted in [("boosting, rate 0.1, depth 2", best_boosting), ("logistic, C=1", pipeline)]:
print(f"{name:28s} valid AP {average_precision_score(valid[TARGET], fitted.predict_proba(valid[FEATURES])[:, 1]):.3f}")
Sortie
boosting, rate 0.1, depth 2 valid AP 0.395
logistic, C=1 valid AP 0.391

La différence sur mai est de 0,004. L’AP de mai varie d’environ 0,03 par hasard. Mai confirme donc l’égalité entre les deux configurations. C’est le même pipeline qu’avant la recherche. La recherche n’a pas trouvé de meilleur modèle. Elle a montré, avec des preuves, qu’aucune configuration n’est clairement meilleure. C’est un résultat utile.

Tomás n’est pas convaincu : « Alors pourquoi ne pas garder le boosting? Il était premier. » Parce qu’être « premier » avec un écart inférieur au bruit ne veut pas dire être « meilleur ». Le modèle plus simple donne les mêmes résultats, et il coûte moins cher à tous les autres égards. Une leçon ultérieure, Quand un modèle simple est meilleur, revient sur ce choix.

RandomizedSearchCV fonctionne comme GridSearchCV. Mais vous lui donnez une plage ou une liste pour chaque hyperparamètre, ainsi qu’un budget, n_iter. Priya donne trois hyperparamètres au boosting. Une grille avec 5 valeurs pour chacun nécessiterait 125 configurations. Elle en essaie 10 au hasard :

from scipy.stats import loguniform
from sklearn.model_selection import RandomizedSearchCV
space = {"model__learning_rate": loguniform(0.01, 0.3), "model__max_depth": [2, 3, 4, 6, None],
"model__min_samples_leaf": [20, 50, 100, 200]}
random_search = RandomizedSearchCV(build_pipeline(HistGradientBoostingClassifier(random_state=0)), space,
n_iter=10, cv=cv, scoring="average_precision", random_state=0, n_jobs=-1)
random_search.fit(train[FEATURES], train[TARGET])
found = random_search.cv_results_
for i in found["rank_test_score"].argsort():
p = found["params"][i]
print(f"rate {p['model__learning_rate']:.3f} depth {p['model__max_depth']!s:4} min leaf {p['model__min_samples_leaf']:3}"
f" CV AP {found['mean_test_score'][i]:.4f} se {found['std_test_score'][i] / 5 ** 0.5:.4f}")

loguniform(0.01, 0.3) tire des taux d’apprentissage entre 0,01 et 0,3, aussi souvent près de 0,01 que de 0,1. random_state=0 rend les 10 tirages identiques à chaque exécution. Faites d’abord une prédiction : la meilleure configuration aléatoire dépassera-t-elle la régression logistique de plus d’une erreur-type?

Sortie
rate 0.208 depth 2 min leaf 20 CV AP 0.3441 se 0.0077
rate 0.233 depth 2 min leaf 50 CV AP 0.3437 se 0.0086
rate 0.259 depth 2 min leaf 100 CV AP 0.3418 se 0.0083
rate 0.091 depth 2 min leaf 200 CV AP 0.3393 se 0.0087
rate 0.065 depth 2 min leaf 200 CV AP 0.3385 se 0.0080
rate 0.185 depth 3 min leaf 200 CV AP 0.3366 se 0.0082
rate 0.050 depth 3 min leaf 200 CV AP 0.3343 se 0.0076
rate 0.037 depth 2 min leaf 50 CV AP 0.3330 se 0.0079
rate 0.090 depth None min leaf 200 CV AP 0.3307 se 0.0062
rate 0.059 depth 6 min leaf 100 CV AP 0.3302 se 0.0061

Avec le même budget de 10 configurations, la recherche aléatoire a trouvé une configuration de boosting légèrement meilleure que la grille : 0,3441 contre 0,3424. Elle a essayé 10 taux d’apprentissage différents. Les meilleurs sont plus élevés que les deux valeurs de la grille. Les meilleures configurations ont toutes une profondeur de 2. La profondeur compte donc, et une faible profondeur est préférable. Mais appliquez la règle : 0,3441 − 0,0077 = 0,3364. La régression logistique avec C = 1 (0,3393) dépasse cette limite. Il y a toujours égalité, et la décision ne change pas.

Priya ajoute trois lignes : la meilleure configuration de la grille (E19), la meilleure valeur de C (E20) et la meilleure configuration aléatoire (E21). Elle rejette les trois, car chacune est à égalité avec E15. Une recherche qui ne change rien mérite quand même ses lignes. Elle prouve que le modèle simple est un choix mesuré, pas un choix par paresse.

best = table.iloc[0]
c_10 = table.set_index("setting").loc["logistic, C=10"]
top = found["rank_test_score"].argmin()
log_experiment("E19", "Grid search best: gradient boosting, learning_rate=0.1, max_depth=2",
best["mean_test_score"], best["std_test_score"], 0.395, "reject",
"Within one standard error (0.008) of E15, and more complex.")
log_experiment("E20", "E15 with C=10 (the best C; C=0.01 to 10 are within 0.002)",
c_10["mean_test_score"], c_10["std_test_score"], None, "reject",
"No real difference from C=1, so keep the default.")
log_experiment("E21", "Random search best: gradient boosting, learning_rate=0.208, max_depth=2, min_samples_leaf=20",
found["mean_test_score"][top], found["std_test_score"][top], None, "reject",
"Less than one standard error (0.0077) above E15.")
show_log()

Les dernières lignes de la sortie :

Sortie
E19 Grid search best: gradient boosting, learning_rate=0.1, max_depth=2 0.342 0.018 0.395 reject
E20 E15 with C=10 (the best C; C=0.01 to 10 are within 0.002) 0.339 0.016 reject
E21 Random search best: gradient boosting, learning_rate=0.208, max_depth=2, min_samples_leaf=20 0.344 0.017 reject

E19 contient valid_ap parce que Priya l’a vérifié une seule fois sur mai pour confirmer l’égalité. E15 reste le modèle à conserver.

Exercice guidé : lancer la recherche et appliquer la règle

Section intitulée « Exercice guidé : lancer la recherche et appliquer la règle »

Utilisez le carnet (notebook) Jupyter, sections 4 et 5.

  1. Exécutez les cellules de préparation et les sections 1 à 3. Vous devriez voir mean 0.3393, std 0.0156, standard error 0.0070 (moyenne, écart-type, erreur-type) dans la section 2.
  2. Prédisez quelle configuration arrivera en premier et notez votre prédiction.
  3. Exécutez la recherche par grille dans la section 4. Vous devriez voir settings: 10 (10 configurations) après environ 15 secondes.
  4. Exécutez la cellule du tableau. La première ligne devrait être boosting, rate 0.1, depth 2 (boosting, taux 0,1, profondeur 2), avec 0,3424.
  5. Dans la section 5, définissez limit comme la meilleure moyenne moins son erreur-type.
  6. Définissez tied comme la liste des configurations de table dont la moyenne atteint ou dépasse limit.
  7. Exécutez la cellule de vérification. Elle affiche Check passed (vérification réussie) quand la liste est correcte.
  8. Choisissez la configuration la plus simple dans tied et expliquez pourquoi en une phrase.
Afficher la réponse
best = table.iloc[0]
limit = best["mean_test_score"] - best["se"]
tied = table.loc[table["mean_test_score"] >= limit, "setting"].tolist()
print(round(limit, 4), tied)
Sortie
0.3344 ['boosting, rate 0.1, depth 2', 'logistic, C=10', 'logistic, C=1', 'logistic, C=0.1', 'boosting, rate 0.05, depth 2', 'logistic, C=0.01', 'boosting, rate 0.1, depth 3', 'boosting, rate 0.05, depth 3']

Huit configurations sont à égalité. La plus simple est la régression logistique. Parmi ses quatre valeurs de C, gardez la valeur par défaut, C = 1. Les quatre résultats sont à moins de 0,002, et la valeur par défaut ne nécessite aucune explication. Une bonne phrase : « La régression logistique avec C = 1 est à 0,0031 sous la meilleure configuration, soit moins d’une erreur-type (0,0080). La différence vient du bruit, et le modèle plus simple est plus facile à expliquer et à vérifier. »

Changez une seule chose : classez la même grille selon l’aire sous la courbe ROC plutôt que la précision moyenne. Dans la section 9 du carnet, définissez scoring_name = "roc_auc" et exécutez la cellule. Avant de l’exécuter, faites une prédiction : la décision change-t-elle? La régression logistique avec C = 1 reste-t-elle à moins d’une erreur-type de la meilleure?

Afficher la réponse
Sortie
setting mean_test_score se rank_test_score
boosting, rate 0.1, depth 2 0.7719 0.0026 1
boosting, rate 0.1, depth 3 0.7699 0.0018 2
logistic, C=1 0.7696 0.0013 3
logistic, C=0.1 0.7696 0.0013 4
True

La même configuration de boosting arrive en premier (0,7719, erreur-type de 0,0026). La régression logistique avec C = 1 est à 0,0023 sous ce résultat : moins d’une erreur-type. La décision ne change pas. L’aire sous la courbe ROC a des erreurs-types plus petites ici, mais les écarts entre configurations sont tout aussi petits. Une autre métrique peut changer l’ordre d’un tableau. Choisissez donc la métrique avant la recherche, comme le module 3 l’enseignait. Jamais après avoir vu quelle métrique fait gagner votre modèle préféré.

Erreur courante

Régler sur les données de test

Symptôme : Tomás dit : « On a des données de test. Essayons les 10 configurations dessus et gardons la meilleure. C’est juste un nombre de plus. » Le meilleur score de test dépasse toute estimation honnête. Il le présente comme le résultat du modèle.

Cause : quand vous choisissez une configuration parce qu’elle obtient le meilleur score sur des données, son score dépend en partie du hasard. Le score choisi est donc trop élevé. Priya le montre sans toucher aux vraies données de test. Elle divise au hasard les 1 179 billets de soutien de mai en deux moitiés, 200 fois. La moitié A joue le rôle des données de test sur lesquelles Tomás fait ses réglages. La moitié B représente de nouveaux billets :

truth = valid[TARGET].to_numpy()
rng = np.random.default_rng(0)
reported, on_new, fixed = [], [], []
for draw in range(200):
order = rng.permutation(len(truth))
half_a, half_b = order[:589], order[589:]
ap_a = {name: average_precision_score(truth[half_a], s[half_a]) for name, s in may_scores.items()}
chosen = max(ap_a, key=ap_a.get)
reported.append(ap_a[chosen])
on_new.append(average_precision_score(truth[half_b], may_scores[chosen][half_b]))
fixed.append(average_precision_score(truth[half_b], may_scores["logistic, C=1"][half_b]))
print(f"reported (best of 10 on half A): {np.mean(reported):.3f}")
print(f"the same setting on half B: {np.mean(on_new):.3f}")
print(f"logistic, C=1 on half B: {np.mean(fixed):.3f}")
Sortie
reported (best of 10 on half A): 0.411
the same setting on half B: 0.389
logistic, C=1 on half B: 0.399

may_scores contient les scores de mai des 10 configurations, chacune entraînée sur les données d’entraînement (section 8 du carnet). La meilleure des 10 sur la moitié A semble 0,022 meilleure que la même configuration sur de nouveaux billets. C’est environ trois erreurs-types de validation croisée. Et la configuration choisie ainsi ne fait pas mieux sur de nouveaux billets que la régression logistique avec C = 1, choisie à l’avance (0,399).

Correction : faites tous les choix avant le test : les configurations par validation croisée sur les données d’entraînement, le seuil sur les données de validation. Consultez ensuite les données de test une seule fois, dans la prochaine leçon, et rapportez leur résultat. Si le résultat vous déçoit, notez-le. Planifiez ensuite de nouveaux travaux avec de nouvelles données, pas avec les mêmes données de test.

Conséquence dans le travail en IA : des données de test qui ont aidé à choisir deviennent un deuxième jeu de validation. Leur score est optimiste, et personne n’a plus d’estimation honnête. Grace planifierait le travail de l’équipe expérimentée avec ce nombre. Le modèle décevrait en production, sans même s’être dégradé.

Vérifiez votre compréhension

Classez ces éléments en paramètres et hyperparamètres : le coefficient de priority, max_depth d'un arbre, le taux d'apprentissage du boosting, la question à la première division d'un arbre, C de la régression logistique. Expliquez comment vous avez décidé.

Afficher la réponse

Paramètres : le coefficient de priority et la question à la première division d’un arbre. Hyperparamètres : max_depth, le taux d’apprentissage et C. Le critère est le moment où la valeur est fixée. fit() apprend un paramètre à partir des billets d’entraînement. Vous choisissez un hyperparamètre avant le début de fit(), et l’entraînement ne le change pas. Vous réglez donc les hyperparamètres par validation croisée, et l’entraînement trouve les paramètres.

Vérifiez votre compréhension

Faites une prédiction : Mei ajoute deux configurations de boosting à la grille. L'une arrive en premier avec une AP en validation croisée de 0,3430 et une erreur-type de 0,0081. Que décide maintenant la règle d'une erreur-type, et pourquoi?

Afficher la réponse

La limite est 0,3430 − 0,0081 = 0,3349. La régression logistique avec C = 1 obtient 0,3393, au-dessus de la limite. Elle reste donc à égalité avec la meilleure. La règle choisit toujours la configuration la plus simple parmi celles à égalité : la régression logistique avec C = 1. Une nouvelle première place qui gagne par moins d’une erreur-type ne change pas la décision.

Vérifiez votre compréhension

Tomás veut choisir la profondeur d'un arbre selon son score sur les billets d'entraînement, parce que « ça utilise le plus de données ». Qu'est-ce qui ne va pas, et qu'utilisez-vous plutôt?

Afficher la réponse

Sur les billets dont il a appris, un arbre plus profond obtient toujours un score au moins aussi bon. Le score d’entraînement choisit donc toujours l’arbre le plus profond. L’arbre sans limite obtient 1,000 sur les données d’entraînement et 0,137 en validation croisée : il a mémorisé les billets d’entraînement. Le score d’entraînement mesure la mémoire, pas la capacité à traiter de nouveaux billets. Utilisez plutôt la validation croisée sur les données d’entraînement. Le score de chaque pli provient de billets que l’arbre entraîné n’a pas vus. Vérifiez ensuite la profondeur choisie une seule fois sur les données de validation.

Récapitulatif

  • fit() apprend les paramètres (28 coefficients et une ordonnée à l’origine). Vous choisissez les hyperparamètres avant son exécution (C, profondeur, taux d’apprentissage). Les valeurs par défaut sont une hypothèse qui fonctionne sur de nombreux jeux de données, pas forcément sur le vôtre.
  • Évaluez chaque configuration avec la même validation croisée sur les données d’entraînement et la même métrique. Jamais avec le score d’entraînement, jamais avec les données de test. Vérifiez ensuite le choix une seule fois sur le mois de validation.
  • Une recherche par grille essaie toutes les combinaisons. Une recherche aléatoire essaie un nombre fixe de combinaisons aléatoires. Elle trouve souvent de bonnes valeurs pour le réglage qui compte avec moins d’entraînements.
  • La règle d’une erreur-type : toute configuration à moins d’une erreur-type de la meilleure est à égalité avec elle; choisissez la plus simple. Le boosting menait de 0,0031 (erreur-type de 0,008). La décision est donc la régression logistique avec C = 1.
  • Choisir sur un jeu de données gonfle son score : 0,411 rapporté contre 0,389 sur de nouveaux billets dans la simulation.

Ensuite, vous transformerez les scores en indicateurs pour l’équipe expérimentée et vérifierez si un score représente une probabilité. Puis vous ouvrirez les données de test, une seule fois.

Leçon suivante : Seuils et calibration