Czym jest wzmocnienie gradientowe i dlaczego ma super moce?

gradient boosting xgb wzmocnienie gradientowe

– Tatusiu, co robiłeś dzisiaj w pracy? – zapytała Jagódka kiedy odbierałem ją z Otylką z przedszkola.

–  Dzisiaj na szybciutko sprawdzałem z moimi kolegami i koleżankami z pracy czy nowe dane, które pozyskaliśmy, pomogą nam ulepszyć aktualne modele budowane techniką wzmocnienia gradientowego czyli tak zwaną metodą gradient boosting’u.

– Co Ty tatusiu do mnie mówisz? – skrzywiła się słysząc takie dziwne sformułowanie pierwszy raz. – A co to jest ten gafent bumpstink?

– Pamiętasz jak ostatnio byliśmy w restauracji i mamusia zamówiła zupę kasztanową? – Jagódka pokiwała głową i się oblizała. Nie wiem czy to na myśl o tej zupie czy o naleśnikach, które zamawia prawie wszędzie.

– Była tak przepyszna, że zdecydowaliśmy się ugotować taką samą w domku. To była ta zupka, którą gotowaliśmy w ostatnią sobotę. Najpierw rozpoczęliśmy od czegoś prostego. Na początku ugotowaliśmy warzywka i kasztany w słonej wodzie. Po spróbowaniu okazało się, że nasza zupa ma całkiem inny smak niż ta w restauracji i w sumie w ogóle nie była dobra. Była za mało doprawiona, taka…hmm.. bez smaku. Chcieliśmy to poprawić więc dodaliśmy troszkę pieprzu, takie większe czarne kulki zwane zielem angielskim i uschnięte listki z drzewa laurowego. Zupka miała już lepszy smak, ale była zbyt płynna. Wobec czego dodaliśmy więcej kasztanów i ziemniaków, a po ich ugotowaniu tym głośnym urządzeniem zmiksowaliśmy zupkę. Dalej nam coś nie pasowało więc dodaliśmy dla smaku odrobinę suszonych śliwek. Troszkę za mało, ale Otylcia nam po cichu połowę wyjadła. I chyba ktoś jej w tym pomógł. Jagoda zaczęła się śmiać, bo przypomniała sobie jak podkradała z siostrą suszone owoce. Po tych poprawkach zupka zaczęła nabierać ciekawszego smaku, podobnego do tej z restauracji. Rozpoczęliśmy od czegoś prostego, a każdy kolejny nasz krok sprawiał, że zupka stawała się coraz lepsza. Na tym polega ta metoda.

Nie wiem czy zrozumiała samą metodę. Natomiast mam nadzieję, że zapamięta, że nawet jak kiedyś popełni jakiś błąd to często można go naprawić.

Skąd nazwa wzmocnienie gradientowe (ang. Gradient Boosting)?

Samo sformułowanie „Gradient Boosting” (wzmocnienie gradientowe) brzmi bardzo tajemniczo. Zatem rozszyfrujmy skąd taka nazwa.

Czym jest gradient?

Gradient – pole wektorowe wskazujące kierunki najszybszych wzrostów wartości danego pola skalarnego.

Definicja w portalu wikipedia

Aby zwizualizować to zagadnienie wyobraź sobie, że w jednym garnku gotujesz zupę dla 300 osób. Aby zupa się zmieściła garnek musi być olbrzymi. We wnętrzu tak wielkiego naczynia temperatura rozkłada się nierównomiernie – gdzieś będzie cieplej a gdzieś zimniej. Na szczęście masz pod ręką 🙂 funkcję T(x,y,z), która mówi o temperaturze w garnku. Zmienne x, y, z to konkretny punkt w przestrzeni, w którym ta funkcja zwraca mam temperaturę. Wówczas gradient funkcji T wskazywałby nam kierunek, gdzie temperatura jest najmniejsza.

Z tego powodu gradient będzie wykorzystywany do wskazywania kierunku, w którym nasz model ma się poprawiać. Ale największym zagrożeniem jest to, że możemy wpaść w minimum lokalne.

A czym jest Boosting?

Boosting to tak zwane przyśpieszenie. Polega na tym, że w każdym kolejnym kroku chcemy poprawić wcześniejsze błędy.

W pierwszym kroku budujemy proste drzewo decyzyjne. Następnie, mając informacje o działaniu tego drzewa, budujemy kolejne drzewko, które próbuje naprawić błąd tego wcześniejszego. I tak dalej. Uruchamiamy kolejne iteracje działające w taki sam sposób, tzn. próbując naprawiać wszystkie wcześniejsze decyzje. A korzystając z gradientu wiemy, w którym kierunku należy to robić. To jest idea przyśpieszonego uczenia.

Metoda ta trochę przypomina ideę metodologii Agile, gdzie praca odbywa się w sprintach i po każdym przebiegu staramy się ulepszać proces.

Mam nadzieję, że mój Agile Coach Piotrek nie będzie się wstydził za mnie za powyższą wizualizację, na której chciałem pokazać moje rozumienie idei Agile.

A jak to działa dokładniej?

Bardziej matematycznie można byłoby to podsumować, że po każdym przebiegu próbujemy dopasować nasz predyktor do błędu resztkowego popełnionego przez poprzedni predyktor.

Mam nadzieję, że poniższa wizualizacja bardziej Wam to wyjaśni.

Krok 0 – tutaj mamy przedstawiony nasz zbiór uczący. Poniższe wartości (kropki) będziemy starali się przewidywać.

Krok I – budujemy pierwsze proste drzewo decyzyjne. Prognozę drzewa nanosimy na wykres zieloną kreską.

Krok II – naprawiamy błędy wcześniejszego drzewa, czyli wyliczamy reszty i dla nich budujemy kolejne drzewo. Reszty wyliczamy poprzez odjęcie od wartości rzeczywistej wartości przewidywanej.

Teraz możemy zobaczyć jak nasz predyktor (suma predyktorów h1 i h2) po dwóch krokach się polepszył:

Widać, że w coraz lepiej odwzorowuje wartość do przewidywania, mimo, że były dopiero dwa kroki. Potwórzmy ten cykl jeszcze raz

Krok III – naprawiamy błędy wcześniejszych dwóch drzew. Poniżej wykres reszt oraz kolejny predyktor:

Teraz możemy zobaczyć jak nasz predyktor (suma predultora h1, h2 i h3) po trzech krokach się polepszył:

Porównajmy jeszcze kolejne predyktory, aby zobaczyć jak się w tym konkretnym przykładzie dopasowywał:

Mam nadzieję, że teraz jest już w miarę intuicyjne co stoi za tą matematyką. Gdybyś chciał dokładniej poznać matematyczne zależności to pod tym linkiem znajdziesz świetny artykuł napisany na Analytics Vidhya.

Jakie są rodzaje implementacji?

Wzmocnienie gradientowe pierwszy raz zostało zaprezentowane w Arcing the Edge (link do artykułu) w 1997, natomiast w ostatnim dziesięcioleciu zostało udoskonalone. Istnieje wiele różnych rodzajów implementacji.

Z doświadczenia wiem, że wybierając którąkolwiek z pierwszych dwóch metod będziecie mieć bardzo zbliżone wyniki. Jeśli zatem nie walczycie o miejsca po przecinku (np. konkurs na Kaggle) to nie będzie dużej różnicy.

Jakie są podstawowe hiperparametry z których korzystam?

Osobiście mam największe doświadczenie z implementacją XGB zatem przybliżę Wam pokrótce hiperparametry dla tej metody wzmocnienia gradietowego.

Główne:

  • learning_rate / eta [default=0.3]: parametr mówiący po każdej wyliczonej iteracji jaki krok chcemy dać do przodu. Im większy krok tym szybciej zbliżamy się do celu, ale jeśli będzie zbyt duży to możemy nie dojść do najlepszego wyniku.
  • max_depth [default=6]: maksymalna głębokość prostych drzew. Im głębsze drzewa tym model jest mocniejszy, ale trzeba uważać by nie przeuczyć modelu.
  • n_estimators : liczba tych prostych drzewek, które chcemy zbudować.
  • min_child_weight: mówi o minimalnej liczbie obserwacji w każdym liściu drzewa. Im większa waga tym model bardziej konserwatywny – potrzebujemy większej wagi by dokonać danego podziału.
  • gamma [default=0]: odpowiada za zmniejszenie strat wymaganych do utworzenia kolejnego węzła liści. Im większa waga tym model jest bardziej konserwatywny.
  • seed  [default=0]: nasionko, które służy do generowania liczb losowych.

Do walki z przeuczeniem:

  • subsample [default=1]: Informacja jaki procent obserwacji chcemy brać do budowy prostego drzewka
  • colsample_bytree [default=1]: Informacja jaki procent charakterystyk chcemy brać do budowy prostego drzewka
  • colsample_bylevel [default=1]: Informacja jaki procent charakterystyk chcemy losować do budowy prostego drzewka po każdym kolejnym podziale danych (split)
  • max_delta_step [default=0]: Maksymalny krok delta pozwalający na wyjście z każdego liścia. Wartość 0 oznacza brak ograniczeń. Jeśli jest ustawiona na wartość dodatnią, może pomóc w uczynieniu kroku aktualizacji bardziej konserwatywnym. Zwykle ten parametr nie jest potrzebny, ale może pomóc w regresji logistycznej, gdy klasa jest wyjątkowo niezrównoważona. Ustawienie wartości 1-10 może pomóc w kontrolowaniu aktualizacji.

Kolejne dwa to regularyzacje (również pomagają w walce z overfittingiem czyli przeuczeniem).

  • alpha [default=0]: odpowiada za parametr alfa przy regularyzacji L1 (Lasso). Im większa waga tym model jest bardziej konserwatywny.

Można myśleć, że im większa alpha tym mniej charakterystyk jest branych pod uwagę i wyłapywane są bardziej istotne. Stopniowo odrzuca współliniowe atrybuty i pozostawia zbiór najbardziej istotnych.

  • lambda [default=1]: odpowiada za parametr lambda przy regularyzacji L2 (Ridge regression).  Im większa waga tym model jest bardziej konserwatywny.

Intuicyjnie im większe lambda to wagi będą bliskie zeru.

Więcej o regularyzacji możecie zobaczyć na filmiku profesora Aleksandra Ihlera.

Pozostałe parametry

Są jeszcze pozostałe parametry, które zostaną dobrane i warto mieć świadomość, że można je modyfikować:

  • eval_metric: metryka walidacji modelu. Domyślnie dla problemu regresji jest błąd średniej kwadratowej (RMSE) a dla klasyfikacji stosunek dobrych przypisani do złych (accurency).

W razie potrzeby istnieje możliwość zdefiniowania własnych metryk sukcesu lub użycia wbudowanych.

  • objective: możliwość ustawienia rodzaju regresji do wyliczeń algotymów dla XGBoosta. Domyślnie ustawiona jest regresja liniowa.
  • tree_method: po wybraniu gpu_hist wykorzystujemy pełną moc kart graficznych i znacznie przyśpieszamy wyliczenia. Ostatnio podczas modelu liczenia ma moim laptopie trwało to jedynie 30 minut a na komputerach dostosowanych do obliczeń z kartami trwało zaledwie kilkanaście sekund.

Jak napisać kod w Python?

Wczytanie bibliotek:

import numpy as np
import pandas as pd
import xgboost as xgb 

from sklearn.metrics import roc_auc_score, accuracy_score # wczytanie metryk sukcesu
from sklearn.model_selection import train_test_split

pd.options.display.max_columns = 250

Abyśmy mogli zobaczyć jak działa gradient boosting proponuję pobrać dane z Kaggle z konkursu Santander (link do przykładowych danych). Są one już na tyle oczyszczone, że nie ma potrzeby robić żadnej obróbki. Zatem będą idealne do szybkiej implementacji gradient boosting’u. Pobierz jedynie zbiór train.csv, który podzielimy na zbiór do budowy modelu oraz dwa walidacyjne.

Wczytujemy dane oraz możemy się im przyjrzeć.

df = pd.read_csv('../data_raw/train.csv')
print(df.shape)
df.head()

Możemy sobie przygotować funkcje pomocnicze, które pomogą przygotować wczytanie danych do odpowiednich zborów:

def get_feats(df): #dzieki temu nie będę brał niepotrzebnych kolumn do modelowania
    feats = [f for f in df.columns if f not in ['ID_code','target']]
    return feats

def get_X(df): #do pobierania macierzy X czyli cech które będę starał się opisywac modelem
    return df[ get_feats(df) ].values

def get_y(df, target_var='target'): #wektor y - informacja o predycji. w przypadku przykładowych danych to ":target"
    return df[target_var].values

Warto jeszcze sprawdzić przykładowe metryki jak zadziałał nasz model. Zróbmy to na podstawie metryk AUC oraz GINI:

def create_measures(y,y_pred): 
    score_test = roc_auc_score(y, y_pred)
    Gini_index = 2*score_test - 1
        
    d = {'AUC': [round(score_test,4)], 'GINI': [round(Gini_index,4)]}
    d = pd.DataFrame.from_dict(d)
    
    return d

def calculating_metrics(X_train, X_val, X_oot, y_train, y_val, y_oot):
    #prawdopodobieństwa:
    y_pred_test = model.predict_proba(X_train)[:, 1]
    y_pred_test_val = model.predict_proba(X_val)[:, 1]
    y_pred_test_oot = model.predict_proba(X_oot)[:, 1]

    test = create_measures(y_train,y_pred_test)
    val = create_measures(y_val,y_pred_test_val)
    oot = create_measures(y_oot,y_pred_test_oot) 

    measures =  pd.concat([test,val,oot])
    measures.set_index([pd.Index(['TRAIN', 'VAL', 'OOT'])]) 
    return measures

Dokonujemy podziału zbioru wejściowego:

X, y = get_X(df), get_y(df) 
# świadomie chcę mieć dwa niezależne zbiory walidacyjne. Przyda mi się na przyszłość jak będę robił optymalizację hiperparametrów
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=2019)
X_test, X_val, y_test, y_val = train_test_split(X_test, y_test, test_size=0.5, random_state=2019)

print('TRAIN:',X_train.shape, y_train.shape)
print('TEST:',X_test.shape, y_test.shape)
print('VALIDATION:',X_val.shape, y_val.shape)

i budujemy model na parametrach domyślnych:

model = xgb.XGBClassifier()
model.fit(X_train, y_train)  
measures = calculating_metrics(X_train, X_test, X_val, y_train, y_test, y_val)
measures

Możesz dla siebie pobawić się różnymi parametrami, które opisałem wcześniej i sprawdzić jak wpływają na moc modelu. Poniżej przykład, jak wystarczy odrobinę zmodyfikować parametry by uzyskać na próbkach walidacyjnych znaczną poprawę o ponad 7 punktów procentowych wg metryki GINI.

model = xgb.XGBClassifier(max_depth = 5, n_estimators=150, subsample = 0.75)
model.fit(X_train, y_train)  
measures = calculating_metrics(X_train, X_test, X_val, y_train, y_test, y_val)
measures

Chcesz inną implementacje np. LightGBM? Oczywiście to żaden problem. Wystarczy wczytać model (import) oraz podmienić model:

import lightgbm as lgb

model = lgb.LGBMClassifier(max_depth = 5, n_estimators = 150, subsample=0.75)
model.fit(X_train, y_train)  
measures = calculating_metrics(X_train, X_test, X_val, y_train, y_test, y_val)
measures

Zobacz od razu na powyższym przykładzie, że różnica w mocy na podobnych parametrach to zaledwie 0.0019.

Kod dostępny na Github:

https://github.com/MamczurMiroslaw/gradient_boosting_example/blob/master/Gradient_boosting_example.ipynb

Podsumowanie

Mam nadzieję, że w zrozumiały sposób przedstawiłem Wam metodę gradient boosting’u i dostrzegliście jej super moce związane z prostotą i bardzo skutecznym działaniem. Oraz, że będziecie z niej korzystać przy kolejnych projektach jeśli do tej pory nie mieliście okazji.

Życzę najlepszych modeli!

.

7 Comments on “Czym jest wzmocnienie gradientowe i dlaczego ma super moce?”

  1. I just like the helpful information you provide to
    your articles. I will bookmark your weblog and test again here frequently.
    I am somewhat certain I’ll be informed many new stuff right here!
    Best of luck for the following!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *