Przejdź do treści

Pipeline w SciKit Learn

Co to są pipelines w sci-kit learn i jak je wykorzystać? Czyli bardziej efektywne szukanie najlepszego modelu.

Photo by JJ Ying on Unsplash

Jeśli zajmujesz się tworzeniem modeli na przykład klasyfikujących jakieś dane to pewnie wielokrotnie powtarzasz te same kroki:

  • wczytanie danych
  • oczyszczenie danych
  • uzupełnienie braków
  • przygotowanie dodatkowych cech (feature engineering)
  • podział danych na treningowe i testowe
  • dobór hyperparametrów modelu
  • trenowanie modelu
  • testowanie modelu (sprawdzenie skuteczności działania)

I tak w kółko, z każdym nowy modelem.

Jest to dość nudne i dość powtarzalne. Szczególnie jak trzeba wykonać różne kroki transformacji danych w różnej kolejności – upierdliwe staje się zmienianie kolejności w kodzie.

Dlatego wymyślono pipelines.

Dlatego w tym i kolejnych postach zajmiemy się tym mechanizmem.

Zaczniemy od podstaw data science, czyli…

Wszystkie elementy jakich będziemy używać znajdują się w ramach biblioteki scikit-learn. Zaimportujemy co trzeba plus kilka modeli z oddzielnych bibliotek.

Skąd wziąć dane? Można użyć wbudowanych w sklearna irysów czy boston housing ale mi zależało na znalezieniu takiego datasetu, który będzie zawierał cechy zarówno ciągłe jak i kategoryczne. Takim zestawem jest Adult znany też jako Census Income, a ściągnąć go można z UCI (studenci i absolwenci AGH mogą mylić z UCI w budynku C-1).

Pobieramy (potrzebne nam będą pliki adult.data oraz adult.test) dane, wrzucamy surowe pliki do katalogu data/ i wczytujemy.

Zobaczmy jakie mamy typy danych w kolumnach:

A teraz standardowo – dzielimy dane na zbiór treningowy i testowy. Przy okazji z całej ramki danych wyciągamy kolumnę year_income jako Y, a resztę jako X. Szalenie wygodnym jest nazywanie cech zmienną X a targetów y – przy Ctrl-C + Ctrl-V ze StackOverflow niczego właściwie nie trzeba robić ;)

Sporą część wstępnej analizy pomijam w tym wpisie, ale jeśli nie wiesz dlaczego wybieram takie a nie inne kolumny to zrób samodzielnie analizę danych w tym zbiorze.

Czas na trochę informacji o rurociągach.

W dużym uproszczeniu są to połączone sekwencyjnie (jedna za drugą, wyjście pierwszej trafia na wejście drugiej i tak dalej do końca) operacje. Operacje czyli klasy, które posiadają metody .fit() i .transform().

Rurociąg może składać się z kilku rurociągów połączonych jeden za drugim – taki model zastosujemy za chwilę.

Powiedzieliśmy sobie wyżej, że pierwszy krok to przygotowanie danych – odpowiednie transformacje danych źródłowych i ewentualnie uzupełnienie danych brakujących. W dzisiejszym przekładzie brakujące dane (około 7% całości) po prostu wyrzuciliśmy przez dropna(), więc uzupełnieniem braków się nie zajmujemy. Ale może w kolejnej części już tak.

Drugim krokiem jest przesłanie danych odpowiednio obrobionych do modelu i jego wytrenowanie.

No to do dzieła, już konkretnie – transformacja danych

Zatem na początek przygotujemy sobie fragmenty całego rurociągu odpowiedzialnego za transformacje kolumn. Mamy dwa typy kolumn, zatem zbudujemy dwa małe rurociągi.

Pierwszy będzie odpowiedzialny za kolumny z wartościami liczbowymi. Nie wiemy czy są to wartości ciągłe (jak na przykład wiek) czy dyskretne (tutaj taką kolumną jest education_num mówiąca o poziome edukacji) i poniżej bierzemy wszystkie jak leci. Znowu: porządna EDA wskaże nam odpowiednie kolumny.

Najpierw wybieramy wszystkie kolumny o typie numerycznym, a potem budujemy mini-rurociąg transformer_numerical, którego jedynym krokiem będzie wywołanie StandardScaler() zapisane pod nazwą num_trans (to musi być unikalne w całym procesie). Kolejny krok łatwo dodać – po prostu dodajemy kolejnego tupla w takim samym schemacie.

Co nam to daje? Ano daje to tyle, że mamy konkretną nazwę dla konkretnego kroku. Później możemy się do niej dostać i na przykład zmienić: zarówno metodę wywoływaną w tym konkretnym kroku jak i parametry tej metody.

To samo robimy dla kolumn z wartościami kategorycznymi – budujemy mini-rurociąg transformer_categorical, który w kroku cat_trans wywołuje OneHotEncoder().

Co dalej? Z tych dwóch małych rurociągów zbudujemy większy – preprocessor. Właściwie to będzie to swego rodzaju rozgałęzienie – ColumnTransformer który jedne kolumny puści jednym mini-rurociągiem, a drugie – drugim. I znowu: tutaj może być kilka elementów, oddzielne przepływy dla konkretnych kolumn (bo może jedne ciągłe chcemy skalować w jeden sposób, a inne w inny? A może jedne zmienne chcemy uzupełnić średnią a inne medianą?) – mamy pełną swobodę.

Cała rura to złożenie odpowiednich elementów w całość – robiliśmy to już wyżej:

Teraz cały proces wygląda następująco:

  • najpierw preprocessing:
    • dla kolumn liczbowych wykonywany jest StandardScaler()
    • dla kolumn kategorycznych – OneHotEncoder()
  • złożone dane przekazywane są do RandomForestClassifier()

Proces trenuje się dokładne tak samo jak model – poprzez wywołanie metody .fit():

Oczywiście predykcja działa tak samo jak zawsze:

Są też metody zwracające prawdopodobieństwo przypisania do każdej z klas .predict_proba() oraz jego logarytm .predict_log_proba().

Po wytrenowaniu na danych treningowych (cechy X_train, target y_train) możemy zobaczyć ocenę modelu na danych testowych (odpowiednio X_test i y_test):

Świetnie, świetne, ale to samo można bez tych pipelinów, nie raz na Kaggle tak robili i działało. Więc po co to wszystko?

Ano po to, co nastąpi za chwilę.

Mamy cały proces, każdy jego krok ma swoją nazwę, prawda? A może zamiast StandardScaler() lepszy będzie MinMaxScaler()? A może inna klasa modeli (zamiast lasów losowych np. XGBoost?). A gdyby sprawdzić każdy model z każdą transformacją? No to się robi sporo kodu… A nazwane kroki w procesie pozwalają na prostą podmiankę!

Zdefiniujmy sobie przestrzeń poszukiwań najlepszego modelu i najlepszych transformacji:

Teraz w zagnieżdżonych pętlach możemy sprawdzić każdy z każdym podmieniając klasyfikatory i transformatory (cała pętla trochę się kręci):

Teraz w jednej tabeli mamy wszystkie interesujące dane, które mogą posłużyć nam chociażby do znalezienia najlepszego modelu:

model num_trans cat_trans score time_elapsed
CatBoostClassifier StandardScaler OrdinalEncoder 0.871 50.595
CatBoostClassifier MinMaxScaler OrdinalEncoder 0.871 55.328
CatBoostClassifier StandardScaler OneHotEncoder 0.871 46.503
CatBoostClassifier MinMaxScaler OneHotEncoder 0.871 65.044
XGBClassifier StandardScaler OneHotEncoder 0.871 4.458
LGBMClassifier StandardScaler OneHotEncoder 0.871 3.736
LGBMClassifier MinMaxScaler OneHotEncoder 0.871 2.319
XGBClassifier StandardScaler OrdinalEncoder 0.870 1.435
XGBClassifier MinMaxScaler OrdinalEncoder 0.870 5.215
XGBClassifier MinMaxScaler OneHotEncoder 0.869 3.056
LGBMClassifier StandardScaler OrdinalEncoder 0.869 2.620
LGBMClassifier MinMaxScaler OrdinalEncoder 0.869 2.999
XGBClassifier Normalizer OneHotEncoder 0.862 8.008
CatBoostClassifier Normalizer OneHotEncoder 0.859 57.878
LGBMClassifier Normalizer OneHotEncoder 0.859 2.356
LGBMClassifier Normalizer OrdinalEncoder 0.859 0.893
XGBClassifier Normalizer OrdinalEncoder 0.857 17.738
CatBoostClassifier Normalizer OrdinalEncoder 0.857 66.315
SVC StandardScaler OneHotEncoder 0.853 36.520
LogisticRegression StandardScaler OneHotEncoder 0.850 1.581
LogisticRegression MinMaxScaler OneHotEncoder 0.849 1.079
RandomForestClassifier StandardScaler OrdinalEncoder 0.847 3.022
RandomForestClassifier MinMaxScaler OrdinalEncoder 0.846 2.647
RandomForestClassifier StandardScaler OneHotEncoder 0.843 21.878
RandomForestClassifier Normalizer OneHotEncoder 0.843 19.976
LogisticRegression Normalizer OneHotEncoder 0.842 1.440
RandomForestClassifier MinMaxScaler OneHotEncoder 0.842 18.986
RandomForestClassifier Normalizer OrdinalEncoder 0.840 3.454
SVC Normalizer OneHotEncoder 0.836 36.707
SVC MinMaxScaler OneHotEncoder 0.834 39.465
LogisticRegression StandardScaler OrdinalEncoder 0.818 1.284
LogisticRegression MinMaxScaler OrdinalEncoder 0.816 4.131
KNeighborsClassifier StandardScaler OneHotEncoder 0.812 0.143
KNeighborsClassifier StandardScaler OrdinalEncoder 0.810 0.883
KNeighborsClassifier Normalizer OneHotEncoder 0.810 0.125
KNeighborsClassifier Normalizer OrdinalEncoder 0.807 0.690
ExtraTreeClassifier Normalizer OneHotEncoder 0.805 0.833
ExtraTreeClassifier StandardScaler OneHotEncoder 0.803 0.666
KNeighborsClassifier MinMaxScaler OneHotEncoder 0.801 0.147
ExtraTreeClassifier MinMaxScaler OrdinalEncoder 0.800 0.187
KNeighborsClassifier MinMaxScaler OrdinalEncoder 0.799 0.652
SVC StandardScaler OrdinalEncoder 0.798 17.911
ExtraTreeClassifier StandardScaler OrdinalEncoder 0.798 0.203
ExtraTreeClassifier MinMaxScaler OneHotEncoder 0.797 0.541
ExtraTreeClassifier Normalizer OrdinalEncoder 0.794 0.221
LogisticRegression Normalizer OrdinalEncoder 0.784 13.174
SVC Normalizer OrdinalEncoder 0.769 22.420
SVC MinMaxScaler OrdinalEncoder 0.748 19.661
DummyClassifier MinMaxScaler OneHotEncoder 0.630 0.114
DummyClassifier Normalizer OrdinalEncoder 0.627 0.105
DummyClassifier Normalizer OneHotEncoder 0.626 0.131
DummyClassifier StandardScaler OrdinalEncoder 0.623 0.097
DummyClassifier StandardScaler OneHotEncoder 0.623 0.113
DummyClassifier MinMaxScaler OrdinalEncoder 0.617 0.096

Ale najlepszy może być w różnych kategoriach – nie tylko skuteczności, ale też na przykład czasu uczenia czy też stabilności wyniku. Zobaczmy podstawowe statystyki dla typów modeli:

