Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

MLflow - śledzenie eksperymentów

Trzy lekcje temu wytrenowałeś las losowy w scikit-learn, potem karawanę drzew w XGBoost, a na końcu sieć neuronową w PyTorch. Za każdym razem zmieniałeś coś drobnego: raz głębokość drzewa, raz liczbę estymatorów, raz szybkość uczenia. Za każdym razem patrzyłeś na wypisaną trafność i mówiłeś sobie "o, teraz lepiej". A teraz odpowiedz mi bez zaglądania nigdzie, @name: który dokładnie zestaw ustawień dał ten najlepszy wynik? I gdzie leży plik z tamtym modelem?

Znam tę ciszę. Widziałem ją w niejednym obozie badawczym: dwudziestu tropicieli wraca z sawanny, każdy z garścią pomiarów zapisanych na skrawku papieru albo w pamięci, a wieczorem przy ognisku okazuje się, że nikt nie potrafi powtórzyć obserwacji, która wydawała się najciekawsza. Notatka się rozmokła, karta wypadła z kieszeni, a autor pamięta tylko, że "zwierzę było duże". W uczeniu maszynowym rolę rozmokłej kartki gra komórka notatnika, którą nadpisałeś, i zmienna, którą przypisałeś ponownie.

Ta lekcja jest o prowadzeniu dziennika ekspedycji, którego nie da się zgubić. Poznasz MLflow - narzędzie, które przy każdym treningu zapisuje w jednym miejscu trzy rzeczy: parametry, z jakimi model powstał, metryki, jakie osiągnął, oraz sam model jako plik gotowy do wczytania. Zobaczysz, jak zalogować pierwszy eksperyment, jak obejrzeć wyniki w przeglądarce, jak zlecić logowanie automatowi i jak z dwudziestu prób wyłuskać zwycięzcę jedną komendą.

Notes, którego nikt nie prowadzi

Zanim sięgniemy po narzędzie, zobaczmy, jak wygląda śledzenie eksperymentów zrobione ręcznie. Prawie każdy przechodzi przez ten etap: zakładasz zwykłą listę, po każdym treningu dopisujesz do niej słownik z ustawieniami i wynikiem, a na koniec wybierasz najlepszy wpis wbudowaną funkcją

max
. Parametr
key
mówi jej, według czego porównywać elementy - tutaj według wartości pod kluczem
accuracy
. To działa i jest lepsze niż nic, więc przyjrzyjmy się temu uczciwie, zanim zaczniemy krytykować.

1# reczne sledzenie eksperymentow: lista slownikow
2wyniki = []
3
4wyniki.append({"n_estimators": 100, "max_depth": 10, "accuracy": 0.973})
5wyniki.append({"n_estimators": 300, "max_depth": 6, "accuracy": 0.981})
6
7print(max(wyniki, key=lambda r: r["accuracy"]))
8# {'n_estimators': 300, 'max_depth': 6, 'accuracy': 0.981}

Funkcja

max
wskazała drugi wpis, bo 0,981 jest większe od 0,973. Zwróć uwagę na to, czego ta lista nie zawiera i czego nigdy nie zawierała. Nie ma w niej modelu - są liczby opisujące model, ale nie sam model, więc żeby użyć zwycięzcy, musisz go wytrenować od nowa i mieć nadzieję, że trafisz w te same warunki. Nie ma w niej daty ani wersji danych, więc za tydzień nie odróżnisz wyniku sprzed poprawki w notesie terenowym od wyniku po niej. Nie ma w niej ani jednego wpisu, którego nie dodałeś ręcznie, a ręka zapomina - zwłaszcza przy dwudziestym treningu o północy. I najważniejsze: ta lista żyje w pamięci procesu. Gdy zamkniesz notatnik albo jądro się zrestartuje, cała ekspedycja przepada.

Czym MLflow jest, a czym nie jest

MLflow to platforma do śledzenia eksperymentów uczenia maszynowego: zapisuje parametry, metryki i modele. To jedno zdanie jest odpowiedzią na pytanie, po co w ogóle instaluje się to narzędzie, i warto je zapamiętać w takiej właśnie formie - parametry, metryki, modele. Wszystko inne, co MLflow potrafi, wyrasta z tej trójki.

Odsuńmy od razu trzy nieporozumienia, bo krążą wokół tej nazwy uparcie. Po pierwsze, MLflow nie służy tylko do trenowania modeli - on w ogóle nie trenuje. Nie ma w nim ani jednego algorytmu uczącego. Trening dalej robi scikit-learn, XGBoost albo PyTorch, dokładnie tak jak w poprzednich lekcjach, a MLflow stoi obok z notesem i zapisuje, co się wydarzyło. Gdybyś usunął go z projektu, modele powstawałyby dokładnie takie same, tylko nikt by nie zapisał, jak powstały.

Po drugie, MLflow nie służy do tworzenia interfejsów użytkownika. To nieporozumienie jest podstępne, bo za chwilę uruchomimy interfejs webowy MLflow i zobaczysz w przeglądarce tabelę z wynikami. Ten interfejs jest jednak gotowy i niezmienny: to przeglądarka Twoich eksperymentów, a nie narzędzie, którym zbudujesz ekran dla użytkownika swojej aplikacji. Do budowania własnych interfejsów służą zupełnie inne biblioteki, na przykład Streamlit czy Gradio po stronie Pythona.

Po trzecie, MLflow nie służy do pisania testów jednostkowych. Testy jednostkowe sprawdzają, czy funkcja zwraca to, co powinna, i piszesz je w

pytest
albo
unittest
. MLflow nie orzeka o poprawności kodu - notuje wyniki treningu, nawet jeśli ten trening był kompletnie chybiony. Model o trafności piętnastu procent zostanie zapisany równie skrupulatnie jak ten najlepszy, bo dziennik ekspedycji notuje też nieudane wyprawy.

Instalacja i cztery pojęcia na start

MLflow instalujesz jednym poleceniem menedżera pakietów, tak samo jak wcześniej Pandas czy XGBoost. Pakiet jest spory, bo w środku siedzi serwer webowy i baza danych, więc instalacja potrwa dłużej niż zwykle - to normalne.

1pip install mlflow

Po instalacji masz dwie rzeczy naraz: bibliotekę, którą zaimportujesz w kodzie, oraz program uruchamiany z terminala poleceniem

mlflow
. To ten sam pakiet w dwóch przebraniach i za moment użyjemy obu. W kodzie nie zmieniło się nic innego - żaden import scikit-learn ani NumPy nie wymaga po tym poprawki.

Zanim napiszemy pierwszą linię, cztery słowa, które będą wracać do końca lekcji. Run to jedno uruchomienie treningu: jeden komplet parametrów, jeden komplet metryk, jeden model. Eksperyment to nazwany zbiór runów dotyczących tego samego problemu, na przykład wszystkie próby rozpoznawania gatunków. Parametr to liczba lub napis, które ustawiłeś przed treningiem i które opisują konfigurację. Metryka to liczba, która wyszła z treningu i którą mierzysz jakość. Ta różnica między parametrem a metryką nie jest kosmetyczna: MLflow traktuje je inaczej, o czym przekonasz się w sekcji o porównywaniu runów.

Notes terenowy na tę lekcję

Potrzebujemy czegoś do trenowania, ale skromnie - bohaterem tej lekcji jest dziennik, a nie dane. Zbierzemy trzysta okazów trzech gatunków, każdy opisany dwoma pomiarami: masą w kilogramach i prędkością w kilometrach na godzinę. Generator

np.random.default_rng
znasz z lekcji o NumPy, a
train_test_split
z uczenia nadzorowanego, więc tylko przypomnę, że
stratify=y
pilnuje równych proporcji gatunków w obu częściach, a
random_state=42
sprawia, że podział jest za każdym razem identyczny. Metodzie
normal
podajemy tym razem dwie listy zamiast dwóch liczb: pierwsza to średnie obu pomiarów, druga to ich rozrzuty, a
size=(100, 2)
daje sto wierszy po dwie kolumny.

1import numpy as np
2from sklearn.model_selection import train_test_split
3
4rng = np.random.default_rng(7)
5
6# kazdy gatunek: 100 okazow, kolumny to masa [kg] i predkosc [km/h]
7lew = rng.normal([190, 58], [30, 8], size=(100, 2))
8gepard = rng.normal([55, 110], [10, 12], size=(100, 2))
9hiena = rng.normal([60, 65], [12, 9], size=(100, 2))
10
11X = np.vstack([lew, gepard, hiena])
12y = np.repeat([0, 1, 2], 100)          # 0 = lew, 1 = gepard, 2 = hiena
13
14X_train, X_test, y_train, y_test = train_test_split(
15    X, y, test_size=0.2, random_state=42, stratify=y
16)
17
18print(X_train.shape, X_test.shape)     # (240, 2) (60, 2)

Dwieście czterdzieści zwierząt idzie na trening, sześćdziesiąt czeka na egzamin. Funkcja

np.vstack
skleiła trzy stada jedno pod drugim, więc kolejność wierszy odpowiada kolejności etykiet z
np.repeat
. Zwróć uwagę, że ustawienie ziarna generatora nie ma nic wspólnego z MLflow - to zwykła higiena powtarzalności, która przyda się nam podwójnie, bo za chwilę będziemy porównywać runy między sobą i chcemy, żeby różnice brały się z parametrów modelu, a nie z tego, że wylosowały się inne zwierzęta.

