Обучение населения в Ray Tune

Nov 28 2022
Настройка гиперпараметров является ключевым шагом в выборе модели. Гиперпараметры похожи на настройки, и если вы не обработаете их должным образом, это может плохо сказаться на результатах модели.

Настройка гиперпараметров является ключевым шагом в выборе модели. Гиперпараметры похожи на настройки, и если вы не обработаете их должным образом, это может плохо сказаться на результатах модели. Настройка может производиться вручную или автоматически. В современном мире из-за вычислительных возможностей, большого количества гиперпараметров, большого разнообразия алгоритмов и вспомогательных библиотек, таких как Ray, предпочтительным способом является автоматическая настройка гиперпараметров.

В этой статье мы поговорим об обучении на основе популяции, изучим Ray Tune и рассмотрим пример настройки гиперпараметров. Репозиторий GitHub:https://github.com/lukakap/pbt-tune.git

Фото Гэри Бендига на Unsplash

Что означает ПБТ

Как мы уже упоминали, хорошая производительность модели связана с правильным подбором гиперпараметров. Популяционное обучение — один из очаровательных способов выбора гиперпараметров. Он состоит из двух частей: случайный поиск и умный выбор. На этапе случайного поиска алгоритм случайным образом выбирает несколько комбинаций гиперпараметров. Существует высокая вероятность того, что большинство комбинаций будут иметь низкие показатели производительности, а небольшая часть комбинаций, наоборот, будет иметь лучшие/хорошие показатели производительности. Здесь приходит умный выбор. Второй шаг в цикле, пока мы не достигнем желаемого результата или не исчерпаем количество итераций. Шаг разумного выбора содержит два основных метода: использование и исследование . Эксплойт— заменить комбинацию гиперпараметров на более перспективные, исходя из метрики производительности. Исследовать — произвольно возмущать гиперпараметры (в большинстве случаев они умножаются на какой-то коэффициент), чтобы добавить шум.

Обучение на основе популяции позволяет делать две значимые вещи вместе: распараллелить обучение комбинациям гиперпараметров, учиться у остальной популяции и быстро получать многообещающие результаты.

Разговор о Рэе Тьюне

Ray Tune — это библиотека Python на основе Ray для настройки гиперпараметров с использованием новейших алгоритмов, таких как PBT. Мы будем работать над Ray версии 2.1.0. Изменения можно увидеть в примечаниях к выпуску ниже. Мы также упомянем важные изменения в пути.

Прежде чем перейти к практическим примерам, давайте рассмотрим некоторые основные понятия. Обучаемый — это цель, которая помогает алгоритмам оценивать конфигурации. Он может иметь Class API или Function API, но, согласно документации по настройке лучей, рекомендуется использовать Function API. Пространства поиска — диапазоны значений гиперпараметров. Пробные версии — Tuner создаст несколько конфигураций и запустит в них процессы, поэтому процесс, выполняемый в конфигурации, называется пробной версией. Алгоритм поиска — предлагает конфигурации гиперпараметров, по умолчанию Tune использует случайный поиск в качестве алгоритма поиска. Планировщик — на основе полученных результатов во время процесса обучения планировщики решают, следует ли остановиться или продолжить. Следующее значимое понятиеконтрольно-пропускной пункт . Контрольные точки означают сохранение промежуточных результатов, необходимых для возобновления, а затем продолжения обучения.

В большинстве случаев алгоритм поиска и планировщик могут использоваться вместе в процессе настройки, но есть исключение. Одним из случаев, когда они не используются вместе, является популяционное обучение. В документации Ray Tune PBT находится в части планировщика, но одновременно. Это планировщик, потому что он останавливает испытания на основе результатов, и это поисковик, поскольку у него есть логика для создания новой конфигурации.

Мы используем хорошо известный The Boston Housing Dataset (https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html). Мы можем импортировать этот набор данных из sklearn.

Одним из значительных изменений в Ray Tune стал API выполнения. tune.run() был заменен на Tuner().fit. До обновления мы передавали параметры отдельно, но в новой версии были введены конфиг-классы, которые многое упрощают. Во-первых, связанные параметры сгруппированы вместе, что упрощает чтение и понимание исполняемого кода. А во-вторых, когда вы используете Ray Tune в проекте, некоторые конфигурации для некоторых алгоритмов одинаковы, поэтому вы можете сделать один общий объект класса конфигурации и перемещаться по алгоритмам, что упрощает жизнь.

импорт

# 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

Начнем с обучаемого. Как мы уже упоминали, есть два обучаемых API: на основе функций и на основе классов. Мы напишем обучаемый с помощью Class API.

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

В настройке нам нужно иметь x_train и y_train, чтобы оценить эффективность пробной модели на следующих этапах. Конечно, setup — это функция родительского класса (tune.Trainable), но она дает нам возможность добавлять дополнительные аргументы. Кроме того, нам нужно инициализировать регрессор/модель lgbm в настройках . Мы собираемся переобучать нашу модель на каждой итерации, но на первой мы хотим просто подогнать модель, поэтому нам нужно рассчитывать, на какой итерации мы находимся. На данный момент больше ничего.

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

Начните с save_checkpoint . Мы будем использовать библиотеку joblib для сохранения и восстановления модели. Что нам нужно для спасения? Прежде всего — модель, так как нам всегда нужна предыдущая модель итерации (init_model) для следующей итерации, мы также можем сохранить текущий номер итерации и текущую оценку шага.

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)

В процессе обучения у нас столько же Trainables, сколько и примеров конфигурации. Каждому обучаемому требуется несколько секунд для запуска. С помощью функции reuse_actor мы можем повторно использовать уже запущенный Trainable для новых множественных конфигураций/гиперпараметров. Так нам понадобится меньше Trainable и времени на запуск тоже будет меньше.

Давайте реализуем reset_config , который предоставляет новые гиперпараметры. В reset_config каждая переменная должна быть настроена на новые гиперпараметры, это похоже на новую настройку . Есть один каверзный вопрос, каждый раз разные конфигурации свопают один и тот же Trainable, они процесс начинают с нуля, из-за того что в reset_config пишем как старт? На самом деле нет, потому что после reset_config Trainable вызывает контрольную точку загрузки, если она существует, следовательно, обучение продолжится с последней остановки/контрольной точки.

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

Теперь мы можем создать некоторые конфигурации и запустить эксперимент Tune. Tuner имеет 4 параметра: trainable , param_space , tune_config и run_config . Обучаемый уже реализован. Давайте определим param_space.

Param_space совпадает с уже упомянутым пространством поиска. Во-первых, нам нужно определить список параметров, которые мы собираемся настроить. Для упрощения выберите 3 параметра: learning_rate, num_leaves, max_depth.

У Tune есть собственный API пространства поиска, поэтому мы должны использовать его при определении пространств поиска. Название областей поиска интуитивно понятно, так что давайте без лишних слов посмотрим на результат.

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

Первым параметром расписания обучения базы населения является time_attr. Это атрибут результата обучения для сравнения, который должен монотонно возрастать. Мы выбираем training_iteration в качестве атрибута времени, поэтому, когда мы где-либо упоминаем time_attr, это означает training_iteration. perturbation_interval — как часто должно происходить возмущение. Если мы часто делаем возмущение, то нам нужно часто сохранять и контрольные точки. При этом выберем perturbation_interval равным 4. burn_in_period — возмущение не произойдет, пока не пройдет это количество интервалов (итераций). Будет неверным, если мы с самого начала клонируем состояние лучших исполнителей в малоэффективные модели, так как показатели производительности на ранних этапах нестабильны. Итак, дайте 10 итераций испытаний «burn_in_period», а затем начните возмущение. hyperparam_mutations — это набор гиперпараметров и их пространств поиска, которые можно изменять. Мы хотим возмущать все гиперпараметры изparam_space dict, поэтому hyperparam_mutations будет таким же, как param_space ["params"]. Мы не будем передавать аргументы режима и метрики в PopulationBasedTraining, поскольку мы определяем их в 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()

Теперь мы можем взаимодействовать с результатами, используя объект ResultGrid (анализ). Используя get_best_result , мы можем получить лучший результат из всех испытаний. Также я покажу вам несколько полезных результатов от 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}")