Szkolenie oparte na populacji w Ray Tune

Nov 28 2022
Dostrajanie hiperparametrów jest kluczowym krokiem w wyborze modelu. Hiperparametry są jak ustawienia, jeśli nie obsłużysz ich odpowiednio, może to mieć zły wpływ na wyniki modelu.

Dostrajanie hiperparametrów jest kluczowym krokiem w wyborze modelu. Hiperparametry są jak ustawienia, jeśli nie obsłużysz ich odpowiednio, może to mieć zły wpływ na wyniki modelu. Strojenie można wykonać ręcznie lub automatycznie. W dzisiejszym świecie, ze względu na możliwości obliczeniowe, dużą liczbę hiperparametrów, dużą różnorodność algorytmów i biblioteki pomocnicze, takie jak Ray, preferowanym sposobem jest automatyczne dostrajanie hiperparametrów.

W tym artykule porozmawiamy o treningu opartym na populacji, poznamy Ray Tune i zobaczymy przykład dostrajania hiperparametrów. Repozytorium GitHub:https://github.com/lukakap/pbt-tune.git

Zdjęcie Gary'ego Bendiga na Unsplash

Co oznacza PBT

Jak już wspomnieliśmy, dobre działanie modelu jest związane z prawidłowym doborem hiperparametrów. Population Based Training to jeden z urokliwych sposobów doboru hiperparametrów. Składa się z dwóch części: losowego wyszukiwania i sprytnego wybierania. W kroku wyszukiwania losowego algorytm wybiera losowo kilka kombinacji hiperparametrów. Istnieje duża szansa, że ​​większość kombinacji będzie miała niskie wyniki wydajności, a niewielka część kombinacji wręcz przeciwnie, będzie miała lepsze / dobre wyniki wydajności. Oto mądry wybór. Drugi krok odbywa się w cyklu do momentu osiągnięcia pożądanego rezultatu lub nie wyczerpania liczby iteracji. Sprytny krok wyboru obejmuje dwie główne metody: eksploatować i eksplorować . Wykorzystać— zastąpić kombinację hiperparametrów bardziej obiecującymi, opartymi na metryce wydajności. Eksploruj — losowo zaburzaj hiperparametry (w większości przypadków jest to mnożone przez jakiś współczynnik), aby dodać szum.

Trening oparty na populacji umożliwia jednoczesne robienie dwóch znaczących rzeczy: równoległe trenowanie kombinacji hiperparametrów, badanie od reszty populacji i szybkie uzyskiwanie obiecujących wyników.

Mów o Ray Tune

Ray Tune to oparta na Ray biblioteka Pythona do dostrajania hiperparametrów za pomocą najnowszych algorytmów, takich jak PBT. Będziemy pracować na Ray w wersji 2.1.0. Zmiany można zobaczyć w informacjach o wydaniu poniżej. Wspomnimy również o ważnych zmianach w sposobie.

Zanim przejdziemy do praktycznych przykładów, omówmy kilka podstawowych pojęć. Możliwość trenowania — to cel, który pomaga algorytmom oceniać konfiguracje. Może mieć Class API lub Function API, ale zgodnie z dokumentacją ray tune zalecany jest Function API. Search Spaces — zakresy wartości dla hiperparametrów. Próby — Tuner wygeneruje kilka konfiguracji i uruchomi na nich procesy, więc proces uruchomiony na konfiguracji nazywa się Próbą. Algorytm wyszukiwania — sugeruje konfiguracje hiperparametrów, domyślnie Tune używa wyszukiwania losowego jako algorytmu wyszukiwania. Harmonogram — Na podstawie zgłoszonych wyników podczas procesu szkolenia harmonogramy decydują, czy zatrzymać, czy kontynuować. Następna sensowna koncepcja topunkt kontrolny . Checkpointing to zapisywanie wyników pośrednich, niezbędnych do wznowienia, a następnie kontynuowania treningu.

W większości przypadków algorytm wyszukiwania i harmonogram mogą być używane razem w procesie dostrajania, ale są wyjątki. Jednym z przypadków, gdy nie stosuje się ich razem, jest Population Based Training. W dokumentach Ray Tune PBT jest częścią harmonogramów, ale jest jednym i drugim jednocześnie. Jest harmonogramem, ponieważ zatrzymuje próby na podstawie wyników i jest wyszukiwarką, ponieważ ma logikę do tworzenia nowej konfiguracji.

Korzystamy z dobrze znanego zestawu danych The Boston Housing Dataset (https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html). Możemy zaimportować ten zestaw danych ze sklearn.

Jedną ze znaczących zmian w Ray Tune było wykonanie API. tune.run() zmienił się w Tuner().fit. Przed aktualizacją parametry przekazywaliśmy osobno, ale w nowej wersji wprowadzono klasy konfiguracyjne, co upraszcza wiele rzeczy. Przede wszystkim zgrupowane powiązane parametry, co ułatwia odczytanie i zrozumienie kodu wykonawczego. Po drugie, kiedy używasz Ray Tune w projekcie, niektóre konfiguracje są takie same dla niektórych algorytmów, więc możesz stworzyć jeden wspólny obiekt klasy config i poruszać się po algorytmach, co ułatwia życie.

import

# imports
import json
import os

from joblib import dump, load
from lightgbm import LGBMRegressor
from ray import tune
from ray.air.config import CheckpointConfig
from ray.air.config import RunConfig
from ray.tune.schedulers import PopulationBasedTraining
from ray.tune.tune_config import TuneConfig
from ray.tune.tuner import Tuner
from sklearn.datasets import load_boston
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split

Zacznijmy od trenowalnego. Jak już wspomnieliśmy, istnieją dwa interfejsy API, które można trenować: oparte na funkcjach i oparte na klasach. Napiszemy dający się trenować z Class API.

class TrainableForPBT(tune.Trainable):
    def setup(self, config):
        pass
    
    def step(self):
        pass

W konfiguracji musimy mieć x_train i y_train, aby oszacować wydajność modelu próbnego w przyszłych krokach. Oczywiście setup jest funkcją klasy nadrzędnej (tune.Trainable), ale daje nam możliwość dodania dodatkowych argumentów. Ponadto musimy zainicjować regresor/model lgbm w pliku setup . Zamierzamy przeszkolić nasz model przy każdej iteracji, ale przy pierwszej chcemy po prostu dopasować model, stąd musimy liczyć, w której iteracji się znajdujemy. Nic więcej w tym momencie.

def setup(self, config, x_train, y_train):
    self.current_config = config
    self.x_train = x_train
    self.y_train = y_train
    # need to add about model
    self.model = LGBMRegressor(**self.current_config)
    # count on which iteration are we in
    self.current_iteration = 0
    self.current_step_score = None

def step(self):
    self.current_iteration += 1
    if self.current_iteration == 1:
        self.model.fit(self.x_train, self.y_train)
    else:
        self.model.fit(self.x_train, self.y_train, init_model=self.model)

    self.current_step_score = cross_val_score(estimator=self.model, X=self.x_train, y=self.y_train,
                                              scoring='r2', cv=5).mean()
    results_dict = {"r2_score": self.current_step_score}
    return results_dict

Zacznij od save_checkpoint . Do zapisywania i przywracania modelu użyjemy biblioteki joblib. Co musimy ratować? Po pierwsze — model, ponieważ do następnej iteracji zawsze potrzebujemy modelu poprzedniej iteracji (init_model), możemy też zapisać aktualny numer iteracji i aktualny wynik kroku.

def save_checkpoint(self, tmp_checkpoint_dir):
    path = os.path.join(tmp_checkpoint_dir, "checkpoint")
    with open(path, "w") as f:
        f.write(json.dumps(
            {"current_score": self.current_step_score, "current_step": self.current_iteration}))

    path_for_model = os.path.join(tmp_checkpoint_dir, 'model.joblib')
    dump(self.model, path_for_model)

    return tmp_checkpoint_dir

def load_checkpoint(self, tmp_checkpoint_dir):
    with open(os.path.join(tmp_checkpoint_dir, "checkpoint")) as f:
        state = json.loads(f.read())
        self.current_step_score = state["current_score"]
        self.current_iteration = state["current_step"]

    path_for_model = os.path.join(tmp_checkpoint_dir, 'model.joblib')
    self.model = load(path_for_model)

W procesie szkolenia mamy tyle Trainables, ile próbek konfiguracji. Każdy Trainable potrzebuje kilku sekund, aby rozpocząć. Dzięki funkcji reuse_actor możemy ponownie wykorzystać już rozpoczęte Trainable dla nowych wielu konfiguracji/hiperparametrów. Będziemy więc potrzebować mniej Trainable, a czas poświęcony na uruchomienie będzie również krótszy.

Zaimplementujmy reset_config , który dostarcza nowe hiperparametry. W reset_config każda zmienna musi zostać dostosowana do nowych hiperparametrów, to jak nowa konfiguracja . Jest jedno podchwytliwe pytanie, czy za każdym razem, gdy różne konfiguracje wymieniają ten sam Trainable, rozpoczynają proces od zera, ponieważ w reset_config zapisujemy to jako początek? Właściwie nie, ponieważ po reset_config , Trainable wywołuje punkt kontrolny ładowania, jeśli taki istnieje, dlatego szkolenie będzie kontynuowane od ostatniego przystanku/punktu kontrolnego.

def reset_config(self, new_config):
    self.current_config = new_config
    self.current_iteration = 0
    self.current_step_score = None
    self.model = LGBMRegressor(**self.current_config)
    return True

class TrainableForPBT(tune.Trainable):
    def setup(self, config, x_train, y_train):
        self.current_config = config
        self.x_train = x_train
        self.y_train = y_train
        # need to add about model
        self.model = LGBMRegressor(**self.current_config)
        # count on which iteration are we in
        self.current_iteration = 0
        self.current_step_score = None

    def step(self):
        self.current_iteration += 1
        if self.current_iteration == 1:
            self.model.fit(self.x_train, self.y_train)
        else:
            self.model.fit(self.x_train, self.y_train, init_model=self.model)

        self.current_step_score = cross_val_score(estimator=self.model, X=self.x_train, y=self.y_train,
                                                  scoring='r2', cv=5).mean()
        results_dict = {"r2_score": self.current_step_score}
        return results_dict

    def save_checkpoint(self, tmp_checkpoint_dir):
        path = os.path.join(tmp_checkpoint_dir, "checkpoint")
        with open(path, "w") as f:
            f.write(json.dumps(
                {"current_score": self.current_step_score, "current_step": self.current_iteration}))

        path_for_model = os.path.join(tmp_checkpoint_dir, 'model.joblib')
        dump(self.model, path_for_model)

        return tmp_checkpoint_dir

    def load_checkpoint(self, tmp_checkpoint_dir):
        with open(os.path.join(tmp_checkpoint_dir, "checkpoint")) as f:
            state = json.loads(f.read())
            self.current_step_score = state["current_score"]
            self.current_iteration = state["current_step"]

        path_for_model = os.path.join(tmp_checkpoint_dir, 'model.joblib')
        self.model = load(path_for_model)

    def reset_config(self, new_config):
        self.current_config = new_config
        self.current_iteration = 0
        self.current_step_score = None
        self.model = LGBMRegressor(**self.current_config)
        return True

Teraz możemy stworzyć kilka konfiguracji i przeprowadzić eksperyment Tune. Tuner ma 4 parametry: trainable , param_space , tune_config i run_config . Trainable jest już wdrożony. Zdefiniujmy param_space.

Param_space jest taka sama jak wspomniana już przestrzeń wyszukiwania. Najpierw musimy zdefiniować listę parametrów, które będziemy dostrajać. Dla uproszczenia wybierz 3 parametry: learning_rate, num_leaves, max_depth.

Tune ma własne API Search Space, więc powinniśmy ich używać podczas definiowania przestrzeni wyszukiwania. Nazwy przestrzeni wyszukiwania są intuicyjne, więc bez zbędnych ceregieli zobaczmy wynik.

param_space = {
    "params": {
        "learning_rate": tune.loguniform(1e-5, 1e-1), #between 0.00001 and 0.1
        "num_leaves":  tune.randint(5, 100), #between 5 and 100(exclusive)
        "max_depth": tune.randint(1, 9), #between 1 and 9(exclusive)
    },
}

Pierwszym parametrem harmonogramu treningu bazy populacji jest time_attr. Dla porównania jest to atrybut wyniku treningu, który powinien rosnąć monotonicznie. Wybieramy trainig_iteration jako atrybut czasu, więc kiedy wspominamy o time_attr gdziekolwiek, oznacza to training_iteration. perturbation_interval — jak często ma występować zaburzenie. Jeśli często robimy perturbacje, musimy również często zapisywać punkty kontrolne. Przyjmijmy zatem, że perturbation_interval wynosi 4. burn_in_period — perturbacja nie nastąpi przed upływem podanej liczby interwałów (iteracji). Nie będzie prawdą, jeśli od samego początku sklonujemy stan najlepszych modeli do modeli o słabych wynikach, ponieważ wyniki wydajności są niestabilne na wczesnych etapach. Daj więc 10 iteracji prób burn_in_period, a następnie rozpocznij perturbację. hyperparam_mutations to dyktat hiperparametrów i ich przestrzeni wyszukiwania, które mogą być zaburzone. Chcemy zakłócić wszystkie hiperparametry z plikuparam_space dict, więc hyperparam_mutations będzie taki sam jak param_space [„params”]. Nie będziemy przekazywać argumentów trybu i metryki w PopulationBasedTraining, ponieważ definiujemy je w TuneConfig.

pbt = PopulationBasedTraining(
            time_attr="training_iteration",
            perturbation_interval=4,
            burn_in_period=10,
            hyperparam_mutations=param_space["params"],
        )

tune_config = TuneConfig(metric="r2_score", mode="max", search_alg=None, scheduler=pbt, num_samples=15, reuse_actors=True)

checkpoint_config = CheckpointConfig(checkpoint_score_attribute="r2_score", 
                                     checkpoint_score_order="max", 
                                     checkpoint_frequency=4, 
                                     checkpoint_at_end=True)

run_config = RunConfig(name="pbt_experiment", 
                       local_dir='/Users/admin/Desktop/Dressler/Publications',
                       stop={"training_iteration": 30},
                       checkpoint_config=checkpoint_config)

X, y = load_boston(return_X_y=True)
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

trainable_with_parameters = tune.with_parameters(TrainableForPBT, x_train=x_train, y_train=y_train)

tuner = Tuner(trainable_with_parameters, param_space=param_space["params"], tune_config=tune_config, run_config=run_config)
analysis = tuner.fit()

Teraz możemy wchodzić w interakcje z wynikami za pomocą obiektu ResultGrid (analiza). Za pomocą get_best_result możemy uzyskać najlepszy wynik ze wszystkich prób. Pokażę ci również kilka przydatnych wyników z ResultGrid.

best_trial_id = analysis._experiment_analysis.best_trial.trial_id
best_result = analysis.get_best_result()
best_result_score = best_result.metrics['r2_score']
best_config = best_result.config
best_checkpoint = best_result.checkpoint
best_checkpoint_dir = best_result.checkpoint.as_directory()
print(f"BEST TRIAL ID: {best_trial_id}")
print(f"BEST RESULT SCORE: {best_result_score}")
print(f"BEST CHECKPOINT DIRECTORY: {best_checkpoint}")