Pierwszy run: eksperyment i parametr

Zaczynamy prowadzić dziennik. Trzy wywołania wystarczą na początek. Funkcja

mlflow.set_experiment
przyjmuje nazwę eksperymentu i ustawia go jako bieżący; jeśli taki eksperyment jeszcze nie istnieje, MLflow go zakłada. Na rozgrzewkę zakładamy osobny eksperyment o nazwie "safari-proby", żeby te pierwsze, niekompletne runy nie zaśmiecały docelowego dziennika. Funkcja
mlflow.start_run
otwiera pojedynczy run i używamy jej w konstrukcji
with
, o czym za chwilę powiem więcej. Opcjonalny argument
run_name
nadaje runowi czytelną nazwę - bez niego dostaniesz wygenerowany losowo przydomek w rodzaju "silent-hound-42", zabawny, ale bezużyteczny przy porównywaniu. Wreszcie
mlflow.log_param
zapisuje jeden parametr: najpierw jego nazwa, potem wartość.

1import mlflow
2
3mlflow.set_experiment("safari-proby")
4
5with mlflow.start_run(run_name="las_losowy_v1"):
6    mlflow.log_param("n_estimators", 100)
7    mlflow.log_param("max_depth", 10)
8
9    print(mlflow.active_run().info.run_id)

Po wyjściu z bloku

with
run jest zamknięty i zapisany. Program wypisał długi ciąg znaków - to identyfikator runu, unikalny odcisk palca tego jednego uruchomienia, którym posłużymy się później do wczytania modelu. Nie podaję go tutaj, bo u Ciebie będzie inny; MLflow generuje go losowo dla każdego runu. Zwróć uwagę na to, co się nie zmieniło: w katalogu projektu nie pojawił się żaden plik, który sam byś nazwał, a Twoje dane leżą nietknięte. MLflow założył sobie katalog
mlruns
obok skryptu i to w nim trzyma cały dziennik. Zapamiętaj tę nazwę, bo za moment okaże się, dlaczego interfejs webowy trzeba uruchamiać w tym samym folderze.

Przyjrzyjmy się jeszcze samemu wywołaniu logującemu parametr, bo jego kolejność bywa myląca. Czytane element po elemencie to:

mlflow
,
.
,
log_param
,
(
,
"n_estimators", 100
,
)
. Najpierw pakiet, potem kropka, potem nazwa funkcji, otwierający nawias, w środku najpierw nazwa parametru jako napis, po przecinku jego wartość, i na końcu nawias zamykający. Kolejność argumentów jest nieodwracalna: gdybyś napisał
mlflow.log_param(100, "n_estimators")
, w dzienniku pojawiłby się parametr o nazwie "100" i wartości "n_estimators". Apostrofy i cudzysłowy są w Pythonie równoważne, więc
'n_estimators'
i
"n_estimators"
znaczą dokładnie to samo.

Skoro jesteśmy przy

with
: da się otworzyć run po staremu, wywołując
mlflow.start_run()
bez konstrukcji
with
i zamykając go potem
mlflow.end_run()
. Moja rekomendacja jest jednoznaczna: zawsze używaj
with
.
Powód jest praktyczny, nie estetyczny. Jeśli trening rzuci wyjątek w połowie - a rzuci, prędzej czy później - wersja z
with
i tak zamknie run, oznaczając go jako przerwany. Wersja bez
with
zostawi go otwartym, bo wywołanie
end_run
nigdy nie nastąpi. Wszystko, co zalogujesz potem, po cichu dopisze się do tamtego kalekiego runu, a gdy spróbujesz otworzyć następny, MLflow przerwie program komunikatem, że jakiś run jest już aktywny. Pół godziny szukania, dlaczego "nic się nie zapisuje w nowym runie", to cena za trzy zaoszczędzone znaki.

Metryka to wynik, parametr to decyzja

Parametry mamy zapisane, ale dziennik bez wyników jest tylko listą życzeń. Do zapisania wyniku służy

mlflow.log_metric
, które przyjmuje takie same dwa argumenty co
log_param
: nazwę i wartość. Różnica siedzi gdzie indziej. Parametr wolno ustawić w danym runie tylko raz i może być czymkolwiek - liczbą, napisem, nazwą algorytmu. Metryka musi być liczbą, bo MLflow rysuje z niej wykresy i sortuje po niej tabele; próba zalogowania napisu jako metryki skończy się błędem. Za to metrykę można zapisywać wielokrotnie w trakcie jednego runu, co przydaje się przy sieciach neuronowych, gdzie po każdej epoce chcesz odnotować bieżącą stratę.

Wytrenujmy więc las losowy, którego składnię znasz z lekcji o scikit-learn, i zapiszmy jego trafność. Metrykę

accuracy_score
poznałeś przy uczeniu nadzorowanym - to odsetek poprawnych przewidywań.

1from sklearn.ensemble import RandomForestClassifier
2from sklearn.metrics import accuracy_score
3
4with mlflow.start_run(run_name="las_losowy_v2"):
5    mlflow.log_param("n_estimators", 100)
6    mlflow.log_param("max_depth", 10)
7
8    model = RandomForestClassifier(
9        n_estimators=100, max_depth=10, random_state=42
10    )
11    model.fit(X_train, y_train)
12
13    trafnosc = accuracy_score(y_test, model.predict(X_test))
14    mlflow.log_metric("accuracy", trafnosc)

Trafność wypadnie w okolicach 98 procent - nie podaję dokładnej liczby w komentarzu, bo zależy ona od wersji scikit-learn, więc uruchom kod i sprawdź u siebie. Ważniejsze jest co innego: zauważ, że trening wygląda dokładnie tak samo jak w lekcji o scikit-learn. Ta sama klasa, te same hiperparametry, ta sama metoda

fit
. MLflow nie wszedł modelowi w drogę ani o milimetr, nie zmienił jego wyniku i nie spowolnił obliczeń w sposób, który dałoby się zauważyć. Dołożyliśmy wyłącznie trzy wywołania, które opisują to, co i tak się działo. Zwróć też uwagę na powtórkę liczb: setka i dziesiątka występują dwa razy każda, raz w
log_param
, raz w konstruktorze modelu. To najsłabszy punkt ręcznego logowania i za dwie sekcje pokażę, jak się go pozbyć.

Model to też wynik, który warto zapisać

Zostaje trzeci element obiecanej trójki. Model jest obiektem Pythona, więc nie zmieści się ani w parametrze, ani w metryce - do jego zapisania MLflow ma osobne funkcje, po jednej dla każdej biblioteki. Ten podział nazywa się flavor, czyli smak:

mlflow.sklearn
obsługuje modele scikit-learn,
mlflow.xgboost
modele XGBoost,
mlflow.pytorch
sieci PyTorch. Moduł smaku importujesz osobno, obok samego
mlflow
. Funkcja
log_model
przyjmuje model i nazwę podkatalogu, w którym ma go zapisać wewnątrz runu; przyjęło się wpisywać tam po prostu "model".

1import mlflow.sklearn
2
3with mlflow.start_run(run_name="las_losowy_v3"):
4    model = RandomForestClassifier(n_estimators=100, random_state=42)
5    model.fit(X_train, y_train)
6
7    mlflow.sklearn.log_model(model, "model")

Po tym wywołaniu w katalogu

mlruns
obok parametrów i metryk wylądował komplet plików: zserializowany model, lista wersji bibliotek potrzebnych do jego wczytania i opis oczekiwanego wejścia. Dzięki temu zwycięzcę sprzed miesiąca da się wczytać bez ponownego treningu, i to na innym komputerze. Ten drugi argument bywa nazywany różnie: starsze dokumentacje mówią o
artifact_path
, nowsze o
name
. Podanie go pozycyjnie, tak jak wyżej, oszczędza Ci tego wyboru, a jeśli kiedyś zobaczysz ostrzeżenie o przestarzałej nazwie argumentu, będzie chodziło dokładnie o to miejsce. Sam obiekt
model
nie zmienił się przy tym ani trochę: nadal możesz na nim wołać
predict
, bo
log_model
tylko go odczytał.

Modelem nie kończy się lista rzeczy wartych zachowania. Do runu można dołączyć dowolny plik - wykres ważności cech, plik CSV z pomyłkami, tekstową notatkę - i służy do tego

mlflow.log_artifact
, któremu podajesz ścieżkę do istniejącego pliku. Nie pokazuję tego w osobnym bloku, bo wymagałoby najpierw wygenerowania wykresu, ale pamiętaj, że taka możliwość istnieje: artefakt to w słowniku MLflow każdy plik przypięty do runu, a model jest po prostu jego szczególnym przypadkiem.

Kompletny run od początku do końca

Zbierzmy teraz wszystko w jeden przepis, bo dokładnie w takiej postaci będziesz go używał w praktyce. Ten fragment jest wart zapamiętania jako szkielet: otwierasz run, logujesz decyzje, trenujesz, mierzysz, logujesz wyniki i zapisujesz model. Nowość jest jedna - funkcja

mlflow.log_params
w liczbie mnogiej, która przyjmuje słownik i zapisuje wszystkie jego pary naraz. Ten sam słownik podajemy potem konstruktorowi modelu z użyciem gwiazdek
**
, które rozpakowują go na argumenty nazwane, więc żadna wartość nie jest już wpisana w dwóch miejscach.

1import mlflow
2import mlflow.sklearn
3from sklearn.ensemble import RandomForestClassifier
4from sklearn.metrics import accuracy_score
5
6parametry = {"n_estimators": 200, "max_depth": 8, "random_state": 42}
7
8mlflow.set_experiment("safari-klasyfikacja")
9
10with mlflow.start_run(run_name="las_losowy_finalny"):
11    mlflow.log_params(parametry)
12    mlflow.log_param("liczba_okazow_treningowych", len(X_train))
13
14    model = RandomForestClassifier(**parametry)
15    model.fit(X_train, y_train)
16
17    trafnosc = accuracy_score(y_test, model.predict(X_test))
18    mlflow.log_metric("accuracy", trafnosc)
19
20    mlflow.sklearn.log_model(model, "model")

To jest komplet: parametry, metryka i model w jednym runie, czyli dokładnie ta trójka, od której zaczęliśmy lekcję. Zwróć uwagę, że wywołanie

set_experiment
przełączyło nas z rozgrzewkowego "safari-proby" na docelowe "safari-klasyfikacja" - poprzednie, niekompletne runy zostały tam, gdzie były, i nie będą mieszać w porównaniach. Liczbę okazów treningowych zalogowałem jako parametr, nie jako metrykę, i to nie przypadek: to fakt o danych wejściowych, a nie wynik pomiaru jakości. Gdyby wpadł między metryki, MLflow zaproponowałby Ci wykres liczby okazów w czasie, co nie znaczy nic. Zauważ wreszcie, że słownik
parametry
posłużył dwa razy, ale zdefiniowany jest raz, więc nie da się już zalogować innych ustawień niż te faktycznie użyte. Sam
with
nie zwrócił żadnej wartości do dalszego użytku i niczego nie wypisał - cały ślad tego uruchomienia poszedł na dysk.

mlflow ui - przeglądarka Twoich eksperymentów

Dziennik istnieje, ale jak dotąd oglądasz go tylko przez dziurkę od klucza, czyli przez wydruki w terminalu. Pora otworzyć obozową mapę. Interfejs webowy MLflow uruchamiasz poleceniem

mlflow ui
wpisanym w terminalu - nie w Pythonie - w tym samym katalogu, w którym leży folder
mlruns
. To ważne: polecenie czyta dziennik ze ścieżki względnej, więc uruchomione piętro wyżej pokaże pustą tabelę i będziesz przekonany, że nic się nie zapisało.

1mlflow ui
2mlflow ui --port 5000

Obie linie robią to samo, bo 5000 to port domyślny - drugą wersję pokazuję po to, żebyś wiedział, czym go zmienić, gdy port będzie zajęty przez inny program. Po uruchomieniu wejdź w przeglądarce pod adres wypisany w terminalu i zobaczysz tabelę runów: nazwa, czas, parametry i metryki w kolumnach. Możesz sortować po metryce, zaznaczyć kilka runów i porównać je na wykresie. Serwer działa, dopóki go nie zatrzymasz, a przerwanie go skrótem klawiszowym niczego nie kasuje - dane siedzą w

mlruns
, interfejs tylko je wyświetla.

Trzy inne polecenia, które ludzie próbują wpisać w tym miejscu, nie zadziałają, i warto wiedzieć dlaczego.

python mlflow.py
nie ma prawa się udać, bo taki plik nie istnieje w Twoim projekcie - a jeśli sam go stworzysz, żeby "było czym uruchomić", zrobisz sobie prawdziwą krzywdę: plik o nazwie
mlflow.py
w katalogu roboczym przesłoni prawdziwą bibliotekę i
import mlflow
przestanie działać w całym projekcie.
mlflow start
kończy się komunikatem o nieznanym poleceniu, bo w programie
mlflow
nie ma podpolecenia
start
; są między innymi
ui
,
server
,
run
i
models
.
run mlflow
jest odwrócone - komenda zawsze zaczyna się od nazwy programu, a poza tym
mlflow run
służy do uruchamiania projektu MLflow z katalogu lub repozytorium, a nie do pokazywania interfejsu.

Autolog - kiedy nie chce się pisać log_param

Wróćmy do tej niezręczności z powtarzaniem liczb w dwóch miejscach. Model scikit-learn przecież zna swoje ustawienia - siedzą w nim jako atrybuty - więc przepisywanie ich ręcznie do dziennika jest pracą, którą powinna wykonać maszyna. I wykonuje. Wywołanie

mlflow.sklearn.autolog()
sprawia, że MLflow sam loguje parametry i metryki modeli scikit-learn, bez ani jednego
log_param
z Twojej strony. Wystarczy wywołać je raz, przed treningiem.

1import mlflow
2import mlflow.sklearn
3from sklearn.ensemble import RandomForestClassifier
4
5mlflow.sklearn.autolog()
6
7with mlflow.start_run(run_name="las_losowy_auto"):
8    model = RandomForestClassifier(n_estimators=150, max_depth=12)
9    model.fit(X_train, y_train)

W tym runie nie ma ani jednego jawnego wywołania logującego, a mimo to w dzienniku znajdziesz komplet hiperparametrów lasu, metryki policzone na zbiorze treningowym i zapisany model. Mechanizm jest prosty: autolog podmienia metodę

fit
na własną wersję, która najpierw notuje ustawienia, potem woła oryginał, a na końcu zapisuje wyniki. Właśnie dlatego działa tylko dla bibliotek, które MLflow zna. Zwróć uwagę, że trening nie zmienił się ani na jotę - to nadal Twoje wywołanie
fit
robi całą robotę, a autolog jedynie je podsłuchuje.

Skoro mowa o granicach tego mechanizmu, wyjaśnijmy, czego on nie robi. Autolog nie czyści danych: brakujące pomiary i błędne wpisy zostaną w tabeli dokładnie tam, gdzie były, a od porządków są metody Pandas, które poznałeś przy notesie terenowym. Autolog nie trenuje modeli - gdybyś usunął linię z

fit
, w dzienniku nie pojawiłoby się nic poza pustym runem, bo nie byłoby czego podsłuchać. Autolog nie tworzy raportów PDF ani żadnych innych dokumentów; wyniki oglądasz w interfejsie webowym albo pobierasz do kodu, o czym za chwilę. Robi jedną rzecz: automatycznie loguje parametry i metryki modeli danej biblioteki.

Odpowiedniki dla innych bibliotek nazywają się przewidywalnie:

mlflow.xgboost.autolog()
dla XGBoost i
mlflow.pytorch.autolog()
dla PyTorcha. Istnieje też
mlflow.autolog()
bez nazwy smaku, które włącza automatyczne logowanie dla wszystkich rozpoznanych bibliotek naraz. W praktyce najlepiej sprawdza się połączenie obu podejść: autolog bierze na siebie hiperparametry modelu, a Ty dokładasz ręcznie te informacje, o których biblioteka nie ma pojęcia - wersję danych, nazwę tropiciela, numer wyprawy.

Porównywanie runów bez klikania

Interfejs webowy jest wygodny do oglądania, ale gdy runów jest sto pięćdziesiąt, chcesz zadać pytanie kodem, a nie oczami. Do tego służy

mlflow.search_runs
, które zwraca DataFrame znany Ci z lekcji o Pandas - ze wszystkim, co potrafisz na nim zrobić. Najpierw trzeba jednak wskazać, o który eksperyment pytamy, a robi to
mlflow.get_experiment_by_name
: przyjmuje nazwę, zwraca obiekt eksperymentu, z którego wyciągamy pole
experiment_id
. Funkcja szukająca oczekuje listy identyfikatorów, bo pozwala przeszukać kilka eksperymentów naraz, więc nasz jeden identyfikator opakowujemy w nawiasy kwadratowe.

1import mlflow
2
3eksperyment = mlflow.get_experiment_by_name("safari-klasyfikacja")
4runy = mlflow.search_runs(experiment_ids=[eksperyment.experiment_id])
5
6print(runy.shape)
7print(runy[["params.n_estimators", "params.max_depth", "metrics.accuracy"]].head())

Dostajesz zwykłą tabelę: jeden wiersz na run, a w kolumnach wszystko, co zalogowałeś. Nazwy kolumn mają przedrostki -

params.
dla parametrów i
metrics.
dla metryk - bo bez nich parametr i metryka o tej samej nazwie zderzyłyby się w jednej kolumnie. Jest tu jedna pułapka, na którą wpada każdy: wartości w kolumnach
params.
są napisami, a nie liczbami
, bo MLflow przechowuje parametry jako tekst. Sortowanie po
params.n_estimators
ustawi więc "100" przed "50", jak w słowniku, a nie jak w matematyce. Kolumny
metrics.
są za to prawdziwymi liczbami zmiennoprzecinkowymi i po nich sortuje się normalnie.

Skoro metryki są liczbami, znalezienie zwycięzcy to jedno wywołanie

sort_values
z lekcji o Pandas, z
ascending=False
dla porządku malejącego. Pierwszy wiersz posortowanej tabeli wyciągniemy przez
iloc[0]
, czyli po pozycji, a z niego kolumnę
run_id
- ten sam identyfikator, który wypisaliśmy przy pierwszym runie. Model wczytujemy funkcją
load_model
, podając adres w formacie
runs:/identyfikator/nazwa_podkatalogu
. Adres składamy f-stringiem, bo identyfikator jest zmienną.

1import mlflow.sklearn
2
3najlepsze = runy.sort_values("metrics.accuracy", ascending=False)
4id_zwyciezcy = najlepsze.iloc[0]["run_id"]
5
6zwyciezca = mlflow.sklearn.load_model(f"runs:/{id_zwyciezcy}/model")
7print(zwyciezca.predict(X_test)[:5])

Wczytany model jest zwyczajnym obiektem scikit-learn i odpowiada na

predict
tak, jakby dopiero co wyszedł z treningu - a przecież mógł powstać miesiąc temu na innym komputerze. To jest cały sens zapisywania modelu razem z metrykami. Zwróć uwagę, że
sort_values
nie zmienił tabeli
runy
, tylko zwrócił posortowaną kopię, dokładnie tak jak w lekcji o Pandas, oraz że człon
model
w adresie to ta sama nazwa podkatalogu, którą podaliśmy wcześniej jako drugi argument
log_model
. Gdybyś tam wpisał "las", adres musiałby brzmieć
runs:/identyfikator/las
.

I stąd bierze się praktyczna zasada, o którą warto oprzeć całą swoją dyscyplinę pracy: loguj model w każdym runie, który ma jakąkolwiek szansę wygrać. Gdyby na szczycie posortowanej tabeli wylądował run, w którym zapisałeś samą metrykę - jak nasz rozgrzewkowy "las_losowy_v2" - wczytanie skończyłoby się błędem o braku artefaktu, bo pod tym adresem po prostu nic nie leży. Właśnie dlatego trzymamy próby w osobnym eksperymencie, a w docelowym lądują wyłącznie kompletne runy.

Model Registry - jedno miejsce na model produkcyjny

Adres z identyfikatorem runu jest precyzyjny, ale nieporęczny: nikt nie zapamięta trzydziestu dwóch znaków szesnastkowych, a w kodzie aplikacji taki napis wygląda jak przypadek. Dlatego MLflow ma rejestr modeli - katalog nazwanych modeli z numerowanymi wersjami. Model trafia do rejestru w chwili logowania, jeśli dopiszesz argument

registered_model_name
. Za pierwszym razem powstaje wersja pierwsza, a każde kolejne zalogowanie pod tą samą nazwą dokłada wersję drugą, trzecią i tak dalej. Twoje poprzednie modele nigdzie przy tym nie znikają.

1with mlflow.start_run(run_name="kandydat_do_rejestru"):
2    model = RandomForestClassifier(n_estimators=200, random_state=42)
3    model.fit(X_train, y_train)
4
5    mlflow.sklearn.log_model(
6        model, "model", registered_model_name="KlasyfikatorSafari"
7    )

Od tej chwili model ma nazwę czytelną dla człowieka i wersję, a w interfejsie webowym pojawia się osobna zakładka z rejestrem. Nic poza tym się nie zmieniło - run wygląda jak każdy inny, parametry i metryki logujesz tak samo, a sam obiekt

model
dalej nadaje się do użytku w tym samym skrypcie.

Wczytanie z rejestru wygląda znajomo, zmienia się tylko przedrostek adresu:

models:
zamiast
runs:
, potem nazwa modelu, a po ukośniku numer wersji.

1zarejestrowany = mlflow.sklearn.load_model("models:/KlasyfikatorSafari/1")
2print(zarejestrowany.predict(X_test)[:5])

Jedynka na końcu to numer wersji, a nie liczba modeli - żeby sięgnąć po nowszą, wpisujesz dwójkę. Kod korzystający z modelu nie musi już wiedzieć nic o runach ani identyfikatorach. W starszych materiałach spotkasz w tym miejscu zapis z nazwą etapu, na przykład

models:/KlasyfikatorSafari/Production
; to pozostałość po mechanizmie etapów, który MLflow uznał za przestarzały i zastąpił aliasami - własnymi etykietami w rodzaju
@produkcja
, które przypinasz do wybranej wersji i przepinasz, gdy zwycięzca się zmieni. Zasada jest ta sama: aplikacja pyta o nazwę, a Ty decydujesz, która wersja się pod nią kryje.

Pętla strojenia z dziennikiem w tle

Na koniec połączmy dziennik ze strojeniem hiperparametrów z poprzedniej lekcji. Przy ręcznym przeglądzie kombinacji MLflow pokazuje pełnię możliwości: każda kombinacja dostaje własny run, więc po skończonej pętli masz gotową tabelę do porównania, zamiast wpatrywać się w przewijający się terminal. Użyjemy

cross_val_score
, które znasz z uczenia nadzorowanego - dzieli dane treningowe na pięć części i zwraca pięć ocen, po jednej na każdy podział. Z tej piątki logujemy dwie liczby: średnią, czyli spodziewaną jakość, i odchylenie standardowe, czyli stabilność wyniku między podziałami.

1import mlflow
2import mlflow.xgboost
3import xgboost as xgb
4from sklearn.model_selection import cross_val_score
5
6mlflow.set_experiment("safari-xgboost-strojenie")
7
8kombinacje = [
9    {"max_depth": 3, "learning_rate": 0.1},
10    {"max_depth": 5, "learning_rate": 0.1},
11    {"max_depth": 3, "learning_rate": 0.05},
12    {"max_depth": 5, "learning_rate": 0.05},
13]
14
15for parametry in kombinacje:
16    with mlflow.start_run():
17        mlflow.log_params(parametry)
18
19        model = xgb.XGBClassifier(**parametry, n_estimators=100, random_state=42)
20        oceny = cross_val_score(model, X_train, y_train, cv=5)
21
22        mlflow.log_metric("cv_srednia", oceny.mean())
23        mlflow.log_metric("cv_odchylenie", oceny.std())
24
25        model.fit(X_train, y_train)
26        mlflow.log_metric("test_accuracy", model.score(X_test, y_test))
27        mlflow.xgboost.log_model(model, "model")

Cztery kombinacje, cztery runy, dwanaście metryk - i ani jednej liczby zapisanej na skrawku papieru. Nie podaję wyników w komentarzach, bo zależą od wersji XGBoost i od danych, a poza tym nie o nie tu chodzi: chodzi o to, że po zakończeniu pętli wystarczy

mlflow.search_runs
z poprzedniej sekcji, żeby zobaczyć wszystkie cztery próby obok siebie. Zwróć uwagę na dwie rzeczy. Po pierwsze,
with
stoi wewnątrz pętli, nie na zewnątrz. Gdyby było odwrotnie, wszystkie kombinacje wpadłyby do jednego runu, a przy drugim obrocie pętli
log_params
przerwałby program - pamiętasz, że parametr wolno w runie zapisać tylko raz, a druga kombinacja próbuje podmienić
max_depth
z trójki na piątkę. Po drugie, model XGBoost zapisujemy smakiem
mlflow.xgboost
, a nie
mlflow.sklearn
, mimo że
XGBClassifier
udaje klasyfikator scikit-learn; każda biblioteka ma własny sposób serializacji i własny zestaw wersji do zapamiętania.

Podsumowanie

  • MLflow służy do śledzenia eksperymentów ML: zapisuje parametry, metryki i modele. Nie trenuje modeli, nie służy do budowania interfejsów użytkownika i nie zastępuje testów jednostkowych.
  • Run to jedno uruchomienie treningu, eksperyment to nazwany zbiór runów. Otwierasz run przez
    with mlflow.start_run():
    - wersja bez
    with
    zostawia run otwarty po każdym wyjątku.
  • Parametr logujesz sześcioma elementami:
    mlflow
    ,
    .
    ,
    log_param
    ,
    (
    ,
    "n_estimators", 100
    ,
    )
    - najpierw nazwa, potem wartość. Wiele naraz przyjmuje
    log_params
    ze słownikiem.
  • Metryka musi być liczbą i wolno ją zapisywać wielokrotnie w jednym runie; parametr może być czymkolwiek, ale tylko raz.
  • Model zapisujesz smakiem właściwym dla biblioteki:
    mlflow.sklearn.log_model
    ,
    mlflow.xgboost.log_model
    ,
    mlflow.pytorch.log_model
    . Dowolny plik dołączasz przez
    log_artifact
    .
  • Interfejs webowy uruchamiasz w terminalu poleceniem
    mlflow ui
    , w katalogu z folderem
    mlruns
    . Nie
    python mlflow.py
    , nie
    mlflow start
    , nie
    run mlflow
    .
  • mlflow.sklearn.autolog()
    automatycznie loguje parametry i metryki modeli scikit-learn. Nie czyści danych, nie trenuje modeli i nie generuje raportów PDF.
  • mlflow.search_runs
    zwraca DataFrame - wartości w kolumnach
    params.
    są napisami, a w
    metrics.
    liczbami.
  • Rejestr modeli daje modelowi nazwę i numerowane wersje:
    registered_model_name
    przy zapisie, adres
    models:/Nazwa/1
    przy odczycie.

Przed Tobą trzy projekty zamykające ten moduł: klasyfikator z Pipeline i walidacją krzyżową, XGBoost ze strojeniem hiperparametrów i sieć neuronowa w PyTorch. Zrób każdy z nich wewnątrz

with mlflow.start_run():
- to dziesięć minut pracy więcej, a zamiast trzech skryptów zostanie Ci dziennik ekspedycji, do którego wrócisz za pół roku. Bo model bez zapisanych parametrów jest jak okaz bez etykiety: piękny w gablocie, bezużyteczny w nauce.

Przejdź do CodeWorlds