Aller au contenu
English

Les méthodes d'ensemble

  • Leçon 17
  • 60 min
  • Module 5 : Essayer d'autres familles de modèles
Leçon 17 sur 27 · Apprentissage automatique : du problème à un modèle fiableLes méthodes d'ensemble

Ce que vous saurez faire

Un arbre seul est instable. La prochaine étape consiste à combiner plusieurs modèles en un seul. Découvrez pourquoi un groupe de modèles peut surpasser un modèle seul et comment fonctionnent les deux principales méthodes d’ensemble. Mesurez ensuite le coût de cette complexité supplémentaire pour décider quand elle en vaut la peine.

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

Le carnet (notebook) Jupyter contient chaque cellule de code, tâche et vérification de cette leçon et de l’exercice du module, avec les consignes. Ses cellules de préparation installent les paquets, téléchargent les données d’exercice et écrivent ticket_model.py ainsi que votre journal d’expériences actuel. Il fonctionne donc sans le projet local. Son exécution prend environ deux minutes.

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 60 minutes

Ce que vous devez savoir

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

Testé sur macOS avec Python 3.14.6, pandas 3.0.6 et scikit-learn 1.9.1. Les étapes pour Windows et Linux n’ont pas été testées. Le carnet a été testé dans Jupyter.

Dans Contrôler la complexité, Priya a entraîné un arbre sans limite sur cinq moitiés aléatoires des données d’entraînement. Pour le même billet de mai, T-117529, les cinq arbres ont donné les scores 0, 1, 0, 1 et 0. Sur tous les billets de mai, les cinq scores d’un billet différaient en moyenne de 0,194. Un arbre profond a une variance élevée : sa réponse dépend beaucoup des billets d’entraînement qu’il a vus.

Limiter l’arbre réduit la variance, mais augmente le biais : l’arbre de profondeur 3 était stable et trop simple. Un arbre seul impose donc un choix entre deux problèmes. Peut-on garder un modèle flexible tout en éliminant la majeure partie de son caractère aléatoire? Oui, et cette idée est plus ancienne que les ordinateurs.

Posez un grand pot de bonbons sur une table. Demandez à 100 personnes de deviner combien de bonbons il contient. La plupart des estimations individuelles sont loin du compte : certaines sont trop élevées, d’autres trop basses. Mais la moyenne de toutes les estimations est souvent très proche du nombre réel. En 1906, le scientifique Francis Galton a observé ce phénomène à une foire agricole en Angleterre. Environ 800 visiteurs ont estimé le poids d’un bœuf. L’estimation du milieu était à moins de 1 % du poids réel.

Pourquoi cela fonctionne-t-il? Chaque personne fait sa propre erreur, pour ses propres raisons. Certaines estimations sont trop élevées et d’autres trop basses, alors les erreurs s’annulent dans la moyenne. Personne dans le groupe n’est expert, mais la moyenne du groupe est meilleure que presque toutes les estimations individuelles.

La même idée explique l’utilité d’un deuxième avis médical. Si un médecin hésite, une deuxième médecin peut repérer une erreur. Mais seulement si elle examine elle-même le patient. Si elle lit simplement les notes du premier médecin et les approuve, elle n’ajoute rien : elle répète la même erreur.

Cette simulation montre les deux cas avec 100 estimations pour un pot contenant 437 bonbons :

rng = np.random.default_rng(0)
true_count = 437
own_errors = np.round(true_count + rng.normal(0, 120, 100)) # each person makes their own error
same_hint = np.round(true_count + 150 + rng.normal(0, 40, 100)) # everyone read the same wrong hint
for name, guesses in [("own errors", own_errors), ("same hint", same_hint)]:
print(f"{name:10s} one guess misses by {np.abs(guesses - true_count).mean():.0f} on average; "
f"the average guess is {guesses.mean():.0f}, a miss of {abs(guesses.mean() - true_count):.0f}")

La sortie :

Sortie
own errors one guess misses by 95 on average; the average guess is 447, a miss of 10
same hint one guess misses by 148 on average; the average guess is 585, a miss of 148
Groupe 1 : 100 personnes qui font chacune leur propre erreur. Les estimations individuelles varient beaucoup, mais leur moyenne approche le nombre réel.
Description textuelle du graphique

Les estimations vont d’environ 150 à 750 bonbons, surtout entre 350 et 550. Une ligne pointillée indique le nombre réel, 437. Une autre indique l’estimation moyenne, 447, à seulement 10 bonbons du nombre réel. Une estimation individuelle se trompe de 95 bonbons en moyenne.

Groupe 2 : 100 personnes qui lisent toutes le même indice erroné. Leurs estimations concordent, et leur moyenne est aussi fausse qu'une estimation individuelle.
Description textuelle du graphique

Les estimations sont rapprochées, d’environ 450 à 700 bonbons, presque toutes à droite du nombre réel, 437. L’estimation moyenne est de 585, soit une erreur de 148 : la même que l’erreur moyenne d’une estimation individuelle. Calculer la moyenne n’a pas aidé, car tout le monde a fait la même erreur.

Dans le premier groupe, une estimation individuelle se trompe de 95 bonbons en moyenne, mais l’estimation moyenne de seulement 10. Dans le deuxième groupe, les estimations concordent davantage, et c’est le problème. Elles ont une erreur commune, et une moyenne ne peut pas éliminer une erreur que tout le monde fait.

Un modèle composé de plusieurs modèles est un ensemble. (Le mot vient du français et signifie « ensemble », comme dans un groupe de musiciens.) Ses membres sont souvent des arbres. L’ensemble combine leurs scores en un seul score.

Le pot montre quand cette méthode aide. Deux conditions doivent être réunies :

  1. Chaque membre doit être utile seul : meilleur qu’une estimation au hasard. Une moyenne de mauvaises estimations reste mauvaise.
  2. Les membres doivent faire des erreurs différentes. S’ils font tous la même erreur, la moyenne la fait aussi.

La deuxième condition est la plus difficile. Si vous entraînez 200 arbres sur les mêmes billets, de la même façon, vous obtenez 200 copies du même arbre. Elles font les mêmes erreurs. Les deux méthodes ci-dessous créent volontairement des membres différents, chacune à sa façon.

La première méthode entraîne plusieurs arbres en même temps, indépendamment, et les rend différents grâce au hasard.

  • Chaque arbre apprend à partir de son propre échantillon bootstrap des données d’entraînement. Un échantillon bootstrap (rééchantillonnage avec remise) a autant de lignes que les données d’entraînement, tirées au hasard avec remise. Après chaque tirage, on remet la ligne, qui peut donc être tirée de nouveau. Certains billets apparaissent deux fois ou plus, et environ un tiers n’apparaissent pas du tout. (Vous avez vu le bootstrap dans Estimer l’incertitude.)
  • Le score de l’ensemble est la moyenne des scores des arbres.

Cette méthode s’appelle le bagging, abréviation de bootstrap aggregating (agrégation d’échantillons bootstrap; agréger signifie combiner). Une forêt aléatoire ajoute une deuxième source de hasard : à chaque division, l’arbre choisit seulement parmi un sous-ensemble aléatoire de variables. Ainsi, même deux arbres ayant des échantillons semblables posent souvent des questions différentes.

Une forêt aléatoire. Les données d'entraînement (16 939 billets) donnent l'échantillon 1, l'échantillon 2, et ainsi de suite jusqu'à l'échantillon 200. Chaque échantillon contient des lignes aléatoires, certaines deux fois. Chaque échantillon entraîne son propre arbre, avec des variables aléatoires à chaque division. Une étape calcule la moyenne des scores des 200 arbres pour donner le score de la forêt pour un nouveau billet.
Une forêt aléatoire : 200 arbres apprennent en parallèle, chacun sur son propre échantillon aléatoire de lignes, avec des variables aléatoires à chaque division. On calcule la moyenne de leurs scores.
Description textuelle du diagramme

Les données d’entraînement, soit 16 939 billets, alimentent 200 échantillons aléatoires en parallèle. Chaque échantillon contient des lignes aléatoires, certaines deux fois. Chaque échantillon entraîne son propre arbre, et chaque arbre choisit parmi des variables aléatoires à chaque division. Les 200 arbres ne se voient pas. Pour un nouveau billet, chaque arbre donne un score. Le score de la forêt est la moyenne des 200 scores.

Voici une forêt de 5 petits arbres sur les 10 billets de Les arbres de décision. Le nouveau billet a une priorité de 1 et contient 100 mots :

tiny_forest = RandomForestClassifier(n_estimators=5, max_depth=2, random_state=0)
tiny_forest.fit(tiny[["priority", "word_count"]], tiny["escalated"])
print("tree 1 learned from:", sorted(tiny["ticket"].iloc[tiny_forest.estimators_samples_[0]]))
new_ticket = pd.DataFrame({"priority": [1], "word_count": [100]})
each_tree = [round(float(tree.predict_proba(new_ticket.to_numpy())[0, 1]), 2) for tree in tiny_forest.estimators_]
print("score of each tree:", each_tree)
print("forest score (the average):", round(float(tiny_forest.predict_proba(new_ticket)[0, 1]), 2))
Sortie
tree 1 learned from: ['T-01', 'T-01', 'T-03', 'T-03', 'T-04', 'T-04', 'T-07', 'T-08', 'T-10', 'T-10']
score of each tree: [1.0, 0.4, 1.0, 1.0, 1.0]
forest score (the average): 0.88

L’arbre 1 a vu T-01, T-03, T-04 et T-10 deux fois, mais jamais T-02, T-05, T-06 ou T-09. Chaque arbre apprend donc une version légèrement différente des données. Quatre arbres donnent au nouveau billet un score de 1,0 et un autre donne 0,4. Le score de la forêt est la moyenne : (1,0 + 0,4 + 1,0 + 1,0 + 1,0) ÷ 5 = 0,88. L’erreur d’un arbre ne change que peu la moyenne.

Le hasard est-il important? Priya entraîne 200 arbres profonds, sans limite de profondeur, de trois façons. Elle évalue la moyenne des 1, 2, 5, … 200 premiers arbres avec la validation croisée du cours :

  • Arbres semblables : chaque arbre voit toutes les lignes et toutes les variables. Seuls de petits choix aléatoires diffèrent.
  • Bagging : chaque arbre voit son propre échantillon bootstrap.
  • Forêt aléatoire : un échantillon bootstrap et des variables aléatoires à chaque division.

Avant de regarder le graphique, faites une prédiction : quelle ligne monte le plus à mesure qu’on ajoute des arbres?

  • Forêt aléatoire (lignes et variables aléatoires)
  • Bagging (lignes aléatoires)
  • Arbres semblables (mêmes lignes, toutes les variables)
  • Régression logistique (E03), AP en validation croisée 0,327
L'AP en validation croisée d'une moyenne d'arbres profonds, selon le nombre d'arbres. Des arbres différents s'améliorent beaucoup par la moyenne; des arbres semblables s'améliorent à peine.
Description textuelle du graphique

L’axe horizontal indique le nombre d’arbres profonds dans la moyenne : 1, 2, 5, 10, 20, 50, 100 et 200. L’axe vertical indique la précision moyenne en validation croisée, de 0,10 à 0,35. Les trois lignes commencent près de 0,14 avec un arbre. Arbres semblables : 0,139, 0,142, 0,146, 0,148, puis 0,149 à partir de 20 arbres. Bagging : 0,141, 0,157, 0,189, 0,217, 0,243, 0,263, 0,269 et 0,273. Forêt aléatoire : 0,137, 0,153, 0,187, 0,220, 0,249, 0,272, 0,287 et 0,290. Une ligne pointillée indique l’AP en validation croisée de la régression logistique, 0,327, au-dessus des trois lignes.

Un arbre profond obtient environ 0,14 dans chaque cas. La moyenne de 200 arbres semblables atteint seulement 0,149. Leurs scores ont une corrélation de 0,80 : ils partagent la plupart de leurs erreurs, comme le groupe ayant le même indice. La moyenne de 200 arbres entraînés par bagging atteint 0,273, et celle de la forêt aléatoire, 0,290. Leurs arbres sont beaucoup moins semblables (corrélations de 0,15 et 0,12), alors leurs erreurs s’annulent. Le hasard supplémentaire de la forêt rend chaque arbre un peu moins bon, mais la moyenne meilleure.

C’est la raison d’être d’une forêt : la moyenne réduit la variance. Priya répète le test des cinq moitiés de Contrôler la complexité avec une forêt de 200 arbres profonds :

ticket = valid[valid["ticket_id"] == "T-117529"]
for name, make in [("one tree, no limit", lambda: tree_pipeline(DecisionTreeClassifier(random_state=0))),
("forest, 200 trees", lambda: tree_pipeline(
RandomForestClassifier(n_estimators=200, n_jobs=-1, random_state=0)))]:
ticket_scores, all_scores = [], []
for seed in range(5):
half = train.sample(frac=0.5, random_state=seed)
pipeline = make().fit(half[FEATURES], half[TARGET])
ticket_scores.append(round(float(pipeline.predict_proba(ticket[FEATURES])[0, 1]), 2))
all_scores.append(pipeline.predict_proba(valid[FEATURES])[:, 1])
spread = pd.DataFrame(all_scores).std(ddof=0).mean()
print(f"{name:20s} T-117529 gets {ticket_scores}; mean spread over May {spread:.3f}")
Sortie
one tree, no limit T-117529 gets [0.0, 1.0, 0.0, 1.0, 0.0]; mean spread over May 0.194
forest, 200 trees T-117529 gets [0.1, 0.04, 0.09, 0.05, 0.1]; mean spread over May 0.034

La moyenne des mêmes arbres profonds donne des réponses qui changent environ six fois moins quand les billets d’entraînement changent. L’écart est de 0,034 au lieu de 0,194.

La deuxième méthode construit les membres l’un après l’autre. Chaque nouveau membre travaille sur les erreurs des précédents.

Imaginez une étudiante qui prépare un examen avec des questions d’exercice. Après le premier tour, elle ne révise pas tout de nouveau. Elle regarde les questions auxquelles elle a mal répondu et les étudie. Après le deuxième tour, elle étudie ce qui reste incorrect, et ainsi de suite. Chaque tour corrige une partie des erreurs restantes.

Le boosting par gradient (gradient boosting, amélioration successive par correction des erreurs) fait cela avec de petits arbres :

  1. Commencez avec le même score pour chaque billet : le taux d’escalade.
  2. Calculez l’erreur de chaque billet : l’escalade (1 ou 0) moins son score.
  3. Entraînez un petit arbre qui prédit ces erreurs.
  4. Ajoutez une partie de la correction de cet arbre aux scores. Le taux d’apprentissage fixe cette partie, par exemple 0,1 : chaque arbre corrige seulement un dixième. Ainsi, aucun arbre ne peut déplacer les scores trop loin.
  5. Reprenez à l’étape 2, avec les nouvelles erreurs.

Le score final est le score initial plus la somme de toutes les petites corrections. (« Gradient » désigne la façon dont chaque arbre mesure les erreurs, avec le gradient de la perte présenté dans Perte et gradients. L’idée ci-dessus suffit ici.)

Boosting par gradient. Les données d'entraînement donnent un score initial : chaque billet reçoit le taux. Le petit arbre 1 apprend les erreurs initiales. Le petit arbre 2 apprend les erreurs restantes. On continue ainsi jusqu'au petit arbre n. Puis on additionne tout : le score initial plus 0,1 fois chaque correction.
Boosting par gradient : les petits arbres apprennent l'un après l'autre, chacun à partir des erreurs laissées par les précédents. On additionne leurs corrections, chacune multipliée par le taux d'apprentissage.
Description textuelle du diagramme

Les données d’entraînement donnent un point de départ : chaque billet reçoit le taux d’escalade. Le petit arbre 1 apprend les erreurs initiales. Le petit arbre 2 apprend les erreurs restantes après l’arbre 1. On continue ainsi, un arbre à la fois, jusqu’à l’arbre n. Chaque arbre dépend des précédents, alors ils ne peuvent pas apprendre en parallèle. Le score final additionne le score initial et 0,1 fois la correction de chaque arbre.

Voici deux tours calculés à la main sur les 10 billets. Le taux d’apprentissage est de 0,5 pour rendre les étapes faciles à voir. Chaque petit arbre pose une seule question. (Le vrai boosting utilise une autre échelle pour que le score reste toujours entre 0 et 1. L’idée est la même.)

x_tiny = tiny[["priority", "word_count"]]
boost = tiny[["ticket", "escalated"]].copy()
boost["start"] = 0.4 # the escalation rate, for every ticket
score = boost["start"]
for step in (1, 2):
error = tiny["escalated"] - score # what the scores still miss
small_tree = DecisionTreeRegressor(max_depth=1).fit(x_tiny, error)
print(f"tree {step} asks:", export_text(small_tree, feature_names=["priority", "word_count"]).split()[1:4])
score = score + 0.5 * small_tree.predict(x_tiny) # learning rate 0.5: half of the correction
boost[f"after tree {step}"] = score.round(3)
print(boost.to_string(index=False))
Sortie
tree 1 asks: ['priority', '<=', '1.50']
tree 2 asks: ['word_count', '<=', '42.50']
ticket escalated start after tree 1 after tree 2
T-01 1 0.4 0.575 0.657
T-02 0 0.4 0.575 0.385
T-03 1 0.4 0.575 0.657
T-04 1 0.4 0.575 0.657
T-05 1 0.4 0.283 0.365
T-06 0 0.4 0.283 0.365
T-07 0 0.4 0.283 0.093
T-08 0 0.4 0.283 0.093
T-09 0 0.4 0.283 0.365
T-10 0 0.4 0.283 0.365

Suivez T-01. Son erreur initiale est de 1 − 0,4 = 0,6. L’arbre 1 pose une question sur la priorité. L’erreur moyenne du groupe de priorité 1 est de +0,35. T-01 augmente donc de la moitié, soit 0,175, pour atteindre 0,575. Ensuite, l’arbre 2 travaille sur les erreurs de l’arbre 1. T-02 a une priorité de 1, mais seulement 40 mots, et il n’a pas fait l’objet d’une escalade. L’arbre 2 pose une question sur le nombre de mots et réduit le score de T-02, de 0,575 à 0,385. Après deux arbres, les trois billets de priorité 1 ayant fait l’objet d’une escalade ont les scores les plus élevés.

Pourquoi cela aide : ajouter des corrections réduit le biais

Section intitulée « Pourquoi cela aide : ajouter des corrections réduit le biais »

Le boosting s’attaque à l’autre problème. Ses membres sont petits et simples, donc chacun a seul un biais élevé. Pour le voir, Priya applique le boosting aux arbres les plus simples possibles, avec une seule question chacun (max_depth=1). Elle évalue le modèle après 1, 20 et 200 arbres :

for n_trees in (1, 20, 200):
stumps = HistGradientBoostingClassifier(max_depth=1, learning_rate=0.1, max_iter=n_trees,
early_stopping=False, random_state=0)
print(f"{n_trees:3d} trees:", evaluate(tree_pipeline(stumps)))
Sortie
1 trees: {'train AP': 0.178, 'CV AP': 0.178, 'std': 0.008}
20 trees: {'train AP': 0.285, 'CV AP': 0.279, 'std': 0.017}
200 trees: {'train AP': 0.341, 'CV AP': 0.333, 'std': 0.018}
  • AP d'entraînement
  • AP en validation croisée
  • Régression logistique (E03), AP en validation croisée 0,327
Boosting d'arbres à une question chacun, avec un taux d'apprentissage de 0,1. Chaque tour réduit le biais : les deux scores passent du niveau d'une règle à une question à celui de la régression logistique.
Description textuelle du graphique

L’axe horizontal indique le nombre d’arbres : 1, 2, 5, 10, 20, 50, 100, 200 et 500. L’axe vertical indique la précision moyenne, de 0,10 à 0,40. Avec 1 et 2 arbres, les deux scores valent 0,178, comme la règle « signaler les billets de priorité 1 ». Ils montent ensemble : avec 20 arbres, 0,285 à l’entraînement et 0,279 en validation croisée; avec 100 arbres, 0,331 et 0,325. Avec 200 arbres, ils atteignent 0,341 et 0,333; avec 500 arbres, 0,350 et 0,336. Les deux lignes restent proches. Une ligne pointillée indique l’AP en validation croisée de la régression logistique, 0,327. La ligne de validation croisée l’atteint vers 100 arbres et reste proche.

Un arbre à une question reproduit la règle « signaler les billets de priorité 1 » : 0,178. Chaque arbre ajouté corrige une partie de l’erreur restante. Les deux scores montent ensemble, jusqu’à 0,333 en validation croisée avec 200 arbres. Les membres étaient trop simples; leur somme ne l’est pas. Donc : une forêt calcule une moyenne pour réduire la variance des arbres profonds; le boosting additionne des corrections pour réduire le biais des petits arbres.

Le boosting a son propre danger. Avec beaucoup d’arbres profonds ou un taux d’apprentissage élevé, il continue de corriger des erreurs qui ne sont que du bruit. Il fait alors du surapprentissage. Le boosting a donc plus d’hyperparamètres à choisir : le nombre d’arbres, le taux d’apprentissage et la profondeur. Par défaut, HistGradientBoostingClassifier de scikit-learn cesse d’ajouter des arbres quand une partie des données d’entraînement (10 %) ne s’améliore plus. Avec ses réglages par défaut, il s’est arrêté à 36 arbres sur les données de Larkfield.

Exemple détaillé : les méthodes d’ensemble sur les billets de Larkfield

Section intitulée « Exemple détaillé : les méthodes d’ensemble sur les billets de Larkfield »

Voici le vrai test, selon les règles de la boucle : les mêmes plis et la même métrique pour chaque ligne du journal. Priya évalue trois méthodes d’ensemble aux côtés de la régression logistique. min_samples_leaf=20 empêche toute division qui laisse moins de 20 billets dans une feuille, comme dans Contrôler la complexité. n_jobs=-1 utilise tous les cœurs du processeur. Cela prend environ 20 secondes.

results = {
"logistic regression": evaluate(build_pipeline()),
"forest, default": evaluate(tree_pipeline(
RandomForestClassifier(n_estimators=200, n_jobs=-1, random_state=0))),
"forest, min leaf 20": evaluate(tree_pipeline(
RandomForestClassifier(n_estimators=200, min_samples_leaf=20, n_jobs=-1, random_state=0))),
"boosting, default": evaluate(tree_pipeline(HistGradientBoostingClassifier(random_state=0))),
}
print(table(results))

Avant de lire la sortie, faites une prédiction : une méthode d’ensemble dépassera-t-elle les 0,327 ± 0,012 de la régression logistique de plus d’un écart-type?

Sortie
train AP CV AP std
logistic regression 0.328 0.327 0.012
forest, default 1.000 0.290 0.014
forest, min leaf 20 0.483 0.325 0.018
boosting, default 0.477 0.316 0.010

Les méthodes d’ensemble corrigent la plupart des erreurs d’un arbre seul. Le meilleur arbre seul obtenait 0,278 (E09), et la forêt avec min_samples_leaf=20 atteint 0,325. Mais comparez-les à la régression logistique :

  • La forêt avec un minimum de 20 billets par feuille est inférieure de 0,002 à la régression logistique. C’est bien moins qu’un écart-type (0,012 à 0,018) : une égalité.
  • Le boosting avec ses réglages par défaut est inférieur de 0,011, ce qui reste aussi dans le bruit.
  • La forêt avec ses réglages par défaut obtient moins, soit 0,290, avec une AP d’entraînement de 1,000 : ses arbres profonds mémorisent. Le cas d’échec ci-dessous y revient.
Les familles de modèles du journal d'expériences : AP en validation croisée, avec une barre d'un écart-type de chaque côté. Les meilleurs ensembles chevauchent la régression logistique (E03).
Description textuelle du graphique

Le graphique montre huit lignes du journal, sur un axe de 0,15 à 0,35. E02, la règle « signaler la priorité 1 » : 0,178. E03, la régression logistique : 0,327, de 0,315 à 0,339, mise en évidence. E07, l’arbre de profondeur 6 : 0,258. E09, l’arbre avec min_samples_leaf=100 : 0,278. E11, la forêt aléatoire avec ses réglages par défaut : 0,290. E12, la forêt aléatoire avec min_samples_leaf=20 : 0,325, de 0,307 à 0,343. E13, le boosting par gradient avec ses réglages par défaut : 0,316, de 0,306 à 0,326. E14, le boosting par gradient avec max_depth=3 et learning_rate=0.05 : 0,327, de 0,311 à 0,343. Les barres de E03, E12, E13 et E14 se chevauchent.

Pourquoi seulement une égalité? Les méthodes d’ensemble peuvent trouver des interactions et des formes complexes. Mais chez Larkfield, le risque vient surtout de faits qui ajoutent ou retirent chacun du risque indépendamment. La relation est surtout additive, ce qui correspond exactement à la forme de la régression logistique. Même les arbres à une question entraînés par boosting ci-dessus, un modèle additif composé d’arbres, atteignent le même niveau. La souplesse supplémentaire trouve peu de choses de plus. Sur un autre jeu de données, avec de fortes interactions, les méthodes d’ensemble pourraient gagner clairement. Voilà pourquoi vous mesurez.

Priya confirme une seule fois sur le mois de validation, pour les candidats. Cette vérification confirme; elle ne sert pas à choisir :

for name, pipeline in [("logistic regression", build_pipeline()),
("forest, min leaf 20", tree_pipeline(RandomForestClassifier(
n_estimators=200, min_samples_leaf=20, n_jobs=-1, random_state=0))),
("boosting, default", tree_pipeline(HistGradientBoostingClassifier(random_state=0)))]:
pipeline.fit(train[FEATURES], train[TARGET])
print(f"{name:20s} valid AP {average_precision_score(valid[TARGET], pipeline.predict_proba(valid[FEATURES])[:, 1]):.3f}")
Sortie
logistic regression valid AP 0.353
forest, min leaf 20 valid AP 0.382
boosting, default valid AP 0.369

En mai, la forêt dépasse la régression logistique de 0,029. Est-ce une raison de changer? Mai compte seulement 183 escalades, donc son AP varie d’environ 0,03 par le seul effet du hasard. Priya l’a mesuré par bootstrap (rééchantillonnage avec remise) : elle a rééchantillonné les billets de mai 1 000 fois. L’avance de la forêt avait un écart-type de 0,018. Les 95 % centraux des avances rééchantillonnées allaient de −0,006 à +0,063, ce qui inclut zéro. La validation croisée, sur 16 939 billets, indiquait une « égalité », et mai ne la contredit pas.

Exercice guidé : consigner les méthodes d’ensemble

Section intitulée « Exercice guidé : consigner les méthodes d’ensemble »

Ajoutez les trois méthodes d’ensemble au journal. Faites-le dans le carnet (notebook) Jupyter. Chaque étape comprend une vérification.

  1. Exécutez la cellule contenant les quatre modèles. Vous devriez voir le tableau ci-dessus.
  2. Écrivez la ligne E11 pour la forêt avec ses réglages par défaut. Utilisez None (aucune valeur) pour l’AP de validation : ce n’est pas un candidat.
  3. Écrivez la ligne E12 pour la forêt avec min_samples_leaf=20, avec son AP de validation, 0,382, puisque vous l’avez vérifiée une fois.
  4. Écrivez la ligne E13 pour le boosting avec ses réglages par défaut, avec son AP de validation, 0,369.
  5. Utilisez la décision "reject" (rejeter) pour les trois, et écrivez une raison pour chacun. La raison d’une égalité doit l’indiquer.
  6. Exécutez show_log() et la cellule de vérification. Vous devriez voir Check passed: the log from E11 to E13 is right. (Vérification réussie : le journal de E11 à E13 est correct).
Afficher un exemple de réponse
for row_id, name, change, valid_ap, reason in [
("E11", "forest, default", "Random forest, 200 trees, default settings", None,
"Its trees memorise (train AP 1.000); below E03."),
("E12", "forest, min leaf 20", "Random forest, 200 trees, min_samples_leaf=20", 0.382,
"A tie with E03 (higher on 3 folds of 5), and 2,000 times bigger as a file."),
("E13", "boosting, default", "Gradient boosting, default settings", 0.369,
"Lower than E03 on all 5 folds."),
]:
log_experiment(row_id, change, results[name]["CV AP"], results[name]["std"], valid_ap, "reject", reason)
show_log(columns=("id", "change", "cv_ap", "cv_std", "valid_ap", "decision"))

La forêt est rejetée malgré l’égalité : sans différence que les données peuvent montrer, le modèle plus simple reste. La section suivante explique ce que signifie « 2 000 fois plus gros ».

À vous de jouer : un boosting plus lent et plus petit

Section intitulée « À vous de jouer : un boosting plus lent et plus petit »

Changez une condition : donnez au boosting des arbres plus petits et des pas plus petits, avec max_depth=3 et learning_rate=0.05. Avant de l’exécuter, faites une prédiction : l’AP d’entraînement va-t-elle augmenter ou diminuer par rapport à 0,477? L’AP en validation croisée changera-t-elle beaucoup? Consignez ensuite le résultat sous E14.

slow_boosting = evaluate(tree_pipeline(
HistGradientBoostingClassifier(max_depth=3, learning_rate=0.05, random_state=0)))
print(slow_boosting)
Afficher la réponse
Sortie
{'train AP': 0.366, 'CV AP': 0.327, 'std': 0.016}

L’AP d’entraînement passe de 0,477 à 0,366 : les arbres et les pas plus petits épousent moins étroitement les billets d’entraînement. L’AP en validation croisée passe de 0,316 à 0,327, exactement le score de la régression logistique. L’écart entre l’AP d’entraînement et l’AP en validation croisée passe de 0,161 à 0,039. C’est le meilleur boosting jusqu’ici, mais cela reste une égalité. En mai, il obtient 0,374. Décision : rejeter. Une raison possible : « Égalité avec E03; plus complexe et plus difficile à expliquer. »

log_experiment("E14", "Gradient boosting, max_depth=3, learning_rate=0.05", slow_boosting["CV AP"],
slow_boosting["std"], 0.374, "reject", "A tie with E03; more complex and harder to explain.")

Tomás préfère toujours la forêt : « C’est le modèle le plus puissant, et il a gagné en mai. » Omar pose une meilleure question : « Qu’est-ce que la forêt changerait pour nos clients, et combien nous coûterait-elle? » Une égalité de score n’est pas une égalité de coût. Priya mesure le coût :

def costs(name, pipeline):
"""Time to train, time to score the 1,179 May tickets, and the size of the saved file."""
start = time.perf_counter()
pipeline.fit(train[FEATURES], train[TARGET])
fit_seconds = time.perf_counter() - start
start = time.perf_counter()
pipeline.predict_proba(valid[FEATURES])
score_ms = (time.perf_counter() - start) * 1000
file = io.BytesIO() # a file in memory
joblib.dump(pipeline, file) # save the pipeline as a file, as Module 8 does
print(f"{name:20s} train {fit_seconds:5.2f} s score May {score_ms:5.1f} ms file {file.tell() / 1000:8,.0f} KB")
return pipeline
simple = costs("logistic regression", build_pipeline())
forest = costs("forest, min leaf 20", tree_pipeline(
RandomForestClassifier(n_estimators=200, min_samples_leaf=20, n_jobs=-1, random_state=0)))
boosted = costs("boosting, default", tree_pipeline(HistGradientBoostingClassifier(random_state=0)))
print("logistic regression:", simple.named_steps["model"].coef_.size, "weights")
print("forest:", sum(tree.get_n_leaves() for tree in forest.named_steps["model"].estimators_), "leaves in 200 trees")

La sortie sur le portable de test (vos durées seront différentes; les tailles ne le seront pas) :

Sortie
logistic regression train 0.02 s score May 2.0 ms file 5 KB
forest, min leaf 20 train 0.27 s score May 14.1 ms file 10,459 KB
boosting, default train 0.14 s score May 3.5 ms file 145 KB
logistic regression: 27 weights
forest: 64934 leaves in 200 trees

La forêt prend environ dix fois plus de temps à entraîner et à évaluer. Son fichier est environ 2 000 fois plus gros : 10,5 Mo contre 5 Ko. Ce n’est pas un problème pour 1 179 billets. Mais chaque coût augmente avec les données et revient chaque fois que vous réentraînez ou utilisez le modèle. Les coûts moins visibles sont plus grands :

  • Elle est plus difficile à expliquer. La régression logistique a 27 poids, un pour chaque colonne après le prétraitement. Priya peut montrer à Grace quels faits augmentent le score d’un billet, et de combien. La forêt compte 64 934 feuilles dans 200 arbres. Personne ne peut les lire. Pour expliquer une décision, vous avez besoin d’outils supplémentaires, qui donnent seulement une approximation. Expliquer les prédictions montre ce que vous pouvez faire.
  • Elle est plus difficile à vérifier et inspire moins facilement confiance. Une banque, un hôpital ou un organisme de réglementation peut exiger qu’une personne puisse comprendre et vérifier le modèle. Quand un modèle simple est meilleur examine ce type de règles.
  • Elle est plus difficile à déboguer. Quand les scores deviennent incorrects après un changement des données, quelques poids sont faciles à examiner. Des milliers de feuilles ne le sont pas.
  • Elle a plus d’hyperparamètres. Le nombre d’arbres, la profondeur, la taille des feuilles, le taux d’apprentissage. Plus de réglages signifient plus d’expériences, et chaque consultation du mois de validation réduit un peu son utilité. Essayez assez de réglages, et l’un d’eux gagnera en mai par chance.
  • Elle peut faire du surapprentissage de plus de façons. Comme le montre ci-dessous la forêt avec ses réglages par défaut.

Un modèle complexe doit donc mériter sa place : il doit dépasser le modèle simple de plus que le bruit. Le gain doit aussi justifier ces coûts. Chez Larkfield, les données ne montrent aucun gain. Priya conserve la régression logistique, E03. La réponse d’Omar : la forêt ne changerait rien pour les clients que les données peuvent montrer. Elle coûterait davantage à utiliser, à expliquer et à maintenir.

Ce n’est pas une règle contre les méthodes d’ensemble. Une forêt ou un boosting gagne souvent clairement sur des données avec de fortes interactions, comme plusieurs types de données de capteurs. Ils en valent aussi la peine quand un petit gain vaut beaucoup d’argent. Le boosting par gradient est l’une des familles de modèles les plus performantes pour les tableaux de données. Mesurez, puis décidez.

Erreur courante

Le score parfait

Symptôme : Tomás présente la forêt avec ses réglages par défaut en utilisant son score sur les données d’entraînement :

print("random forest, default settings:", results["forest, default"])
Sortie
random forest, default settings: {'train AP': 1.0, 'CV AP': 0.29, 'std': 0.014}

« AP d’entraînement de 1,000 : elle trouve chaque escalade. » Mais en validation croisée, la même forêt obtient 0,290. C’est moins que la forêt avec min_samples_leaf=20 (0,325) et que la régression logistique (0,327).

Cause : avec les réglages par défaut, chaque arbre grandit jusqu’à ce que chaque feuille soit pure. Chaque arbre mémorise son échantillon bootstrap. La forêt attribue donc des scores presque parfaits aux billets dont elle a appris. La moyenne de 200 arbres qui mémorisent aide beaucoup (un seul de ces arbres obtient seulement 0,137), mais pas assez.

Correction : ne jugez jamais un modèle selon son score sur les billets dont il a appris. Comparez les modèles par validation croisée, avec la moyenne ± l’écart-type, et confirmez sur le mois de validation. Limitez les arbres, par exemple avec min_samples_leaf.

Conséquence dans le travail en IA : un score sur les données d’entraînement est le nombre « trop beau pour être vrai » le plus courant dans les rapports. Une équipe qui met la forêt en production à cause de « 1,000 » planifie le travail de l’équipe senior avec un modèle moins performant et plus cher que le modèle simple.

Vérifiez votre compréhension

Expliquez la différence entre une forêt aléatoire et le boosting par gradient en trois phrases : comment les membres sont entraînés, comment ils sont combinés et quel problème (biais ou variance) chacun réduit surtout.

Afficher la réponse

Une forêt aléatoire entraîne indépendamment plusieurs arbres profonds, en même temps, chacun sur son propre échantillon bootstrap. Elle utilise aussi des variables choisies au hasard à chaque division. Elle calcule la moyenne de leurs scores, ce qui réduit surtout la variance des arbres profonds. Le boosting par gradient entraîne de petits arbres l’un après l’autre, chacun sur les erreurs laissées par les arbres précédents. Il additionne leurs corrections, pondérées par le taux d’apprentissage, et réduit ainsi surtout le biais des petits arbres.

Vérifiez votre compréhension

Nommez trois coûts du remplacement de la régression logistique de Larkfield par la forêt (minimum de 20 billets par feuille), malgré l'égalité de leurs scores en validation croisée.

Afficher la réponse

Trois éléments parmi ceux-ci : elle est plus lente à entraîner et à évaluer (environ dix fois); son fichier est environ 2 000 fois plus gros; une personne ne peut pas la lire (64 934 feuilles contre 27 poids), donc elle est plus difficile à expliquer à Grace ou à un organisme de réglementation; elle est plus difficile à déboguer; elle a plus d’hyperparamètres, donc plus d’expériences et plus de risque de choisir par chance sur le mois de validation.

La tâche du module : comparez deux familles de modèles selon la même procédure de validation, et expliquez la différence sans supposer que le modèle plus complexe est meilleur. La même procédure signifie les mêmes variables, les mêmes 5 plis, le même score et le même ordre des étapes. Décidez par validation croisée sur les données d’entraînement, puis confirmez une fois sur mai.

Exemple détaillé : la régression logistique contre la forêt, pli par pli

Section intitulée « Exemple détaillé : la régression logistique contre la forêt, pli par pli »

Une moyenne ± un écart-type résume cinq nombres. Avec les mêmes plis, vous pouvez aussi comparer les deux modèles sur chaque pli. compare() exécute deux pipelines sur les mêmes 5 plis. La fonction affiche l’AP par pli, la moyenne ± l’écart-type, l’AP d’entraînement et la différence sur chaque pli :

def compare(name_a, pipeline_a, name_b, pipeline_b):
"""Two pipelines on the same 5 folds of train: AP per fold, mean ± std, the
difference per fold, and the train AP (fit and scored on all train tickets)."""
a = cross_val_score(pipeline_a, train[FEATURES], train[TARGET], cv=cv, scoring="average_precision")
b = cross_val_score(pipeline_b, train[FEATURES], train[TARGET], cv=cv, scoring="average_precision")
for name, pipeline, folds in ((name_a, pipeline_a, a), (name_b, pipeline_b, b)):
pipeline.fit(train[FEATURES], train[TARGET])
train_ap = average_precision_score(train[TARGET], pipeline.predict_proba(train[FEATURES])[:, 1])
print(f"{name:20s} folds {folds.round(3)} CV AP {folds.mean():.3f} ± {folds.std():.3f} train AP {train_ap:.3f}")
print(f"{'b minus a':20s} folds {(b - a).round(3)} mean {(b - a).mean():+.3f}")
return a, b
folds_a, folds_b = compare("logistic regression", build_pipeline(), "forest, min leaf 20", tree_pipeline(
RandomForestClassifier(n_estimators=200, min_samples_leaf=20, n_jobs=-1, random_state=0)))
Sortie
logistic regression folds [0.316 0.31 0.342 0.339 0.327] CV AP 0.327 ± 0.012 train AP 0.328
forest, min leaf 20 folds [0.318 0.294 0.326 0.344 0.342] CV AP 0.325 ± 0.018 train AP 0.483
b minus a folds [ 0.002 -0.016 -0.016 0.005 0.015] mean -0.002

La forêt gagne sur trois plis et perd sur deux, et le signe de la différence change d’un pli à l’autre. Elle n’est pas systématiquement meilleure ou moins bonne : c’est une égalité. Son AP d’entraînement, 0,483, dépasse largement son AP en validation croisée : elle apprend donc des détails qui ne se répètent pas. En mai, elle dépassait l’autre modèle de 0,029, dans la plage de variation due au hasard d’environ 0,03. Décision : conserver la régression logistique, car le modèle plus complexe ne montre aucun avantage réel et coûte davantage.

  1. Choisissez deux modèles de deux familles différentes : régression logistique, arbre limité, forêt aléatoire ou boosting par gradient. N’utilisez pas la paire de l’exemple détaillé.
  2. Prédisez lequel gagne, et de combien.
  3. Exécutez compare(). Lisez la différence par pli et l’écart entre l’AP d’entraînement et l’AP en validation croisée de chaque modèle.
  4. Décidez uniquement à partir des résultats de validation croisée. Écrivez un paragraphe : la décision, les nombres qui l’appuient et la raison.
  5. Confirmez ensuite une seule fois sur mai, et dites si mai change quelque chose.
Afficher la réponse modèle : la régression logistique contre le boosting par gradient
folds_a, folds_b = compare("logistic regression", build_pipeline(),
"boosting, default", tree_pipeline(HistGradientBoostingClassifier(random_state=0)))
Sortie
logistic regression folds [0.316 0.31 0.342 0.339 0.327] CV AP 0.327 ± 0.012 train AP 0.328
boosting, default folds [0.313 0.298 0.323 0.322 0.324] CV AP 0.316 ± 0.010 train AP 0.477
b minus a folds [-0.003 -0.012 -0.019 -0.017 -0.003] mean -0.011

Décision : conserver la régression logistique.

  • Le boosting est inférieur sur les cinq plis, de 0,003 à 0,019 (moyenne de 0,011). Sur les mêmes plis, une différence qui garde toujours le même signe est constante, même si elle est petite.
  • L’AP d’entraînement du boosting est de 0,477, contre une AP en validation croisée de 0,316 : un écart de 0,161. Il apprend des détails des billets d’entraînement qui ne se répètent pas. La régression logistique n’a presque aucun écart (0,328 et 0,327).
  • Pourquoi le modèle plus simple gagne : la relation dans ces données est surtout additive, exactement comme la régression logistique. La souplesse supplémentaire du boosting trouve surtout du bruit ici.

Puis la confirmation sur mai, une seule fois : régression logistique 0,353, boosting 0,369. Le boosting dépasse l’autre modèle de 0,016 en mai, bien dans la plage de variation due au hasard d’environ 0,03. Cela ne change pas la décision. Changer de modèle pour cette raison reviendrait à choisir selon mai.

Si vous avez choisi un arbre limité (par exemple min_samples_leaf=100) : il est inférieur sur les cinq plis. La différence va de 0,036 à 0,061 (moyenne de 0,049), une défaite claire. Si vous avez choisi le boosting lent (E14) : les différences sont −0,003, −0,007, −0,003, −0,002 et +0,015, une moyenne de 0,000 : une égalité.

Vérifiez votre compréhension

Rappel : un ensemble calcule la moyenne de ses membres. Quelles deux conditions les membres doivent-ils respecter pour que la moyenne soit meilleure qu'un seul membre?

Afficher la réponse

Chaque membre doit être utile seul (meilleur qu’une réponse au hasard), et les membres doivent faire des erreurs différentes. Les erreurs différentes s’annulent dans la moyenne, comme les estimations du nombre de bonbons dans un bocal. Les erreurs communes à tous les membres restent dans la moyenne. Voilà pourquoi une forêt aléatoire utilise des échantillons bootstrap et des variables choisies au hasard : ils rendent les arbres différents.

Vérifiez votre compréhension

Expliquez ou prédisez : Tomás construit une « forêt » de 200 arbres profonds où chaque arbre voit toutes les lignes d'entraînement et toutes les variables. Prédisez son AP en validation croisée par rapport à un seul arbre profond (0,137), et expliquez.

Afficher la réponse

Elle sera presque identique à celle d’un seul arbre, un peu supérieure à 0,137 : sur ces données, environ 0,149. Sans échantillons bootstrap ni variables choisies au hasard, les arbres sont presque identiques et font donc les mêmes erreurs. Calculer la moyenne de copies d’une erreur ne supprime pas l’erreur. Une vraie forêt aléatoire d’arbres profonds atteint 0,290, parce que ses arbres sont différents.

Vérifiez votre compréhension

Pour terminer : la forêt (minimum de 20 billets par feuille) a une AP en validation croisée de 0,325 ± 0,018 et une AP de validation de 0,382; la régression logistique obtient 0,327 ± 0,012 et 0,353. Expliquez la différence à Grace. Dites quel modèle vous remettriez et quel résultat vous ferait changer d'avis.

Afficher la réponse

Sur 16 939 billets d’entraînement, évalués sur les mêmes 5 plis, les deux modèles sont à égalité. La forêt est inférieure de 0,002, et elle gagne sur trois plis et perd sur deux. L’avance de 0,029 en mai reste dans la plage de variation due au hasard d’environ 0,03, car mai compte seulement 183 escalades. Les données ne montrent donc aucune différence réelle. Je remets la régression logistique : elle est aussi bonne, plus petite et plus rapide. L’équipe de Grace peut aussi voir pourquoi un billet a été signalé. Je changerais d’avis si la forêt était systématiquement meilleure sur les mêmes plis, de plus que le bruit. Le gain devrait aussi justifier le coût supplémentaire. Cela pourrait arriver, par exemple, après l’ajout de nouvelles variables qui créent de fortes interactions.

Dans ce module, vous avez parcouru plusieurs fois la boucle de développement des modèles. Votre journal d’expériences compte maintenant 14 lignes : deux références de base, une régression logistique et onze expériences avec d’autres réglages et familles de modèles. Chaque expérience a été évaluée sur les mêmes 5 plis, avec la même métrique. Chacune comporte une décision et une raison. Résultat : la régression logistique, E03, reste le modèle à garder. Les arbres perdent, et les méthodes d’ensemble arrivent à égalité.

Remise à zéro : pour revenir à un état connu avant le module 6 :

  1. Exécutez python get_data.py --reset dans le dossier du projet. Cette commande télécharge à nouveau les données et les vérifie.
  2. Si ticket_model.py ne fonctionne plus, réécrivez-le à partir de Prétraiter avec un pipeline : écrire ticket_model.py. Le module 6 le modifie dans Sélectionner les caractéristiques utiles.
  3. Si vous avez perdu votre journal d’expériences, exécutez le carnet (notebook) Jupyter de la solution de cette leçon depuis le début. Il écrit les lignes E01 à E14 dans son propre fichier experiment_log.csv. Téléchargez ce fichier et placez-le dans le dossier de votre projet.

Récapitulatif

  • L’apprentissage automatique est itératif : changez une chose, mesurez-la sur les mêmes plis avec la même métrique, consignez-la et gardez ce qui aide.
  • Un arbre de décision pose des questions dans l’ordre. Un arbre profond mémorise (AP d’entraînement : 1,000; AP en validation croisée : 0,137). Les limites comme max_depth et min_samples_leaf sont des hyperparamètres, choisis par validation croisée.
  • Une méthode d’ensemble combine plusieurs modèles. Elle aide seulement si ses membres sont utiles et font des erreurs différentes, comme une foule aux estimations indépendantes.
  • Une forêt aléatoire fait la moyenne d’arbres profonds entraînés sur des échantillons bootstrap (tirés avec remise), avec des variables aléatoires. Elle réduit la variance (dispersion de 0,194 → 0,034). Le gradient boosting (amplification par gradient) ajoute de petits arbres qui corrigent chacun les erreurs des arbres précédents. Il réduit le biais (0,178 → 0,333 avec 200 arbres à une question).
  • Sur les billets de soutien de Larkfield, les meilleures méthodes d’ensemble égalent la régression logistique (0,325 et 0,327 contre 0,327 ± 0,012). C’est parce que la relation est surtout additive. Un modèle complexe coûte plus cher à exécuter, à expliquer et à maintenir. Il doit donc justifier sa place par un gain supérieur au bruit.

Ensuite, dans le module 6, vous améliorez le modèle en changeant ses variables plutôt que sa famille.

Leçon suivante : Sélectionner les caractéristiques utiles