Introduction au ML — Séance 8 (TP)

La Séance 7 a réglé un arbre au mieux : AUC 0,73 sur le paludisme, puis un plafond.
Télécharger le notebook Ouvrir dans Colab Les sorties sont celles enregistrées dans le notebook : rien n'est réexécuté.

La force du nombre : forêts aléatoires et boosting

Dr. El Hadji Bassirou TOURÉ · DMI · FST · UCAD


La Séance 7 a réglé un arbre au mieux : AUC \(0{,}73\) sur le paludisme, puis un plafond. Ce TP construit à la main les idées qui dépassent l’arbre unique : le vote de la foule, le bootstrap (et son \(63\,\%\)), l’out-of-bag, la forêt aléatoire et son importance des variables, puis le gradient boosting déroulé résidu par résidu — avant de confronter le tout à scikit-learn et d’aboutir à une leçon contre-intuitive : sur ces données, le modèle le plus puissant n’est pas le meilleur.

Durée estimée : 1h30. Exécuter les cellules dans l’ordre, de haut en bas.

Partie 0 — Mise en place

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

np.set_printoptions(precision=4, suppress=True)
pd.set_option("display.precision", 4)
print("Outils prêts. NumPy", np.__version__, "· pandas", pd.__version__)
Outils prêts. NumPy 2.4.4 · pandas 3.0.2
# Ne pas modifier cette cellule : générateur officiel DataSANTÉ-221
def generate_datasante221(n=10_000, seed=221):
    rng = np.random.default_rng(seed)
    age = np.clip(rng.gamma(2.5, 14, n), 5, 85).round(0).astype(int)
    glycemie = np.clip(rng.normal(5.5 + 0.05*age, 1.8), 3.0, 18.0).round(1)
    hemoglobine = np.clip(rng.normal(12.5 - 0.02*age, 1.5), 6.0, 17.0).round(1)
    fievre = np.clip(rng.normal(37.5, 0.9, n), 36.0, 41.5).round(1)
    saison = rng.choice([0, 1], size=n, p=[0.55, 0.45])
    duree = (1.0 + 0.05*age + 0.6*glycemie - 0.3*hemoglobine
             + 1.5*saison + rng.normal(0, 1.8, n)).clip(0.5, 30).round(1)
    proba_palu = 1/(1+np.exp(-(-3 + 1.5*saison + 0.8*(fievre>38.5))))
    palu = (rng.uniform(0,1,n) < proba_palu).astype(int)
    return pd.DataFrame({'age':age,'glycemie':glycemie,'hemoglobine':hemoglobine,
                         'fievre':fievre,'saison':saison,
                         'duree_hospit_j':duree,'paludisme':palu})

df = generate_datasante221()
colonnes = ["age", "glycemie", "hemoglobine", "fievre", "saison"]
print("dimensions :", df.shape)
dimensions : (10000, 7)
# Le découpage de référence du cours (Séances 5 à 7) et la CV stratifiée
from sklearn.model_selection import train_test_split, StratifiedKFold

X = df[colonnes]
y = df["paludisme"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    random_state=42, stratify=y)
cv5 = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
print("train :", len(y_train), "· test :", len(y_test), "·", int(y_test.sum()), "positifs au test")
train : 8000 · test : 2000 · 235 positifs au test

Partie 1 — La sagesse de la foule, en simulation

Des jurés qui se trompent une fois sur trois, mais dont les erreurs sont indépendantes : la majorité se trompe-t-elle moins ? Vérifions par simulation.

# Des jurés à 65 % de réussite, erreurs indépendantes, vote majoritaire
rng = np.random.default_rng(8)
def proba_majorite(M, p=0.65, essais=20000):
    votes = (rng.uniform(0, 1, (essais, M)) < p).sum(axis=1)
    return (votes > M/2).mean()

for M in [1, 3, 11, 51]:
    print(f"{M:2d} jurés : la majorité a raison {proba_majorite(M):.3f} du temps")
 1 jurés : la majorité a raison 0.647 du temps
 3 jurés : la majorité a raison 0.718 du temps
11 jurés : la majorité a raison 0.858 du temps
51 jurés : la majorité a raison 0.987 du temps

Lecture. Un seul juré : \(0{,}65\). Onze jurés : \(\approx 0{,}85\). Cinquante et un : \(\approx 0{,}99\). Des votants médiocres mais indépendants forment un collège excellent — c’est tout le pari des ensembles. Le mot clé est indépendants : la Partie 2 va fabriquer cette indépendance entre arbres.

Exercice. Modifier proba_majorite pour des jurés à \(p = 0{,}55\) (à peine mieux que le hasard) et afficher le résultat pour \(M = 1, 11, 101\). La foule sauve-t-elle encore des votants aussi faibles ?

# Jurés à 55 % : proba de la majorité pour M = 1, 11, 101

La condition d’indépendance, en chiffres

Le théorème de Condorcet suppose les erreurs indépendantes. Que se passe-t-il si les votants se ressemblent ? Simulons des jurés dont les votes sont corrélés.

# Des jurés corrélés : un "climat d'opinion" commun + un avis propre
def proba_majorite_correlee(M, p=0.65, rho=0.0, essais=20000):
    # rho = poids du signal commun (0 = independants, 1 = tous identiques)
    climat = rng.uniform(0, 1, (essais, 1)) < p          # avis commun a tous
    propre = rng.uniform(0, 1, (essais, M)) < p          # avis individuel
    tirage_commun = rng.uniform(0, 1, (essais, M)) < rho # qui suit le climat ?
    votes_corrects = np.where(tirage_commun, climat, propre).sum(axis=1)
    return (votes_corrects > M/2).mean()

for rho in [0.0, 0.3, 0.9]:
    p51 = proba_majorite_correlee(51, rho=rho)
    print(f"51 jures a 65%, correlation {rho} : majorite correcte {p51:.3f}")
51 jures a 65%, correlation 0.0 : majorite correcte 0.986
51 jures a 65%, correlation 0.3 : majorite correcte 0.734
51 jures a 65%, correlation 0.9 : majorite correcte 0.647

Lecture. Indépendants (\(\rho = 0\)) : \(\approx 0{,}99\). Fortement corrélés (\(\rho = 0{,}9\)) : à peine mieux que \(0{,}65\) — les \(51\) jurés ne valent presque pas mieux qu’un seul. La leçon est sans appel et prépare toute la suite : ce n’est pas le nombre de votants qui compte, mais leur indépendance. Toute la mécanique des forêts vise à fabriquer cette indépendance entre arbres.

Partie 2 — Le bootstrap : varier les données

Un échantillon bootstrap tire \(n\) patients avec remise parmi \(n\) : certains en double, d’autres absents (les out-of-bag).

# Un tirage bootstrap sur six patients, à la main
patients = np.array([1, 2, 3, 4, 5, 6])
tirage = rng.integers(0, 6, 6)              # 6 indices avec remise
echantillon = patients[tirage]
vus = np.unique(echantillon)
oob = np.setdiff1d(patients, vus)
print("échantillon bootstrap :", sorted(echantillon))
print("patients vus :", vus, "· out-of-bag :", oob)
échantillon bootstrap : [np.int64(1), np.int64(2), np.int64(2), np.int64(4), np.int64(4), np.int64(6)]
patients vus : [1 2 4 6] · out-of-bag : [3 5]
# La fraction de points uniques converge vers 1 - 1/e
for n in [6, 100, 10000]:
    fractions = [len(np.unique(rng.integers(0, n, n))) / n for _ in range(2000)]
    print(f"n = {n:5d} : fraction unique = {np.mean(fractions):.4f}")
print("\nlimite théorique 1 - 1/e =", round(1 - 1/np.e, 4))
n =     6 : fraction unique = 0.6677
n =   100 : fraction unique = 0.6340
n = 10000 : fraction unique = 0.6321

limite théorique 1 - 1/e = 0.6321

Lecture. Quel que soit \(n\) (pourvu qu’il soit grand), un tirage bootstrap retient environ \(\mathbf{63\,\%}\) des patients ; les \(\approx 37\,\%\) restants sont out-of-bag. La raison : la probabilité qu’un patient échappe aux \(n\) tirages est \(\big(1 - \tfrac1n\big)^n \to 1/e \approx 0{,}368\).

# Vérification directe de la formule (1 - 1/n)^n
for n in [6, 100, 10000]:
    print(f"n = {n:5d} : (1 - 1/n)^n = {(1 - 1/n)**n:.4f}  -> vus = {1-(1-1/n)**n:.4f}")
n =     6 : (1 - 1/n)^n = 0.3349  -> vus = 0.6651
n =   100 : (1 - 1/n)^n = 0.3660  -> vus = 0.6340
n = 10000 : (1 - 1/n)^n = 0.3679  -> vus = 0.6321

Partie 3 — Le bagging et l’évaluation out-of-bag

Le bagging entraîne un arbre par échantillon bootstrap, puis agrège. Comme chaque patient est OOB pour environ un tiers des arbres, on obtient une validation gratuite.

Un bagging fait main (pour comprendre l’agrégation)

Avant la version sklearn, construisons un mini-bagging nous-mêmes : tirer des échantillons bootstrap, entraîner un arbre sur chacun, puis moyenner leurs probabilités. On vérifie au passage que l’ensemble bat un arbre seul.

# Bagging maison : 30 arbres profonds sur des echantillons bootstrap
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score

n = len(X_train)
probas_test = []
for b in range(30):
    idx = rng.integers(0, n, n)                       # echantillon bootstrap
    arbre = DecisionTreeClassifier(random_state=b)
    arbre.fit(X_train.iloc[idx], y_train.iloc[idx])
    probas_test.append(arbre.predict_proba(X_test)[:, 1])

# un arbre seul vs la moyenne des 30
auc_un = roc_auc_score(y_test, probas_test[0])
auc_moyenne = roc_auc_score(y_test, np.mean(probas_test, axis=0))
print(f"un arbre profond seul   : AUC {auc_un:.4f}")
print(f"moyenne de 30 arbres    : AUC {auc_moyenne:.4f}")
un arbre profond seul   : AUC 0.5519
moyenne de 30 arbres    : AUC 0.6431

Lecture. Un arbre profond seul a une AUC médiocre et instable (il a tout mémorisé) ; la moyenne de trente arbres baggés la relève nettement. C’est le bagging dans sa forme la plus nue : bootstrap → entraîner → moyenner. La version de scikit-learn ajoute à cela le score out-of-bag.

# Bagging de 100 arbres avec score out-of-bag
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import roc_auc_score

bag = BaggingClassifier(DecisionTreeClassifier(random_state=0),
                        n_estimators=100, oob_score=True,
                        random_state=0, n_jobs=-1)
bag.fit(X_train, y_train)
print("score OOB (accuracy, gratuit) :", round(bag.oob_score_, 4))
print("AUC test du bagging :", round(roc_auc_score(y_test, bag.predict_proba(X_test)[:, 1]), 4))
score OOB (accuracy, gratuit) : 0.8688
AUC test du bagging : 0.6601

Lecture. Le score OOB (\(0{,}869\)) est calculé sans toucher au test : chaque patient a été prédit par les arbres qui ne l’avaient pas vu. C’est le rôle de la validation croisée de la Séance 7, mais intégré au bagging — pratique pour un premier diagnostic.

Partie 4 — La forêt aléatoire

La forêt ajoute un second hasard : à chaque coupure, seules max_features variables tirées au sort sont candidates. Les arbres se décorrèlent, et la moyenne s’améliore.

# Une forêt de 300 arbres
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

foret = RandomForestClassifier(n_estimators=300, random_state=0, n_jobs=-1)
foret.fit(X_train, y_train)
auc_cv = cross_val_score(foret, X_train, y_train, cv=cv5, scoring="roc_auc").mean()
print("AUC CV de la forêt :", round(auc_cv, 4))
print("AUC test de la forêt :", round(roc_auc_score(y_test, foret.predict_proba(X_test)[:, 1]), 4))
AUC CV de la forêt : 0.657
AUC test de la forêt : 0.6807
# Le plateau : ajouter des arbres ne sur-apprend jamais
for ne in [1, 5, 20, 50, 100, 300, 500]:
    m = RandomForestClassifier(n_estimators=ne, random_state=0, n_jobs=-1)
    s = cross_val_score(m, X_train, y_train, cv=cv5, scoring="roc_auc").mean()
    print(f"{ne:3d} arbres : AUC CV = {s:.4f}")
  1 arbres : AUC CV = 0.5335
  5 arbres : AUC CV = 0.5833
 20 arbres : AUC CV = 0.6293
 50 arbres : AUC CV = 0.6485
100 arbres : AUC CV = 0.6516
300 arbres : AUC CV = 0.6570
500 arbres : AUC CV = 0.6574

Lecture. L’AUC monte (\(0{,}53 \to 0{,}65\)) puis plafonne vers \(0{,}66\) : ajouter des arbres à une forêt ne fait jamais sur-apprendre. n_estimators n’est donc pas à régler finement — on en met « assez » (quelques centaines), le surcoût est en temps de calcul.

Vérifier la formule de variance d’une moyenne corrélée

Le deck affirme que la variance de la moyenne de \(B\) modèles corrélés tend vers \(\rho\sigma^2\), et non vers \(0\). Vérifions-le par simulation — c’est l’argument mathématique qui justifie pourquoi il faut décorréler les arbres.

# B variables de variance 1, correlees deux a deux a rho, via un facteur commun
def variance_moyenne(B, rho, n=40000):
    commun = rng.normal(0, 1, (n, 1))                    # partage par tous
    propre = rng.normal(0, 1, (n, B))                    # propre a chacun
    arbres = np.sqrt(rho)*commun + np.sqrt(1-rho)*propre # correlation rho, variance 1
    moyenne = arbres.mean(axis=1)
    return moyenne.var()

print("rho   B=1     B=10    B=100   theorie B->inf (= rho)")
for rho in [0.0, 0.3, 0.9]:
    v1 = variance_moyenne(1, rho); v10 = variance_moyenne(10, rho); v100 = variance_moyenne(100, rho)
    print(f"{rho}   {v1:.3f}   {v10:.3f}   {v100:.3f}   {rho:.3f}")
rho   B=1     B=10    B=100   theorie B->inf (= rho)
0.0   0.992   0.100   0.010   0.000
0.3   1.000   0.371   0.308   0.300
0.9   1.004   0.908   0.903   0.900

Lecture. À \(\rho = 0\) (arbres indépendants), la variance tombe comme \(1/B\) : \(1 \to 0{,}1 \to 0{,}01\). À \(\rho = 0{,}9\) (arbres semblables, le défaut du bagging sur une variable dominante), elle plafonne à \(0{,}90\) : cent arbres ne valent presque pas mieux qu’un seul. La forêt existe précisément pour faire baisser ce \(\rho\) — c’est la traduction chiffrée de tout ce qui précède.

Exercice. Comparer une forêt standard à des extra-trees (ExtraTreesClassifier, qui tirent aussi les seuils au hasard, donc décorrèlent davantage) sur le paludisme, en AUC de validation croisée. L’import est from sklearn.ensemble import ExtraTreesClassifier.

# Forêt vs Extra-Trees (300 arbres chacun), AUC CV
# L'importance des variables — à lire avec prudence
importances = pd.Series(foret.feature_importances_, index=colonnes).sort_values(ascending=False)
print(importances.round(4))
glycemie       0.2555
hemoglobine    0.2526
age            0.2451
fievre         0.1925
saison         0.0542
dtype: float64

