Formation basée sur la population dans Ray Tune

Nov 28 2022
Le réglage des hyperparamètres est une étape clé dans la sélection du modèle. Les hyperparamètres sont comme des paramètres, si vous ne les gérez pas correctement, cela peut avoir un impact négatif sur les résultats du modèle.

Le réglage des hyperparamètres est une étape clé dans la sélection du modèle. Les hyperparamètres sont comme des paramètres, si vous ne les gérez pas correctement, cela peut avoir un impact négatif sur les résultats du modèle. Le réglage peut être fait manuellement ou automatiquement. Dans le monde d'aujourd'hui, en raison des capacités de calcul, d'un nombre élevé d'hyperparamètres, d'une grande variété d'algorithmes et de bibliothèques d'assistance comme Ray, la méthode préférée consiste à régler automatiquement les hyperparamètres.

Dans cet article, nous parlerons de la formation basée sur la population, explorerons Ray Tune et verrons un exemple de réglage d'hyperparamètres. Dépôt GitHub :https://github.com/lukakap/pbt-tune.git

Photo de Gary Bendig sur Unsplash

Ce que signifie PBT

Comme nous l'avons déjà mentionné, la bonne performance du modèle est liée à la sélection correcte des hyperparamètres. La formation basée sur la population est l'une des manières charmantes de choisir les hyperparamètres. Il se compose de deux parties : la recherche aléatoire et le choix intelligent. Dans l'étape de recherche aléatoire, l'algorithme choisit aléatoirement plusieurs combinaisons d'hyperparamètres. Il y a de fortes chances que la plupart des combinaisons aient des scores de performance faibles et qu'une petite partie des combinaisons au contraire aient de meilleurs/bons scores de performance. Voici le choix intelligent. La deuxième étape est dans un cycle jusqu'à ce que nous obtenions le résultat souhaité ou que nous n'épuisions pas le nombre d'itérations. L'étape de choix astucieux contient deux méthodes principales : exploit et explore . Exploiter— remplacer la combinaison des hyperparamètres par d'autres plus prometteurs, basés sur la métrique de performance. Explorer - perturbe aléatoirement les hyperparamètres (dans la plupart des cas, il est multiplié par un certain facteur) pour ajouter du bruit.

La formation basée sur la population permet de faire deux choses significatives ensemble : paralléliser la formation des combinaisons d'hyperparamètres, étudier à partir du reste de la population et obtenir rapidement des résultats prometteurs.

Parlez de Ray Tune

Ray Tune est une bibliothèque python basée sur Ray pour le réglage des hyperparamètres avec les derniers algorithmes tels que PBT. Nous allons travailler sur Ray version 2.1.0. Les changements peuvent être vus dans les notes de version ci-dessous. Nous mentionnerons également des changements importants dans la manière.

Avant de passer aux exemples pratiques, passons en revue quelques concepts de base. Entraînable — est un objectif qui aide les algorithmes à évaluer les configurations. Il peut avoir une API de classe ou une API de fonction, mais selon la documentation de ray tune, l'API de fonction est recommandée. Espaces de recherche — plages de valeurs pour les hyperparamètres. Essais - Tuner générera plusieurs configurations et exécutera des processus dessus, de sorte que le processus exécuté sur une configuration s'appelle Essai. Algorithme de recherche - suggère des configurations d'hyperparamètres, par défaut, Tune utilise la recherche aléatoire comme algorithme de recherche. Planificateur — Sur la base des résultats signalés au cours du processus de formation, les planificateurs décident d'arrêter ou de continuer. Le prochain concept significatif estpoints de contrôle . Le point de contrôle consiste à enregistrer les résultats intermédiaires, nécessaires pour reprendre puis poursuivre l'entraînement.

Dans la plupart des cas, l'algorithme de recherche et le planificateur peuvent être utilisés ensemble dans le processus de réglage, mais il existe une exception. L'un des cas, lorsqu'ils ne sont pas utilisés ensemble, est la formation basée sur la population. Dans Ray Tune docs, PBT fait partie des planificateurs, mais c'est les deux en même temps. C'est un planificateur car il arrête les essais en fonction des résultats et c'est un chercheur car il a la logique de créer une nouvelle configuration.

Nous utilisons le célèbre Boston Housing Dataset (https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html). Nous pouvons importer cet ensemble de données depuis sklearn.

Un changement significatif dans Ray Tune était l'API d'exécution. tune.run() est devenu Tuner().fit. Avant la mise à jour, nous passions les paramètres séparément, mais dans la nouvelle version, des classes de configuration ont été introduites, ce qui simplifie beaucoup de choses. Tout d'abord, regrouper les paramètres associés, ce qui facilite la lecture et la compréhension du code d'exécution. Et deuxièmement, lorsque vous utilisez Ray Tune dans un projet, certaines configurations sont les mêmes pour certains algorithmes, vous pouvez donc créer un objet de classe de configuration commun et déplacer les algorithmes, ce qui facilite la vie.

importations

# 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

Commençons par entraînable. Comme nous l'avons déjà mentionné, il existe deux API entraînables : basée sur les fonctions et basée sur les classes. Nous allons écrire entraînable avec Class API.

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

Dans la configuration, nous devons avoir x_train et y_train pour estimer l'efficacité du modèle d'essai dans les étapes futures. Bien sûr, setup est la fonction (tune.Trainable) de la classe parente, mais elle nous donne la possibilité d'ajouter des arguments supplémentaires. De plus, nous devons initialiser le régresseur/modèle lgbm dans le fichier setup . Nous allons recycler notre modèle à chaque itération, mais sur la première, nous voulons juste ajuster le modèle, donc nous devons compter sur quelle itération nous sommes. Rien de plus à ce stade.

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

Commencez par save_checkpoint . Nous utiliserons la bibliothèque joblib pour enregistrer et restaurer le modèle. Que devons-nous économiser ? Tout d'abord - modèle, puisque nous avons toujours besoin du modèle d'itération précédent (init_model) pour la prochaine itération, nous pouvons également enregistrer le numéro d'itération actuel et le score d'étape actuel.

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)

Dans le processus de formation, nous avons autant de Trainables que d'échantillons de configuration. Chaque Trainable a besoin de plusieurs secondes pour démarrer. Avec la fonctionnalité reuse_actor, nous pouvons réutiliser Trainable déjà démarré pour de nouvelles configurations/hyperparamètres multiples. Nous aurons donc besoin de moins de Trainable et le temps passé sur le démarrage sera également moindre.

Implémentons reset_config , qui fournit de nouveaux hyperparamètres. Dans reset_config , chaque variable doit être ajustée à de nouveaux hyperparamètres, c'est comme new setup . Il y a une question délicate, chaque fois que différentes configurations échangent le même Trainable, démarrent-elles le processus à partir de zéro, en raison du fait que dans reset_config nous l'écrivons comme le début ? En fait, non, car après reset_config , Trainable appelle le point de contrôle de chargement s'il existe, par conséquent, la formation continuera à partir du dernier arrêt/point de contrôle.

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

Nous pouvons maintenant créer des configurations et exécuter l'expérience Tune. Tuner a 4 paramètres : trainable , param_space , tune_config et run_config . Trainable est déjà implémenté. Définissons param_space.

Param_space est identique à l'espace de recherche déjà mentionné. Tout d'abord, nous devons définir une liste de paramètres que nous allons régler. Pour simplifier, choisissez 3 paramètres : learning_rate, num_leaves, max_depth.

Tune possède sa propre API Search Space, nous devons donc les utiliser lorsque nous définissons des espaces de recherche. Le nom des espaces de recherche est intuitif, alors voyons le résultat sans plus tarder.

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)
    },
}

Le premier paramètre du programme de formation de base de la population est time_attr. Il s'agit de l'attribut de résultat d'entraînement pour la comparaison, qui devrait être quelque chose qui augmente de manière monotone. Nous choisissons trainig_iteration comme attribut de temps, donc quand nous mentionnons time_attr n'importe où, cela signifie training_iteration. perturbation_interval — à quelle fréquence la perturbation doit-elle se produire. Si nous perturbons souvent, nous devons également enregistrer les points de contrôle souvent. Par la présente, choisissons perturbation_interval à 4. burn_in_period — la perturbation ne se produira pas avant que ce nombre d'intervalles (itération) ne soit passé. Ce ne sera pas vrai si nous clonons l'état des plus performants vers des modèles peu performants dès le début, car les scores de performance sont instables aux premiers stades. Donnez donc 10 itérations d'essais burn_in_period, puis commencez la perturbation. hyperparam_mutations est un dict des hyperparamètres et de leurs espaces de recherche, qui peuvent être perturbés. On veut perturber tous les hyperparamètres de laparam_space dict, donc hyperparam_mutations sera le même que param_space ["params"]. Nous ne transmettrons pas les arguments de mode et de métrique dans PopulationBasedTraining, tels que nous les définissons dans 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()

Maintenant, nous pouvons interagir avec les résultats en utilisant l'objet ResultGrid (analyse). En utilisant get_best_result, nous pouvons obtenir le meilleur résultat de tous les essais. Je vais également vous montrer quelques résultats utiles de 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}")