ML Atlas

Le machine learning en partant de zéro · Leçon 4.6 · 25 sur 33 leçons prêtes · 14 min

Pipeline : du tableau à la prédiction

En bref

Le scaler fait partie du modèle et n’apprend que sur l’entraînement. Ajusté sur le test lui-même, il donne 44,1 % au lieu de 99,0 % pour des manchots un par un.

Échauffement Deux questions de leçons précédentes. Une minute, sans jamais bloquer la leçon.
De la leçon 4.5 : Fuite de données Vous comblez les valeurs manquantes avec la moyenne de tout le fichier, séparez en entraînement et test, entraînez sur l’entraînement et évaluez sur le test. Quelle étape fuit ?

Voir la réponse

Bonne réponse : Combler les trous avec la moyenne calculée sur tout le fichier

La moyenne inclut les données de test, donc le test influence l’entraînement. Ajustez l’imputation sur l’entraînement seulement, idéalement dans un pipeline.

De la leçon 4.4 : Ingénierie des variables Vrai ou faux : « plus de variables font un meilleur modèle ».

Voir la réponse

Bonne réponse : Faux, car les variables sans information ajoutent du bruit et peuvent baisser l’exactitude

200 colonnes de bruit font tomber le kNN de 98,1 % à 77,4 %. Ce qui aide, c’est une variable porteuse d’information sous une bonne forme, comme x² pour le cépage 2.

Dans la leçon 4.3, nous avons mis des mesures à l’échelle, et dans la leçon 4.5, nous avons cherché des fuites. Aujourd’hui, nous assemblons tout cela en une seule machine. Un pipeline est une liste d’étapes qui transforme un tableau brut en prédiction : mise à l’échelle, encodage, modèle. Dans scikit-learn, c’est un seul objet : on l’ajuste comme un modèle et on l’interroge comme un modèle. La question clé : dans quel ordre ?

Prédisez, puis vérifiez Vous avez un tableau de manchots et voulez évaluer un kNN sur un test mis de côté. Il faut d’abord standardiser les mesures : soustraire à chaque colonne sa moyenne et la diviser par son écart-type. Un scaler est l’outil qui calcule cette moyenne et cet écart-type. Faut-il d’abord mettre à l’échelle, puis découper les données en entraînement et test, ou l’inverse ?
Quel est votre degré de certitude ?

Labo interactif

Ce labo interactif fonctionne dans un navigateur avec JavaScript activé. Le texte qui l’entoure décrit ce qu’il montre.

Voir la réponse

Bonne réponse : D’abord découper, puis ajuster le scaler sur l’entraînement seulement et convertir le test avec lui

Découper, et ajuster le scaler sur l’entraînement seulement ; en validation croisée, dans chaque fold séparément. Sur les manchots, un scaler ajusté sur toutes les données donne par hasard la même chose : 99,0 %, les mêmes prédictions pour 102 manchots du test sur 102. Mais la moyenne du test entre quand même dans le scaler. Et mettre le test à l’échelle avec sa propre moyenne casse le modèle : quand les manchots arrivent un par un, l’exactitude tombe à 44,1 %.

À vous

  • Commencez par le scaler de l’entraînement. Passez au scaler de toutes les données. Combien de prédictions changent, et pourquoi ?
  • Choisissez le scaler ajusté sur le test et changez la façon dont arrivent les manchots. Quand le modèle cesse-t-il de fonctionner ?
  • Assemblez le pipeline à partir des blocs. Lequel des trois pièges est le plus tentant ?

Une fois vérifié dans le lab, mettons des mots sur ce que vous voyez.

L’idée en mots simples

Un scaler n’est pas une préparation des données « avant le modèle ». C’est la première partie du modèle. Pour chaque colonne, il garde deux nombres : une moyenne et un écart-type. D’abord il les apprend, puis il convertit avec eux. Comme toute partie d’un modèle, il n’apprend que sur l’entraînement. Le test se convertit avec les mêmes nombres, exactement comme plus tard chaque nouveau manchot.

Un scaler ajusté sur le test lui-même enfreint cette règle : le test reçoit sa propre moyenne et sa propre dispersion. Quand tout le test arrive à la fois et ressemble à l’entraînement, le résultat a l’air bon : 98,0 %. Quand arrivent des lots d’une seule île, ce n’est plus que 82,4 %. Dans les données, Torgersen n’a que des manchots Adelie. Une fois leur propre moyenne retirée, le modèle en reconnaît 6 sur 13. Quand les manchots arrivent un par un, l’exactitude tombe à 44,1 %. Un seul manchot n’a pas de dispersion, donc après la conversion il vaut zéro dans chaque colonne. Le modèle les appelle tous « Adelie », exactement comme « toujours Adelie ».

Un pipeline colle les étapes en un seul objet, ici le scaler et le kNN. Ajusté sur l’entraînement, il apprend d’un coup les moyennes, les écarts-types et le modèle lui-même, et pour prédire il ne fait que convertir et interroger. La validation croisée avec un pipeline réajuste le scaler dans chaque fold, donc aucun fold de test ne touche l’apprentissage. À quoi cela ressemble dans scikit-learn, la couche « En code » en bas de la leçon le montre.

Jak to policzono
  • Manchots : Palmer Penguins (Gorman, Williams et Fraser, 2014 ; licence CC0), 342 manchots avec quatre mesures. Découpage stratifié : 240 pour apprendre, 102 pour le test. Les points du lab sont 20 découpages avec d’autres graines.
  • Modèle : kNN avec k = 5 sur les mesures standardisées (moyenne 0, écart-type 1). Le scaler est ajusté sur l’entraînement, sur les 342 manchots ou sur le test lui-même : tout à la fois, séparément pour chaque île ou séparément pour chaque manchot.

Pourquoi ça marche

En usage réel, un modèle reçoit des manchots qui n’étaient pas dans les données, souvent un par un. Il n’y a alors ni « toutes les données » ni « tout le test ». Il y a de nouveaux manchots et les nombres retenus pendant l’apprentissage. Une évaluation est honnête quand le test suit exactement le même chemin. Un pipeline l’impose : il n’apprend que sur l’entraînement, et tout le reste est seulement converti et évalué.

Un scaler ajusté sur toutes les données enfreint aussi cette règle, mais en silence. Sur les manchots, cela ne change presque rien : la longueur moyenne de la nageoire sur 342 et sur 240 manchots diffère de 0,2 mm, et dans chacun des 20 découpages du lab les prédictions sont les mêmes. Le résultat apparent et le résultat honnête diffèrent ici de 0. Dans le code ci-dessous, scikit-learn donne sur 50 découpages les mêmes prédictions dans 50 cas sur 50. Mais un scaler ne voit pas les étiquettes. Une étape qui les voit peut fuir beaucoup au même endroit : dans la leçon 4.5, choisir les variables sur toutes les données a fait un « modèle » à partir de bruit pur. Un pipeline protège des deux, donc vous n’avez pas à vérifier à chaque fois si, cette fois, la fuite est petite.

Une erreur fréquente

Vrai ou faux : « On peut aussi ajuster le scaler sur le test lui-même ; après tout, le test et l’entraînement ont alors la même échelle ». Faux. Le test reçoit sa propre moyenne et sa propre dispersion, alors que le modèle a appris avec les nombres de l’entraînement. Sur les manchots, cela paraît inoffensif seulement quand tout le test arrive à la fois : 98,0 %. Par lots d’une île, c’est 82,4 %, un par un 44,1 %. Sur les 20 découpages du lab, en moyenne 76,9 % et 42,9 %, alors que le pipeline donne 99,2 %. Dans le code ci-dessous : à la fois 0,971, par île 0,786, un par un 0,437, autant que « toujours Adelie ».

La règle est simple : le scaler apprend sur l’entraînement, et le test et les nouvelles données sont seulement convertis avec les mêmes nombres. Un pipeline le fait pour vous.

Quiz

Trois questions pour vérifier vos acquis. Choisissez une réponse ou dépliez la solution sous chaque question. Au clavier : les flèches choisissent, Entrée ou Espace valide.

1. Pourquoi ajuste-t-on le scaler sur la partie d’entraînement seulement ?
Voir la réponse

Bonne réponse : Parce que le scaler fait partie du modèle et que le test doit jouer des données jamais vues

Le test joue de nouveaux manchots. La moyenne et la dispersion sont aussi quelque chose que le modèle apprend, donc on les calcule sur l’entraînement seulement.

2. Les manchots arrivent au modèle un par un, et quelqu’un ajuste le scaler sur chacun séparément. Que se passe-t-il ?
Voir la réponse

Bonne réponse : Chaque manchot devient zéro dans chaque colonne, et le modèle dit la même chose de chacun

Un seul manchot n’a pas de dispersion, donc après avoir retiré sa propre moyenne il ne reste que des zéros. Le modèle les appelle tous « Adelie » : 44,1 %, autant que « toujours Adelie ».

3. Que fait la validation croisée sur l’entraînement quand le scaler est dans le pipeline ?
Voir la réponse

Bonne réponse : Il réajuste le scaler dans chaque fold, seulement sur la partie d’apprentissage de ce fold

Un pipeline est un seul modèle, donc dans chaque fold toute la machine apprend de zéro. Le fold qui joue le test ne touche ni le scaler ni le kNN.

À retenir

  • Un scaler, un encodeur et toute autre étape qui apprend quelque chose font partie du modèle : ils n’apprennent que sur l’entraînement.
  • Un pipeline colle les étapes en un seul objet, donc la validation croisée les réajuste dans chaque fold.
  • Un scaler ajusté sur le test lui-même casse le modèle : 44,1 % au lieu de 99,0 % pour des manchots qui arrivent un par un.
  • Un scaler ajusté sur toutes les données fuit peu (ici 0 prédiction changée), mais une étape qui voit les étiquettes peut fuir beaucoup au même endroit.

Pour aller plus loin : formule, code, pratique

Les maths

Scaler : z = (x − μ) / σ, où μ et σ sont la moyenne et l’écart-type de la colonne calculés sur l’entraînement. Pipeline P = (S, M) : P.fit(X, y) calcule μ et σ sur X, puis entraîne le modèle M sur S(X). La prédiction est P.predict(x) = M(S(x)) pour chaque nouveau x, toujours avec les mêmes μ et σ.

fit_transform sur une seule ligne : μ = x et σ = 0 (scikit-learn remplace un écart-type nul par 1), donc z = (x − x) / 1 = 0 dans chaque colonne, quel que soit le manchot.

En code (Python)

En code, le scaler apprend avec fit, convertit avec transform, et fit_transform fait les deux à la fois. La même expérience dans scikit-learn : un pipeline sur l’entraînement, fit_transform sur le test tout à la fois, par île et manchot par manchot, et pour finir un scaler ajusté sur toutes les données avant le découpage, sur 50 découpages.

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier

df = pd.read_csv("data/raw/penguins.csv")
cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]
d = df.dropna(subset=cols)
X, y = d[cols], d["species"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=1)
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier(5))  # le scaler fait partie du modèle
print(f"train {len(y_tr)}, test {len(y_te)}; CV on the training part: {cross_val_score(pipe, X_tr, y_tr, cv=5).mean():.3f}")
pipe.fit(X_tr, y_tr)
print(f"test, scaler fitted on the training part: {pipe.score(X_te, y_te):.3f}")
knn = pipe[-1]  # le kNN ajusté, sans son scaler
batch = knn.score(StandardScaler().fit_transform(X_te), y_te)  # fit_transform sur le test
right = [knn.predict(StandardScaler().fit_transform(part)) == y_te[part.index] for _, part in X_te.groupby(d["island"])]
one = [knn.predict(StandardScaler().fit_transform(X_te.loc[[i]]))[0] == y_te[i] for i in X_te.index]
print(f"fit_transform on the test: all at once {batch:.3f}, by island {np.concatenate(right).mean():.3f}, one at a time {np.mean(one):.3f}")
print(f"always Adelie: {(y_te == 'Adelie').mean():.3f}")
same = equal = 0
for s in range(50):
    a, b, ya, yb = train_test_split(X, y, test_size=0.3, stratify=y, random_state=s)
    honest = make_pipeline(StandardScaler(), KNeighborsClassifier(5)).fit(a, ya).predict(b)
    scaler = StandardScaler().fit(X)  # scaler.fit(X) : tous les manchots, avant le découpage
    leaky = KNeighborsClassifier(5).fit(scaler.transform(a), ya).predict(scaler.transform(b))
    same += (honest == leaky).all()
    equal += (leaky == yb).sum() == (honest == yb).sum()
print(f"scaler.fit(X) before the split, 50 splits: the same predictions in {same} of 50, the same test accuracy in {equal} of 50")

Sortie

train 239, test 103; CV on the training part: 0.983
test, scaler fitted on the training part: 0.961
fit_transform on the test: all at once 0.971, by island 0.786, one at a time 0.437
always Adelie: 0.437
scaler.fit(X) before the split, 50 splits: the same predictions in 50 of 50, the same test accuracy in 50 of 50

Le découpage diffère de celui du lab (103 manchots de test au lieu de 102), donc les nombres diffèrent un peu aussi. L’image est la même : fit_transform sur tout le test à la fois donne même 0,971 par chance, par île 0,786 et un par un 0,437, autant que « toujours Adelie ». Un scaler ajusté sur toutes les données ne change aucune prédiction dans ces 50 découpages.

Pour les praticiens
  • Dans scikit-learn : make_pipeline ou Pipeline([...]). Quand les colonnes sont de types différents, la première étape est un ColumnTransformer : mettre à l’échelle les nombres, encoder les catégories en one-hot, combler les trous.
  • Réglage : GridSearchCV(pipe, param_grid) règle tout le pipeline à l’intérieur des folds. Les paramètres d’une étape portent son nom en préfixe, p. ex. kneighborsclassifier__n_neighbors.
  • Sauvegardez le pipeline entier (joblib.dump(pipe, "model.joblib")), pas le modèle seul. Ainsi, en production, personne ne peut oublier la mise à l’échelle ni convertir de nouvelles données avec une autre moyenne.
  • Combler les trous (leçon 4.2), encoder les catégories (leçon 4.3), créer des variables (leçon 4.4) et sélectionner des variables (leçon 4.5) : mettez chacune de ces étapes dans le pipeline.

Aller plus loin dans l’Atlas

Articles de l’encyclopédie sur les notions de cette leçon :