Jak nie zabłądzić w lesie … losowym?

Drzewo decyzyjne to jeden z ciekawszych i użytecznych algorytmów uczenia maszynowego. Jest bardzo intuicyjny i łatwy w interpretacji. Jednakże jego głównym mankamentem jest to, że nie daje dobrych wyników. Drzewo decyzyjne jest też bardzo czułe na małe zmiany w zbiorze treningowym. Często również nie wykorzystuje całości informacji zawartej w poszczególnych kolumnach. Fajnie byłoby gdyby udało się zachować zalety tego algorytmu i jednocześnie w jakiś sprytny sposób osłabić jego wady. Czy jest to możliwe?

Wiele różnych drzew

W artykule Ile tak naprawdę są warte nasze modele? sprawdzaliśmy skuteczność pojedynczego drzewa decyzyjnego w zależności od zbiorów testowego i treningowego. Uzyskiwaliśmy modele, które różniły się skutecznością predykcji nawet o 10%. Na pierwszy rzut oka mogło to być nieco dziwne, jednakże zbiór, na którym pracowaliśmy, był dość mały a, tak jak wspomniałem powyżej, drzewa decyzyjne są bardzo czułe na małe zmiany w danych.

Jeśli w trakcie przebiegu pętli zapisywalibyśmy sobie wytrenowane modele, to zauważylibyśmy, że faktycznie bardzo dużo z nich jest całkowicie różnych. Jest to dla nasz oczywista wada. Ale gdyby tak przerobić tę wadę na zaletę?

Użyjmy drzew do zbudowania lasu

Podstawową ideą stojącą za lasem losowym jest zbudowanie wielu różnych drzew i użycie ich do głosowania nad wynikiem. Obojętnie czy to w procesie klasyfikacji, czy regresji. Dzięki takiemu podejściu cały proces staje się nieco bardziej stabilny i daje lepsze wyniki. Ale czy na pewno?

Jak stworzyć las?

W Scikit-Learn funkcją odpowiedzialną za budowanie lasu do klasyfikacji jest RandomForestClassifier. Funkcja ta bazuje na znanej już nam funkcji DecisionTreeClassifier. Wszystkie hiperparametry, które poznaliśmy przy okazji drzewa decyzyjnego przekazywane są przez nią do funkcji budującej poszczególne drzewa.

Funkcję RandomForestClassifier uruchamiamy na całym zbiorze train. Nie uruchamiamy jej wielokrotnie, bo funkcja ta sama z siebie trenuje wiele drzew. Ale w jaki sposób funkcja ta będzie tworzyć różne drzewa? No bo jeśli trenowalibyśmy drzewa na tym samym zbiorze train, to te drzewa byłyby takie same.

Bagging

Przypomnijmy sobie z czasów liceum pewien proces statystyczny zwany losowaniem ze zwracaniem. W procesie tym losujemy jakąś obserwację ze zbioru obserwacji, dodajemy ją do nowego zbioru i umieszczamy ją z powrotem w bazowym zbiorze obserwacji (zwracamy ją do zbioru). W ten sposób ta sama obserwacja może zostać wylosowana kilka razy i kilka razy skopiowana do nowego zbioru. Powtarzamy to losowanie tak długo, aż ilość wszystkich obserwacji w nowym zbiorze (łącznie z duplikatami), będzie równa ilości obserwacji w starym zbiorze. Taki sposób tworzenia nowego zbioru nazywa się bootstrap. Jest to również nazwa hiperparametru przekazywanego do funkcji RandomForestClassifier. Żeby każde drzewo miało nieco inny zbiór do treningu, proces ten jest powtarzany przed treningiem dla każdego drzewa w lesie. Jeżeli więc będziemy trenować 100 drzew w lesie, to będziemy tworzyć dla nich 100 zbiorów poprzez losowanie ze zwracaniem.

Bagging to skrótowa nazwa od Bootstrap aggregating – potocznie nazywana jest tak metoda tworzenia zbiorczych (agregacja) klasyfikatorów, które składają się z wielu prostych klasyfikatorów, które są takie same, ale trenowane są na zbiorach stworzonych przez bootstrap. Słówko to dość często powtarza się w kontekście lasów losowych i podobnych algorytmów – warto je więc sobie zapamiętać.

Praktyka

Powtórzmy eksperyment z wielokrotnym trenowaniem i testowaniem na różnych układach zbiorów train i test.

import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import tqdm

breast_cancer = load_breast_cancer()
X = pd.DataFrame(breast_cancer["data"], 
                 columns = breast_cancer["feature_names"])
y = pd.Series(breast_cancer["target"])

hiperparametry = {"criterion": 'gini', 
                  "max_depth": 5, 
                  "min_samples_leaf": 1, 
                  "min_samples_split": 2, 
                  "n_estimators": 100, 
                  "bootstrap": True, 
                  "max_features": None, 
                  "random_state": 42}
estymator = RandomForestClassifier(random_state = hiperparametry["random_state"], 
                                   criterion = hiperparametry["criterion"], 
                                   max_depth = hiperparametry["max_depth"], 
                                   min_samples_leaf = hiperparametry["min_samples_leaf"], 
                                   min_samples_split = hiperparametry["min_samples_split"], 
                                   n_estimators = hiperparametry["n_estimators"], 
                                   bootstrap = hiperparametry["bootstrap"],
                                   max_features = hiperparametry["max_features"])

wyniki_1 = pd.DataFrame()
for state in tqdm.trange(100):
    X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                                        test_size = 0.25, 
                                                        random_state = state)
    estymator.fit(X = X_train, y = y_train)
    wyniki_1 = wyniki_1.append({"bootstrap": estymator.score(X = X_test, y = y_test)}, 
                           ignore_index=True)

Kod ten w zasadzie jest taki sam jak użyty w przykładnie w artykule Ile tak naprawdę są warte nasze modele?. Dodałem tutaj jednak trzy nowe hiperparametry: n_estimators, bootstrap i max_features.

Hiperparametr n_estimators steruje ilością drzew hodowanych w lesie. Domyślnie jest ustawiony na 10, ale według mnie ta liczba jest zbyt mała. 100 lepiej się sprawdzi w naszym przypadku. Jeśli trenujemy las 100 razy, a w każdym lesie jest 100 drzew, to razem trenujemy 10_000 drzew. Brzmi to, jak dość duża liczba, jednak przy wielkości naszego zbioru zajmuje to mniej niż dwie minuty.

Drugi nowy hiperparametr to bootstrap i mówi on nam, czy w czasie tworzenia wewnętrznego nowego zbioru treningowego dla każdego drzewa ma być użyte losowanie ze zwracaniem, czy ma być użyty cały bazowy zbiór. Domyślnie ustawiony jest na True. Czy ustawienie tutaj False ma sens? Tak, może mieć to sens, bo mamy jeszcze jeden nowy hiperparametr max_features, którego sensowność poznamy poniżej.

Podzbiór cech

Bootstrap zapewnia nam tworzenie nowych zbiorów treningowych dla każdego drzewa. Opiera się on na losowaniu obserwacji, czyli wierszy w ramce danych. A gdyby tak pójść o krok dalej i losować również kolumny? Ten pomysł zawiera się właśnie w hiperparametrze max_features.

Poprzez hiperparametr max_features informujemy funkcję modelującą, ile cech ma wylosować do rozważenia przy każdym podziale. W ten sposób, każdy podział w drzewach jest dokonywany przy takim samym kryterium, ale rozważane są inne cechy. Dzięki temu cechy, które mogłyby nigdy nie wpływać na kształt drzewa, w tym momencie mają szansę się pojawić.

W klasyfikacji domyślną wartością będzie max_features = ”auto” co będzie skutkowało dopuszczeniem ilości cech równej pierwiastkowi z liczby wszystkich cech. We wcześniejszym przykładzie używaliśmy wszystkich cech, sprawdźmy teraz, co wydarzy się, jak dołożymy użycie ich podzbiorów. Dodatkowo sprawdźmy jeszcze wyniki lasu bez bootstrap.

Histogramy
Histogramy
bootstrap bootstrap+max_features max_features tree
count 100.000000 100.000000 100.000000 100.000000
mean 0.954336 0.959091 0.960000 0.936224
std 0.017709 0.015474 0.015497 0.020927
min 0.902098 0.916084 0.916084 0.881119
25% 0.944056 0.951049 0.951049 0.923077
50% 0.958042 0.958042 0.958042 0.937063
75% 0.965035 0.972028 0.972028 0.951049
max 0.993007 0.986014 0.986014 0.986014

Wyniki nie są tutaj jednoznaczne. Średnia wypadła najlepiej w przypadku max_features, jednakże maksimum było przy bootstrap. Jednakże patrząc na odchylenie standardowe i różnicę pomiędzy maksimum i minimum widzimy, że opcje z max_features są lepsze. Co ciekawe, widzimy również, że dowolna wersja lasu losowego, jest przeciętnie o wiele lepsza niż pojedyncze drzewo decyzyjne z tymi samymi hiperparametrami.

Konkluzja

Pytanie, na które odpowiedziałem w tym artykule, brzmiało: Czy używając wielu słabych klasyfikatorów ograniczonych do podzbiorów kolumn i wierszy możemy uzyskać coś silnego? Zbudowaliśmy las, korzystając z drzew z wcześniej dobranymi hiperparametrami. Drzewa te widziały różne wiersze (bootstrap = True) i różne kolumny (max_features = „auto”) i wspólnie głosowały nad wynikiem. Okazało się, że odpowiedź ta brzmi: Tak. Jest to nieco zaskakująca konkluzja. Ja to tłumaczę sobie w następujący sposób: Możemy spytać się najlepszego eksperta, jakiego znamy (drzewo decyzyjne) o opinię. Możemy też znaleźć wielu ekspertów, którzy mają inną edukację (max_features) i inne doświadczenia życiowe (bootstrap) i poprosić ich o opinię. Następnie na bazie wszystkich tych opinii wybieramy tę, która najczęściej się powtarza. Faktem jest to, że algorytm lasu losowego działa i jest lepszy od drzewa decyzyjnego. Sądzę, że zawsze warto go użyć tam gdzie również używaliśmy drzew decyzyjnych.

Kursy Online

Jeśli jesteś zainteresowany zakupem wideo kursów online które przygotowałem, sprawdź tę stronę – może akurat opublikowałem tam kupony zniżkowe 🙂

Dodaj komentarz

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