Исходники: https://github.com/facebookresearch/hydra
Документация: https://hydra.cc/docs/intro/

Hydra это фреймворк для управления файлами конфигурации, заточенный под ML-проекты.

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

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

  • Вы можете захотеть попробовать различные библиотеки, для которых потребуются разные настройки. Как вариант, их можно поместить в один конфиг-файл, но это сделает его трудно читаемым. Или вы можете разнести их на отдельные конфиги, с дублированием общей части, что вскоре станет не удобно поддерживать. Помимо этого вам потребуется как-то разделить их вызов в коде.

Все эти проблемы (и даже больше) позволяет решить Hydra. Ее ключевые особенности:

  • Иерархическая конфигурация

  • Автоматическое логирование

  • Переопределение параметров из командной строки

  • Запуск множества заданий с помощью одной команды

  • И пр.

А теперь посмотрим как все это работает. Но начнем как обычно с установки…

Установка

Тут все просто. Выполняем в командной строке:

pip install hydra-core --upgrade

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

Начало

Сначала самым простым образом задействуем гидру.
Создадим ML-проект с такой структурой:

config
- config.yaml
app.py
df.csv

Содержимое файлов:

#config.yaml
model: lgbm
params:
 learning_rate: 0.05
 n_estimators: 100
 max_depth: 9

Здесь мы указываем модель, для которой этот конфиг, и задаем параметры ее обучения.

#app.py
import hydra
import pandas as pd
from omegaconf import OmegaConf, DictConfig
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from lightgbm import LGBMRegressor
import logging
log = logging.getLogger(__name__)


@hydra.main(version_base=None, config_path='config', config_name='config')
def my_app(cfg: DictConfig):
    print(OmegaConf.to_yaml(cfg))
    
    # Загружаем данные
    df = pd.read_csv('df.csv')
    X = df.drop(columns='target')
    y = df['target']

    # Формируем трейн/тест
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.30, random_state=42)

    # Обучение модели
    model = LGBMRegressor()
    lgbm_params = OmegaConf.to_container(cfg['params'])
    model.set_params(**lgbm_params)
    model.fit(X_train, y_train)
    
    # Оценка
    y_pred = model.predict(X_test)
    mse = round(mean_squared_error(y_test, y_pred), 2)
    log.info(f'MSE: {mse}')

if __name__ == "__main__":
    my_app()

Что мы тут делаем:

  • Посредством декоратора @hydra.main подгружаем конфиги из указанной папки.

  • Загружаем датасет и делим его на трейн и тест.

  • Обучаем модель на параметрах, указанных в конфиге. При этом мы конвертируем исходный конфиг DictConfig в стандартный Dict, поскольку именно его принимает метод set_params.

  • Делаем предсказание и подсчитываем скор. Обратите внимание, что вывод информации осуществляется через модуль logger − это важно − только данные выведенные через logger будут логироваться гидрой (увидим дальше). Обычный print останется только в консоли.

З.Ы. Вы можете использовать одни параметры конфига как значения для других:

server:
  ip: "127.0.0.1"
  port: "8080"
  address: "${server.ip}:${server.port}"

Дальше мы будем модифицировать части этого кода, исследуя разные возможности гидры...

Конфигурация загружается как объект DictConfig, который похож на стандартный словарь питона, но предоставляет несколько дополнительных функций (например, вложенность). Подробнее о возможносях DictConfig можно прочитать в официальной документации: https://omegaconf.readthedocs.io/en/latest/usage.html#access-and-manipulation

Логирование

Запустим приложение:

> python3 app.py
model: lgbm
params:
  learning_rate: 0.05
  n_estimators: 100
  max_depth: 9

[2022-10-31 19:53:29,960][main][INFO] - MSE: 2948.27

Сразу после запуска появится папка outputs, в которой hydra будет логировать все запуски. Каждый запуск сохраняется в отдельной папке: outputs/YYYY-mm-dd/HH-MM-SS

Теперь пойдем поменяем в конфиге какой-нибудь гиперпараметр и заново запустим программу. Вуаля − у нас новая папка со всеми логами. 

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

Вы можете при запуске изменить папку выгрузки логов:

> python3 app.py hydra.run.dir=my_folder

Что содержат эти логи:

  • config.yaml — копия файла конфигурации, переданного через декоратор в функцию.

  • hydra.yaml — копия служебного конфигурационного файла Hydra.

  • overrides.yaml — копия параметров, заданных через командную строку и которые изменяют одно из значений из конфиг файла (посмотрим как это делается далее).

  • <file_name>.log — здесь будут храниться данные переданные через модуль logging.

По умолчанию в лог попадают категории info и warning, но посредством параметров можно логировать и debug:

> python3 app.py hydra.verbose=true

Переопределение

При запуске мы можем переопределить значения конфига в командной строке:

> python3 app.py params.max_depth=11
model: lgbm
params:
  learning_rate: 0.05
  n_estimators: 100
  max_depth: 11

[2022-10-31 21:40:13,889][main][INFO] - MSE: 2943.45

Переопределенные таким образом параметры будут сохранены в отдельном логе - overrides.yaml.

Помимо этого вы можете добавить новые параметры, которых нет в конфиге, или удалить существующие:

> python3 app.py +params.reg_alpha=0.5
> python3 app.py ~params.n_estimators

Группы конфигураций

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

Изменим структуру проекта, добавим в папку config подпапку для управления параметрами датасета:

config
- dataset
  - ds1.yaml
  - ds2.yaml
- config.yaml
app.py
df.csv

config.yaml останется прежнем, а вот два новых YAML файла содержит следующую информацию:

#ds1.yaml
name: ds1
columns: [age,bp,s1,s2,s5,s6]
target: target
#ds2.yaml
name: ds2
columns: [sex,bmi,s3,s4]
target: target

Также изменим способ загрузки датасета, чтобы брал информацию о колонках из конфига:

# Загружаем данные
df = pd.read_csv('df.csv')
X = df[cfg['dataset']['columns']]
y = df[cfg['dataset']['target']]

К значениям конфига можно обращаться двумя способами:

cfg.dataset.image.channels

cfg['dataset']['classes']

Далее при запуске нам нужно указать какой конфиг использовать:

> python3 app.py dataset=ds1
dataset:
  name: ds1
  columns:
  - age
  - bp
  - s1
  - s2
  - s5
  - s6
  target: target
model: lgbm
params:
  learning_rate: 0.05
  n_estimators: 100
  max_depth: 9

[2022-10-31 22:15:28,575][main][INFO] - MSE: 3580.19

Как вы можете видеть, хотя наши конфиги разделены, в приложение приходит один целостный конфиг.

Но каждый раз запускать может быть неудобно, поэтому вы можете в основном конфиге определить дефолтные модули:

#config.yaml
defaults:
 - dataset: ds1

model: lgbm
params:
  learning_rate: 0.05
  n_estimators: 100
  max_depth: 9

Аналогичным образом мы можем определить настройки для других типовых “модулей”, для машинного обучения:

  • Разные архитектуры моделей (AlexNet, ResNet50).

  • Разные наборы данных.

  • Разные оптимизаторы.

  • Разные планировщики.

  • Разные способы заполнения пропусков.

  • Разные функции потерь.

  • И куча всего другого…

Multirun

Допустим мы таким образом наизобретали кучу “модулей”. Запускать каждый из них по  отдельности, перебирая всевозможные варианты − утомительно. Поэтому гидра может это делать в автоматическом режиме с помощью функции multirun:

> python3 app.py -m dataset=ds1,ds2
[2022-10-31 22:48:22,886][HYDRA] Launching 2 jobs locally
[2022-10-31 22:48:22,886][HYDRA]        #0 : dataset=ds1
...
[2022-10-31 22:48:25,233][main][INFO] - MSE: 3580.19
[2022-10-31 22:48:25,234][HYDRA]        #1 : dataset=ds2
...
[2022-10-31 22:48:25,656][main][INFO] - MSE: 3414.61

Здесь мы попросили перебрать два конфига для датасета. Каждый из них будет объединен с базовым конфигом и передан на вход приложению. Если бы у нас были еще конфиги, например для моделей, то гидра бы сформировала и выполнила все их возможные комбинации.

В отличии от одиночного запуска при мульти-запуске логи сохраняются в папке multirun, в которой помимо даты и времени также создается отдельная подпапка (с порядковым номером) для каждого отдельного задания.

Базовая функция multirun запускает задания локально и последовательно, но используя специальные плагины вы можете запускать код параллельно или даже удаленно на нескольких узлах:

Инициализация объектов

Усложним задачу. Допустим мы хотим попробовать различные модели. Но мы не хотим это как-то обыгрывать в коде − мы просто хотим задавать необходимый класс в конфиге. Добавим в нашу структуру новую папку model и два конфига под две модели:

config
- dataset
  - ds1.yaml
  - ds2.yaml
- model
  - catboost.yaml
  - lgbm.yaml
- config.yaml
app.py
df.csv

Содержимое новых YAML-файлов:

#catboost.yaml
_target_: catboost.CatBoostRegressor
learning_rate: 0.05
n_estimators: 100
max_depth: 9
silent: True
#lgbm.yaml
_target_: lightgbm.LGBMRegressor
learning_rate: 0.05
n_estimators: 100
max_depth: 9
verbose: -1

Обратите внимание на специальный ключ _target_ в котором указываем класс, который хотим задействовать для создания модели (при этом отдельно импортировать этот класс в приложении не нужно). Все остальные значения, указанные на том же уровне, будут использованы как параметры при инициализации класса.

Поменяем код обучения модели:

...
from hydra.utils import instantiate
...

    # Обучение модели
    model = instantiate(cfg['model'])
    model.fit(X_train, y_train)

...

Здесь мы использовали специальную функцию hydra.utils.instantiate, через которую создали экземпляр модели, заданный в конфиге. В остальном код остался прежнем.

Укажем дефолтную модель в базовом конфиге и уберем оттуда все предыдущие настройки модели:

defaults:
 - dataset: ds1
 - model: lgbm

Запускаем:

> python3 app.py
dataset:
  name: ds1
  columns:
  - age
  - bp
  - s1
  - s2
  - s5
  - s6
  target: target
model:
  target: lightgbm.LGBMRegressor
  learning_rate: 0.05
  n_estimators: 100
  max_depth: 9

[2022-11-01 11:30:49,648][main][INFO] - MSE: 3580.19

Тут гидра взяла дефолтные конфиги. И попробуем запустить мультиран на всем, что у нас есть:

> python3 app.py -m dataset=ds1,ds2 model=catboost,lgbm
[2022-11-01 12:33:16,334][HYDRA] Launching 4 jobs locally
[2022-11-01 12:33:16,334][HYDRA]        #0 : dataset=ds1 model=catboost
...
[2022-11-01 12:33:17,089][main][INFO] - MSE: 3295.95
[2022-11-01 12:33:17,090][HYDRA]        #1 : dataset=ds1 model=lgbm
...
[2022-11-01 12:33:17,652][main][INFO] - MSE: 3580.19
[2022-11-01 12:33:17,653][HYDRA]        #2 : dataset=ds2 model=catboost
...
[2022-11-01 12:33:17,863][main][INFO] - MSE: 3330.02
[2022-11-01 12:33:17,864][HYDRA]        #3 : dataset=ds2 model=lgbm
...
[2022-11-01 12:33:18,124][main][INFO] - MSE: 3414.61

Как видите, гидра перебрала все возможные комбинации.

Jupyter Notebook

Иногда нам может понадобиться загрузить наш конф-файл отдельно, а не передавать в основную функцию программы. Например, в ячейке Jupyter Notebook. Делается это посредством Compose API − альтернативного способа загрузки файлов конфигурации. Делается это примерно так:

from hydra import initialize, compose
from omegaconf import OmegaConf

with initialize(version_base=None, config_path="cloud_app/conf"):
    cfg = compose(overrides=["+db=mysql"])
    print(cfg)

Более подробно читайте в документации: https://hydra.cc/docs/advanced/compose_api/

Прочее

В этой статье содержится лишь часть возможностей гидры. С остальными вы сможете ознакомится в документации. Например:

---------------

Код из статьи: https://github.com/slivka83/article/tree/main/hydra

Мой телеграм-канал

Комментарии (0)