ML Atlas

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.

Ta ilustracja działa w przeglądarce z włączonym JavaScriptem: na 60 próbkach czystego szumu (1000 kolumn, losowe etykiety) wybór 10 cech przed walidacją krzyżową daje pozorne 82% trafności, a wybór wewnątrz każdego podziału uczciwe 43% — poziom zgadywania.

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)]), potem Pipeline([('pre', pre), ('clf', model)]).
  • Każdy krok z fit (imputacja, skalowanie, selekcja, PCA, target encoding, SMOTE) musi siedzieć w pipelinie. Do resamplingu użyj imblearn.pipeline.Pipeline.
  • Strojenie: GridSearchCV(pipe, {'clf__C': [0.1, 1, 10]}) — podwójne podkreślenie wskazuje krok.
  • set_config(transform_output='pandas') lub get_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) przed train_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.

Zobacz też