score
time
model mean std min max mean std min max
CatBoostClassifier 0.867 0.007 0.857 0.871 56.944 7.826 46.503 66.315
XGBClassifier 0.867 0.006 0.857 0.871 6.652 5.861 1.435 17.738
LGBMClassifier 0.866 0.006 0.859 0.871 2.487 0.941 0.893 3.736
RandomForestClassifier 0.843 0.003 0.840 0.847 11.661 9.491 2.647 21.878
LogisticRegression 0.826 0.026 0.784 0.850 3.782 4.737 1.079 13.174
KNeighborsClassifier 0.806 0.005 0.799 0.812 0.440 0.340 0.125 0.883
SVC 0.806 0.042 0.748 0.853 28.781 9.784 17.911 39.465
ExtraTreeClassifier 0.800 0.004 0.794 0.805 0.442 0.277 0.187 0.833
DummyClassifier 0.624 0.004 0.617 0.630 0.109 0.013 0.096 0.131

Tutaj tak na prawdę nie mierzymy stabilności modelu – podajemy różnie przetworzone dane do tego samego modelu. Stabilność można zmierzyć puszczając na model fragmentaryczne dane, co można zautomatyzować poprzez KFold/RepeatedKFold (z sklearn.model_selection), ale dzisiaj nie o tym.

Sprawdźmy który rodzaj modelu daje najlepszą skuteczność:

Ostatnie trzy (XGBoost, LigthGBM i CatBoost) dają najlepsze wyniki i pewnie warto je brać pod uwagę w przyszłości.

A czy są różnice pomiędzy transformatorami?

Przy tych danych wygląda, że właściwie nie ma większej różnicy (nie bijemy się tutaj o 0.01 punktu procentowego poprawy accuracy modelu). Może więc czas treningu jest istotny?

Mamy kilku liderów, ale z tych które dawały najlepsze wyniki warto wziąć pod uwagę XGBoosta i LightGBM.

Dzięki przećwiczeniu kilku modeli mamy dwóch najbardziej efektywnych (czasowo) i efektownych (z najlepszym accuracy) kandydatów do dalszych prac. Wyszukanie ich to kilka linii kodu. Jeśli przyjdzie nam do głowy nowy model – dodajemy go do listy classifiers. Jeśli znajdziemy inny transformator – dopisujemy do listy scalers lub cat_transformers. Nie trzeba kopiować dużych kawałków kodu, nie trzeba właściwie pisać nowego kodu.

Dokładnie tym samym sposobem możemy poszukać hyperparametrów dla konkretnego modelu i zestawu transformacji w pipeline. Ale to już w następnym odcinku.

3 komentarze do “Pipeline w SciKit Learn”

  1. Bardzo dobry i przydatny poradnik. Kompletnie nie znam Azure i zastanawiam się czy tak samo łatwe będzie uruchomienie projektu Javovego? Z tego co wyczytałem, chyba jest możliwość na Azure DevOps zdefiniować pipeliny dla projektów napisanych w Javie.

  2. Cześć, powiem tak… to co tu jest to jakaś poezja!!! Żałuję, że jak zacząłem robić mgr to dopiero teraz po kilku miesiącach to odkryłem. Część modeli już postawiłem… Mam pytanie. Czy potem można wywoływać:
    cm2 = confusion_matrix(target_test, predykcja modelu)
    cm2_display = ConfusionMatrixDisplay(cm2).plot()

    Bo jak tak na razie patrzę to krzywą ROC można wyrysować dla modeli za pomocą:

    import matplotlib.pyplot as plt
    from sklearn.metrics import RocCurveDisplay

    fig, ax = plt.subplots()

    models = [
    („Regresja logistyczna -> RL”, model),
    („Las losowy -> LS”, model2)
    ]

    model_displays = {}
    for name, pipeline in models:
    model_displays[name] = RocCurveDisplay.from_estimator(
    pipeline, data_test, target_test, ax=ax, name=name
    )
    _ = ax.set_title(„ROC curve”)

    I pod nazwy modeli podkładamy no właśnie co? :/

    1. Edit do confusion matrix* chodzi mi o to czy można w jakiś sposób podpiąć jeszcze predykcję modelu w pętli tak, żeby potem móc wyświetlić wszystko na wykresach.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *