Wstęp
Gradient Boost jest algorytmem z rodziny metod zespołowych, a konkretnie boostingu. Metoda ta wykorzystuje wiele prostych algorytmów typu drzewo decyzyjne, aby zredukować zarówno bias i wariancje modelu. W niniejszym artykule przedstawię dokładny opis działania tego algorytmu na zadaniach regresji. Myślę, że w znacznym stopniu ułatwi to zrozumienie bardziej złożonych algorytmów. Np. LightGBM czy XGBoost, które wywodzą się od Gradient Boosta.
W celu ułatwienia zrozumienia działania GB postanowiłem podzielić omówienie tego algorytmu na dwa artykuły: dotyczący regresji oraz dotyczący klasyfikacji. Sam artykuł, ma dwie sekcje. W pierwszej omawiam główne założenia działania tego algorytmu w prosty sposób. W drugiej natomiast, wchodzę bardziej w szczegóły. Zapraszam do czytania!
Główne założenia GB
Porównanie do typowego boostingu
Jak wspomniałem we wstępie, GB należy do algorytmów boostingu. Zakładam, że czytelnik ma pojęcie o głównych założeniach tego typu metod, jeżeli nie, to odsyłam do mojego poprzedniego artykułu: Bagging i Boosting. GB uczy drzewa decyzyjne, które zazwyczaj są bardziej rozbudowane w tym algorytmie niż sam korzeń (przeważnie wybiera się od 8 do 32 liści). Podobnie jak inne algorytmy boostingu, następne budowane drzewo jest oparte na błędach predykcji poprzedniego oraz nadawana jest mu waga. Waga ta jednak jest stała dla wszystkich drzew, więc może być to pierwsza różnica, gdzie wiele algorytmów boostingu nadaje różne wagi następującym drzewom, w oparciu o ich wyniki predykcji. Takie drzewa są budowane aż do osiągniecia warunków stopu, czyli albo skończy się liczba wskazanych do stworzenia przez nas drzew, albo kolejne drzewa nie zwiększą jakości dopasowania do danych.
Pierwsze drzewo
Niech poniższa tabela przedstawia nasze dane:
Wzrost | Kolor oczu | Płeć | Zarobki w tys. (target) |
---|---|---|---|
160 | niebieski | M | 3 |
160 | zielony | K | 3.5 |
150 | niebieski | K | 5 |
180 | szary | M | 5 |
150 | zielony | M | 3.4 |
140 | niebieski | K | 4.3 |
Pierwszą rzeczą jaką robimy jest policzenie średniej ze zmiennej celu. Avg Zarobki = 4.03. Jest to nasz pierwszy punkt odniesienia do predykcji zarobków dla każdego.
Kolejnym krokiem jest zbudowanie drzewa na błędach poprzedniego drzewa. Jednak nie mieliśmy poprzedniego drzewa, ale mieliśmy średnią. Błędami jest różnica pomiędzy zaobserwowanymi zarobkami a przewidzianymi (4.03):
Pseudo\,reszty=(wartości\,zaobserwowane-wartości\,przewidziane)
Wzrost | Kolor oczu | Płeć | Zarobki w tys. (target) | Pseudo reszty |
---|---|---|---|---|
160 | niebieski | M | 3 | -1.03 |
160 | zielony | K | 3.5 | -0.53 |
150 | niebieski | K | 5 | 0.97 |
180 | szary | M | 5 | 0.97 |
150 | zielony | M | 3.4 | -0.63 |
140 | niebieski | K | 4.3 | 0.27 |
Mając pseudo reszty (nasze błędy), budujemy drzewo decyzyjne, gdzie będą one zmienną celu. Tak, może brzmieć to dziwnie, ale zaraz wszystko się wyjaśni. Zmiennymi objaśniającymi pozostają natomiast wzrost, kolor oczu i płeć:

W naszym przykładzie mamy tylko 4 liście, ale jak napisałem wyżej zwykle używa się od 8 do 32. Jak widać mamy mniej liści niż reszt w tabeli. W dwóch przypadkach, 2 wiersze danych idą do tego samego liścia, a więc te liście (od lewej – pierwszy i trzeci), zostają uzupełnione średnią z wartości w liściu:

Kolejny krok to połączenie oryginalnego liścia (czyli naszej średniej) z naszym nowym drzewem, żeby dokonać nowej predykcji zarobków. Wygląda to tak:
(Średnie zarobki) + learning_rate * (wartość liścia z reguły decyzyjnej na podstawie wiersza obserwacji). Tą wartością będzie -1.03 bo płeć to M, kolor oczu Niebieski. Dlaczego learning rate? Oto odpowiedź:
4.03 + (-1.03) = 3
Bez learning rate dostajemy wynik z oryginalnego zbioru danych. Wariancja takiego modelu jest ogromna i model jest do niczego. Learning rate przymuje wartości w przedziale (0,1). Jak to wygląda z LR:
4.03 + 0.1(-1.03) = 3.927
Co prawda nowa predykcja nie jest tak dobra jak poprzednia, ale jest lepsza niż predykcja zrobiona na poprzednim liściu (czyli sredniej, pred = 4.03). Learning rate powoduje, że pokonujemy małe kroczki w dobrym kierunku. Takie podejście gwarantuje lepszą predykcje na zbiorze testowym poprzez zmniejszenie wariancji modelu. Kolejnym krokiem będzie zrobienie drugiego i kolejnych drzew.
Drugie i następne drzewa
Jak poprzednio liczymy sobie pseudo reszty, z pomocą naszego wzorku (zaobserwowane – przewidziane). Tym razem do przewidywanie wykorzystujemy już pierwszy liść i poprzednie drzewo z learning ratem – jak na przykładzie wyżej. Reszta dla pierwszego wiersza wygląda następująco:
Reszty = (3 - (4.03 + 0.1(-1.03)) = -0.927
I tak postępujemy dla każdego kolejnego wiersza. Więc nasza tabelka prezentuje się teraz tak:
Wzrost | Kolor oczu | Płeć | Zarobki w tys. (target) | Pseudo reszty |
---|---|---|---|---|
160 | niebieski | M | 3 | -0.927 |
160 | zielony | K | 3.5 | -0.477 |
150 | niebieski | K | 5 | 0.908 |
180 | szary | M | 5 | 0.953 |
150 | zielony | M | 3.4 | -0.647 |
140 | niebieski | K | 4.3 | 0.208 |
Nasze pseudo reszty zmniejszyły się w porównaniu do poprzedniej tabelki co potwierdza, że zrobiliśmy mały kroczek w dobrym kierunku. Co dalej?
Następnym krokiem jest zbudowanie kolejnego drzewa w oparciu o pseudo reszty i powtórzenie kroków, które do tej pory omówiliśmy. Miejmy na uwadze, że nowe drzewa nie muszą być takie same jak poprzednie. Mogą mieć inny podział według zmiennych więcej lub mniej liści itd. Główna myśl pozostaje ta sama. Jeżeli w liściu będzie więcej wartości to zostają one uśrednione itd. Potem następuje dodawanie drzew więc cały GB wygląda tak:
Srednie\,zarobki + 0.1*drzewo_1 + 0.1*drzewo_2 + 0.1*drzewo_3...
Predykcja
Kiedy uczenie algorytmu dobiegnie końca. Mamy taki łańcuch drzew do, którego możemy wstawić wiersz obserwacji, żeby otrzymać przewidywaną wartość zarobków w tym przypadku.
Jeżeli chodzi o takie omówienie GB dla regresji w pigułce to tyle, jak widać nic skomplikowanego. Polecam jednak czytać dalej, bo okazuje się, że matematycznie te modele również są bardzo proste. Dodatkowo rozwieje to z pewnością parę wątpliwości, które do tej pory mogły się pojawić.
Wkraczając w detale
Wszystko co do tego momentu omówiliśmy było w gruncie rzeczy bardzo ogólne, ale takie miało być. Dało nam to generalną wiedzę jak mniej więcej GB działa. Chcemy jednak poznać trochę więcej detali tego algorytmu, więc trochę sformalizujmy ten wywód.
Pierwszy krok
To czego potrzebujemy, aby zacząć opisywać działanie algorytmu to dane wejściowe oraz różniczkowalna Funkcja Straty (Loss Function). Funkcja straty będzie miarą tego, jak dobrze możemy przewidzieć zarobki. Zdefiniujmy formalnie te dwa pojęcia:
dane = \{(x_i,y_i\}^n_{i=1}\\.\\Funkcja\,straty\,=L(y_i,F(x))\\.\\L(y_i,F(x))=\frac{1}{2}(Obserwacja-Predykcja)^2
F(x) -są to wartości przewidziane, y – wartości zaobserwowane.
Tak naprawdę w funkcji straty, ta 1/2 nie jest potrzebna. Ponieważ wystarczy miara błędu wartości obserwowanych od przewidzianych wziętych do kwadratu. Jak wspomniałem wyżej, wymagamy, żeby funkcja straty była różniczkowalna. Dodanie 1/2 nie zmienia właściwości f. straty natomiast ułatwia matmę, ze względu na to, że w GB dużo się różniczkuje. Tak będzie wyglądało liczenie pochodnej względem wartości przewidzianych z 1/2:
\frac{d}{d Predykcja}=\frac{1}{2}(Obserwacja-Predykcja)^2 = \\=-(Obserwacja - Predykcja)
Drugi krok
Robimy to co w przykładzie ogólnym. Czyli inicjujemy nasze pierwsze „drzewo” a tak naprawdę liść, czyli stałą wartość, która była średnią. Gdzie ta wartość jest zdefiniowana formalnie jako:
F_0(x) = \argmin_\gamma{\sum_{i=1}^{n}}L(y_i,\gamma)
Gamma są to przewidziane wartości. Ten wzór możemy przeczytać tak, że szukamy takich wartości gamma(przewidzianych), które minimalizują tą sume. Ta suma to po prostu wszystkie dodane do siebie wartości funkcji straty.
Ogólnie, gdy mamy dosyć skomplikowaną funkcję straty to sam proces wyszukiwania minimum możemy wykonać za pomocą algorytmów optymalizacyjnych takich jak Gradient Descent. W naszej sytuacji, jest to w ogóle nie potrzebne. Mamy bardzo prostą funkcję straty, dlatego możemy znaleźć minimum analitycznie – licząc pochodną (po predykcji) i przyrównując ją do 0. Mając już wyżej policzoną pochodną po predykcji możemy to przedstawić na naszym zbiorze danych zarobków. Podstawiamy wartości do wzoru:
-3+predykcja-3.5 + predykcja - 5 + \\+predykcja-5 + predykcja-3.4+predykcja -\\-4.3 + predykcja = 0\\.\\.\\6predykcja =24.2\\predykcja=4.03
Jak widać wartością, która minimalizuje sumę naszych funkcji straty jest 4.03, która jest niczym innym jak średnią z wartości. Teraz wiemy dlaczego taką wartość obraliśmy na początku.
Trzeci krok
W tym momencie, będziemy chcieli iteracyjnie generować drzewa :
Dla drzew od m=1 do M:
(1) Policz pseudoreszty rim
r_{im}=-\bigg[\frac{\partial{L(y_i,F(x_i))}}{\partial{F(x_i)}}\bigg]_{F(x)=F_{m-1}(x)},i=1,...,n
Dla naszej funkcji ta pochodna to była po prostu: -(wartość zaobserwowana – wartość przewidziana), tutaj mnożymy to razy -1 i otrzymamy (w. z. – w. p), czyli zwykłe reszty. Dalej, dla rim m oznacza nr. drzewa, natomiast i oznacza nr. obserwacji, dla której liczone są reszty. Z kolei wyrażenie:
F(x) = F_{m-1}(x)
Oznacza, że dla drzewa pierwszego m=1, jako F(x) wstawimy F0(x), gdzie w naszym przypadku była to średnia = 4.03. Przykładowo pierwsza reszta dla pierwszego drzewa będzie wyglądała tak:
r_{1,1} = (3-4.03)
Ta pochodna:
\bigg[\frac{\partial{L(y_i,F(x_i))}}{\partial{F(x_i)}}\bigg]
to nic innego jak Gradient funkcji straty. Stąd właśnie wywodzi się nazwa algorytmu Gradient boosting.
(2) Wytrenuj drzewo regresyjne na wartościach rim oraz zbiorze predyktorów xi i utwórz obszary decyzyjne (terminal regions) Rj,m. Zmienną celu są reszty, zmienne objaśniające pozostają te same. Z kolei, obszary decyzyjne to liście nowo utworzonego drzewa. I tak obszar R1,1, będzie oznaczał pierwszy liść drzewa nr1 itd.
(3) Dla obszaru j = 1…Jm oblicz:
\gamma_{jm} = \argmin_\gamma\sum_{x_i\in{R_{ij}}}L(y_i,F_{m-1}(x_i)+\gamma)
Dla każdego liścia w drzewie obliczamy wartość gamma, która minimalizuje powyższą sumę. Jest to takie samo działanie jakie wykonywaliśmy, w przypadku liczenie pierwszego liścia czyli średniej. Z tym, że teraz bierzemy pod uwagę poprzednią predykcję oraz to, że w liściach mamy więcej wartości. Pokażę to na przykładzie:
Liść R(1,1) ma dwie wartości= {0.27, 0.97}, więc i dwie obserwacje, które zostały tam zaklasyfikowane(x=3 i x=6). Więc podstawmy to do wzoru:
\gamma_{1,1} = \argmin_\gamma\bigg[\frac{1}{2}(5-(4.03 + \gamma))^2+\\+\frac{1}{2}(4.3-(4.03 + \gamma))^2\bigg]
I następnie to wyrażenie minimalizujemy, względem gammy. W tym przypadku gamma = 0.62 jak pamiętamy, wszystko to sprowadziło się do policzenia średniej arytmetycznej z wartości z liścia dzięki naszej przychylnej funkcji straty.
(4) Zaktualizuj Fm(x) – dokonujemy nowej predykcji dla każdej obserwacji, gdy estymujemy pierwsze drzewo m=1 to F1(x).
F_m(x) = F_{m-1}(x) +\nu\sum_{j=1}^{J_m}\gamma_{jm}I(x\in{R_{jm}})
Pierwsza część równania jest raczej prosta do zrozumienia, dla każdej obserwacji F jest to wartość predykcji poprzedniego drzewa (dla drzewa 0 to będzie średnia). Dalej, wyrażenie sumy, które mówi nam, że powinniśmy dodać wszystkie gammy ze wszystkich liści, w których może znajdować się obserwacja x. Jest to taka ochrona na wypadek, gdyby obserwacja x mogła przynależeć do kilku liści jednocześnie. Pozostaje nam znak nu(to śmieszne v), czyli nasz learning rate. Przyjmuje on wartości z zakresu od 0 do 1. Niskie wartości zmniejszają efekt każdego drzewa na ostateczną predykcję, duże na odwrót.
Przykładowo dla wiersza poniżej, LR = 0.1:
Wzrost | Kolor oczu | Płeć | Zarobki w tys. (target) | gamma |
---|---|---|---|---|
160 | niebieski | M | 3 | -1.03 |
F1(x) = 4.03 + 0.1*(-1.03)
Ostatni krok
Wynik to Fm(x). Czyli jak mamy m=100 drzew to predykcja z drzewa nr 100 jest naszym wynikiem wyjściowym dla obserwacji x. Tego drzewa będziemy używać do predykcji na nowych próbkach.
Implementacja w python
W celu pokazania działania algorytmu na danych, wykorzystam GB zaimplementowany w pakiecie scikit-learn. Dane to zbiór cen domów z Bostonu, czyli zbiór typu piaskownica do testowania algorytmów regresyjnych.
Parametryzacja GB
Paramatery, które możemy modyfikować a wpływają na pracę modelu możemy podzielić na trzy główne grupy:
- Specyfikacja drzew – 7 parametrów
- Specyfikacja boostingu – 3 parametry
- Inne
Specyfikacja drzew
Parametry związane z budową drzew w iteracyjnych etapach boostingu:
- min_samples_split – w tym parametrze definiujemy minimalną liczbę obserwacji, która jest wymagana do podzielenia węzła wewnętrznego w drzewie. Parametru tego możemy używać do kontroli nadmiernego dopasowania. Wysokie wartości tego parametru powodują zmniejszanie overfittingu, z kolei bardzo niskie dadzą słabe dopasowanie do danych. Ten parametr powinniśmy dostrajać np. używając walidacji krzyżowej.
- min_samples_leaf – minimalna liczba obserwacji, która musi znaleźć się w liściu by został utworzony. Kolejny parametr służący do kontrolowania nadmiernego dopasowania, analogicznie jak w przykładzie powyżej. Więcej minimalnych próbek w liściu, to mniejsze dopasowanie do danych.
- min_weight_fraction_leaf – taki sam parametr jak nr. 2, z tym że definiujemy tu jaki procent całego zbioru danych ma się znaleźć jako minimum żeby liść został utworzony. Korzystamy albo z nr. 2 albo z nr.3.
- max_depth – maksymalna głębokość drzewa. Głębokość drzewa to po prostu długość reguł decyzyjnych. Podobnie jak pozostałe, służy do kontroli overfittingu. Im głębsze drzewo tym bardziej dopasuje się do danych i na odwrót. Ten parametr, również powinniśmy rozważyć w dostrajaniu za pomocą CV.
- max_leaf_nodes – parametr o którym wspominałem na początku artykułu – czyli maksymalna liczba liści. Ustaliliśmy, że optymalna liczba liści w GB waha się w przedziale od 8 – 32 liści. Ta druga liczba powinna być raczej naszą górną granicą. Pamiętajmy, że w algorytmach uczenia zespołowego chodzi o wykorzystanie prostych algorytmów a nie rozbudowanych. W sytuacji, gdy definiujemy ten parametr parametr nr.4 nie będzie brany pod uwagę.
- max_features -liczba parametrów, która ma być rozważana w celu budowania drzewa względem kryterium jakości. Możemy wybrać trzy wartości 'auto’, 'sqrt’, 'log2′. Jako zasadę kciuka stosuję się tą drugą czyli pierwiastek kwadratowy z liczby naszych zmiennych. Z jednym zastrzeżeniem, że musi być wykorzystane minimum 30-40% dostępnych zmiennych. Czyli gdy mamy dużo zmiennych lepiej wybrać inną miarę np. auto lub logarytm o podstawie 2.
- min_impurity_decrease – jest to miara „czystości” węzła oparta o miarę entropii. Im bardziej uporządkowany węzeł otrzymamy tym bardziej zmniejszymy impurity. W tym parametrze ustalamy minimalną wartość (0,1) o jaką ma być zmniejszona impurity, żeby utworzyć nowy węzeł. Również pozwala ograniczać nadmierne dopasowanie modelu. Im wyższe wartości tego parametru tym większe restrykcje narzucamy, ale również wybieramy tylko najbardziej informatywne zmienne.
Specyfikacja boostingu
Parametry związane z samym procesem boostingu:
- learning rate – znany nam już parametr, przyjmuje wartości z przedziału od 0 do 1. Preferowane są niższe wartości tego parametru, ze względu na ograniczanie nadmiernego dopasowanie i zwiększenie dokładności predykcji związane z „nauką małych kroczków”, o której pisałem w pierwszej części artykułu.
- n_estimators – jest to liczba drzew, które sekwencyjnie wykorzystujemy do uczenia w modelu. GB raczej nie ulega przeuczeniu, ale powinniśmy kontrolować również ten parametr, bo teoretycznie im więcej drzew tym GB może ulec overfittingowi. Warto brać pod uwagę ustalony wcześniej learning rate, im niższe jego wartości tym więcej drzew jest wymagane do uczenia. Do strojenia najlepiej wykorzystać CV.
- subsample – procent obserwacji jakie mają być wykorzystane do nauki całego drzewa. Standardowo, parametr ten jest ustalony na wartość=1. Czyli cały zbiór danych jest wykorzystywany do nauki. Jeżeli ustalimy wartość <1, to w rezultacie otrzymujemy trochę inny algorytm: Stochastic Gradient Boosting. Autor tego pomysłu twierdzi, że wartości subsample z przedziału [0.5,0.8], prowadzą do ograniczenia nadmiernego dopasowania oraz poprawy jakości generalizacji modelu.
Inne
Pozostałe parametry, które możemy modyfikować aby wpłynąć na jakość modelu:
- loss –czyli funkcja straty. Standardowa działa dobrze, dlatego ten parametr raczej będziemy modyfikować w rzadkich sytuacjach, gdy wiemy, że konkretna funkcja będzie sprawowała się lepiej dla danego problemu. Odsyłam do dokumentacji scikita.
- init – parametr, który pozwala wskazać na estymator, którego output chcemy zastosować jako wstępne oszacowanie GB. Czyli pewnego rodzaju start nauki algorytmu już od pewnego punktu. Swego rodzaju stacking.
- warm_start – parametr ten jest użyteczny gdy chcemy aby nasz model w pewnym sensie nie zaczynał od zera. To znaczy, gdy mamy jakiś inny mniejszy zbiór danych podobnych do oryginalnego datasetu, możemy na nich wytrenować estymator z warm_startem. Pozwoli to na ustalenie pewnych parametrów modelu i gdy użyjemy tego estymatora ponownie na naszym zbiorze danych estymacja przebiegnie zdecydowanie szybciej. Warm-start zachowuje informacje o poprzednim uczeniu modelu, jednak jest to zdecydowanie coś innego niż partial_fit, gdzie uczymy model w batchach. Tutaj zachowujemy informacje o poprzednim dopasowanym estymatorze i od tego punktu zaczynamy nowe uczenie.
Implementacja
Na początku musimy zaimportować potrzebne nam biblioteki, klasy oraz załadować zbiór danych:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_boston from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.ensemble import GradientBoostingRegressor #GB from sklearn.model_selection import validation_curve #krzywa walidacji #zbiór danych X, y = load_boston(return_X_y=True)
Następnie zdefiniuję funkcję do rysowania krzywej walidacji, ponieważ będę chciał sprawdzić różne wartości kilku parametrów:
def krzywa_walidacji(parametry, train_scores, test_scores): train_mean = np.mean(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) train_std = np.std(train_scores, axis=1) test_std = np.std(test_scores, axis=1) #uczenie plt.plot(parametry, train_mean, color='red', marker='o', markersize=5, label='Dokładność uczenia') plt.fill_between(parametry, train_mean+train_std, train_mean-train_std, alpha=0.20, color='red') #walidacja plt.plot(parametry, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='Dokładność walidacji') plt.fill_between(parametry, test_mean+test_std, test_mean-test_std, alpha=0.20, color='green') #parametry pozostałe plt.grid() plt.legend(loc='lower right') plt.xlabel('Wartość testowanego parametru') plt.ylabel('neg_mean_squared_error') plt.show()
Przejdźmy zatem do implementacji GB z różnymi parametrami. Na pierwszy ogień pójdzie parametr max_leaf_nodes. Ustaliłem dla niego listę wartości od 2 do 32 w odstępie 2. Jak widać, zainicjowałem pipline, żeby ułatwić sobie pracę ze standaryzacją oraz użyciem GB. Następnie, wykorzystałem klasę validation_curve aby przetestować jakość zdefiniowane wartości wcześniej wspomnianego parametru. Wybraną miarą jest ujemny błąd średniokwadratowy.
#Dzielimy zbiór na uczący i testowy X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.1) #tworzymy pipline najpierw wystandaryzujemy nasze zmienne potem użyjemy gradientboosta pipe = Pipeline([('ss', StandardScaler()), ('gb',GradientBoostingRegressor())]) #nasze parametry, których optymalną wartość chcemy poznać max_leaf = list(range(2,33,2)) #krzywa walidacji train_score, test_score = validation_curve(estimator=pipe, X=X_train, y=y_train, param_name='gb__max_leaf_nodes', param_range=max_leaf, cv=10, scoring='neg_mean_squared_error') krzywa_walidacji(max_leaf, train_score, test_score)

To co można zauważyć to fakt, że początkowe zwiększanie maksymalnej liczby liści gwarantuje spory przyrost jakości algorytmu. Zarówno na zbiorze uczącym jak i testowym, później natomiast jakość stabilizuje się. Jest to zgodne z tym co napisałem na początku, że w GB nie stosujemy najprostszych drzew złożonych jedynie z korzenia, a troszkę bardziej rozbudowane.
Zobaczmy jakie wyniki zaprezentuje nam następny parametr czyli n_estimators. Jego wartości ustaliłem jako 10 liczb całkowitych rozłożonych równo na skali wartości od 50 do 3000.
#nasze parametry, których optymalną wartość chcemy poznać n_trees = list(np.linspace(50,3000,10).astype('int')) #10 roznych wartosci drzew od 50 do 3000 #krzywa walidacji train_score, test_score = validation_curve(estimator=pipe, X=X_train, y=y_train, param_name='gb__n_estimators', param_range=n_trees, cv=10, scoring='neg_mean_squared_error') krzywa_walidacji(n_trees, train_score, test_score)

W przypadku tego parametru, nie mamy już tak jednoznacznej odpowiedzi. Widać lekką poprawę przy użyciu większej liczby drzew niż 50, ale nie jest to znaczący wzrost. Dodatkowo jak widać, odchylenie od średniej jest bardzo duże. Ustalenie liczby drzew jest też zależne od learning ratu, jako że testujemy wartości pojedynczych parametrów a resztę pozostawiamy default, nie znajdujemy optymalnych wartości. Być może, gdybyśmy ustalili bardzo niski Learning rate zauważylibyśmy wysoki skok jakości modelu w przypadku zwiększania liczby drzew.
Przejdźmy do kolejnego parametru, który chce przetestować czyli min_samples. Jest to minimalna liczba próbek w weźle aby taki został utworzony. Dla tego parametru ustaliłem 20 wartości na skali liczb od 2 do 20.
#nasze parametry, których optymalną wartość chcemy poznać min_samples = list(np.linspace(2,20, 20).astype('int')) #list(np.linspace(2,20, 20).round(2)) #krzywa walidacji train_score, test_score = validation_curve(estimator=pipe, X=X_train, y=y_train, param_name='gb__min_samples_split', param_range=min_samples, cv=10, scoring='neg_mean_squared_error') krzywa_walidacji(min_samples, train_score, test_score)

Tutaj również przy testowaniu różnych wartości tylko tego parametru nie uzyskujemy znaczącej poprawy jakości. Jego optymalna wartość może mieć znaczenie przy konfiguracji również innych parametrów.
Sprawdźmy ostatni parametr na dzisiaj, czyli learning_rate. Ustaliłem 20 wartości równo rozłożonych na skali od 0.01 do 1.
#nasze parametry, których optymalną wartość chcemy poznać learning_rate = list(np.linspace(0.01, 1, 20)) #krzywa walidacji train_score, test_score = validation_curve(estimator=pipe, X=X_train, y=y_train, param_name='gb__learning_rate', param_range=learning_rate, cv=10, scoring='neg_mean_squared_error') krzywa_walidacji(learning_rate, train_score, test_score)

Widzimy o wiele ciekawsze wyniki, niż przy pozostałych parametrach. Zwiększenie wartości learning rate z 0.01 do około 0.1 powoduje drastyczny wzrost jakości modelu. Jednakże, wraz z zwiększaniem tej wartości dokładność walidacji maleje co wskazuje na przeuczenie modelu. Ciekawym jest fakt, że jakość modelu utrzymuje się od wartości mniej więcej 0.1 do prawie 0.6, później następuje spadek i zwiększenie odchylenia od średniej. Obserwacje tego wykresu potwierdzają tezę, że małe kroczki w uczeniu drzew pomagają zmniejszyć nadmierne dopasowanie modelu do danych i zwiększyć jego jakość.
Implementację tego algorytmu zwieńczymy wyszukiwaniem optymalnych wartości po siatce, czyli grid search. Przeszukamy różne wartości parametrów, które ustaliłem w oparciu o obserwacje powyższych wykresów, na koniec sprawdzimy wyniki z najlepszego znalezionego modelu na zbiorze testowym.
min_samples = [2,3,5,10,15] max_leaf = [3,5,10,20] n_trees = [100,500,1000] lr = [0.1,0.15,0.2] params = [{'gb__min_samples_split': min_samples, 'gb__max_leaf_nodes': max_leaf, 'gb__max_features':['log2'], 'gb__learning_rate':lr, 'gb__n_estimators': n_trees}] grid_search = GridSearchCV(estimator=pipe, param_grid=params, scoring='neg_mean_squared_error', cv=5, n_jobs=-1) gs = grid_search.fit(X_train,y_train) print("Najlepszy wynik: {}".format(gs.best_score_)) print("Najlepsza konfiguracja: ") gs.best_params_

from sklearn.metrics import mean_squared_error gb = gs.best_estimator_ gb.fit(X_train,y_train) y_pred = gb.predict(X_test) print("Mse testu: {}".format(-mean_squared_error(y_test,y_pred)))

Przeszukiwanie po siatce dało nam ciekawe wyniki. Widzimy, że przy 500 drzewach optymalną wartością learning ratu było 0.2. wartości maksymalnej ilości liści oraz minimalnej liczby próbek do podziału węzła zostały wybrane w połowie ustalonych przedziałów wartości. Oczywiście można zdefiniować o wiele więcej wartości do przeszukiwania siatki, tutaj mamy jedynie przykład możliwości. Wynik mse na zbiorze uczącym jest bardzo podobny na zbiorze testowym, co pokazuje, że mimo użycia grid searcha nasz algorytm nie uległ przeuczenia i uzyskaliśmy bardzo dobre wyniki.
Podsumowanie
W artykule omówiliśmy dokładną zasadę działania GB w przypadku zadań regresji. Omówiliśmy go od prostej strony koncepcyjnej jak i weszliśmy bardziej w szczegóły, żeby dowiedzieć się co kryją jego „bebechy” :D. Dodatkowo, omówiliśmy możliwe do modyfikacji parametry, których jest naprawdę sporo. Pokazuje to, że Gradient Boost jest bardzo elastycznym i wysoce modyfikowalnym algorytmem. Na koniec dokonaliśmy sobie implementacji tego modelu w Pythonie i przetestowaliśmy ustalanie różnych wartości niektórych parametrów.
Myśle, że jeżeli chodzi o część dotyczącą regresji nie pozostało nam nic więcej do omówienia. Tak naprawdę, z gradient boosta na dobrą sprawę się już nie korzysta. Jednakże, wiele topowych algorytmów boostingu, które cały czas się używa na produkcji tj. XGBoost, CatBoost, LightGBM wywodzi sie od tego algorytmu, więc warto było go omówić. Na sto procent pojawi się omówienie XGBoosta na naszym blogu i wtedy zrozumienie jego działania będzie zdecydowanie prostsze, po zapoznaniu się z Gradient Boostem. Za jakiś czas pojawi się również omówienie GB w przypadku zadań klasyfikacji, więc zachęcam do sprawdzania bloga. Trzymajcie się!