Ile tak naprawdę są warte nasze modele?

Całkiem nieźle idzie nam przetwarzanie danych do postaci ramki danych. Sprawnie wykorzystujemy Pythona i różne moduły, które oferują funkcje modelujące. Dzielimy dane na zbiory treningowe i testowe. Uzyskujemy całkiem niezłe wyniki i zaczynamy odczuwać zadowolenie. Ale czy aby na pewno jest to już ten moment? Czy aby na pewno stworzyliśmy coś, co ma sens? Coś, co przybliża nas do rozwiązania problemu? Czy może stworzyliśmy coś bardzo ładnego (i może skomplikowanego), ale niewiele bardziej efektywnego niż „mądrość ludowa”?

Jak ugryźć taki problem? Okazuje się, że Scikit-Learn daje nam fajne narzędzie, które może się przydać w ocenie wartości naszego modelu – jest to tzw. dummy estimator.

DummyClassifier, DummyRegressor

Dummy – sztuczny, fałszywy, na niby. W taki sposób można przełożyć to słowo. W kontekście estymatora ja lubię to sobie tłumaczyć jako „mądrość ludowa”. Byłby to więc estymator (w mojej intuicji), który zachowuje się jak rada starszych w wiosce, którzy nie jedno już widzieli i nie przejmują się nowinkami wymyślonymi przez młodych. I w powstałej sytuacji podejmują decyzje w taki sposób, jaki sugeruje im rozsądek. Czasem się mylą, czasem im się udaje.

Aby ocenić jakość naszego „udziwnionego” nowego estymatora, możemy porównać go do dummy estymatora i sprawdzić, który jest lepszy. Jeśli w tym miejscu przegrywamy, to zdecydowanie nie powinniśmy go używać. No bo skoro prostszy i rozsądny estymator ma lepsze wyniki, to nie ma to sensu.

Stabilność modelu

Zanim zabierzemy się za porównywanie modelów do DummyClassifier, powinniśmy się jeszcze zastanowić nad stabilnością modelu. We wcześniejszym artykule (Tuning hiperparametrów na przykładzie drzewa decyzyjnego) pokazałem, jak możemy użyć zbiorów train i test do obiektywnej oceny modelu. W tej sytuacji pojawia się jednak pewien problem. Użyliśmy pojedynczej kombinacji train i test. W małych zbiorach może to powodować sytuację, że jakieś konkretne „ciekawe” obserwacje wylądują tylko w jednym z nich. Natomiast gdybyśmy użyli innego podziału, mogłoby się okazać, że te ciekawe obserwacje wylądowały w innym zbiorze i że hiperparametry, które wybraliśmy, dają gorsze wyniki. Jak ocenić w ten sposób dobrane hiperparametry?

Jeżeli nasz zbiór jest na tyle mały, że trenowanie modelu jest dość krótkie, możemy zastosować podejście z powtarzanym trenowaniem i ocenianiem modelu. Tworzymy sobie pętlę z licznikiem od 0 do np. 99. W każdym przebiegu pętli podajemy licznik jako wartość parametru random_state w funkcji train_test_split. Tworzymy w ten sposób zbiory treningowe i testowe, które za każdym razem będą się różniły jakimiś obserwacjami. W tym samym przebiegu pętli trenujemy model i od razu dokonujemy jego oceny na zbiorze testowym. Zapisujemy sobie wyniki. W ten sposób uzyskamy 100 różnych ocen tego samego modelu w zależności od tego, jakie dane treningowe i testowe mu podamy. Jeżeli odchylenie standardowe tych ocen będzie „małe”, to przyjmujemy, że nasz model jest w miarę stabilny. Nie będziemy raczej zbyt często sprawdzać, czy odchylenie jest małe, czy duże, będziemy raczej porównywać je z odchyleniem uzyskanym w ten sam sposób na bazie innego modelu. Zobaczmy, jak wygląda kod Pythona, w którym robimy coś takiego. Użyję w nim hiperparametrów wyznaczonych przez GridSearchCV w artykule Tuning hiperparametrów na przykładzie drzewa decyzyjnego:

# Importy

import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split

# Wczytywanie danych z Scikit-Learn

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

# Definicja modelu

hiperparametry = {'criterion': 'gini', 'max_depth': 5, 'min_samples_leaf': 1, 'min_samples_split': 2}
estymator = DecisionTreeClassifier(random_state = 42,
                                   criterion = hiperparametry["criterion"],
                                   max_depth = hiperparametry["max_depth"],
                                   min_samples_leaf = hiperparametry["min_samples_leaf"],
                                   min_samples_split = hiperparametry["min_samples_split"])

# Sprawdzanie wyników modelowania na róznych zbiorach treningowych i testowych

wyniki = pd.DataFrame()
for state in range(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 = wyniki.append({"dokładność_drzewo": estymator.score(X = X_test, y = y_test)},
                           ignore_index=True)

Spójrzmy na histogram i violin plot uzyskanych wyników:

Histogram - dokładność drzewo
Histogram – dokładność drzewo
Violin plot - dokładność drzewo
Violin plot – dokładność drzewo

Widzimy tutaj, że rozrzut naszych wyników jest całkiem spory. Najniższy wynik, jaki uzyskaliśmy, wynosił nieco ponad 88%, a najwyższy był w okolicach 98%. Jedyne co różniło te dwa modele to inny podział na zbiory testowe i treningowe. Wygląda to dość ciekawie. Użyjmy więc DummyClassifier i porównajmy nasz model z „mądrością ludową”.

Pierwszy sprawdzian

from sklearn.dummy import DummyClassifier

dummy = DummyClassifier(random_state = 42)

wyniki = pd.DataFrame()
for state in range(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)
    dummy.fit(X = X_train, y = y_train)
    wyniki = wyniki.append({"dokładność_drzewo": estymator.score(X = X_test, y = y_test),
                            "dokładność_dummy": dummy.score(X = X_test, y = y_test)},
                           ignore_index=True)

Pierwszym faktem, który rzuca się w oczy, jest to, że obiekt DummyClassifier ma sygnaturę zgodną z sygnaturami innych funkcji modelujących w Scikit-Learn. Możemy go więc trenować i oceniać w ten sam sposób. Jest to bardzo wygodne. Zobaczmy histogram uzyskanych wyników:

Histogram 2 - dokładność drzewo i dummy classifier
Histogram 2 – dokładność drzewo i dummy classifier

Na powyższym histogramie widzimy wyraźnie, że wyniki uzyskane przez DummyClassifier są zdecydowanie gorsze. Jest to dobra wskazówka. Widzimy również, że żaden słupek wyników uzyskanych przez DummyClassifier nie pokrywa się ze słupkami uzyskanymi przez DecisionTreeClassifier. To bardzo dobra wiadomość – jakkolwiek byśmy nie mieszali elementami zbiorów test i tran „mądrość ludowa” nie będzie lepsza niż nasz model.

Różne wersje mądrości ludowych

DummyClassifier oferuje 5 różnych „mądrości ludowych”. Domyślną, która została użyta powyżej to stratified. Ta strategia podejmowania decyzji polega na tym, że nasz klasyfikator w czasie treningu patrzy sobie na proporcje występowania klas i je zapisuje. W czasie dokonywania predykcji losuje klasy z zachowaniem tych proporcji. Oznacza to tyle, że jeśli np. w zbiorze testowym było 3/4 osób zdrowych i 1/4 osób chorych, to na każde 4 predykcje nasz klasyfikator wskaże jedną losowo wybraną osobę jako chorą.

Drugą „mądrością ludową” jest strategia most_frequent która skutkuje podejmowaniem ciągle takiej samej decyzji na podstawie najczęściej występującej klasy w zbiorze treningowym. Jeśli mamy zbiór 3/4 osób zdrowych to nasz klasyfikator-mędrzec będzie mówił, że wszyscy, którzy do niego przychodzą, są zdrowi. Co wbrew pozorom może dawać całkiem niezłe wyniki, jeśli faktycznie ludzie, którzy trafili do zbiorów treningowych i testowych, są prawie zawsze zdrowi.

Trzecią strategią w klasyfikacji jest uniform. Jest to strategia polegająca na losowym wybieraniu klasy z takim samym prawdopodobieństwem. Klasyfikator-mędrzec rzucałby po prostu monetą i dokonywał takich predykcji.

W klasyfikatorze DummyClassifier mamy jeszcze dwie strategie, ale nie zawsze będą one przydatne. Jedna to prior – działa tak samo, jak most_frequent ale dodatkowo uzupełnia wyniki metody predict_proba o prawdopodobieństwo uzyskania takiej klasy. Może to być przydatne, jeżeli głębiej badamy pewność predykcji. Ostatnia strategia constant polega na dawaniu jednej stałej wybranej odpowiedzi.

Więcej DummyClassifier

Powtórzmy więc wcześniejszą pętlę, ale rozbudujmy ją o sprawdzenie wyników trzech powyższych dummy – mędrców:

dummy_stratified = DummyClassifier(random_state = 42, strategy = "stratified")
dummy_most_frequent = DummyClassifier(random_state = 42, strategy = "most_frequent")
dummy_uniform = DummyClassifier(random_state = 42, strategy = "uniform")

wyniki = pd.DataFrame()
for state in range(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)
    dummy_stratified.fit(X = X_train, y = y_train)
    dummy_most_frequent.fit(X = X_train, y = y_train)
    dummy_uniform.fit(X = X_train, y = y_train)
    wyniki = wyniki.append({"dokładność_drzewo": estymator.score(X = X_test, y = y_test),
                            "dokładność_dummy_stratified": dummy_stratified.score(X = X_test, y = y_test),
                            "dokładność_dummy_most_frequent": dummy_most_frequent.score(X = X_test, y = y_test),
                            "dokładność_dummy_uniform": dummy_uniform.score(X = X_test, y = y_test)},
                           ignore_index=True)

Wyświetlmy histogram uzyskanych wyników:

Histogram 3 - dokładność drzewo i różne dummy classifier
Histogram 3 – dokładność drzewo i różne dummy classifier

Dołożenie dwóch kolejnych klasyfikatorów dummy nie wiele zmieniło w ogólnej sytuacji. Strategia uniform dała gorsze wyniki (w sumie należałoby się tego spodziewać przy decyzjach polegających na rzucaniu monetą), natomiast strategia most_frequent dawała nieco lepsze.

Co ciekawe, całkiem sporo predykcji wszystkich DummyClassifier było trafnych powyżej 50% (zdarzyła się nawet jedna predykcja testowa trafna w 70%). Jest to informacja dla nas (badaczy), że nawet dokonywanie pozornie przypadkowych predykcji może dawać racjonalne wyniki. W powyższej sytuacji, gdybyśmy skonstruowali klasyfikator, który ma skuteczność około 65%, nie moglibyśmy się nim pochwalić, bo mądrość ludowa daje podobne wyniki. Okazuje się jednak, że klasyfikator DecisionTreeClassifier z wyznaczonymi przez nas parametrami spisuje się znacznie lepiej.

Porównanie dwóch własnych klasyfikatorów

Dołóżmy do naszego kodu dodatkową pętlę, która będzie wykorzystywała proces wyznaczony przez TPOT we wcześniejszym artykule (Automatyczne Uczenie Maszynowe – TPOT). Jedyną zmianą, jaką tutaj wprowadziłem to dodanie parametrów random_state = 42 żeby u każdego ten sam kod dał takie same wyniki.

from sklearn.pipeline import make_pipeline, make_union
from tpot.builtins import StackingEstimator
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.svm import LinearSVC

exported_pipeline = make_pipeline(
    StackingEstimator(estimator=GradientBoostingClassifier(learning_rate=0.001, max_depth=2, max_features=0.5, min_samples_leaf=5, min_samples_split=13, n_estimators=100, subsample=0.6500000000000001, random_state = 42)),
    StackingEstimator(estimator=LinearSVC(C=10.0, dual=True, loss="hinge", penalty="l2", tol=0.0001, random_state = 42)),
    LinearSVC(C=25.0, dual=False, loss="squared_hinge", penalty="l1", tol=0.01, random_state = 42)
)

wynik_tpot = []
for state in range(100):
    X_train, X_test, y_train, y_test = train_test_split(X, y,
                                                        test_size = 0.25,
                                                        random_state = state)
    exported_pipeline.fit(X = X_train, y = y_train)
    wynik_tpot.append(exported_pipeline.score(X_test, y_test))

wyniki["dokładność_tpot"] = wynik_tpot

Kod ten będzie wykonywał się chwilkę dłużej. Sprawdźmy histogram uzyskanych wyników i porównajmy go z wynikami z drzewa decyzyjnego:

Histogram 4 - dokładność drzewo i TPOT
Histogram 4 – dokładność drzewo i TPOT

Natychmiast możemy zauważyć, że wyniki procesu wybranego przez TPOT są lepsze niż drzewo decyzyjne z parametrami wybranymi przez GridSearchCV. Zerknijmy na tabelkę powstałą w wyniku użycia metody describe().

dokładność_drzewo dokładność_tpot
count 100.000000 100.000000
mean 0.936224 0.968601
std 0.020927 0.014959
min 0.881119 0.923077
25% 0.923077 0.958042
50% 0.937063 0.972028
75% 0.951049 0.979021
max 0.986014 0.993007

Tutaj sprawa jest jasna. Model TPOT ma średnią dokładność 0.968601 a drzewo 0.936224. Odchylenie standardowe TPOT to 0.014959 a drzewa 0.020927. Wychodzi więc na to, że nasze starannie zbudowane drzewo ma mniejszą trafność predykcji niż proces uzyskany przez automat TPOT.

Konkluzja

Używanie zbiorów train i test to dobry sposób na uniknięcie danych z przyszłości do przewidzenia przyszłości. Jednakże przy małych zbiorach danych można uzyskać w ten sposób modele zbudowane pod konkretny układ danych. Aby tego uniknąć, warto trenować i testować modele na różnych kombinacjach obserwacji w zbiorach train i test. Warto również zastanowić się nad estymatorem bazowym, czyli właśnie jakąś taką rozsądną mądrością ludową. W ten sposób od razu wyznaczamy sobie minimum, które musimy przekroczyć, żeby w ogóle móc mówić, że udało nam się „zrobić coś sensownego”.

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 *