ML Atlas

Machine Learning von Grund auf · Lektion 4.6 · 25 von 33 fertigen Lektionen · 14 Min.

Pipeline: von der Tabelle zur Vorhersage

Kurz gesagt

Der Skalierer ist Teil des Modells und lernt nur am Training. Am Test selbst angepasst, ergibt er 44,1% statt 99,0%, wenn die Pinguine einzeln ankommen.

Aufwärmen Zwei Fragen aus früheren Lektionen. Eine Minute, und sie blockiert die Lektion nicht.
Aus Lektion 4.5: Datenleck Du füllst fehlende Werte mit dem Mittelwert der ganzen Datei, teilst in Training und Test, trainierst auf dem Training und bewertest auf dem Test. Welcher Schritt ist undicht?

Antwort anzeigen

Richtige Antwort: Das Auffüllen der Lücken mit dem Mittelwert der ganzen Datei

Der Mittelwert enthält die Testdaten, also formt der Test das Training. Passe die Imputation nur auf dem Training an, am besten in einer Pipeline.

Aus Lektion 4.4: Feature Engineering Wahr oder falsch: „Mehr Merkmale ergeben ein besseres Modell.“

Antwort anzeigen

Richtige Antwort: Falsch, weil Merkmale ohne Information Rauschen bringen und die Genauigkeit senken können

200 Rauschspalten drücken kNN von 98,1% auf 77,4%. Was hilft, ist ein Merkmal mit Information in guter Form, wie x² für Rebsorte 2.

In Lektion 4.3 haben wir Messungen skaliert, in Lektion 4.5 nach Datenlecks gesucht. Heute setzen wir das zu einer Maschine zusammen. Eine Pipeline ist eine Liste von Schritten, die eine rohe Tabelle in eine Vorhersage verwandelt: Skalieren, Kodieren, Modell. In scikit-learn ist sie ein einziges Objekt: Du passt sie an wie ein Modell und fragst sie wie ein Modell. Die entscheidende Frage lautet: in welcher Reihenfolge?

Erst vorhersagen, dann prüfen Du hast eine Tabelle mit Pinguinen und willst kNN auf einem zurückgehaltenen Test bewerten. Die Messungen müssen zuerst standardisiert werden: von jeder Spalte den Mittelwert abziehen und durch ihre Standardabweichung teilen. Ein Skalierer ist das Werkzeug, das diesen Mittelwert und diese Standardabweichung berechnet. Erst skalieren und dann die Daten in Training und Test aufteilen, oder umgekehrt?
Wie sicher bist du?

Interaktives Labor

Dieses interaktive Labor läuft im Browser mit aktiviertem JavaScript. Der Text drumherum beschreibt, was es zeigt.

Antwort anzeigen

Richtige Antwort: Erst aufteilen, dann den Skalierer nur am Training anpassen und damit den Test umrechnen

Aufteilen und den Skalierer nur am Training anpassen, in der Kreuzvalidierung in jedem Fold für sich. Bei den Pinguinen ergibt ein Skalierer aus allen Daten zufällig dasselbe: 99,0%, dieselben Vorhersagen für 102 von 102 Testpinguinen. Aber der Mittelwert des Tests gelangt trotzdem in den Skalierer. Und den Test mit seinem eigenen Mittelwert zu skalieren, macht das Modell kaputt: Kommen die Pinguine einzeln, fällt die Genauigkeit auf 44,1%.

Probier es aus

  • Beginne mit dem Skalierer aus dem Training. Wechsle zum Skalierer aus allen Daten. Wie viele Vorhersagen ändern sich, und warum?
  • Wähle den am Test angepassten Skalierer und ändere, wie die Pinguine ankommen. Wann hört das Modell auf zu funktionieren?
  • Setze die Pipeline aus den Blöcken zusammen. Welche der drei Fallen lockt am meisten?

Wenn du es im Lab geprüft hast, geben wir dem, was du siehst, einen Namen.

Die Idee in einfachen Worten

Ein Skalierer ist keine Datenvorbereitung „vor dem Modell“. Er ist der erste Teil des Modells. Für jede Spalte hat er zwei Zahlen: einen Mittelwert und eine Standardabweichung. Zuerst lernt er sie, dann rechnet er damit um. Wie jeder Teil eines Modells lernt er nur am Training. Den Test rechnest du mit denselben Zahlen um, genauso wie später jeden neuen Pinguin.

Ein am Test selbst angepasster Skalierer bricht diese Regel: Der Test bekommt einen eigenen Mittelwert und eine eigene Streuung. Kommt der ganze Test auf einmal und ähnelt er dem Training, sieht das Ergebnis gut aus: 98,0%. Kommen Gruppen von einer Insel, sind es nur noch 82,4%. Auf Torgersen gibt es in den Daten nur Adelie-Pinguine. Zieht man ihren eigenen Mittelwert ab, erkennt das Modell 6 von 13. Kommen die Pinguine einzeln, fällt die Genauigkeit auf 44,1%. Ein einzelner Pinguin hat keine Streuung, also ist er nach dem Umrechnen in jeder Spalte null. Das Modell nennt jeden „Adelie“ und tut damit genau das, was „immer Adelie“ tut.

Eine Pipeline klebt die Schritte zu einem Objekt zusammen, hier Skalierer und kNN. Am Training angepasst, lernt sie Mittelwerte, Standardabweichungen und das Modell selbst in einem Zug, und bei der Vorhersage rechnet sie nur um und fragt. Die Kreuzvalidierung mit einer Pipeline passt den Skalierer in jedem Fold neu an, also berührt kein Testfold das Lernen. Wie das in scikit-learn aussieht, zeigt die Ebene „Im Code“ unten in der Lektion.

Jak to policzono
  • Pinguine: Palmer Penguins (Gorman, Williams und Fraser, 2014; Lizenz CC0), 342 Pinguine mit vier Messungen. Stratifizierte Aufteilung: 240 zum Lernen, 102 für den Test. Die Punkte im Lab sind 20 Aufteilungen mit anderen Seeds.
  • Modell: kNN mit k = 5 auf standardisierten Messungen (Mittelwert 0, Standardabweichung 1). Der Skalierer wird am Training angepasst, an allen 342 Pinguinen oder am Test selbst: auf einmal, getrennt für jede Insel oder getrennt für jeden Pinguin.

Warum das funktioniert

Im echten Einsatz bekommt ein Modell Pinguine, die nicht in den Daten waren, oft einzeln. Dann gibt es weder „alle Daten“ noch „den ganzen Test“. Es gibt neue Pinguine und die Zahlen, die beim Lernen gespeichert wurden. Eine Bewertung ist ehrlich, wenn der Test genau denselben Weg geht. Eine Pipeline erzwingt das: Sie lernt nur am Training, und alles andere wird nur umgerechnet und bewertet.

Ein Skalierer aus allen Daten bricht diese Regel auch, nur leise. Bei den Pinguinen ändert das so gut wie nichts: Die mittlere Flossenlänge von 342 und von 240 Pinguinen unterscheidet sich um 0,2 mm, und in jeder der 20 Aufteilungen des Labs sind die Vorhersagen dieselben. Scheinbares und ehrliches Ergebnis unterscheiden sich hier um 0. Im Code unten liefert scikit-learn auf 50 Aufteilungen dieselben Vorhersagen in 50 von 50. Aber ein Skalierer sieht die Labels nicht. Ein Schritt, der sie sieht, kann an derselben Stelle stark lecken: In Lektion 4.5 machte die Merkmalsauswahl auf allen Daten aus reinem Rauschen ein „Modell“. Eine Pipeline schützt vor beidem, also musst du nicht jedes Mal prüfen, ob das Leck diesmal klein ist.

Ein häufiger Fehler

Richtig oder falsch: „Den Skalierer kann man auch am Test selbst anpassen, schließlich haben Test und Training dann dieselbe Skala“. Falsch. Der Test bekommt einen eigenen Mittelwert und eine eigene Streuung, während das Modell mit den Zahlen aus dem Training gelernt hat. Bei den Pinguinen wirkt das nur harmlos, wenn der ganze Test auf einmal kommt: 98,0%. In Gruppen von einer Insel sind es 82,4%, einzeln 44,1%. Über die 20 Aufteilungen des Labs im Mittel 76,9% und 42,9%, während die Pipeline 99,2% ergibt. Im Code unten: auf einmal 0,971, nach Inseln 0,786, einzeln 0,437, so viel wie „immer Adelie“.

Die Regel ist einfach: Der Skalierer lernt am Training, und Test und neue Daten werden nur mit denselben Zahlen umgerechnet. Eine Pipeline erledigt das für dich.

Quiz

Drei Fragen zur Selbstkontrolle. Wähle eine Antwort oder klappe die Lösung unter der Frage auf. Mit der Tastatur: Pfeiltasten wählen, Enter oder Leertaste bestätigt.

1. Warum passt du den Skalierer nur am Trainingsteil an?
Antwort anzeigen

Richtige Antwort: Weil der Skalierer Teil des Modells ist und der Test Daten spielen soll, die das Modell nie sah

Der Test spielt neue Pinguine. Mittelwert und Streuung sind auch etwas, das das Modell lernt, also werden sie nur am Training berechnet.

2. Die Pinguine kommen einzeln beim Modell an, und jemand passt den Skalierer an jedem einzeln an. Was passiert?
Antwort anzeigen

Richtige Antwort: Jeder Pinguin wird in jeder Spalte zu null, und das Modell sagt über jeden dasselbe

Ein einzelner Pinguin hat keine Streuung, also bleiben nach dem Abziehen seines eigenen Mittelwerts nur Nullen. Das Modell nennt jeden „Adelie“: 44,1%, so viel wie „immer Adelie“.

3. Was macht die Kreuzvalidierung am Training, wenn der Skalierer in der Pipeline sitzt?
Antwort anzeigen

Richtige Antwort: Er passt den Skalierer in jedem Fold neu an, nur an dessen Teil zum Lernen

Eine Pipeline ist ein Modell, also lernt in jedem Fold die ganze Maschine von vorn. Der Fold, der gerade den Test spielt, berührt weder den Skalierer noch kNN.

Das Wichtigste

  • Ein Skalierer, ein Kodierer und jeder andere Schritt, der etwas lernt, sind Teil des Modells: Sie lernen nur am Training.
  • Eine Pipeline klebt die Schritte zu einem Objekt zusammen, also passt die Kreuzvalidierung sie in jedem Fold neu an.
  • Ein am Test selbst angepasster Skalierer macht das Modell kaputt: 44,1% statt 99,0%, wenn die Pinguine einzeln ankommen.
  • Ein Skalierer aus allen Daten leckt wenig (hier 0 geänderte Vorhersagen), aber ein Schritt, der die Labels sieht, kann an derselben Stelle stark lecken.

Tiefer: Formel, Code, Praxis

Die Mathematik

Skalierer: z = (x − μ) / σ, wobei μ und σ Mittelwert und Standardabweichung der Spalte sind, berechnet am Training. Pipeline P = (S, M): P.fit(X, y) berechnet μ und σ an X und trainiert dann das Modell M an S(X). Die Vorhersage ist P.predict(x) = M(S(x)) für jedes neue x, immer mit denselben μ und σ.

fit_transform an einer einzigen Zeile: μ = x und σ = 0 (scikit-learn ersetzt eine Standardabweichung von null durch 1), also z = (x − x) / 1 = 0 in jeder Spalte, egal welcher Pinguin.

Im Code (Python)

Im Code lernt der Skalierer mit fit, rechnet mit transform um, und fit_transform macht beides auf einmal. Dasselbe Experiment in scikit-learn: eine Pipeline am Training, fit_transform am Test auf einmal, nach Inseln und Pinguin für Pinguin, und zum Schluss ein Skalierer, angepasst an allen Daten vor der Aufteilung, über 50 Aufteilungen.

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))  # der Skalierer ist Teil des Modells
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]  # das angepasste kNN ohne seinen Skalierer
batch = knn.score(StandardScaler().fit_transform(X_te), y_te)  # fit_transform am 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): alle Pinguine, vor der Aufteilung
    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")

Ausgabe

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

Die Aufteilung ist eine andere als im Lab (103 Testpinguine statt 102), also unterscheiden sich auch die Zahlen ein wenig. Das Bild ist dasselbe: fit_transform auf dem ganzen Test auf einmal ergibt durch Glück sogar 0,971, nach Inseln 0,786 und einzeln 0,437, so viel wie „immer Adelie“. Ein Skalierer aus allen Daten ändert in diesen 50 Aufteilungen keine einzige Vorhersage.

Für die Praxis
  • In scikit-learn: make_pipeline oder Pipeline([...]). Haben die Spalten verschiedene Typen, ist der erste Schritt ein ColumnTransformer: Zahlen skalieren, Kategorien one-hot kodieren, Lücken füllen.
  • Tuning: GridSearchCV(pipe, param_grid) stimmt die ganze Pipeline innerhalb der Folds ab. Parameter eines Schritts tragen den Namen des Schritts als Präfix, z. B. kneighborsclassifier__n_neighbors.
  • Speichere die ganze Pipeline (joblib.dump(pipe, "model.joblib")), nicht nur das Modell. Dann kann im Betrieb niemand das Skalieren vergessen oder neue Daten mit einem anderen Mittelwert umrechnen.
  • Lücken füllen (Lektion 4.2), Kategorien kodieren (Lektion 4.3), neue Merkmale (Lektion 4.4) und Merkmalsauswahl (Lektion 4.5): Pack jeden dieser Schritte in die Pipeline.

Mehr im Atlas

Einträge der Enzyklopädie zu den Ideen dieser Lektion: