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.
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 ?
Choisissez une réponse et votre degré de certitude.
Répondez pour débloquer le labo.
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.
À 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.
Télécharger le notebook (.ipynb). Ouvrez-le dans Jupyter ou importez-le dans Google Colab ; il télécharge lui-même les données manquantes.
Pour les praticiens
- Dans scikit-learn :
make_pipelineouPipeline([...]). Quand les colonnes sont de types différents, la première étape est unColumnTransformer: 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.
Cela vous a-t-il aidé ?
Merci ! Cela nous aide à nous améliorer.
Aller plus loin dans l’Atlas
Articles de l’encyclopédie sur les notions de cette leçon :