DVC + Hydra: легко меняем и запускаем ML эксперименты

Введение

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

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

Примерный цикл работы представлен на рис. 1:

Рис. 1. Основной цикл работы (изображение можно увеличить).

Рис. 1. Основной цикл работы (изображение можно увеличить).

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

Остаётся вопрос как лучше всего хранить конфигурационные файлы и организовать запуск экспериментов.

Запуск экспериментов

Для запуска экспериментов можно написать свою логику, но на это надо потратить время и при необходимости использовать такой подход в разных проектах ещё нужно задуматься об универсальности. Ещё вариант использовать какие-то готовые инструменты из продвинутых MLOps решений. Обычно это нужно когда есть необходимость запуска распределённых экспериментов или нужна поддержка сложных сценариев.

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

  • Работа с хранением и версионированием данных.

  • Запуска конвейеров (pipeline).

  • Отслеживание метрик, параметров и их сравнение.

  • Управление и запуск экспериментов.

В рамках данной статьи основной интерес будут представлять возможности по управлению и запуском экспериментов. В DVC можно организовать эксперименты в виде очереди задач. Задача это запуск стадий (stages) из dvc.yaml с выполнением какого-то кода. Когда все задачи завершены остаётся выбрать лучший эксперимент и применить изменения к основной кодовой базе. При определённой организации проекта большую часть можно автоматизировать. Некоторые сведения по работе с экспериментами есть в статье.

Допустим есть параметр alpha у какой-то модели. Есть уже написанный код и определён этап (stage) train. Необходимо проверить train при разных значениях параметра.

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

Управление конфигурациями в проекте

Обычно конфигурационные файлы хранятся в виде набора файлов популярных форматов: json, yaml, toml, ini и т. п. В этих файлах хранятся какие-то настройки для всего проекта. В целом легче управлять одним файлом, а не их набором.

Набор требований к решению по управлению конфигурациями такие:

  • Все итоговые настройки иметь в одном файле.

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

  • Переиспользование некоторых значений внутри конфигурационного файла для сокращения дублирования.

Hydra как раз позволяет делать то что нужно. В DVC есть соответствующая интеграция.

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

├───configs
│   │   train.yaml
│   │
│   ├───dataset
│   │       bin_cls.yaml
│   │       bin_cls_noise.yaml
│   │
│   └───model
│           dec_tree.yaml
│           log_reg.yaml

В файле train.yaml указано какие части использовать:

...
defaults:
  - model: log_reg
  - dataset: bin_cls
  - _self_

...

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

model:
    ... # содержимое bin_cls.yaml
...

dataset:
    ... # содержимое log_reg.yaml

...

Hydra основана на OmegaConf, поэтому какие-то функции из OmegaConf будут работать также и в Hydra.

Есть возможность переиспользовать значения из разных частей.

Более подробно про другие аспекты.

Добавляя новые файлы в соответствующие группы можно в определённых рамках расширять проект без переписывания кода.

Практический пример

Весь пример представлен в репозитории на GitHub.

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

│   train.yaml
│
├───dataset
│       bin_cls.yaml
│       bin_cls_noise.yaml
│
└───model
        dec_tree.yaml
        log_reg.yaml

Есть два набора данных для бинарной классификации: базовый (bin_cls) и дополнительно с зашумлёнными метками классов (bin_cls_noise). Две модели: дерево принятия решений (dec_tree) и логистическая регрессия (log_reg). Основной конфигурационный файл: train.yaml.

Мы хотим проверить насколько хорошо работают разные модели при разных параметрах на наборе данных bin_cls или на bin_cls_noise. Можно выделить два варианта как это организовать в DVC:

  1. Использовать hydra composition.

  2. Не использовать hydra composition и добавить промежуточный этап, который создаёт итоговый конфигурационный файл.

У всех вариантов есть свои преимущества и недостатки. В любом случае легко можно добавить новую модель с учётом сохранения соглашении об интерфейсе

Hydra composition

При включении этого режима DVC перед запуском указанного этапа (stage) с помощью Hydra соберёт итоговый конфигурационный файл и сохранить его в файл params.yaml.

Плюсы:

  • Полный конфигурационный файл соберётся автоматически.

  • В dvc.yaml можно использовать значения всех параметров, которые есть в файле. Например:

stages:
  train:
    cmd: python ./train.py
    deps:
        - ${data_dir} # Из params.yaml
    outs:
        - ${exp_dir} # Из params.yaml
  • При изменении какого-то параметра полный конфигурационный файл автоматически обновится и DVC поймёт когда нужно перезапускать этап, если файл params.yaml указан как deps в dvc.yaml.

  • Можно использовать секцию params для указания того какие части конфигурационного файла являются параметрами.

  • При использовании dvc exp run -S ... с указанием перебора параметров файл params.yaml будет содержать актуальные значения.

Минусы:

  • Отсутствие полного контроля над тем как создаётся конфигурационный файл и разрешаются ссылки внутри него.

  • Некоторые значение невозможно задать заранее. Например, они определяются динамически на основе данных. В этом случае придётся заполнять значение ??? и в params.yaml это значение так и останется. Придётся обрабатывать в коде.

  • Если есть несколько этапов, то всем этапам придётся принимать полный конфигурационный файл и брать ту часть, которая нужна. В некоторых случах можно обойтись сменой значений в dvc.yaml.

Без Hydra composition c промежуточным этапом

Здесь необходимо написать дополнительный этап, которая эмулирует работу того что делает DVC с Hydra composition.

Примерный код такой:

Примерный код для получения полного конфигурационного файла

import hydra
from omegaconf import OmegaConf


@hydra.main(version_base="1.3", config_path="configs", config_name="main")
def main(config):
    ...
    OmegaConf.save(config, ..., resolve=True)

Далее в dvc.yaml итоговый файл указывается как зависимость и в секции vars, чтобы можно было использовать значения из него:

stages:
  train:
    vars:
      - путь_до_созданного_файла
    cmd: python ./train.py
    deps:
        - путь_до_созданного_файла

Основные плюсы такие же как с Hydra composition. Дополнительно появляется возможность определять какие-то динамические значения и влиять на формирование итогового конфигурационного файла.

Это можно комбинировать с использованием отдельных этапов без привязки к основному конфигурационному файлу. Например, dvc.yaml состоит из следующих этапов как на рис. 2.

Рис. 2. Некоторые стадии (изображение можно увеличить).

Рис. 2. Некоторые стадии (изображение можно увеличить).

Этапы A и B могут принимать свои конфигурационные файлы. Например, это какие-то общие для проекта действия. Этап C генерирует полный конфигурационный файл. Этап D это тот этап, который будет запускаться через dvc exp run ....

Минусы:

  • При запуске экспериментов нужно указывать какой файл менять при выполнении dvc exp run, иначе сформируется конфигурационный файл с одинаковыми значениями.

  • Если есть несколько этапов, то всем этапам придётся принимать полный конфигурационный файл и брать ту часть, которая нужна.

  • Возможно, dvc exp run придётся запускать с force-downstream, чтобы этап, отвечающая за создание конфигурационного файла всегда выполнялась и создавал актуальный конфигурационный файл.

В примере из репозитория использован вариант с Hydra composition так как хорошо подходит для начала.

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

dvc exp run --queue -S model=log_reg -S model.C=1.0e-2,100  train
dvc exp run --queue -S model=dec_tree -S model.max_depth=6,12  train

Вывод будет примерно таким:

Queueing with overrides '{'params.yaml': ['model=log_reg', 'model.C=0.01']}'.
Queued experiment 'lyric-feel' for future execution.
Queueing with overrides '{'params.yaml': ['model=log_reg', 'model.C=100']}'.
Queued experiment 'kooky-taka' for future execution.

Все эксперименты добавлены в очередь задач. Посмотреть статус можно через dvc queue status. Примерный вывод:

Task     Name        Created    Status
52dbbd4  lyric-feel  01:21 PM   Queued
6ef169b  kooky-taka  01:21 PM   Queued
610131c  meaty-name  01:21 PM   Queued
3e6aab7  famed-life  01:21 PM   Queued

Worker status: 0 active, 0 idle

На данный момент 0 активных исполнителей. Для запуска расчётов необходимо добавить исполнителей:

dvc queue start -j 2

Для ускорения расчётов добавим двух исполнителей. После этого вывод dvc queue status будет примерно такой:

Task     Name        Created    Status
52dbbd4  lyric-feel  01:21 PM   Running
6ef169b  kooky-taka  01:21 PM   Running
610131c  meaty-name  01:21 PM   Queued
3e6aab7  famed-life  01:21 PM   Queued

Worker status: 2 active, 0 idle

Когда успешно посчитаются все задачи статус будет Success. Если были какие-то ошибки в ходе вычисления, то будет соответствующий статус:

Task     Name        Created    Status
52dbbd4  lyric-feel  01:21 PM   Success
6ef169b  kooky-taka  01:21 PM   Success
610131c  meaty-name  01:21 PM   Success
3e6aab7  famed-life  01:21 PM   Success

Worker status: 0 active, 0 idle

При необходимости посмотреть логи достаточно выполнить dvc queue logs , где это уникальное имя задачи. Оно доступно в виде переменной окружения DVC_EXP_NAME. Иногда это полезно, если параллельно используется внешняя система для логирования.

Эксперименты можно сравнить между собой. В dvc.yaml была указана секция с метрикой-доля правильных ответов (accuracy). Она показывается при сравнении и является ориентиром какой эксперимент оказался лучше. Для быстрого отчёта можно выполнить команду:

dvc exp show --only-changed --sort-by accuracy --sort-order desc

При желании можно сохранить в других форматах. При сохранении в Markdown будет примерно таблица (не все столбцы отображены) как показана ниже:

Experiment

Created

accuracy

model._target_

model.penalty

model.solver

model.C

model.max_depth

workspace

-

0.87667

sklearn.linear_model.LogisticRegression

l1

liblinear

0.01

-

master

01:20 PM

0.92333

sklearn.linear_model.LogisticRegression

l1

liblinear

0.01

-

├── 998d475 [famed-life]

01:26 PM

0.97333

sklearn.tree.DecisionTreeClassifier

-

-

-

12

├── 266d93e [meaty-name]

01:26 PM

0.97

sklearn.tree.DecisionTreeClassifier

-

-

-

6

├── 7a03577 [lyric-feel]

01:26 PM

0.92333

sklearn.linear_model.LogisticRegression

l1

liblinear

0.01

-

└── c5f9a0b [kooky-taka]

01:26 PM

0.87

sklearn.linear_model.LogisticRegression

l1

liblinear

100

-

В ней выполненные эксперименты с сортировкой по значению accuracy, а также существующие ветки и текущая рабоча копия. При активном использовании VS Code можно установить расширение для DVC. Можно смотреть базовую информацию, но в интерфейсе.

Когда эксперимент выбран можно быстро его сохранить в виде отдельной ветки:

dvc exp branch wormy-yate branch-from-exp

После создания ветки можно посмотреть что новые параметры содержатся только в params.yaml и разница будет такой:

diff --git a/params.yaml b/params.yaml
index 36234f8..7218b6e 100644
--- a/params.yaml
+++ b/params.yaml
@@ -1,8 +1,6 @@
 model:
-  _target_: sklearn.linear_model.LogisticRegression
-  penalty: l1
-  solver: liblinear
-  C: 0.01
+  _target_: sklearn.tree.DecisionTreeClassifier
+  max_depth: 6
 dataset:
   n_samples: 1000
   n_features: 10

Значения в оригинальных конфигурационных файлах, в configs, остаются прежними. Это может приводить ошибкам, если заново собрать конфигурационный файл, а не использовать значения из params.yaml. Нужно следить за тем с какими файлами вызывается этап при повторном запуске. Каких-то простых способ автор не знает, кроме ручного обновления оригинальных файлов, чтобы всегда было актуальное состояние. В этом случае значения из configs можно рассматривать как стартовую точку откуда брать всю структуру, но модифицированные значения параметров уже хранятся в params.yaml.

Для удаления лишней информации необходимо выполнить:

dvc exp remove -A
dvc queue remove --all

Выводы

В данной статье был рассмотрен пример организации ML-проекта с использованием DVC и Hydra для быстрого изменения основных настроек и запуска локальных экспериментов с перебором различных параметров. Такой подход позволяет управлять изменениями и результатами экспериментов. Это делает процесс более организованным и структурированным, позволяя более просто фиксировать изменения в случае успеха.

Есть определённые проблемы с тем что нужно подстраиваться под логику работы DVC, с поддержкой актуального состояния всех конфигурационных файлов в проекте и производительность. Мы каждый раз запускаем отдельный процесс для проверки комбинации параметров. В некоторых случаях это может быть не очень эффективно. В других случаях издержки приемлемые. Например, для проектов с обучением нейронных сетей. Иногда нет возможности запускать сразу несколько параллельных обучений и естественным образом образуется очередь из задач. С помощью такого подхода можно запланировать обучение с проверкой разных идей и оставить всё считаться на ночь, а днём проверить какие результаты получилось и что осталось посчитать.

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

© Habrahabr.ru