Это мы юзаем: библиотека Optuna в Python для оптимизации гиперпараметров

d55c508f376810918db8f6888b9eef6d.jpg

Привет, Хабр!

Гиперпараметры — это параметры, которые не учатся в процессе обучения модели. Они задаются заранее. От выбора гиперпараметров напрямую зависит качество и эффективность модели, а их оптимизация может улучшить результаты предсказаний.

Традиционный подход к оптимизации гиперпараметров включает в себя grid search и random search, иногда они могут быть неэффективными и времязатратными, особенно когда пространство гиперпараметров велико.

Когда я впервые столкнулся с необходимостью настроить сотни параметров в своей нейросети, задача показалась мне Сизифовым трудом. Каждый параметр мог значительно изменить результат, и пространство поиска казалось бесконечным. И немного просидев на стековерфлой я нашел либу Optuna, которая позоволила оптимизировать этот процесс.

Optuna решает проблему оптимизации гиперпараметров, предоставляя легковесный фреймворк для автоматизации поиска оптимальных гиперпараметров. Она использует алгоритмы, такие как TPE, CMA-ES, и даже поддерживает пользовательские алгоритмы.

Optuna полностью написана на Python и имеет мало зависимостей. В этой статье рассмотрим её основной функционал.

Установим:

pip install optuna

Создание первого исследования и определение функции цели

Исследование в Optuna представляет собой оптимизационную сессию, в рамках которой происходит поиск оптимальных гиперпараметров.

Объект Study

Для начала работы с Optuna необходимо создать объект Study. Это можно сделать с помощью функции optuna.create_study(), которая имеет два основных параметра: study_name и direction.

study_name — это имя исследования, уникальный идентификатор, который можно использовать для ссылки на исследование в будущем.

direction указывает, что вы пытаетесь сделать: максимизировать "maximize" или минимизировать "minimize" целевую метрику.

Пример создания исследования для максимизации метрики:

import optuna

study = optuna.create_study(study_name="my_first_study", direction="maximize")

Функция цели

Функция цели — это основа процесса оптимизации в Optuna. Эта функция принимает объект trial в качестве аргумента и возвращает числовое значение метрики, которую нужно максимизировать или минимизировать. Optuna автоматически вызывает эту функцию, передавая ей различные комбинации гиперпараметров для поиска оптимального решения

Optuna имеет несколько методов suggest_* для определения диапазона значений каждого гиперпараметра, их нужно использовать внутри функции цели.

suggest_float используется для предложения float значений гиперпараметров в заданном диапазоне. Можно также указать шаг для дискретизации.

learning_rate = trial.suggest_float('learning_rate', 1e-5, 1e-1, log=True)

Ищем значение learning_rate в диапазоне от 1e-5 до 1e-1 с логарифмическим масштабом

suggest_categorical используется для выбора из списка категориальных значений, т.е строки, числа или другие неупорядоченные категории:

codeoptimizer = trial.suggest_categorical('optimizer', ['adam', 'sgd', 'rmsprop'])

Optuna выберет один из трех оптимизаторов для оценки.

Для целочисленных гиперпараметров используется suggest_int. Можно также использовать параметр step, если нужны целочисленные значения, изменяющиеся не на каждый следующий инкремент, а с определенным шагом.

codenum_layers = trial.suggest_int('num_layers', 1, 10)

Optuna будет исследовать количество слоев от 1 до 10 включительно.

suggest_loguniform используется для предложения гиперпараметра из логарифмического распределения. Метод подходит для гиперпараметров, которые изменяются в широком диапазоне и где меньшие значения являются более чувствительными.

learning_rate = trial.suggest_loguniform('learning_rate', 1e-5, 1e-2)

suggest_discrete_uniform предлагает вещественное число, которое изменяется на фиксированный шаг:

feature_fraction = trial.suggest_discrete_uniform('feature_fraction', 0.4, 1.0, 0.1)

suggest_uniform предлагает вещественное число из равномерного распределения. Он похож на suggest_float, но всегда предполагает равномерное распределение без возможности логарифмического масштаба.

coef = trial.suggest_niform('coef', -1.0, 1.0)

Также можно юзать условную логику при определении гиперпараметров:

optimizer = trial.suggest_categorical('optimizer', ['adam', 'rmsprop', 'sgd'])

if optimizer == 'adam':
    learning_rate = trial.suggest_float('adam_lr', 1e-5, 1e-1, log=True)
elif optimizer == 'rmsprop':
    learning_rate = trial.suggest_float('rmsprop_lr', 1e-5, 1e-2, log=True)
else:
    learning_rate = trial.suggest_float('sgd_lr', 1e-4, 1e-1, log=True)

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

import numpy as np
from sklearn.datasets import make_regression
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# датасет
X, y = make_regression(n_samples=1000, n_features=20, noise=0.1)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2)

def objective(trial):
    # предложение гиперпараметров
    alpha = trial.suggest_float('alpha', 1e-10, 1.0, log=True)

    # создание и обучение модели
    model = Ridge(alpha=alpha)
    model.fit(X_train, y_train)
    
    # предсказание и вычисление MSE
    y_pred = model.predict(X_valid)
    mse = mean_squared_error(y_valid, y_pred)
    
    return mse

study.optimize(objective, n_trials=100)

Optuna исследует значения alpha в указанном диапазоне с логарифмическим масштабом log=True.

Алгоритмы оптимизации

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

import optuna

def objective(trial):
    x = trial.suggest_float('x', -10, 10)
    return (x - 2) ** 2

study = optuna.create_study(sampler=optuna.samplers.TPESampler())
study.optimize(objective, n_trials=100)

print(study.best_params)  # находит оптимальное значение 'x'

Мнимизируем квадратное отклонение от 2, используя TPE для предложения значений 'x'. TPE адаптируется после каждого испытания, быстро сужая поиск к оптимальному значению.

CMA-ES является стратегией эволюционной оптимизации, которая особенно хороша когда пространство параметров велико или когда отсутствует явная градиентная информация. CMA-ES адаптирует ковариационную матрицу на основе исторических данных о поиске, что позволяет ему масштабировать поиск по мере нахождения лучших решений:

def objective(trial):
    x = trial.suggest_float('x', -10, 10)
    y = trial.suggest_float('y', -10, 10)
    return (x - 2) ** 2 + (y + 3) ** 2

study = optuna.create_study(sampler=optuna.samplers.CmaEsSampler())
study.optimize(objective, n_trials=100)

print(study.best_params)  # находит оптимальные значения 'x' и 'y'

Минимизируем функцию двух переменных, используя CMA-ES для предложения значений 'x' и 'y'. CMA-ES эффективно исследует пространство, адаптируя стратегию поиска на основе предыдущих результатов.

Многокритериальная оптимизация

В Optuna для решения задач многокритериальной оптимизации используется класс optuna.study.create_study с аргументом directions, который принимает список, указывающий направление оптимизации minimize или maximize для каждой из целей. Затем, функция цели должна возвращать кортеж значений, соответствующих каждой из оптимизируемых метрик.

Допустим, хочется оптимизировать классификатор так, чтобы максимизировать точность на валидационном наборе данных и одновременно минимизировать количество используемых признаков для предотвращения переобучения:

import optuna
import sklearn.datasets
import sklearn.linear_model
import sklearn.model_selection

def objective(trial):
    iris = sklearn.datasets.load_iris()
    x, y = iris.data, iris.target

    # выбор гиперпараметров
    alpha = trial.suggest_float('alpha', 1e-5, 1e-1, log=True)
    ratio = trial.suggest_float('l1_ratio', 0, 1)

    # модель
    classifier = sklearn.linear_model.ElasticNet(alpha=alpha, l1_ratio=ratio)
    
    # разделение данных
    x_train, x_valid, y_train, y_valid = sklearn.model_selection.train_test_split(x, y, test_size=0.25)
    
    # обучение модели
    classifier.fit(x_train, y_train)
    accuracy = sklearn.model_selection.cross_val_score(classifier, x_valid, y_valid, n_jobs=-1, cv=3).mean()
    
    # колво выбранных признаков (ненулевых коэффициентов)
    n_features = sum(coef != 0 for coef in classifier.coef_)

    return accuracy, n_features

study = optuna.create_study(directions=['maximize', 'minimize'])
study.optimize(objective, n_trials=100)

print("Number of finished trials: ", len(study.trials))
print("Best trial:")
trial = study.best_trial

print("  Values: ", trial.values)
print("  Params: ")
for key, value in trial.params.items():
    print(f"    {key}: {value}")

Используем классификатор ElasticNet из sklearn.linear_model, который сочетает в себе L1 и L2 регуляризацию. Оптимизируем два гиперпараметра: alpha и l1_ratio, и две цели: точность классификации accuracy и кол-во используемых признаков n_features.

Пользовательские атрибуты

Пользовательские атрибуты можно добавить к испытанию через метод set_user_attr объекта trial. Эти атрибуты сохраняются вместе с результатами испытания и могут быть извлечены для анализа в любой момент после завершения испытания:

import optuna

def objective(trial):
    x = trial.suggest_float("x", -10, 10)
    trial.set_user_attr("description", "This is a trial to optimize x.")
    trial.set_user_attr("iteration", 1)
    return (x - 2) ** 2

study = optuna.create_study()
study.optimize(objective, n_trials=10)

# извлечение и печать пользовательских атрибутов для всех испытаний
for trial in study.trials:
    print(f"Trial {trial.number} Attributes:", trial.user_attrs)

Добавляем два пользовательских атрибута: description, который описывает цель испытания, и iteration, который может отслеживать номер итерации в серии экспериментов.

После завершения оптимизации пользовательские атрибуты можно извлечь из объекта trial. Это делается через свойство user_attrs объекта trial, которое возвращает словарь атрибутов, добавленных во время испытания:

best_trial = study.best_trial
print("Best Trial Description:", best_trial.user_attrs["description"])
print("Best Trial Iteration:", best_trial.user_attrs["iteration"])

CLI

Optuna CLI предлагает несколько команд для работы с исследованиями.

Чтобы создать новое исследование через CLI есть optuna create-study. Можно указать имя исследования и направление оптимизации

optuna create-study --study-name "my_study" --direction "minimize"

После выполнения этой команды будет создано новое исследование с указанным именем и направлением оптимизации, и вы будет получено сообщение:

[I 2024-10-10 10:00:00,000] A new study created in RDB with name: my_study

Чтобы просмотреть список всех доступных исследований — optuna studies.

optuna studies

Выведет список всех исследований, доступных в текущей БД Optuna.

Чтобы добавить испытания в существующее исследование, можно использовать команду optuna ask-and-tell:

optuna ask --study-name "my_study"

Затем сообщите Optuna результаты этого испытания:

optuna tell --study-name "my_study" --trial-number 0 --values 0.5

Optuna CLI предлагает визуализацию результатов через команду optuna dashboard. Эта команда запускает локальный сервер с веб-интерфейсом, где и будут результаты:

optuna dashboard --study-name "my_study" --storage "sqlite:///example.db"

Более подробно с Optuna можно ознакомиться на их гитхабе.

А узнать практические инструменты аналитики можно в рамках онлайн-курсов от ведущих экспертов рынка. С полным каталогом курсов OTUS можно ознакомиться по ссылке.

© Habrahabr.ru