2 proste i skuteczne metody optymalizacji parametrów modelu

optymalizacja parametrów modelu, data science, python, sklearn

Większość algorytmów używanych w uczeniu maszynowym podlega procesowi parametryzacji. Oznacza to, że możemy wpływać na ich dopasowanie do danych z pomocą zestawu dostępnych parametrów.

Z tego artykułu dowiesz się:
1. Jakie metody doboru parametrów wykorzystuję najczęściej?
2. Jakie korzyści płyną z ich użycia?
3. Jakie są ich wady i zalety?
4. Jak możesz optymalizować parametry modelu w Python?

Istnieje cała masa dostępnych sposobów doboru parametrów, zarówno w R, jak i w Python. W tym wpisie przedstawiam dwa z nich, które lubię najbardziej i których używam najczęściej: GridSearchCV i RandomizedSearchCV.

By nie skupiać się zbytnio na teorii, przedstawię ideę działania obu metod i ich porównanie bazując na przykładzie. Posłużę się problemem, który rozwiązywałem przy ostatnim projekcie.

Import niezbędnych bibliotek i przygotowanie zbioru

Importuję niezbędne biblioteki.

import pandas as pd # data wrangling
import numpy as np # funkcje matematyczne
from sklearn.model_selection import train_test_split # funkcja sluzaca do podzialu zbioru
from sklearn.tree import DecisionTreeClassifier as tree # algorytm drzewa decyzyjnego
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV # dzisiejsi bohaterowie :)
from scipy.stats import randint # generowanie liczb całkowitych z rozkładu jednostajnego

Wczytuję zbiór danych. Zawężę zbiór do listy zmiennych zawartych w zmiennej

best_features

. Jest to lista najlepszych zmiennych, które odkryłem podczas poprzedniej analizy zbioru.

df = pd.read_csv('data/abt.csv')
y = df.Wynik # skopiowanie zmiennej celu
best_features = ['Lata_pracy', 'Poz_hist_kred', 'Umowa_o_pracę', 'Saldo_konta',
'Przychody', 'Stan_cywilny___l', 'Stan_cywilny___y',
'Pochodzenie___ff','Pochodzenie___h']
df = df[best_features]

Dzielę zbiór na część uczącą i treningową. Parametr

random_state

jest datą wykonywania eksperymetu: 20 kwietnia 2018.

x_tr, x_te, y_tr, y_te = train_test_split(df, y, test_size = 0.2, random_state = 20042018, stratify = y)

Przygotowanie słowników parametrów

Wielokrotnie w tym wpisie używam słowa „metoda”. Należy je jednak rozumieć je jako „sposób”. GridSearchCV i RandomizedSearchCV są bowiem klasami. Przy ich inicjowaniu jednym z parametrów jest słownik parametrów docelowego modelu, wraz z ich wartościami. Definiuję zatem dwa słowniki parametrów.

params_gs = {'criterion':('entropy', 'gini'),
'splitter':('best','random'),
'max_depth':np.arange(1,6),
'min_samples_split':np.arange(3,8),
'min_samples_leaf':np.arange(1,5)}

params_rs = {'criterion':('entropy', 'gini'),
'splitter':('best','random'),
'max_depth':randint(1,6),
'min_samples_split':randint(3,8),
'min_samples_leaf':randint(1,5)}

Jak widzisz, w przypadku GS definiujemy parametry, wraz z listą wartości. Przy RS sytuacja wygląda nieco inaczej: dla parametrów przyjmujących wartości numeryczne, definiujemy rozkład i zares, z jakiego ma być losowana wartość parametru.

Podczas poszukiwania parametrów kluczowe są dwa elementy:

  1. Czas, po jakim zwracany jest wynik.
  2. Jakość modelu bazującego o znalezione parametry.

Kilka założeń dotyczących przeprowadzonego badania:

  • Metody będą analizowane pod kątem: uzyskanej dokładności modelu (accuracy) i czasu wykonywania (z użyciem metody
    timeit

    ) po jakim zwracany jest wynik.

  • Algorytmem, którego parametry będę optymalizować jest drzewo decyzyjne.
  • Zbadam stabilność obu metod z użyciem współczynnika zmienności.
  • Po przeprowadzeniu testów dla obu metod, przejdę do ich szczegółowego porównania.

Przeprowadzenie testów – GridSearchCV

Badanie czasu wyszukiwania optymalnych parametrów GridSearchCV:

model = tree()
gs = GridSearchCV(tree(), cv = 10, param_grid = params_gs, scoring = 'accuracy')
timeit gs.fit(x_tr, y_tr) # 12.7 s

Dla zdefiniowanego słownika parametrów, liczba wszystkich możliwych kombinacji wynosi 400. Tyle też razy zostanie zbudowany model i wykonany test krzyżowy.Na mojej maszynie powyższy kod wykonywał się średnio 12.7 s.

Przejdźmy teraz do badania jakości predykcji modelu opartego o znaleziony zestaw parmetrów. Definiuję dwie listy, w których będę zapisywać wyniki:

cv_score_gs

(średnie accuracy testu krzyżowego), oraz

final_score_gs

(accuracy otrzymane z walidacji na zbiorze testowym). Wyniki będą dodawane do list w 100 iteracjach. Dzięki temu na koniec uzyskamy uśrednione wyniki dla obu zbiorów, co pozwoli stwierdzić, czy dobór parametrów nie prowadzi do przeuczenia modelu.

Parametry, jakie przyjmuje GS, to kolejno:

  • tree()

    – algorytm, którego parametry są optymalizowane,

  • cv = 10

    – liczba zbiorów w walidacji krzyżowej,

  • param_grid = params_gs

    – zdefiniowany wcześniej słownik parametrów drzewa,

  • scoring = 'accuracy'

    – miara jakości, jakiej używamy,

  • n_jobs = -1

    – liczba jednocześnie przetwarzanych procesów (wartość -1 oznacza maksymalną możliwą liczbę równoległych procesów).

Z użyciem wybranych optymalnych parametrów budowany jest model drzewa decyzyjnego. Zostaje on dopasowany do zbioru uczącego i przetestowany na zbiorze testowym.

cv_score_gs = []
final_score_gs = []

for i in range(0, 100):
    print('Iteracja: ' + str(i))
    gs = GridSearchCV(tree(), cv = 10, param_grid = params_gs, scoring = 'accuracy', n_jobs = -1)
    gs.fit(x_tr, y_tr)
    cv_score_gs.append(gs.best_score_)

# test modelu - parametry GridSearchCV
model_1 = tree(**gs.best_params_)
model_1.fit(x_tr, y_tr)
final_score_gs.append(model_1.score(x_te, y_te))
gs.best_score_

jest średnim wynikiem accuracy uzyskanym podczas walidacji krzyżowej, na 10 zbiorach, dla najlepszego zestawu parametrów. Wyświetlam teraz uzyskane średnie wyniki i wskaźnik zmienności (uzyskane wyniki zawarłem w komentarzu).

# Średnie Accuracy
pritn(np.mean(cv_score_gs)) # 0.873
print(np.mean(final_score_gs)) # 0.88

# Współczynnik zmienności
print(np.std(cv_score_gs)/np.mean(cv_score_gs) * 100) # 0.34
print(np.std(final_score_gs)/np.mean(final_score_gs) * 100) # 1.338

Średnia wartość accuracy mówi nam o tym średniej dokładność najlepszych modeli, zbadanej w na zbiorze uczącym (walidacja krzyżowa) i zbiorze testowym. Możemy również porównać jak bardzo finalny wynik różnił się od tego uzyskanego w GridSearchCV.

Współczynniki zmienności mówi nam o zróżnicowaniu rozkładu wyników. Dzięki niemu możemy wyciągać wnioski na temat stabilności metody. W tym przypadku wynik w okolicach 1% należy uznać za zadowalający.

Przeprowadzenie testów – RandomizedSearchCV

Przeprowadzam proces badania wydajności i jakości dla RandomizedSearch. Zaczynam od czasu wykonywania. Warto w tym punkcie zaznaczyć, że każdorazowo będę ustawiać parametr

n_iter = 20

. Oznacza to, że proces losowania wartości parametrów z rozkładu, będzie wykonywany 20 razy.

model = tree()
rs = RandomizedSearchCV(tree(), cv = 10, n_iter = 20, param_distributions = params_rs)
timeit rs.fit(x_tr, y_tr) # 564 ms

Średni czas wykonania, to 564 ms. Jest więc szybciej o ponad 22 razy w stosunku do GridSearchCV. Jest to spodziewany wynik mając na uwadze 20 razy większą liczbę wykonań (20 vs 400).

Schemat wykonywania eksperymentu jest bardzo podobny do tego, w którym badałem GS. Różnicą są dwa parametry jakie przyjmuje RS:

  • param_distributions = params_rs

    – zamiast słownika wartości, mamy „słownik rozkładów”,

  • n_iter = 20

    – liczba iteracji, w których losujemy nowe wartości parametrów algorytmu.

Jaki to wszystko ma wpływ na dokładność? Przekonajmy się. 🙂

cv_score_rs = []
final_score_rs = []

for i in range(0, 100):
   print('Iteracja: ' + str(i))
   rs = RandomizedSearchCV(tree(), cv = 10, n_iter = 20, param_distributions = params_rs, n_jobs = -1)
   rs.fit(x_tr, y_tr)
   cv_score_rs.append(rs.best_score_)
   # test modelu - parametry RandomizedSearchCV
   model_2 = tree(**rs.best_params_)
   model_2.fit(x_tr, y_tr)
   final_score_rs.append(model_2.score(x_te, y_te))

Sprawdźmy teraz finalne wyniki (rezultaty w komentarzu):

# Średnie cv score
np.mean(cv_score_rs) # 0.864
np.mean(final_score_rs) # 0.883

# Współczynnik zmienności
np.std(cv_score_rs)/np.mean(cv_score_rs) * 100 # 0.526
np.std(final_score_rs)/np.mean(final_score_rs) * 100 # 1.224

Porównanie wyników: GridSearchCV vs RandomizedSearchCV

GridSearchCV RandomizedSearchCV
Średni czas wykonywania [s] 12.7 0.564
Średnie accuracy – zbiór uczący 0.873 0.864
Średnie accuracy – zbiór testowy 0.88 0.883
Współczynnik zmienności – zbiór uczący [%] 0.34 0.526
Współczynnik zmienności – zbiór testowy [%] 1.338 1.224

Wnioski:

  • Współczynnik zmienności – w obu przypadkach jest porównywalnie zadowalający.
  • Średnie accuracy – zbiór uczący – wynik na plus dla GS,
  • Średnie accuracy – zbiór testowy – wynik na plus dla RS. Przyczyna? Prawdopodobnie minimalnie za wysokie dopasowanie w do zbioru uczącego w przypadku GS.
  • Czas wykonywania – duży plus dla RS.

Warto w tym momemncie wspomnieć o jeszcze jednej dużej zalecie RS. Wyobraź sobie sytuację, w której dodajesz do metody wyszukiwania parametrów nowy parametr. Nie jesteś jednak pewien jego wpływu na model.W przypadku RS nowy parametr (który być może nie wpływa pozytywnie na wynik) nie zwiększa nam czasu wykonywania procesu. Nie ma większego wpływu na liczbę wykonać walidacji krzyżowej. Jeśli zdefiniowaliśmy, np.

n_iter = 20

, to RS wykona walidację krzyżową dla parametrów dokładnie 20 razy. Dodatkowe parametry w słowniku nie zwiększają liczby iteracji. Odwrotnie jest w GS. Tu dodatkowa lista wartości parametrów o długości n, zwiększy n-krotnie czas wykonywania procesu. Wniosek jest prosty: jeśli nie masz pewności co do wpływu danego parametru na model – skłaniaj się raczej ku RS.

W badanym przypadku użyłem dosyć małego zbioru i małej możliwej liczby kombinacji parametrów dla GS. Czas wykonania na poziomie kilkunastu sekund „nie bolał” aż tak bardzo. Nieco inaczej sytuacja wygląda w przypadku wolniejszych algorytmów. Na jednym z projektów optymalizowaliśmy parametry

GradientBoostingClassifier

, na dosyć dużym zbiorze. Pomimo całkiem solidnej maszyny skrypt z implementacją GridSearch-a wykonywał się… kilka godzin. Z zastosowaniem RS zbliżony wynik (prawdopodobnie) byłby dostępny w kilkanaście minut.

Podsumowanie

GS:

  • sprawdza wszystkie kombinacje dla zadanego słownika parametrów,
  • dosyć wolny, potrzebuje większych zasobów do znalezienia optymalnego rozwiązania – proces poszukiwania parametrów z użyciem walidacji krzyżowej, wykona się dokładnie tyle razy ile istnieje możliwych kombinacji zdefiniowanych wartości parametrów,
  • w teorii powinien pozwolić znaleźć lepszą kombinację parametrów niż RS. W praktyce dla niektórych algorytmów brute force po wybranym słowniku parametrów, może prowadzić do zbyt wysokiego poziomu dopasowania modelu do danych (przeuczenia modelu), nawet pomimo zastosowanej walidacji krzyżowej.

RS:

  • sprawdza dopasowanie modelu do danych dla losowych wartości parametrów (z wybranego zakresu i rozkładu),
  • szybki, potrzebuje mniej zasobów do znalezienia suboptymalnego roziwązania,
  • wykona się jedynie zadaną liczbę razy, niezależnie od liczby możliwych kombinacji wartości parametrów,
  • w teorii mniej dokładny niż GS, natomiast w praktyce, dla niektórych przypadków (również tego opisanego przeze mnie) potrafi w krótszym czasie, znaleźć lepsze rozwiązanie niż GS.


Źródła:

Podobał Ci się ten artykuł?

Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach. Dodatkowo w prezencie wyślę Ci bezpłatny poradnik 🙂

2 Komentarze

  1. [mateuszgrzyb] Podobał Ci się ten artykuł?
    Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach. Dodatkowo w prezencie wyślę Ci bezpłatny poradnik
    [FM_form id="6"]

    [Jaki] Ten formularz się nie pokazuje. Próbowałem bezskutecznie z różnych przeglądarek. Chciałbym się zapisać ale nie mogę niestety.

Dodaj komentarz

Twój adres email nie zostanie opublikowany.


*