Kurs uczenia maszynowego od zera · Lekcja 4.6 · 25 z 33 gotowych lekcji · 14 min
Pipeline: od tabeli do predykcji
W skrócie
Skaler to część modelu i uczy się tylko na treningu. Skaler dopasowany na samym teście daje pingwinom przychodzącym po jednym 44,1% zamiast 99,0%.
Rozgrzewka Dwa pytania z wcześniejszych lekcji. Minuta, a lekcji nie blokuje.
W lekcji 4.3 skalowaliśmy pomiary, a w lekcji 4.5 szukaliśmy wycieku. Dziś składamy to w jedną maszynę. Pipeline to lista kroków, które zamieniają surową tabelę w predykcję: skalowanie, kodowanie, model. W scikit-learn to jeden obiekt: dopasowujesz go jak model i pytasz jak model. Najważniejsze pytanie brzmi: w jakiej kolejności?
Wybierz odpowiedź i pewność.
Odpowiedz, żeby odblokować lab.
Eksperyment
Ten interaktywny eksperyment działa w przeglądarce z włączonym JavaScriptem. Tekst wokół opisuje, co w nim widać.
Pokaż odpowiedź
Poprawna odpowiedź: Najpierw dzielić, a skaler dopasować tylko na treningu i nim przeliczyć test
Dzielić, a skaler dopasować tylko na treningu, w walidacji krzyżowej w każdym foldzie osobno. Na pingwinach skaler z całych danych akurat daje to samo: 99,0%, te same predykcje dla 102 ze 102 pingwinów testowych. Ale średnia testu i tak trafia do skalera. A skalowanie testu jego własną średnią psuje model: gdy pingwiny przychodzą po jednym, trafność spada do 44,1%.
Spróbuj
- Zacznij od skalera z treningu. Przełącz na skaler z całych danych. Ile predykcji się zmienia i dlaczego?
- Wybierz skaler dopasowany na teście i zmieniaj sposób, w jaki przychodzą pingwiny. Kiedy model przestaje działać?
- Ułóż pipeline z bloków. Która z trzech pułapek kusi najbardziej?
Gdy już sprawdzisz to w labie, nazwijmy, co widzisz.
Pomysł w prostych słowach
Skaler to nie jest przygotowanie danych „przed modelem”. To pierwsza część modelu. Na każdą kolumnę ma dwie liczby: średnią i odchylenie. Najpierw się ich uczy, potem nimi przelicza. Jak każda część modelu, uczy się tylko na treningu. Test przeliczasz tymi samymi liczbami, tak samo jak później każdego nowego pingwina.
Skaler dopasowany na samym teście łamie tę zasadę: test dostaje własną średnią i rozrzut. Gdy cały test przychodzi naraz i przypomina trening, wynik wygląda dobrze: 98,0%. Gdy przychodzą partie z jednej wyspy, już tylko 82,4%. Na Torgersen są w danych tylko pingwiny Adelie. Po odjęciu ich własnej średniej model rozpoznaje 6 z 13. Gdy pingwiny przychodzą po jednym, trafność spada do 44,1%. Jeden pingwin nie ma rozrzutu, więc po przeliczeniu jest zerem w każdej kolumnie. Model mówi o każdym „Adelie”, czyli robi to samo co „zawsze Adelie”.
Pipeline skleja kroki w jeden obiekt, tu skaler i kNN. Dopasowany na treningu uczy się naraz średnich, odchyleń i samego modelu, a przy predykcji tylko przelicza i pyta. Walidacja krzyżowa z pipeline’em dopasowuje skaler w każdym foldzie od nowa, więc żaden fold testowy nie dotyka nauki. Jak to wygląda w scikit-learn, pokazuje warstwa „W kodzie” na dole lekcji.
Jak to policzono
- Pingwiny: Palmer Penguins (Gorman, Williams i Fraser, 2014; licencja CC0), 342 pingwiny z czterema pomiarami. Podział stratyfikowany: 240 do nauki, 102 do testu. Kropki w labie to 20 podziałów z innymi ziarnami.
- Model: kNN z k = 5 na pomiarach po standaryzacji (średnia 0, odchylenie 1). Skaler dopasowany na treningu, na wszystkich 342 pingwinach albo na samym teście: naraz, osobno na każdej wyspie albo osobno na każdym pingwinie.
Dlaczego to działa
Model w prawdziwym użyciu dostaje pingwiny, których nie było w danych, często po jednym. Nie ma wtedy „całych danych” ani „całego testu”. Są nowe pingwiny i liczby zapamiętane w czasie nauki. Ocena jest uczciwa, gdy test przechodzi dokładnie tę samą drogę. Pipeline to wymusza: uczy się tylko na treningu, a wszystko inne tylko przelicza i ocenia.
Skaler z całych danych też łamie tę zasadę, tylko po cichu. Na pingwinach prawie nic to nie zmienia: średnia długość płetwy z 342 i z 240 pingwinów różni się o 0,2 mm, a w każdym z 20 podziałów labu predykcje są te same. Wynik pozorny i uczciwy różnią się tu o 0. W kodzie niżej scikit-learn na 50 podziałach daje te same predykcje w 50 z 50. Skaler nie widzi jednak etykiet. Krok, który je widzi, w tym samym miejscu potrafi przeciekać mocno: w lekcji 4.5 wybór cech na całych danych zrobił „model” z czystego szumu. Pipeline chroni przed jednym i drugim, więc nie musisz za każdym razem sprawdzać, czy tym razem wyciek jest mały.
Częsty błąd
Prawda czy fałsz: „Skaler można też dopasować na samym teście, przecież test i trening mają wtedy tę samą skalę”. Fałsz. Test dostaje własną średnią i rozrzut, a model uczył się na liczbach z treningu. Na pingwinach wygląda to niewinnie tylko wtedy, gdy cały test przychodzi naraz: 98,0%. Partiami z jednej wyspy jest 82,4%, po jednym pingwinie 44,1%. W 20 podziałach labu średnio 76,9% i 42,9%, a pipeline daje 99,2%. W kodzie niżej: naraz 0,971, wyspami 0,786, po jednym 0,437, czyli tyle co „zawsze Adelie”.
Zasada jest prosta: skaler uczy się na treningu, a test i nowe dane tylko przelicza tymi samymi liczbami. Pipeline robi to sam.
Quiz
Trzy pytania na sprawdzenie się. Wybierz odpowiedź albo rozwiń odpowiedź pod pytaniem. Z klawiatury: strzałki wybierają, Enter albo spacja zatwierdza.
Najważniejsze wnioski
- Skaler, koder i każdy inny krok, który się czegoś uczy, to część modelu: uczy się tylko na treningu.
- Pipeline skleja kroki w jeden obiekt, więc walidacja krzyżowa dopasowuje je w każdym foldzie od nowa.
- Skaler dopasowany na samym teście psuje model: dla pingwinów przychodzących po jednym 44,1% zamiast 99,0%.
- Skaler z całych danych przecieka mało (tu 0 zmienionych predykcji), ale krok, który widzi etykiety, w tym samym miejscu potrafi przeciekać mocno.
Głębiej: wzór, kod, praktyka
Matematyka
Skaler: z = (x − μ) / σ, gdzie μ i σ to średnia i odchylenie kolumny policzone na treningu. Pipeline P = (S, M): P.fit(X, y) liczy μ i σ na X, a potem uczy model M na S(X). Predykcja to P.predict(x) = M(S(x)) dla każdego nowego x, zawsze z tymi samymi μ i σ.
fit_transform na jednym wierszu: μ = x i σ = 0 (scikit-learn zamienia zerowe odchylenie na 1), więc z = (x − x) / 1 = 0 w każdej kolumnie, niezależnie od pingwina.
W kodzie (Python)
W kodzie skaler uczy się metodą fit, przelicza metodą transform, a fit_transform robi jedno i drugie naraz. Ten sam eksperyment w scikit-learn: pipeline na części treningowej, fit_transform na teście naraz, wyspami i po jednym pingwinie, a na koniec skaler dopasowany na wszystkich danych przed podziałem na 50 podziałach.
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)) # skaler jest częścią modelu
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] # dopasowany kNN bez swojego skalera
batch = knn.score(StandardScaler().fit_transform(X_te), y_te) # fit_transform na teście
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): wszystkie pingwiny, przed podziałem
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")
Wynik
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
Podział jest inny niż w labie (103 pingwiny w teście zamiast 102), więc liczby też są trochę inne. Obraz jest ten sam: fit_transform na całym teście naraz przypadkiem daje nawet 0,971, wyspami 0,786, a po jednym 0,437, tyle co „zawsze Adelie”. Skaler z całych danych nie zmienia w tych 50 podziałach ani jednej predykcji.
Pobierz notebook (.ipynb). Otwórz go w Jupyterze albo wgraj do Google Colab; brakujące dane pobierze sam.
Dla praktyka
- W scikit-learn:
make_pipelinealboPipeline([...]). Gdy kolumny są różnego typu, pierwszym krokiem jestColumnTransformer: skalowanie liczb, kodowanie one-hot kategorii, uzupełnianie braków. - Strojenie:
GridSearchCV(pipe, param_grid)stroi cały pipeline w foldach. Parametry kroków mają przedrostek z nazwą kroku, np.kneighborsclassifier__n_neighbors. - Zapisuj cały pipeline (
joblib.dump(pipe, "model.joblib")), nie sam model. Wtedy w produkcji nie da się zapomnieć o skalowaniu ani przeliczyć nowych danych inną średnią. - Uzupełnianie braków (lekcja 4.2), kodowanie kategorii (lekcja 4.3), nowe cechy (lekcja 4.4) i wybór cech (lekcja 4.5): każdy z tych kroków wkładaj do pipeline’u.
Czy to było pomocne?
Dzięki! To pomaga nam poprawiać Atlas.
Więcej w Atlasie
Hasła encyklopedii o pojęciach z tej lekcji: