← Retour au portfolio RSS

· 16 min de lecture

Évaluer un classifieur pour de vrai

Matrice de confusion, précision, rappel, ROC et PR : choisir la bonne métrique et le bon seuil quand les erreurs n'ont pas le même coût.

Mastering AI · Phase technique · Semaine 1 · Partie 15

Objectif de l’étape

Dans la partie 14, on a jugé les modèles avec l’AUC sans la discuter. Pour le mémoire, ça ne suffit pas : à la fin, il faut décider, région par région, si on la compresse ou non. Et les deux erreurs possibles n’ont pas du tout le même prix. Une région compressée puis relue tout de suite (un faux froid) bloque l’application pendant la décompression. Une région gardée en RAM alors qu’elle ne sert plus (un faux chaud) gaspille un peu de mémoire, sans plus.

À la fin de cet article, tu sauras :

  • lire une matrice de confusion et en tirer précision, rappel, F1 et taux de faux froids ;
  • expliquer pourquoi l’accuracy trompe dès que les classes sont déséquilibrées ;
  • lire une courbe ROC et une courbe précision-rappel, et choisir entre les deux ;
  • fixer le seuil de décision à partir d’un coût asymétrique, sur la validation ;
  • vérifier si les probabilités d’un modèle sont calibrées.

Notions clés

1. La matrice de confusion

Intuition. Pour chaque région, il y a la vérité (chaude ou froide) et la décision du modèle. Ça fait quatre cases, et toutes les métriques ne sont que des façons de combiner ces quatre nombres.

On prend « chaude » comme classe positive ( y = 1 y = 1 : la région est accédée dans les H H prochaines agrégations).

Prédite chaude (on garde) Prédite froide (on compresse)
Vraiment chaude VP : vrai positif FN : faux froid (coûteux)
Vraiment froide FP : faux chaud (peu coûteux) VN : vrai négatif

2. Précision, rappel, F1, et le piège de l’accuracy

Intuition. La précision répond à « quand le modèle dit chaude, a-t-il raison ? ». Le rappel répond à « parmi les régions vraiment chaudes, combien en trouve-t-il ? ».

précision = V P V P + F P rappel = V P V P + F N F 1 = 2 ⋅ précision ⋅ rappel précision + rappel \begin{aligned} \text{précision} &= \frac{VP}{VP + FP} \qquad \text{rappel} = \frac{VP}{VP + FN} \\ F_1 &= \frac{2 \cdot \text{précision} \cdot \text{rappel}}{\text{précision} + \text{rappel}} \end{aligned}

Le F 1 F_1 est une moyenne harmonique : il s’effondre dès que l’un des deux est mauvais.

L’accuracy, la part de décisions correctes, est trompeuse dès que les classes sont déséquilibrées. Sur notre test, 31 % des régions sont chaudes : un modèle qui déclare tout froid obtient 68,9 % d’accuracy… avec un rappel nul. Il compresserait toutes les régions chaudes.

3. Le taux de faux froids

Pour zswap, la question opérationnelle est : parmi les régions qu’on compresse, combien sont relues ?

taux de faux froids = F N F N + V N = P ( chaude ∣ déclarée froide ) \text{taux de faux froids} = \frac{FN}{FN + VN} = P(\text{chaude} \mid \text{déclarée froide})

C’est exactement la probabilité calculée avec Bayes en Required Maths M8. Elle dépend du modèle et de la fréquence de base des régions chaudes : le même modèle n’a pas le même taux de faux froids sur un autre workload.

4. Courbes ROC et précision-rappel

Intuition. Un modèle ne sort pas une décision, il sort un score p ∈ [ 0 , 1 ] p \in [0, 1] . La décision vient d’un seuil : chaude si p ≥ s p \ge s . Chaque seuil donne une matrice de confusion différente ; une courbe les montre toutes d’un coup.

  • La courbe ROC trace le rappel (taux de vrais positifs) en fonction du taux de faux positifs F P / ( F P + V N ) FP / (FP + VN) . Le hasard donne la diagonale.
  • L’AUC ROC a une interprétation probabiliste simple : c’est la probabilité qu’une région chaude tirée au hasard ait un score plus élevé qu’une région froide tirée au hasard. C’est un calcul d’espérance sur des paires, dans l’esprit de Required Maths M7 ; on le vérifie plus bas par simulation.
  • La courbe précision-rappel (PR) trace la précision en fonction du rappel. Le hasard y donne une ligne horizontale à la hauteur de la fréquence de base (0,311 ici). Son aire s’appelle la précision moyenne (AP).

Laquelle choisir ? La ROC ne dépend pas de la fréquence de base : pratique pour comparer des modèles entre workloads. La PR, elle, montre ce qui se passe sur la classe rare : quand les positifs sont peu nombreux, une ROC flatteuse peut cacher une précision médiocre (Saito et Rehmsmeier, 2015).

5. Choisir le seuil avec un coût asymétrique

Intuition. Le seuil 0,5 n’a rien de spécial : il revient à dire qu’un faux froid et un faux chaud coûtent pareil. Ce n’est pas notre cas.

Notons c F N c_{FN} le coût d’un faux froid et c F P c_{FP} celui d’un faux chaud. Pour une région de probabilité p p d’être chaude :

  • la compresser coûte en moyenne p c F N p \, c_{FN} ;
  • la garder coûte en moyenne ( 1 − p ) c F P (1 - p) \, c_{FP} .

On la garde donc quand ( 1 − p ) c F P < p c F N (1 - p)\,c_{FP} < p\,c_{FN} , c’est-à-dire :

p > s ⋆ = c F P c F P + c F N p > s^\star = \frac{c_{FP}}{c_{FP} + c_{FN}}

Avec c F N = 10 c_{FN} = 10 et c F P = 1 c_{FP} = 1 , on obtient s ⋆ = 1 / 11 ≈ 0 , 091 s^\star = 1/11 \approx 0{,}091 . Ce rapport 10 est une hypothèse de travail : mesurer le vrai coût d’une décompression zswap face au gain mémoire fera partie du mémoire.

Ce calcul suppose que p p est une vraie probabilité. Sinon, on choisit le seuil empiriquement, en minimisant le coût moyen sur la validation, jamais sur le test (partie 14).

6. Calibration

Intuition. Un modèle est calibré si, parmi les régions auxquelles il donne p = 0 , 2 p = 0{,}2 , environ 20 % sont vraiment chaudes. Un bon classement (une bonne AUC) ne garantit pas une bonne calibration.

On la mesure de deux façons :

  • un diagramme de fiabilité : on regroupe les prédictions par tranches de p p et on compare le p p moyen au taux réel ;
  • le score de Brier, l’erreur quadratique moyenne sur les probabilités : 1 n ∑ i ( p i − y i ) 2 \frac{1}{n}\sum_i (p_i - y_i)^2 .

Exemple de code

On reprend tel quel le script de la partie 14 (simulation, features, labels), puis on ajoute les quatre blocs ci-dessous à la suite, dans le même fichier. Le découpage temporel a maintenant trois périodes, chacune séparée par une purge de H H :

  • train : t < 1390 t < 1390 ;
  • validation : 1400 ≤ t < 1790 1400 \le t < 1790 , pour choisir le seuil ;
  • test : t ≥ 1800 t \ge 1800 , regardé une seule fois. Il contient le changement de régime de t = 2100 t = 2100 .

Seuil par défaut et matrice de confusion

# Suite du script de la partie 14 : df, FEATURES, H et T sont déjà définis.
from sklearn.metrics import (confusion_matrix, precision_score, recall_score, f1_score,
                             accuracy_score, roc_auc_score, roc_curve,
                             precision_recall_curve, average_precision_score, brier_score_loss)

# --- Trois périodes, chacune purgée de H : train | validation | test ---
t_val, t_test = 1400, 1800
train = df[df.t < t_val - H]
val = df[(df.t >= t_val) & (df.t < t_test - H)]
test = df[df.t >= t_test]

modele = HistGradientBoostingClassifier(max_iter=200, random_state=0)
modele.fit(train[FEATURES], train.y)
p_val = modele.predict_proba(val[FEATURES])[:, 1]
p_test = modele.predict_proba(test[FEATURES])[:, 1]
print(f"taux de chaudes : val {val.y.mean():.3f} | test {test.y.mean():.3f}")


def taux_faux_froids(y, pred):
    """Part des régions déclarées froides (donc compressées) qui sont relues : FN / (FN + VN)."""
    froides = pred == 0
    return (y[froides] == 1).mean()


def resume(y, p, seuil):
    pred = (p >= seuil).astype(int)
    vn, fp, fn, vp = confusion_matrix(y, pred).ravel()
    return dict(seuil=seuil, VP=vp, FP=fp, FN=fn, VN=vn,
                accuracy=accuracy_score(y, pred), precision=precision_score(y, pred),
                rappel=recall_score(y, pred), f1=f1_score(y, pred),
                faux_froids=taux_faux_froids(y.to_numpy(), pred))


# 1. Le seuil par défaut, et le piège de l'accuracy
r = resume(test.y, p_test, 0.5)
print(f"seuil 0,5 : VP = {r['VP']}, FP = {r['FP']}, FN = {r['FN']}, VN = {r['VN']}")
print(f"accuracy {r['accuracy']:.3f} | précision {r['precision']:.3f} | rappel {r['rappel']:.3f} | "
      f"F1 {r['f1']:.3f} | faux froids {r['faux_froids']:.3f}")
print(f"« tout est froid » : accuracy = {1 - test.y.mean():.3f}, rappel = 0")
Seuil 0,5 sur le test Valeur
VP / FP / FN / VN 24 305 / 4 149 / 11 274 / 74 512
Accuracy 0,865
Précision 0,854
Rappel 0,683
F 1 F_1 0,759
Taux de faux froids 0,131
« Tout froid » : accuracy 0,689 (rappel 0)

Avec le seuil par défaut, le modèle a l’air bon (86,5 % d’accuracy, 85 % de précision). Mais 13,1 % des régions compressées sont relues dans la seconde qui suit : un tiers des régions chaudes (11 274 sur 35 579) finit dans zswap.

ROC, PR, et l’AUC comme probabilité

# 2. ROC et PR : des courbes qui ne dépendent d'aucun seuil
auc = roc_auc_score(test.y, p_test)
ap = average_precision_score(test.y, p_test)
rng_auc = np.random.default_rng(0)
i_pos = rng_auc.choice(np.flatnonzero(test.y == 1), 200_000)
i_neg = rng_auc.choice(np.flatnonzero(test.y == 0), 200_000)
p_gagne = (p_test[i_pos] > p_test[i_neg]).mean() + 0.5 * (p_test[i_pos] == p_test[i_neg]).mean()
print(f"AUC ROC = {auc:.3f} | P(score chaude > score froide) = {p_gagne:.3f}")
print(f"précision moyenne (AUC PR) = {ap:.3f} | hasard = {test.y.mean():.3f}")
  • AUC ROC = 0,858, et la probabilité estimée sur 200 000 paires (chaude, froide) vaut aussi 0,858 : l’AUC est bien P ( score d'une chaude > score d'une froide ) P(\text{score d'une chaude} > \text{score d'une froide}) .
  • Précision moyenne = 0,828, contre 0,311 pour le hasard.
  • L’AUC est un peu plus basse qu’en partie 14 (0,864) : le train s’arrête maintenant à t = 1390 t = 1390 au lieu de 1790, pour laisser la place à la validation.

Le seuil qui minimise le coût

# 3. Choisir le seuil avec un coût asymétrique (sur la validation, jamais sur le test)
C_FAUX_FROID, C_FAUX_CHAUD = 10, 1          # un faux froid coûte 10 fois plus cher


def cout_moyen(y, p, seuil):
    pred = p >= seuil
    fn = np.sum((y == 1) & ~pred)            # faux froids : relues alors que compressées
    fp = np.sum((y == 0) & pred)             # faux chauds : mémoire gardée pour rien
    return (C_FAUX_FROID * fn + C_FAUX_CHAUD * fp) / len(y)


seuils = np.linspace(0.01, 0.99, 99)
couts_val = [cout_moyen(val.y.to_numpy(), p_val, s) for s in seuils]
s_val = seuils[int(np.argmin(couts_val))]
s_theorique = C_FAUX_CHAUD / (C_FAUX_CHAUD + C_FAUX_FROID)
print(f"seuil optimal sur la validation : {s_val:.2f} | seuil théorique : {s_theorique:.3f}")
for s in (0.5, s_val):
    r = resume(test.y, p_test, s)
    print(f"test, seuil {s:.2f} : coût = {cout_moyen(test.y.to_numpy(), p_test, s):.3f} | "
          f"précision {r['precision']:.3f} | rappel {r['rappel']:.3f} | "
          f"faux froids {r['faux_froids']:.3f} | régions compressées {(p_test < s).mean():.3f}")
Test Seuil 0,5 Seuil 0,11 (choisi sur la validation)
Coût moyen par région 1,023 0,707
Précision 0,854 0,417
Rappel 0,683 0,899
Taux de faux froids 0,131 0,096
Régions compressées 75,1 % 32,9 %

Trois panneaux sur le test de la partie 15. À gauche, la courbe ROC du gradient boosting (AUC = 0,858) au-dessus de la diagonale du hasard. Au centre, la courbe précision-rappel (AP = 0,828) au-dessus de la ligne du hasard à 0,311, avec les points des seuils 0,5 (rappel 0,68, précision 0,85) et 0,11 (rappel 0,90, précision 0,42). À droite, le coût moyen par région en fonction du seuil, sur la validation et sur le test, avec le seuil 0,11 choisi sur la validation.

Quatre leçons :

  • le seuil trouvé sur la validation (0,11) est proche du seuil théorique (0,091) : les probabilités du modèle sont à peu près utilisables telles quelles ;
  • baisser le seuil réduit le coût de 31 %, mais on compresse beaucoup moins (32,9 % des régions au lieu de 75,1 %). C’est le vrai compromis du mémoire : gain mémoire contre faux froids ;
  • même au meilleur seuil, le taux de faux froids reste à 9,6 % sur le test. La courbe orange de la figure montre pourquoi : après le changement de régime, la courbe de coût se déplace, et le seuil idéal du test n’est plus celui de la validation. On ne s’en sert pas pour choisir (ce serait une fuite), mais c’est un avertissement pour la surveillance en production (partie 28) ;
  • attention à la bonne référence : ne jamais compresser coûte 1 − 0 , 311 = 0 , 689 1 - 0{,}311 = 0{,}689 par région sur le test, puisque chaque région froide devient un faux chaud. Au seuil 0,11, le modèle coûte 0,707 : un peu plus que ne rien faire. Sur la validation, il battait nettement cette référence (0,547 contre 1 − 0 , 301 = 0 , 699 1 - 0{,}301 = 0{,}699 , voir partie 16) ; c’est le changement de régime qui le fait passer derrière. Un modèle se compare donc d’abord à « ne jamais compresser », pas seulement au seuil 0,5.

Calibration

# 4. Calibration : une probabilité de 0,2 veut-elle dire « chaude 1 fois sur 5 » ?
bornes = [0, 0.05, 0.2, 0.4, 0.6, 0.8, 0.95, 1.0001]
for nom, y, p in (("validation", val.y.to_numpy(), p_val), ("test", test.y.to_numpy(), p_test)):
    print(f"{nom} : Brier = {brier_score_loss(y, p):.4f}")
    for a, b in zip(bornes[:-1], bornes[1:]):
        m = (p >= a) & (p < b)
        print(f"   p dans [{a:.2f} ; {min(b, 1):.2f}[ : n = {m.sum():>6} | p moyen {p[m].mean():.3f} | "
              f"taux réel {y[m].mean():.3f}")
Tranche de p p p p moyen (val) Taux réel (val) p p moyen (test) Taux réel (test)
[ 0 ; 0 , 05 [ [0 ; 0{,}05[ 0,020 0,021 0,024 0,043
[ 0 , 05 ; 0 , 2 [ [0{,}05 ; 0{,}2[ 0,116 0,113 0,118 0,138
[ 0 , 2 ; 0 , 4 [ [0{,}2 ; 0{,}4[ 0,275 0,146 0,274 0,170
[ 0 , 4 ; 0 , 6 [ [0{,}4 ; 0{,}6[ 0,470 0,227 0,484 0,249
[ 0 , 6 ; 0 , 8 [ [0{,}6 ; 0{,}8[ 0,716 0,688 0,713 0,602
[ 0 , 8 ; 0 , 95 [ [0{,}8 ; 0{,}95[ 0,902 0,908 0,891 0,870
[ 0 , 95 ; 1 ] [0{,}95 ; 1] 0,987 0,994 0,990 0,982
Score de Brier 0,0827 0,1119
  • Aux extrémités, le modèle est bien calibré. Dans la zone 0,2 à 0,6, il surestime la probabilité d’être chaude : il annonce 47 % de chances, la réalité est 23 %. Environ 15 % des régions de la validation (19 % du test) tombent là, mais elles sont toutes au-dessus du seuil retenu (0,11) : on les garde en RAM de toute façon, donc cette erreur de calibration ne change aucune décision.
  • Ce qui compte pour notre seuil, c’est la zone basse. Sur la validation, elle est très bien calibrée (0,020 annoncé, 0,021 réel). Sur le test, le taux réel double (0,024 annoncé, 0,043 réel) : après le changement de régime, des régions « sûrement froides » se réveillent. Le Brier passe de 0,083 à 0,112.

Fil rouge : les métriques du protocole

Pour le chapitre Méthodologie du mémoire, chaque modèle (régression logistique, LightGBM, LSTM) sera rapporté avec :

Métrique Pourquoi
AUC ROC qualité du classement, indépendante de la fréquence de base, comparable entre workloads
Précision moyenne (AP) qualité sur la classe chaude, sensible au déséquilibre
Précision, rappel, F 1 F_1 au seuil retenu lisibles par un jury
Taux de faux froids au seuil retenu l’impact direct sur la latence avec zswap
Part de régions compressées le gain mémoire, l’autre plateau de la balance
Coût moyen, comparé à « ne jamais compresser » le modèle fait-il mieux que ne rien faire ?
Brier et diagramme de fiabilité les probabilités sont-elles utilisables pour fixer un seuil ?

Le seuil est toujours choisi sur la validation, avec un coût c F N / c F P c_{FN} / c_{FP} annoncé à l’avance, et les écarts entre modèles sont testés avec les outils de Required Maths M9 (bootstrap apparié, McNemar).


Exercice pratique

  1. Taux de faux froids sous 1 %. Trouve, sur la validation, le plus grand seuil qui garde le taux de faux froids sous 1 %. Quelle part des régions compresse-t-on encore ? Applique ce seuil au test : la contrainte tient-elle ? Explique avec la notion 3 et le tableau de calibration.
  2. Autres coûts. Refais le choix du seuil avec un rapport c F N / c F P c_{FN} / c_{FP} de 3, puis de 30. Compare chaque fois le seuil trouvé sur la validation au seuil théorique s ⋆ s^\star .
  3. Calibrer. Recalibre le modèle déjà entraîné sur la validation : CalibratedClassifierCV(FrozenEstimator(modele), method="isotonic").fit(val[FEATURES], val.y) (imports depuis sklearn.calibration et sklearn.frozen). Le Brier du test s’améliore-t-il ? Le seuil optimal se rapproche-t-il de s ⋆ s^\star ?
  4. Le piège de la fréquence de base. Sous-échantillonne au hasard les régions chaudes du test pour qu’elles ne représentent plus que 5 % des exemples. Au seuil 0,5, que deviennent l’accuracy, la précision, le rappel, l’AUC ROC et la précision moyenne ? Lesquelles dépendent de la fréquence de base, et lesquelles non ?
  5. Fil rouge. Écris dans tes notes le paragraphe « Métriques » du protocole : les sept métriques ci-dessus, le rapport de coût choisi et sa justification, et la période sur laquelle le seuil est fixé.

Indice pour la question 1 : sur la validation, un seuil autour de 0,03 suffit, et on ne compresse alors plus qu’environ 15 % des régions. Regarde ensuite le taux de faux froids sur le test.


Ressources pour aller plus loin

  • Documentation scikit-learn : Metrics and scoring: quantifying the quality of predictions et Probability calibration.
  • Saito et Rehmsmeier, The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets (PLOS ONE, 2015).
  • Fawcett, An introduction to ROC analysis (Pattern Recognition Letters, 2006).
  • Elkan, The Foundations of Cost-Sensitive Learning (IJCAI 2001) : d’où vient le seuil c F P / ( c F P + c F N ) c_{FP} / (c_{FP} + c_{FN}) .

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

  • Lire une matrice de confusion et en déduire précision, rappel, F 1 F_1 et taux de faux froids
  • Expliquer pourquoi l’accuracy trompe sur des classes déséquilibrées
  • Interpréter l’AUC ROC comme une probabilité, et choisir entre ROC et PR
  • Dériver le seuil optimal c F P / ( c F P + c F N ) c_{FP} / (c_{FP} + c_{FN}) et le retrouver empiriquement sur la validation
  • Vérifier la calibration d’un modèle avec un diagramme de fiabilité et le score de Brier
  • Comparer le coût d’un modèle à celui de « ne jamais compresser »

Partie suivante : Modèles linéaires et régularisation (partie 16). On ouvre la boîte : la régression logistique du mémoire, ses coefficients, et ce que les pénalités L 1 L_1 et L 2 L_2 de M11 changent en pratique.