Lecture (le piège). La forêt désigne glycémie, hémoglobine et âge comme les plus « importantes » — alors que, par construction des données, le paludisme ne dépend que de la saison et de la fièvre. Ces variables continues offrent une multitude de seuils, et les arbres s’en servent pour fragmenter le bruit : l’importance par impureté favorise les variables à forte cardinalité, même non causales. L’importance suggère, elle ne prouve pas.

Exercice. Entraîner deux forêts de \(300\) arbres, l’une avec max_features=1, l’autre avec max_features=None (toutes les variables à chaque coupure, donc des arbres plus corrélés). Comparer leurs AUC en validation croisée. Laquelle décorrèle le mieux ?

# Deux forêts : max_features=1 contre max_features=None, AUC CV

Partie 5 — Le gradient boosting, à la main

Le boosting enchaîne des arbres : chacun rattrape les résidus (l’erreur) de la somme des précédents. Déroulons-le sur un jouet de régression.

# Jouet : 4 points. On boost avec des souches (arbres de profondeur 1).
from sklearn.tree import DecisionTreeRegressor

xb = np.array([1., 2., 3., 4.]).reshape(-1, 1)
yb = np.array([2., 3., 5., 9.])

F0 = np.full(4, yb.mean())                  # prédiction initiale = la moyenne
print("F0 (moyenne) :", F0)
r1 = yb - F0
print("résidus r1 = y - F0 :", r1.round(3))
print("MSE de F0 :", round(np.mean((yb - F0)**2), 3))
F0 (moyenne) : [4.75 4.75 4.75 4.75]
résidus r1 = y - F0 : [-2.75 -1.75  0.25  4.25]
MSE de F0 : 7.188
# Souche 1 ajustée sur les résidus r1, puis mise à jour F1 = F0 + h1
souche1 = DecisionTreeRegressor(max_depth=1).fit(xb, r1)
h1 = souche1.predict(xb)
F1 = F0 + h1                                 # learning_rate = 1 pour la démo
print("la souche 1 prédit sur r1 :", h1.round(3))
print("F1 = F0 + h1 :", F1.round(3))
r2 = yb - F1
print("nouveaux résidus r2 :", r2.round(3), " -> MSE", round(np.mean(r2**2), 3))
la souche 1 prédit sur r1 : [-1.417 -1.417 -1.417  4.25 ]
F1 = F0 + h1 : [3.333 3.333 3.333 9.   ]
nouveaux résidus r2 : [-1.333 -0.333  1.667  0.   ]  -> MSE 1.167
# Souche 2 ajustée sur r2, puis F2 = F1 + h2
souche2 = DecisionTreeRegressor(max_depth=1).fit(xb, r2)
F2 = F1 + souche2.predict(xb)
print("F2 = F1 + h2 :", F2.round(3))
print("MSE : F0", round(np.mean((yb-F0)**2), 3),
      "-> F1", round(np.mean((yb-F1)**2), 3),
      "-> F2", round(np.mean((yb-F2)**2), 3))
F2 = F1 + h2 : [2.5   2.5   4.167 9.833]
MSE : F0 7.188 -> F1 1.167 -> F2 0.472

Lecture. La MSE chute \(7{,}19 \to 1{,}17 \to 0{,}47\) : chaque arbre rattrape ce que le précédent a laissé. La prédiction finale est la somme des arbres (\(F_0 + h_1 + h_2 + \dots\)), pas leur moyenne — c’est là toute la différence avec la forêt. « Gradient » vient de ce que les résidus sont, pour la MSE, l’opposé du gradient de la perte : le boosting est la descente de gradient de la Séance 5, appliquée fonction par fonction.

Exercice. Sur trois points, \(F_1 = (3, 5, 8)\) et \(y = (4, 4, 10)\). Calculer en NumPy les résidus \(r_2 = y - F_1\), puis \(F_2 = F_1 + 0{,}5 \cdot (0{,}5, 0{,}5, 2)\), et vérifier que la somme des carrés des résidus a diminué (elle passe de \(6\) à \(3{,}125\)).

# Résidus r2, mise à jour F2 avec learning_rate 0,5, et somme des carrés avant/après

Partie 6 — Le gradient boosting en pratique

Sur le paludisme, et le couple décisif learning_rate × n_estimators.

# Gradient boosting sur le paludisme (défauts : 100 arbres, lr=0.1, depth=3)
from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier(random_state=0)
auc_cv = cross_val_score(gb, X_train, y_train, cv=cv5, scoring="roc_auc").mean()
gb.fit(X_train, y_train)
print("AUC CV du gradient boosting :", round(auc_cv, 4))
print("AUC test :", round(roc_auc_score(y_test, gb.predict_proba(X_test)[:, 1]), 4))
AUC CV du gradient boosting : 0.6892
AUC test : 0.7238
# Le couple learning_rate x n_estimators (AUC CV)
for lr in [0.01, 0.1, 0.5, 1.0]:
    ligne = []
    for ne in [50, 200]:
        m = GradientBoostingClassifier(learning_rate=lr, n_estimators=ne, random_state=0)
        s = cross_val_score(m, X_train, y_train, cv=cv5, scoring="roc_auc").mean()
        ligne.append(f"n={ne}: {s:.4f}")
    print(f"learning_rate={lr:4} -> " + " | ".join(ligne))
learning_rate=0.01 -> n=50: 0.6975 | n=200: 0.6968
learning_rate= 0.1 -> n=50: 0.6944 | n=200: 0.6815
learning_rate= 0.5 -> n=50: 0.6736 | n=200: 0.6404
learning_rate= 1.0 -> n=50: 0.6454 | n=200: 0.6101

Lecture. Le meilleur réglage est learning_rate faible et peu d’arbres (\(0{,}01\), \(50\) arbres : \(0{,}698\)) ; un grand pas avec beaucoup d’arbres dégrade (\(1{,}0\), \(200\) : \(0{,}610\)). À l’opposé de la forêt : ici, ajouter des arbres à grand pas fait sur-apprendre. C’est pourquoi ce couple se règle par validation croisée (Séance 7).

Partie 7 — Le grand comparatif : l’arc se referme

Cinq modèles sur le paludisme, jugés à l’AUC en validation croisée et sur le test.

# Le tableau récapitulatif de tout le cours
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

modeles = {
    "logistique (S6)": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000, random_state=0)),
    "arbre réglé prof.3 (S7)": DecisionTreeClassifier(max_depth=3, random_state=0),
    "forêt aléatoire (300)": RandomForestClassifier(n_estimators=300, random_state=0, n_jobs=-1),
    "gradient boosting": GradientBoostingClassifier(random_state=0),
}
lignes = []
for nom, m in modeles.items():
    cv_auc = cross_val_score(m, X_train, y_train, cv=cv5, scoring="roc_auc").mean()
    m.fit(X_train, y_train)
    te_auc = roc_auc_score(y_test, m.predict_proba(X_test)[:, 1])
    lignes.append({"modèle": nom, "AUC CV": round(cv_auc, 4), "AUC test": round(te_auc, 4)})
pd.DataFrame(lignes)
modèle AUC CV AUC test
0 logistique (S6) 0.6914 0.7145
1 arbre réglé prof.3 (S7) 0.6955 0.7272
2 forêt aléatoire (300) 0.6570 0.6807
3 gradient boosting 0.6892 0.7238

Lecture. Surprise : les ensembles ne dominent pas. L’arbre réglé (\(0{,}73\) test) et la logistique (\(0{,}71\)) font aussi bien, voire mieux que la forêt (\(0{,}68\)) et le boosting (\(0{,}72\)). La raison : le risque de paludisme dépend ici de deux variables de façon simple (saison, fièvre) — un petit modèle suffit, et la puissance des forêts se retourne contre elles en cherchant des interactions dans le bruit des variables continues.

La morale n’est pas « les ensembles sont mauvais » (ils dominent sur quantité de problèmes réels), mais qu’aucun modèle n’est supérieur dans l’absolu : la complexité doit être justifiée par les données. À performance égale, on choisit le plus simple.

Mini-défi — Le plateau de la forêt contre la dérive du boosting

Une expérience qui résume la séance : pousser n_estimators pour les deux familles, et regarder l’écart entre AUC d’entraînement et AUC de test.

# Forêt : train et test selon n_estimators
print("FORÊT (plus d'arbres = plateau, jamais de dérive)")
for ne in [5, 50, 500]:
    m = RandomForestClassifier(n_estimators=ne, random_state=0, n_jobs=-1).fit(X_train, y_train)
    tr = roc_auc_score(y_train, m.predict_proba(X_train)[:, 1])
    te = roc_auc_score(y_test, m.predict_proba(X_test)[:, 1])
    print(f"  {ne:3d} arbres : train {tr:.4f} / test {te:.4f}  (écart {tr-te:.3f})")

# Boosting à grand pas : train et test selon n_estimators
print("\nBOOSTING (learning_rate=0.5 : plus d'arbres = sur-apprentissage)")
for ne in [50, 200, 1000]:
    m = GradientBoostingClassifier(learning_rate=0.5, n_estimators=ne,
                                   random_state=0).fit(X_train, y_train)
    tr = roc_auc_score(y_train, m.predict_proba(X_train)[:, 1])
    te = roc_auc_score(y_test, m.predict_proba(X_test)[:, 1])
    print(f"  {ne:4d} arbres : train {tr:.4f} / test {te:.4f}  (écart {tr-te:.3f})")
FORÊT (plus d'arbres = plateau, jamais de dérive)
    5 arbres : train 0.9889 / test 0.6203  (écart 0.369)
   50 arbres : train 1.0000 / test 0.6804  (écart 0.320)
  500 arbres : train 1.0000 / test 0.6815  (écart 0.319)

BOOSTING (learning_rate=0.5 : plus d'arbres = sur-apprentissage)
    50 arbres : train 0.8109 / test 0.7059  (écart 0.105)
   200 arbres : train 0.9102 / test 0.6713  (écart 0.239)
  1000 arbres : train 0.9938 / test 0.6381  (écart 0.356)

Lecture. La forêt sature (train \(1{,}0\), test stable autour de \(0{,}68\) — l’écart ne se creuse plus). Le boosting à grand pas dérive : son train monte vers \(0{,}99\) pendant que son test de \(0{,}71\) à \(0{,}64\) — l’écart explose, signe du sur-apprentissage. Deux mécaniques opposées, une même conclusion : la forêt pardonne l’excès d’arbres, le boosting non.

La Séance 9 quittera le terrain des étiquettes : comment regrouper des patients sans cible (clustering), et résumer des données à beaucoup de variables (réduction de dimension).

Synthèse — à compléter

  1. Agréger des modèles ………. bat chacun d’eux — à condition que leurs erreurs diffèrent (la sagesse de la foule).
  2. Un échantillon ………. tire \(n\) points avec remise et en retient environ ………. % ; les autres sont ………. et offrent une validation gratuite.
  3. La forêt aléatoire ajoute au bagging un tirage de ………. à chaque coupure, pour ………. les arbres ; ajouter des arbres fait ………., jamais sur-apprendre.
  4. Le boosting enchaîne des arbres ………. qui rattrapent les ………. des précédents ; il réduit le ………. et ………. sur-apprendre si le couple learning_rate/n_estimators est mal réglé.
  5. L’importance des variables ………., elle ne ………. pas — et favorise les variables ………..
  6. Leçon centrale : le modèle le plus ………. n’est pas toujours le meilleur ; à performance égale, choisir le plus ………..
Retour au sommet