Capacitación basada en la población en Ray Tune
El ajuste de hiperparámetros es un paso clave en la selección del modelo. Los hiperparámetros son como configuraciones, si no los maneja adecuadamente, puede tener un impacto negativo en los resultados del modelo. La sintonización se puede realizar de forma manual o automática. En el mundo actual, debido a las capacidades computacionales, una gran cantidad de hiperparámetros, una gran variedad de algoritmos y bibliotecas auxiliares como Ray, la forma preferida es ajustar automáticamente los hiperparámetros.
En este artículo, hablaremos sobre el entrenamiento basado en la población, exploraremos Ray Tune y veremos un ejemplo de ajuste de hiperparámetros. Repositorio de GitHub:https://github.com/lukakap/pbt-tune.git
Qué significa PBT
Como ya hemos mencionado, el buen desempeño del modelo está relacionado con la correcta selección de los hiperparámetros. El entrenamiento basado en la población es una de las formas encantadoras de elegir hiperparámetros. Consta de dos partes: búsqueda aleatoria y elección inteligente. En el paso de búsqueda aleatoria, el algoritmo elige aleatoriamente varias combinaciones de hiperparámetros. Existe una alta probabilidad de que la mayoría de las combinaciones tengan puntuaciones de rendimiento bajas y una pequeña parte de las combinaciones, por el contrario, tengan puntuaciones de rendimiento mejores/buenas. Aquí viene la elección inteligente. El segundo paso es en un ciclo hasta que logremos el resultado deseado o no agotemos el número de iteraciones. El paso de elección inteligente contiene dos métodos principales: explotar y explorar . Explotar— reemplazar la combinación de los hiperparámetros por otros más prometedores, en función de la métrica de rendimiento. Explore : perturbe aleatoriamente los hiperparámetros (en la mayoría de los casos, se multiplica por algún factor) para agregar ruido.
El entrenamiento basado en la población permite hacer dos cosas significativas juntas: entrenar en paralelo las combinaciones de hiperparámetros, estudiar del resto de la población y obtener resultados prometedores rápidamente.
Hablar de Ray Tune
Ray Tune es una biblioteca de Python basada en Ray para el ajuste de hiperparámetros con los algoritmos más recientes, como PBT. Trabajaremos en Ray versión 2.1.0. Los cambios se pueden ver en las notas de la versión a continuación. También mencionaremos cambios importantes en la forma.
Antes de pasar a los ejemplos prácticos, repasemos algunos conceptos básicos. Entrenable : es un objetivo que ayuda a los algoritmos a evaluar configuraciones. Puede tener Class API o Function API, pero de acuerdo con la documentación de ray tune, se recomienda Function API. Espacios de búsqueda : rangos de valores para hiperparámetros. Pruebas : Tuner generará varias configuraciones y ejecutará procesos en ellas, por lo que el proceso ejecutado en una configuración se llama Prueba. Algoritmo de búsqueda : sugiere configuraciones de hiperparámetros, de forma predeterminada, Tune utiliza la búsqueda aleatoria como algoritmo de búsqueda. Programador : en función de los resultados informados durante el proceso de capacitación, los programadores deciden si detener o continuar. El siguiente concepto significativo espunto de control Checkpointing significa guardar resultados intermedios, necesarios para reanudar y luego continuar el entrenamiento.
En la mayoría de los casos, el algoritmo de búsqueda y el programador se pueden usar juntos en el proceso de ajuste, pero hay una excepción. Uno de los casos, cuando no se utilizan juntos, es el Entrenamiento Basado en la Población. En los documentos de Ray Tune, PBT está en la parte de los programadores, pero es ambos al mismo tiempo. Es un planificador porque detiene las pruebas en función de los resultados y es un buscador porque tiene la lógica para crear una nueva configuración.
Utilizamos el conocido conjunto de datos de vivienda de Boston (https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html). Podemos importar este conjunto de datos desde sklearn.
Un cambio significativo en Ray Tune fue la API de ejecución. tune.run() ha cambiado a Tuner().fit. Antes de la actualización, pasábamos los parámetros por separado, pero en la nueva versión, se introdujeron las clases de configuración, lo que simplifica muchas cosas. En primer lugar, agrupó los parámetros relacionados, lo que hace que el código de ejecución sea más fácil de leer y comprender. Y segundo, cuando usa Ray Tune en un proyecto, algunas configuraciones son las mismas para algunos algoritmos, por lo que puede crear un objeto de clase de configuración común y moverse entre los algoritmos, lo que facilita la vida.
importaciones
# 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
Comencemos con entrenable. Como ya mencionamos, hay dos API entrenables: basadas en funciones y basadas en clases. Escribiremos entrenable con Class API.
class TrainableForPBT(tune.Trainable):
def setup(self, config):
pass
def step(self):
pass
En la configuración , necesitamos tener x_train e y_train para estimar la eficiencia del modelo de prueba en pasos futuros. Por supuesto, la configuración es la función de la clase principal (tune.Trainable), pero nos da la posibilidad de agregar argumentos adicionales. Además, necesitamos inicializar el regresor/modelo lgbm en la configuración . Vamos a volver a entrenar nuestro modelo en cada iteración, pero en la primera solo queremos ajustar el modelo, por lo tanto, necesitamos saber en qué iteración estamos. Nada más en este punto.
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
Comience con save_checkpoint . Usaremos la biblioteca joblib para guardar y restaurar el modelo. ¿Qué necesitamos para ahorrar? En primer lugar, modelo, dado que siempre necesitamos el modelo de iteración anterior (init_model) para la próxima iteración, también podemos guardar el número de iteración actual y la puntuación de paso actual.
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)
En el proceso de entrenamiento tenemos tantos Trainables como muestras de configuración. Cada Entrenable necesita varios segundos para comenzar. Con la función reuse_actor, podemos reutilizar Trainable ya iniciado para nuevas configuraciones/hiperparámetros múltiples. Por lo tanto, necesitaremos menos Trainable y el tiempo dedicado a la puesta en marcha también será menor.
Implementemos reset_config , que ofrece nuevos hiperparámetros. En reset_config , cada variable debe ajustarse a nuevos hiperparámetros, es como una nueva configuración . Hay una pregunta difícil, cada vez que diferentes configuraciones intercambian el mismo Trainable, ¿comienzan el proceso desde cero, debido al hecho de que en reset_config lo escribimos como el inicio? En realidad, no, porque después de reset_config , las llamadas de Trainable cargan el punto de control, si existe, por lo tanto, el entrenamiento continuará desde la última parada/punto de control.
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
Ahora podemos crear algunas configuraciones y ejecutar el experimento Tune. Tuner tiene 4 parámetros: entrenable , param_space , tune_config y run_config . Trainable ya está implementado. Definamos param_space.
Param_space es el mismo que el espacio de búsqueda ya mencionado. Primero, necesitamos definir una lista de parámetros que vamos a ajustar. Para simplificar, elija 3 parámetros: tasa_de_aprendizaje, número_de_hojas, profundidad_máxima.
Tune tiene su propia API de espacio de búsqueda, por lo que deberíamos usarla cuando definimos espacios de búsqueda. El nombre de los espacios de búsqueda son intuitivos, así que veamos el resultado sin más preámbulos.
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)
},
}
El primer parámetro del programa de entrenamiento de base de población es time_attr. Es el atributo de resultado de entrenamiento para comparación, que debería ser algo que aumente monótonamente. Elegimos iteración_entrenamiento como un atributo de tiempo, por lo que cuando mencionamos atributo_tiempo en cualquier lugar, eso significa iteración_entrenamiento. perturbation_interval — con qué frecuencia debe ocurrir la perturbación. Si hacemos perturbaciones con frecuencia, también debemos guardar los puntos de control con frecuencia. Por lo tanto, elijamos que perturbation_interval sea 4. burn_in_period — la perturbación no ocurrirá antes de que haya pasado este número de intervalos (iteración). No será cierto si clonamos el estado de los modelos con mejor rendimiento en modelos con bajo rendimiento desde el principio, ya que las puntuaciones de rendimiento son inestables en las primeras etapas. Así que dé 10 iteraciones de ensayos burn_in_period y luego comience la perturbación. hyperparam_mutations es un dict de hiperparámetros y sus espacios de búsqueda, que pueden ser perturbados. Queremos perturbar todos los hiperparámetros de laparam_space dict, por lo que hyperparam_mutations será lo mismo que param_space [“params”]. No pasaremos argumentos de modo y métrica en PopulationBasedTraining, tal como los definimos en 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()
Ahora, podemos interactuar con los resultados utilizando el objeto ResultGrid (análisis). Usando get_best_result podemos obtener el mejor resultado de todas las pruebas. También le mostraré algunos resultados útiles 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}")