← Retour au portfolio RSS

· 11 min de lecture

Le cadre du ML supervisé, revu à froid

Données, modèle, perte et généralisation : on reconstruit le cadre mental du ML supervisé avant d'aller plus loin.

Mastering AI · Phase technique · Semaine 1 · Partie 13

Objectif de l’étape

Avant d’empiler les modèles (gradient boosting, LSTM, MLOps…), on remet en place le cadre commun à tout le ML supervisé. Presque chaque problème rencontré plus tard (fuite de données, sur-apprentissage, mauvais choix de métrique) vient d’un cadre mal posé.

À la fin de cet article, tu sauras formuler n’importe quel problème supervisé en quatre éléments, données, modèle, perte, optimisation, et diagnostiquer si un modèle apprend trop ou pas assez.


Notions clés

1. Les données : des exemples ( x , y ) (x, y)

Intuition. On montre au modèle des situations ( x x ) et la bonne réponse ( y y ), puis on espère qu’il devine la réponse pour des situations nouvelles.

  • x x : le vecteur de features, ce qu’on observe. Pour une région mémoire, ce serait par exemple « nombre d’accès sur les 10 dernières agrégations, temps depuis le dernier accès, taille ».
  • y y : le label, ce qu’on veut prédire. Pour notre exemple : « la région est-elle accédée dans les N N prochaines ms ? ».

Le jeu de données est une collection { ( x i , y i ) } i = 1 n \{(x_i, y_i)\}_{i=1}^{n} . Deux grandes familles de problèmes :

Problème y y Exemple
Régression réel temps avant le prochain accès
Classification catégorie chaud / froid

Hypothèse cachée, mais capitale : les exemples d’entraînement et ceux qu’on rencontrera plus tard viennent de la même distribution. C’est elle qui rend la généralisation possible, et c’est elle qui casse quand le workload change (on y reviendra aux parties 18 et 28).

2. Le modèle : une fonction paramétrée f θ f_\theta

Intuition. Un modèle est une famille de fonctions dont on règle les boutons θ \theta . Entraîner revient à choisir la position des boutons.

  • Régression linéaire : f θ ( x ) = θ ⊤ x + b f_\theta(x) = \theta^\top x + b , les boutons sont les poids.
  • Arbre de décision : les boutons sont les seuils et les découpages.
  • Réseau de neurones : des millions de poids.

Choisir un modèle, c’est choisir l’ensemble des fonctions possibles (sa capacité). Trop petit, il ne peut pas représenter la réalité. Trop grand, il peut représenter le bruit aussi.

3. La perte : mesurer l’erreur

Intuition. Il faut un chiffre qui dit « à quel point ce modèle se trompe ». Plus il est petit, mieux c’est.

Pour un exemple, la fonction de perte L ( f θ ( x ) , y ) L(f_\theta(x), y) compare la prédiction à la vérité :

  • erreur quadratique (régression) : L = ( f θ ( x ) − y ) 2 L = (f_\theta(x) - y)^2
  • log-loss (classification) : L = − [ y log p + ( 1 − y ) log ( 1 − p ) ] L = -\big[y \log p + (1-y)\log(1-p)\big] , avec p p la probabilité prédite

Sur tout le jeu d’entraînement, on fait la moyenne, appelée risque empirique :

R ^ ( θ ) = 1 n ∑ i = 1 n L ( f θ ( x i ) , y i ) \hat{R}(\theta) = \frac{1}{n}\sum_{i=1}^{n} L\big(f_\theta(x_i), y_i\big)

D’où viennent ces pertes ? Elles ne sont pas arbitraires : elles découlent du maximum de vraisemblance → Required Maths M8 : vraisemblance, Bayes et entropie.

4. Entraîner = optimiser

Intuition. On cherche la position des boutons qui rend l’erreur moyenne la plus petite possible.

θ ⋆ = arg⁡ min θ R ^ ( θ ) \theta^\star = \arg\min_\theta \; \hat{R}(\theta)

Pour un modèle linéaire, il existe une solution exacte (les équations normales de M2). Pour un réseau de neurones, on descend la pente de R ^ \hat{R} petit à petit → Required Maths M10 : descente de gradient.

5. Le vrai objectif : la généralisation

Intuition. Réussir l’examen avec les questions qu’on a déjà vues ne prouve rien. Ce qui compte, c’est la note sur des questions nouvelles.

Ce qu’on veut vraiment minimiser, c’est le risque réel : l’erreur moyenne sur toutes les données possibles, pas seulement celles qu’on a vues. Comme on ne peut pas le calculer, on l’estime sur un ensemble de test que le modèle n’a jamais vu.

Trois situations typiques :

Erreur train Erreur test Diagnostic
Sous-apprentissage haute haute modèle trop simple
Bon modèle basse basse, proche du train ✅
Sur-apprentissage très basse haute modèle qui apprend le bruit

6. Le compromis biais / variance

Intuition. Imagine qu’on entraîne le même type de modèle sur 200 jeux de données différents tirés de la même source.

  • Biais : en moyenne, le modèle tombe-t-il à côté de la vérité ? (modèle trop rigide)
  • Variance : ses prédictions changent-elles beaucoup d’un jeu de données à l’autre ? (modèle trop sensible)

Pour la perte quadratique, l’erreur attendue se décompose exactement ainsi :

𝔼 [ ( y − f ^ ( x ) ) 2 ] = ( 𝔼 [ f ^ ( x ) ] − f ( x ) ) 2 ⏟ biais 2 + V a r ( f ^ ( x ) ) ⏟ variance + σ 2 ⏟ bruit irréductible \begin{aligned} \mathbb{E}\big[(y - \hat f(x))^2\big] ={}& \underbrace{\big(\mathbb{E}[\hat f(x)] - f(x)\big)^2}_{\text{biais}^2} + \underbrace{\mathrm{Var}\big(\hat f(x)\big)}_{\text{variance}} \\ &+ \underbrace{\sigma^2}_{\text{bruit irréductible}} \end{aligned}

Augmenter la capacité du modèle fait baisser le biais et monter la variance. Le meilleur modèle se trouve entre les deux. La démonstration repose sur les outils de probabilités → Required Maths M7 : variables aléatoires, espérance et variance.


Exemple de code

On fait varier la capacité d’un modèle : des polynômes de degré 1 à 15, ajustés sur une sinusoïde bruitée. Tout tient en un script.

Erreurs train / test selon la complexité

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

rng = np.random.default_rng(42)

def f_vraie(x):
    """La 'réalité' que le modèle ne connaît pas."""
    return np.sin(2 * np.pi * x)

# 1. Données : n points bruités (peu de points, pour bien voir le sur-apprentissage)
n, bruit = 30, 0.25
x = rng.uniform(0, 1, n)
y = f_vraie(x) + rng.normal(0, bruit, n)
X = x.reshape(-1, 1)

# Ici les points sont indépendants : un split aléatoire est légitime.
# (Sur des traces temporelles, ce sera interdit -> partie 14.)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.4, random_state=0
)

def polynome(degre):
    return make_pipeline(
        PolynomialFeatures(degre, include_bias=False),
        StandardScaler(),
        LinearRegression(),
    )

# 2. Modèle + perte + optimisation, pour chaque degré
degres = list(range(1, 16))
err_train, err_test = [], []
for d in degres:
    m = polynome(d).fit(X_train, y_train)
    err_train.append(mean_squared_error(y_train, m.predict(X_train)))
    err_test.append(mean_squared_error(y_test, m.predict(X_test)))

meilleur = degres[int(np.argmin(err_test))]
print(f"Meilleur degré sur le test : {meilleur}")
print(f"Bruit irréductible (sigma^2) : {bruit**2:.4f}")

# 3. Visualisation
fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))

axes[0].plot(degres, err_train, "o-", label="erreur train")
axes[0].plot(degres, err_test, "o-", label="erreur test")
axes[0].axhline(bruit**2, ls="--", c="gray", label="bruit $\\sigma^2$")
axes[0].set_yscale("log")
axes[0].set_xlabel("degré du polynôme (capacité)")
axes[0].set_ylabel("MSE")
axes[0].set_title("Sous- et sur-apprentissage")
axes[0].legend()

grille = np.linspace(0, 1, 300).reshape(-1, 1)
axes[1].scatter(X_train, y_train, s=15, label="train")
axes[1].scatter(X_test, y_test, s=15, marker="x", label="test")
axes[1].plot(grille, f_vraie(grille), "k--", label="vraie fonction")
for d in (1, meilleur, 15):
    axes[1].plot(grille, polynome(d).fit(X_train, y_train).predict(grille),
                 label=f"degré {d}")
axes[1].set_ylim(-2, 2)
axes[1].set_title("Trois niveaux de capacité")
axes[1].legend(fontsize=8)

plt.tight_layout()
plt.show()

À gauche, erreurs train et test en fonction du degré du polynôme (échelle log) : l’erreur train descend sous le bruit σ², l’erreur test dessine un U avec un minimum au degré 3 puis explose jusqu’à plus de 20 au degré 15. À droite, les ajustements de degré 1 (trop rigide), 3 (proche de la vraie fonction) et 15 (oscille entre les points).

Ce que tu dois observer :

  • l’erreur train baisse quand le degré augmente, puis se stabilise autour de 0,026, sous le bruit σ 2 = 0 , 0625 \sigma^2 = 0{,}0625 : le modèle colle aux points mieux que la vraie fonction elle-même, autrement dit il apprend le bruit ;
  • l’erreur test dessine un U : minimale au degré 3 (autour de 0,09), elle remonte ensuite jusqu’à plus de 20 au degré 15 (d’où l’échelle logarithmique) ;
  • le degré 1 rate la forme (biais), le degré 15 ondule entre les points et part dans tous les sens aux extrémités (variance) ;
  • même au mieux, l’erreur test ne descend pas sous σ 2 \sigma^2 : c’est le bruit irréductible.

Mesurer biais et variance directement

On applique littéralement la définition : on ré-entraîne chaque modèle sur 200 jeux de données différents.

import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression

rng = np.random.default_rng(0)

def f_vraie(x):
    return np.sin(2 * np.pi * x)

def polynome(degre):
    return make_pipeline(
        PolynomialFeatures(degre, include_bias=False),
        StandardScaler(),
        LinearRegression(),
    )

x_eval = np.linspace(0.05, 0.95, 50).reshape(-1, 1)

def biais_variance(degre, n_jeux=200, n=30, bruit=0.25):
    preds = np.empty((n_jeux, len(x_eval)))
    for k in range(n_jeux):
        xs = rng.uniform(0, 1, n)
        ys = f_vraie(xs) + rng.normal(0, bruit, n)
        preds[k] = polynome(degre).fit(xs.reshape(-1, 1), ys).predict(x_eval)
    pred_moyenne = preds.mean(axis=0)
    biais2 = np.mean((pred_moyenne - f_vraie(x_eval.ravel())) ** 2)
    variance = np.mean(preds.var(axis=0))
    return biais2, variance

print(f"{'degré':>5} | {'biais²':>8} | {'variance':>8} | {'somme':>8}")
for d in (1, 3, 5, 9, 12):
    b2, v = biais_variance(d)
    print(f"{d:>5} | {b2:8.4f} | {v:8.4f} | {b2 + v:8.4f}")
Degré Biais² Variance
1 0,157 0,019
3 0,003 0,009
5 0,0002 0,017
9 0,003 0,39
12 0,020 4,77

Biais² et variance des polynômes de degré 1, 3, 5, 9 et 12, en échelle logarithmique : le biais² chute dès le degré 3, la variance explose au-delà du degré 5.

Le biais² s’effondre dès le degré 3, puis la variance explose au-delà du degré 5. Leur somme est minimale autour du degré 3 : c’est la formule de la section 6, vérifiée en chiffres.


Fil rouge : le mémoire dans ce cadre

Le sujet de mémoire se pose exactement dans ce cadre :

Élément Prédiction chaud/froid
Exemple une région mémoire DAMON à l’instant t t
x x features calculées sur l’historique de la région (récence, fréquence, périodicité, voisinage…)
y y 1 1 si la région est accédée dans les N N ms suivantes, 0 0 sinon
Modèle régression logistique → LightGBM → LSTM
Perte log-loss, avec un coût asymétrique : un faux froid (région compressée puis relue) coûte plus cher qu’un faux chaud
Généralisation sur la suite de la trace (futur) et sur un autre workload

Deux différences importantes avec l’exemple jouet :

  1. les exemples ne sont pas indépendants : deux instants consécutifs d’une même région se ressemblent. Le split aléatoire devient une fuite (partie 14) ;
  2. la bonne métrique n’est pas la MSE ni l’accuracy, mais un compromis précision/rappel qui tient compte du coût des faux froids (partie 15).

Exercice pratique

  1. Plus de données. Relance le premier script avec n = 200, puis n = 1000. Le meilleur degré change-t-il ? L’écart train/test au degré 15 se réduit-il ? Explique pourquoi avec le compromis biais/variance.
  2. Plus de bruit. Remets n = 30 et passe bruit à 0.6. Que devient le meilleur degré ? Pourquoi un modèle plus simple devient-il préférable ?
  3. Régularisation, en avant-première. Remplace LinearRegression() par Ridge(alpha=1e-3) (import depuis sklearn.linear_model) et garde le degré 15. Compare l’erreur test. Qu’a fait la régularisation à la variance ? (Réponse complète en partie 16.)
  4. Fil rouge. Sans écrire de code, rédige en 5 à 10 lignes la formulation de ton problème chaud/froid : unité d’exemple, liste de features candidates, label exact, horizon N N , ce qu’on entend par « généraliser ». Garde ce texte : on le réutilisera en partie 14.

Indice pour la question 1 : avec plus de données, la variance de chaque modèle diminue, donc on peut se permettre plus de capacité.


Ressources pour aller plus loin

  • An Introduction to Statistical Learning (James, Witten, Hastie, Tibshirani), chapitre 2, gratuit en ligne : la meilleure présentation du compromis biais/variance.
  • The Elements of Statistical Learning, chapitre 7 : la version plus mathématique de la sélection de modèles.
  • Documentation scikit-learn, exemple Underfitting vs. Overfitting.
  • Rappel sur le blog : scikit-learn : le cycle complet d’un modèle.

Ce que tu dois savoir faire avant de passer à la suite

  • Formuler un problème supervisé en quatre éléments : données, modèle, perte, optimisation
  • Expliquer la différence entre risque empirique et risque réel, et pourquoi on a besoin d’un ensemble de test
  • Reconnaître le sous-apprentissage et le sur-apprentissage sur des courbes train/test
  • Expliquer le compromis biais/variance avec tes propres mots, sans formule
  • Avoir rédigé la formulation supervisée de ton problème chaud/froid

Partie suivante : Préparer les données sans fuite, où l’on génère les premières traces mémoire simulées et où l’on découvre pourquoi le split aléatoire de cet article serait une erreur sur ces données.