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.
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?
Wähle eine Antwort und wie sicher du bist.
Antworte, um das Labor freizuschalten.
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.
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.
Notebook herunterladen (.ipynb). Öffne es in Jupyter oder lade es in Google Colab hoch; fehlende Daten lädt es selbst.
Für die Praxis
- In scikit-learn:
make_pipelineoderPipeline([...]). Haben die Spalten verschiedene Typen, ist der erste Schritt einColumnTransformer: 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.
War das hilfreich?
Danke! Das hilft uns, besser zu werden.
Mehr im Atlas
Einträge der Enzyklopädie zu den Ideen dieser Lektion: