10 · Praktyka · 3 min czytania · Interaktywne · aktualizacja
Po co używać Pipeline i ColumnTransformer w scikit-learn?
W skrócie
Pipeline łączy przetwarzanie danych i model w jeden obiekt, dzięki czemu każdy krok uczy się tylko na danych uczących, a walidacja nie przepuszcza wycieku.
Co to jest
Pipeline to łańcuch kroków przetwarzania zakończony modelem, traktowany jak jeden estymator: ma wspólne fit i predict. W scikit-learn tworzy go Pipeline lub make_pipeline, a ColumnTransformer pozwala przetwarzać różne kolumny różnie — liczby imputować i skalować, kategorie kodować one-hot.
Zysk jest podwójny. Po pierwsze porządek: cały przepis od surowej tabeli do predykcji siedzi w jednym obiekcie, który można zapisać, wdrożyć i odtworzyć. Po drugie — ważniejsze — poprawność: gdy pipeline trafia do walidacji krzyżowej, każdy krok jest dopasowywany od nowa tylko na części uczącej danego foldu.
Mechanizm — dlaczego tak działa
Każdy krok przetwarzania, który „uczy się” z danych, jest małym modelem. Imputer zapamiętuje medianę, skaler średnią i odchylenie, enkoder listę kategorii, selektor cech — które kolumny najlepiej korelują z celem. Jeśli dopasujesz go na całym zbiorze przed podziałem, informacja z części walidacyjnej przecieka do uczenia. Walidacja przestaje wtedy symulować sytuację, w której model widzi zupełnie nowe dane.
Siła tego wycieku zależy od tego, ile kroki wiedzą o zmiennej docelowej. Mediana wieku policzona na wszystkich pasażerach zamiast na 80% z nich to drobiazg. Ale wybór cech według korelacji z celem albo kodowanie kategorii średnią celu (target encoding) to już bezpośrednie podglądanie odpowiedzi. Przy wielu cechach i małej liczbie obserwacji zawsze znajdą się kolumny, które przypadkiem pasują do etykiet w całym zbiorze — i będą „działać” w każdym foldzie, bo zostały wybrane właśnie na tych danych.
Pipeline rozwiązuje to strukturalnie, a nie dyscypliną. cross_val_score(pipe, X, y) w każdym foldzie wywołuje pipe.fit na części uczącej i pipe.predict na walidacyjnej, więc nie da się „zapomnieć” o przeliczeniu skalera. To samo dotyczy strojenia hiperparametrów: GridSearchCV na pipelinie stroi jednocześnie parametry przetwarzania i modelu, a nazwy w stylu clf__C lub pre__num__imputer__strategy adresują dowolny krok.
Trzecia korzyść ujawnia się po wdrożeniu. Jeśli przetwarzanie jest osobnym skryptem, a model osobnym plikiem, prędzej czy później wersje się rozjadą i model dostanie dane przygotowane inaczej niż przy uczeniu. Jeden zapisany pipeline usuwa tę klasę błędów.
Na przykładzie
Titanic: cechy liczbowe age, sibsp, parch, fare idą przez imputację medianą i standaryzację, kategoryczne pclass, sex, embarked przez imputację najczęstszą wartością i one-hot. ColumnTransformer zamienia 7 kolumn w 12 cech, a regresja logistyczna na końcu daje 79,6% w 5-krotnej walidacji krzyżowej. Dopasowanie tego samego przetwarzania na całym zbiorze przed walidacją daje… również 79,6%. Imputacja i skalowanie prawie nie niosą informacji o celu, więc tu wyciek jest pomijalny.
Inaczej z selekcją cech. Do 891 wierszy dołożyliśmy 2000 kolumn czystego szumu i losowo przetasowaną etykietę, więc żadnego sygnału nie ma — uczciwy wynik to poziom modelu bazowego, 61,6%. Wybór 20 „najlepszych” kolumn testem F na całym zbiorze, a potem walidacja krzyżowa, daje 67,0% — pozornie sygnał. Ta sama selekcja wewnątrz pipeline'u daje 57,7%, czyli zgodnie z prawdą: nic tu nie ma.
Dane: Titanic
W praktyce
- Szkielet:
ColumnTransformer([('num', make_pipeline(SimpleImputer(strategy='median'), StandardScaler()), num_cols), ('cat', make_pipeline(SimpleImputer(strategy='most_frequent'), OneHotEncoder(handle_unknown='ignore')), cat_cols)]), potemPipeline([('pre', pre), ('clf', model)]). - Każdy krok z
fit(imputacja, skalowanie, selekcja, PCA, target encoding, SMOTE) musi siedzieć w pipelinie. Do resamplingu użyjimblearn.pipeline.Pipeline. - Strojenie:
GridSearchCV(pipe, {'clf__C': [0.1, 1, 10]})— podwójne podkreślenie wskazuje krok. set_config(transform_output='pandas')lubget_feature_names_out()przywraca czytelne nazwy cech.- Zapis:
joblib.dump(pipe, 'model.joblib')— zapisujesz przepis razem z modelem. - Typowy błąd:
StandardScaler().fit_transform(X)przedtrain_test_split.
Najczęstsze pytania
- Czy skalowanie przed podziałem naprawdę coś psuje?
- Zwykle niewiele, bo średnia i odchylenie nie mówią nic o etykiecie. Problem robi się poważny przy krokach, które patrzą na cel (selekcja cech, target encoding) albo przy danych czasowych, gdzie statystyki z przyszłości przeciekają do przeszłości. Pipeline usuwa wątpliwość niezależnie od kroku.
- Co robi `handle_unknown='ignore'`?
- Kategoria niewidziana przy uczeniu jest kodowana jako same zera zamiast wywoływać błąd. To wygodne, ale niebezpieczne: przy literówce w danych produkcyjnych model po cichu traci informację, więc warto takie przypadki logować.
- Pipeline czy ręczny kod w pandas?
- Eksploracja w pandas jest w porządku. Wszystko, co trafia do walidacji i wdrożenia, powinno być w pipelinie, bo tylko wtedy masz gwarancję, że uczenie, walidacja i produkcja przetwarzają dane identycznie.
Źródła
- Dokumentacja scikit-learn: „Pipelines and composite estimators”, https://scikit-learn.org/stable/modules/compose.html
- Dokumentacja scikit-learn: „Common pitfalls and recommended practices”, https://scikit-learn.org/stable/common_pitfalls.html
- Hastie T., Tibshirani R., Friedman J. „The Elements of Statistical Learning”, 2nd ed., 2009, rozdz. 7.10.2 (The Wrong and Right Way to Do Cross-validation).
- Ambroise C., McLachlan G. J. „Selection bias in gene extraction on the basis of microarray gene-expression data”, PNAS 99(10), 2002.