Treinamento baseado na população em Ray Tune

Nov 28 2022
O ajuste de hiperparâmetros é uma etapa fundamental na seleção do modelo. Hiperparâmetros são como configurações, se você não manipulá-los adequadamente, pode ter um impacto ruim nos resultados do modelo.

O ajuste de hiperparâmetros é uma etapa fundamental na seleção do modelo. Hiperparâmetros são como configurações, se você não manipulá-los adequadamente, pode ter um impacto ruim nos resultados do modelo. O ajuste pode ser feito manualmente ou automaticamente. No mundo de hoje, devido aos recursos computacionais, um grande número de hiperparâmetros, uma grande variedade de algoritmos e bibliotecas auxiliares como o Ray, a maneira preferida é ajustar automaticamente os hiperparâmetros.

Neste artigo, falaremos sobre o treinamento baseado em população, exploraremos o Ray Tune e veremos um exemplo de ajuste de hiperparâmetros. Repositório do GitHub:https://github.com/lukakap/pbt-tune.git

Foto de Gary Bendig no Unsplash

O que significa PBT

Como já mencionamos, o bom desempenho do modelo está relacionado à seleção correta dos hiperparâmetros. O treinamento baseado em população é uma das maneiras charmosas de escolher hiperparâmetros. Consiste em duas partes: pesquisa aleatória e escolha inteligente. Na etapa de busca aleatória, o algoritmo escolhe várias combinações de hiperparâmetros aleatoriamente. Há uma grande chance de que a maioria das combinações tenha pontuações de baixo desempenho e uma pequena porção de combinações, ao contrário, tenha pontuações de desempenho melhores/boas. Aí vem a escolha inteligente. A segunda etapa é em um ciclo até atingirmos o resultado desejado ou não esgotarmos o número de iterações. A etapa de escolha inteligente contém dois métodos principais: explorar e explorar . Explorar— substituir a combinação dos hiperparâmetros por outros mais promissores, com base na métrica de desempenho. Explorar — perturbe aleatoriamente os hiperparâmetros (na maioria dos casos é multiplicado por algum fator) para adicionar ruído.

O treinamento baseado em população permite fazer duas coisas significativas juntas: paralelizar o treinamento de combinações de hiperparâmetros, estudar o restante da população e obter resultados promissores prontamente.

Fale sobre Ray Tune

Ray Tune é uma biblioteca python baseada em Ray para ajuste de hiperparâmetros com os algoritmos mais recentes, como PBT. Trabalharemos no Ray versão 2.1.0. As alterações podem ser vistas nas notas de lançamento abaixo. Também mencionaremos mudanças importantes no caminho.

Antes de passar aos exemplos práticos, vamos rever alguns conceitos básicos. Treinável — é um objetivo que ajuda os algoritmos a avaliar as configurações. Pode ter API de classe ou API de função, mas de acordo com a documentação do ray tune, a API de função é recomendada. Search Spaces — intervalos de valores para hiperparâmetros. Trials — O Tuner irá gerar várias configurações e executar processos nelas, então o processo executado em uma configuração é chamado de Trial. Algoritmo de pesquisa — sugere configurações de hiperparâmetros; por padrão, o Tune usa pesquisa aleatória como algoritmo de pesquisa. Agendador — Com base nos resultados relatados durante o processo de treinamento, os agendadores decidem se param ou continuam. O próximo conceito significativo éponto de verificação . Checkpointing significa salvar resultados intermediários, necessários para retomar e depois continuar o treinamento.

Na maioria dos casos, o Algoritmo de Pesquisa e o Agendador podem ser usados ​​juntos no processo de ajuste, mas há exceções. Um dos casos, quando não são usados ​​em conjunto, é o treinamento baseado na população. Nos documentos do Ray Tune, o PBT está na parte dos agendadores, mas são os dois ao mesmo tempo. É um escalonador porque interrompe as tentativas com base nos resultados e é um buscador porque tem a lógica de criar uma nova configuração.

Usamos o conhecido The Boston Housing Dataset (https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html). Podemos importar este conjunto de dados do sklearn.

Uma mudança significativa no Ray Tune foi a API de execução. tune.run() mudou para Tuner().fit. Antes da atualização, passávamos os parâmetros separadamente, mas na nova versão foram introduzidas classes de configuração que simplificam muitas coisas. Em primeiro lugar, agrupamos os parâmetros relacionados, o que torna o código de execução mais fácil de ler e entender. E segundo, quando você usa o Ray Tune em um projeto, algumas configurações são as mesmas para alguns algoritmos, então você pode criar um objeto de classe de configuração comum e mover algoritmos, o que torna a vida mais fácil.

importa

# 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

Vamos começar com treinável. Como já mencionamos, existem duas APIs treináveis: baseadas em função e baseadas em classe. Escreveremos treináveis ​​com API de classe.

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

Na configuração , precisamos ter x_train e y_train para estimar a eficiência do modelo de teste em etapas futuras. Obviamente, setup é a função da classe pai (tune.Trainable), mas nos dá a possibilidade de adicionar argumentos adicionais. Além disso, precisamos inicializar o regressor/modelo lgbm no arquivo setup . Vamos treinar novamente nosso modelo a cada iteração, mas na primeira queremos apenas ajustar o modelo, portanto, precisamos contar em qual iteração estamos. Nada mais neste ponto.

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

Comece com save_checkpoint . Usaremos a biblioteca joblib para salvar e restaurar o modelo. O que precisamos salvar? Primeiro de tudo - modelo, já que sempre precisamos do modelo de iteração anterior (init_model) para a próxima iteração, também podemos salvar o número da iteração atual e a pontuação da etapa atual.

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)

No processo de treinamento, temos tantos Treináveis ​​quanto amostras de configuração. Cada Treinável precisa de vários segundos para começar. Com o recurso reuse_actor, podemos reutilizar o Treinável já iniciado para novas configurações/hiperparâmetros múltiplos. Portanto, precisaremos de menos Treinável e o tempo gasto na inicialização também será menor.

Vamos implementar reset_config , que entrega novos hiperparâmetros. Em reset_config todas as variáveis ​​precisam ser ajustadas para novos hiperparâmetros, é como uma nova configuração . Há uma questão complicada, toda vez que configurações diferentes trocam o mesmo Trainable, elas iniciam o processo do zero, devido ao fato de que em reset_config escrevemos como no início? Na verdade, não, porque após reset_config , as chamadas Trainable carregam o ponto de verificação, se houver, portanto, o treinamento continuará a partir da última parada/ponto de verificação.

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

Agora podemos criar algumas configurações e executar o experimento Tune. Tuner tem 4 parâmetros: trainable , param_space , tune_config e run_config . Treinável já está implementado. Vamos definir param_space.

Param_space é o mesmo que o espaço de pesquisa já mencionado. Primeiro, precisamos definir uma lista de parâmetros que vamos ajustar. Para simplificar, escolha 3 parâmetros: learning_rate, num_leaves, max_depth.

O Tune possui uma API de espaço de pesquisa própria, portanto, devemos usá-los quando definimos os espaços de pesquisa. Os nomes dos espaços de busca são intuitivos, então vamos ver o resultado sem mais delongas.

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

O primeiro parâmetro do cronograma de treinamento de base populacional é time_attr. É o atributo do resultado do treino para comparação, que deveria ser algo que cresce monotonicamente. Escolhemos trainig_iteration como um atributo de tempo, então quando mencionamos time_attr em qualquer lugar, isso significa training_iteration. perturbation_interval — com que frequência a perturbação deve ocorrer. Se fizermos perturbações com frequência, também precisamos salvar os pontos de verificação com frequência. Aqui, vamos escolher perturbation_interval como 4. burn_in_period — a perturbação não acontecerá antes que este número de intervalos (iteração) tenha passado. Não será verdade se clonarmos o estado dos melhores desempenhos para modelos de baixo desempenho desde o início, pois as pontuações de desempenho são instáveis ​​nos estágios iniciais. Portanto, dê 10 iterações de tentativas de burn_in_period e, em seguida, inicie a perturbação. hyperparam_mutations é um dict de hiperparâmetros e seus espaços de busca, que podem ser perturbados. Queremos perturbar todos os hiperparâmetros doparam_space dict, então hyperparam_mutations será o mesmo que param_space [“params”]. Não passaremos argumentos de modo e métrica em PopulationBasedTraining, pois os definimos em 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()

Agora, podemos interagir com os resultados usando o objeto ResultGrid (análise). Usando get_best_result , podemos obter o melhor resultado de todas as tentativas. Também mostrarei alguns resultados úteis do 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}")