Hyperopt i szukanie najlepszego parametru

Kontynuując cykl o Pipelines w SciKit-Learn zajmiemy się tematem kręcenia śrubkami w modelach w poszukiwaniu najlepszego wyniku. Użyjemy do tego pakietu hyperopt.

W poprzednich częściach poznawaliśmy co to "pipeline" w SciKit-Learn, tworzyliśmy własne estymatory (przy okazji ucząc się trochę o programowaniu obiektowym, klasach i dziedziczeniu – nawet jeśli to było nieświadome). Dzisiaj ostatnia część z cyklu – kręcenie śrubkami zwanymi hyperparametrami.

Ostatnio przeszukiwaliśmy siatkę hyperparametrów (wszystkie możliwe kombinacje) przy użyciu wbudowanych w SciKit-Learn mechanizmów (na przykład GridSearchCV). Dzisiaj poszukamy innej metody, innego pakietu. A będzie nim hyperopt.

Co to jest?

Zaczniemy od tego dlaczego hyperopt a nie GridSearch? Ano dlatego, że pakiet hyperopt posiada kilka funkcji, które pozwalają na zdefiniowanie przestrzeni parametrów (czyli elementów na siatce) w sposób w którym nie musimy podawać każdej z wartości (jako np. listę) a możemy użyć funkcji generujących rozkład wartości dla każdego z parametrów. Później określamy np. ile czasu ma trwać szukanie najlepszej kombinacji (albo ile kroków) i pakiet za nas dobiera najlepsze kombinacje. Oczywiście dobór odbywa się na podstawie szukania najmniejszej wartości błędu (najmniejszej straty), a nad tym co jest stratą mamy kontrolę. Może ten opis jest zagmatwany, dlatego dłuższy ale prosty przykład.

Oczywiście zaczynamy od importu pakietów i małej konfiguracji:

Będziemy szukać współrzędnej X wierzchołka paraboli. Bo to taka funkcja, która ma jedno konkretne minimum (lub maksimum) i dość łatwo na tym przykładzie zrozumieć ideę działania samego hyperopt ale też funkcji straty.

Na początek zobaczmy jak nasza parabola wygląda:

Mamy więc wypukłą ku górze krzywą, będziemy szukać punktu najwyżej położonego. Ten punkt ma wartość:

a przyjmuje ją dla argumentu:

Oczywiście można to policzyć matematycznie, ale… nie pamiętam wzoru na wierzchołek paraboli, a wyprowadzać tego poprzez szukanie miejsca zerowego pierwszej pochodnej mi się nie chce.

Będziemy szukać takiej wartości argumentu funkcji straty aby owa strata była jak najmniejsza. Czyli odległość (w sensie wartości funkcji, Y) od naszego wierzchołka paraboli musi być najmniejsza. Przygotujmy więc odpowiednią funkcję straty:

Jak wspomniałem wyżej – stratą jest odległość od wierzchołka i chcemy aby była (idealnie) równa zero. Stąd absolutna wartość powyżej w elemencie loss.

Teraz zdefiniujemy sobie przestrzeń parametrów w jakich będziemy szukać naszego idealnego X. Tutaj pojedziemy od razu po bandzie i powiemy, że mamy 3 warianty budowania wartości X:

Czas na najważniejszą część. Wywołujemy szukanie minimum funkcji straty. Tutaj mówimy, że nie więcej niż 1000 kroków, korzystają z przestrzeni parametrów jak wyżej (wybór jednego z trzech typów rozkładów i w ramach niego jakiejś liczby), a wybór odpowiednich parametrów zostawiamy algorytmowi (tpe.suggest):

Chwilę to trwa i dostajemy najlepszy wynik:

Jak to czytać? Najlepiej sprawdził się zerowy element tablicy wyboru rozkładów (a więc rozkład x_normal) i wartość x w okolicach dwójki. Oczywiście rozkłady są losowe, więc za każdym razem może wyjść nieco inna liczba. Przy zwiększeniu liczby kroków do np. 10 tysięcy dostaniemy lepszą precyzję (testując w okolicach 3 tysięcy kroków miałem dokładność na poziomie 2e-5, po 10 tysiącach strata była na poziomie 7e-7).

No dobrze, ale to jest najlepszy wynik (co może być wystarczające) będący najlepszą kombinacją hyperparametrów (tutaj jednego – wartości X). A może chcemy znać całą historię szukania, ze wszystkimi kombinacjami? Tutaj wchodzi zapisywanie kolejnych prób i obiekty typu "Trials". Zmiana jest minimalna – tworzymy obiekt, a później podajemy go do fmin():

Teraz każdy przebieg mamy zapisany w pamięci i możemy to wykorzystać. Na przykład najlepszy wynik dało:

Co tutaj widać:

  • strata w ramach result / loss
  • użyte parametry w vals (nazwa użyta do oznaczenia rozkładu i wartość jaką przyjął X)

 

Zwróć uwagę, że tutaj było mniej przebiegów, zatem i błąd większy (nie zdążyła znaleźć się ta najlepsza wartość).

Wyniki mamy w formie listy zagnieżdżonych obiektów. Możemy z tego powyjmować interesujące elementy do tabeli i przygotować jakieś zestawienia.

Zobaczmy czy każdy z typów rozkładów był użyty tyle samo razy (czyli czy algorytm rozłożył sobie wszystkie możliwe parametry w siatkę i przeszedł ją jakoś równo, czy używając już zdobytych informacji o stracie optymalizował drogę?

Jak widać nie każdy z rozkładów użyty był tak samo często. Zobaczmy na wykresie jakie X były losowane i jaką dawały stratę:

Wygląda z grubsza, że każda z liczby X była losowana mniej więcej tyle samo razy. Ale ten wykres jest mylący – trzeba to zweryfikować inaczej:

Mamy szeroki zakres (łącznie przestrzeń szerokości 2000 jednostek), losowany na dwa sposoby z rozkładem jednostajnym (właściwie dwoma) – więc każda z liczb powinna być tak samo prawdopodobna do wylosowania. Ale wynik pokazuje zbliżanie się do rozkładu normalnego (przechodząc proces 10 tysięcy razy widać to jeszcze lepiej – średnia zbliża się do naszej dwójki, a odchylenie standardowe maleje tak, że mamy właściwe szpilkę a nie dzwon). W sumie może Wam się udać to wcześniej – jeśli z randint wylosuje się dwójka to od razu jesteśmy w domu, bo strata ma wartość zerową.

Sprawdźmy czy im bliżej końca przebiegu osiągamy coraz lepsze wyniki (coraz mniejsza strata). Tutaj mamy sporą losowość, więc może być różnie.

Tyle wprowadzenia do hyperopt. To jest początek, ale pokrywający spory kawałek możliwości hyperopt. Więcej oczywiście w dokumentacji.

Jak tego użyć?

Najbardziej sensownym i pewnie też najpopularniejszym użyciem jest szukanie najlepszego modelu i kręcenie śrubkami jego hyperparametrów. Zróbmy tak:

  • przygotujemy jakieś sztuczne dane na potrzeby klasyfikacji
  • przygotujmy szkielet – pipeline
  • przygotujmy listę klas modeli (różne ich typy) razem z zakresem hyperparametrów jakimi można kręcić dla każdego z nich
  • wytrenujmy i oceńmy każdą kombinację
  • w wyniku dostaniemy najlepszą wersję (a w gratisie kilka cennych danych poszerzających wiedzę o tym jak hyperparametry wpływają na dany typ modelu)

Przygotujemy dane. Wiele razy w takim przypadku szukałem jakiegoś zbioru, a przecież SciKit-Learn ma to tego znakomite funkcje w module datasets:

Trochę sobie te dane uporządkujemy – po prostu wpakujemy w data frame:

Czas na przygotowanie klasyfikatorów i zakresu zmian ich hyperparametrów.

Właśnie przeczytałem świetną książkę (dla średnio zaawansowanych pythonowców idealna) "Python. Dobre praktyki profesjonalistów" autorstwa Dane Hillard więc przekazuję odrobinę zdobytej wiedzy – można konfigurację każdego z klasyfikatorów ubrać w element typu dict, a potem w jednej pętli przejść przez wszystkie. Poniżej 5 konfiguracji (lista 5 elementów typu słownik) dla 5 różnych klasyfikatorów:

Jednak aby hyperopt mógł dać jakiś wynik potrzebujemy funkcji straty. Oto i ona (wraz z odpowiednim komentarzem):

Teraz kiedy wszystko przygotowane możemy puścić pętlę przez wszystkie zdefiniowane wcześniej klasyfikatory (razem z ich przestrzenią hyperparametrów). Dla każdego z klasyfikatorów poniższy kod:

  • przygotuje pusty pipeline
  • przygotuje przestrzeń hyperparametrów space
  • znajdzie najlepszą konfigurację i ją wyświetli
  • a wszystkie przejścia dla wszystkich modeli dopisze do listy trials_df

 

Całość trochę trwa (u mnie na maszynie 8 CPU @ 16 GB RAM jakieś 30 minut; maszynę VPS mam z webh.pl, chwalę sobie), więc możesz uruchomić kod i iść na przykład na kawę:

Zebrane wyniki (trials_df) mamy w postaci listy słowników, więc przekształćmy ją w zwykły pandasowy data frame:

Teraz możemy porównać wyniki i same modele ze sobą. W klasyfikacji interesuje nam accuracy więc to sobie zobaczmy:

Widzimy tutaj, że każdy z klasyfikatorów był testowany różną ilość razy (różna liczba kropek) – ale to oczywiście zależy od wartości "max_evals" jaką przypisaliśmy w konfiguracji.

Możemy wybrać jeden z klasyfikatorów (na przykład XGBoost – miał najwięcej parametrów i najbardziej był męczony) i sprawdzić jak ma się accuracy w zależności od wartości poszczególnych hyperparametrów:

Oczywiście najlepiej to widać na wykresie, zatem:

Porównajcie powyższe wykresy z tym, co hyperopt zwrócił jako najlepsze wartości wyżej:

Prawda, że spójne?

Oczywiście do pipeline można dodać transformację danych i też to przepuścić przez hyperopt – wtedy robi się kompleksowa maszyna do szukania najlepszego modelu. Warto każdy z tak wytrenowanych modeli zapisać w plikach (w ogóle warto wersjonować modele, razem z informacją o ich parametrach – poczytaj na przykład o MLFlow jeśli ten temat Cię interesuje) aby ewentualnie sprawdzić kilka wytrenowanych wersji na produkcyjnych danych. Można cały proces przygotować tak, że modele douczają się co jakiś czas, a można też tak że z użyciem hyperopt szukamy za każdym razem tego najlepszego.

Zdjęcie okładkowe to Edge2Edge Media z Unsplash

Dodaj komentarz

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