Исследования: инструмент для множественных экспериментов машинного обучения

Процесс работы над проектом в области науки о данных можно разделить на три основных этапа:

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

Инструменты для разных шагов зависят от задачи. Например, в задаче анализа сейсмических данных это может быть segyio для загрузки данных, numpy и pandas для манипуляций с данными. Модели можно обучать с помощью pytorch или tensorflow. На этапе производства полученную модель можно обернуть в веб-приложение с помощью Docker и веб-фреймворков.

Что касается второго шага, вы можете использовать DVC для описания и оценки отдельного эксперимента, MLFlow и TensorBoard для отслеживания и сбора результатов. Однако иногда план экспериментов становится сложным и обширным. Например, когда необходимо попробовать разные архитектуры нейронных сетей, для каждой попробуйте свои гиперпараметры, а также процедуры предварительной обработки данных. Тогда возникает вопрос, как реализовать генерацию конфигураций в более сложных случаях, чем можно охватить, например, GridSearchCV из sklearn? Насколько легко реализовать автоматическое (желательно параллельное) выполнение экспериментов с различными конфигурациями параметров?

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

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

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

Базовый пример

Рассмотрим простейший эксперимент: вызовите функцию power и сохраните ее результат:

Вообще говоря, прогон будет перебирать все возможные конфигурации параметров из домена (здесь он пуст, поэтому есть только одна пустая конфигурация). Затем для каждой конфигурации он выполнит несколько итераций эксперимента (по умолчанию только один) и сохранит результат. По умолчанию Research создает папку и сохраняет там свои результаты, но мы указываем dump_results=False для сохранения результатов в ОЗУ.

Результаты можно увидеть в специальной таблице даже во время проведения исследования. Они хранятся в research.results, который можно преобразовать в pandas.DataFrame, вызвав свойство research.results.df:

Здесь вы можете увидеть уникальный идентификатор эксперимента, имя сохраненной переменной, ее значение и итерацию эксперимента, когда значение было получено. Теперь у нас есть только один эксперимент с одной итерацией (итерации мы обсудим позже). Функция power имеет параметры по умолчанию a=2 и b=3, поэтому она выполнялась с ними.

Теперь усложним эксперимент. Аргументы ключевого слова можно указать в add_callable методе:

Позиционные аргументы могут быть определены args переменной add_callable:

Вы также можете добавить вызовы в исследования следующим образом:

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

Результат всех исследований будет одинаковым: значение power в результатах будет равно 9.

Итак, мы знаем, что Research может что-то выполнять, но все еще не ясно, каковы преимущества этого использования. Далее мы раскроем их по очереди.

Гибкий способ определения области параметров

Первая прибыль - это класс Domain, который предназначен для определения сложных областей параметров эксперимента. В описываемом эксперименте у нас есть два параметра: a и b. Допустим, мы хотим провести эксперимент для всех возможных комбинаций параметров a и b, которые определены списками [2, 3] и [2, 3, 4] соответственно.

EC (сокращение от конфигурации эксперимента) - это именованное выражение для обозначения элементов конфигурации, которые будут назначены эксперименту. В общем, именованное выражение - это способ ссылки на объекты, которые не существуют на момент определения. Таким образом, EC('key') обозначает элемент конфигурации эксперимента, EC() без аргументов обозначает всю конфигурацию эксперимента.

Наиболее распространенным именованным выражением является E, которое позволяет получить Experiment экземпляр, тем самым получив доступ ко всем атрибутам текущего эксперимента. Например, EC() - это псевдоним для E().config.

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

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

Но в этом пока нет ничего особенного; это типичный поиск по сетке.

В дополнение к умножению в домене есть несколько операций, которые позволяют создавать сложные сетки параметров. Например, операция + означает объединение доменов.

См. Изменения параметров add_callable. Здесь мы помещаем в power всю конфигурацию эксперимента как набор аргументов ключевого слова. Например, мы вызовем точноpower(a=2) для конфигурации {'a': 2} из домена.

Первый домен будет генерировать конфиги{'a': 2}, {'a': 3}, а второй {'b': 2}, _50 _, _ 51_, поэтому домен генерирует пять конфигов.

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

Domain также имеет оператор @ для скалярного (поэлементного) произведения для списков значений одинаковой длины. Например, Domain({'a': [2, 3]}) @ Domain({'b': [3, 4]}) выдаст две конфигурации: {'a': 2, 'b': 3}, {'a': 3, 'b': 4}.

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

  • вы можете запускать эксперименты с одной и той же конфигурацией несколько раз (может пригодиться, если ваш эксперимент недетерминированный, например, обучение нейронной сети)
  • случайный выбор конфигураций из Domain
  • используйте семплеры BatchFlow вместо списков в Domain (что чрезвычайно полезно для работы с непрерывными параметрами)
  • изменять домен во время выполнения исследования на основе результатов
  • однопараметрический Domain, например Domain({'a': [1, 2]}), может быть определен как Option('a', [1, 2]).

Все это описано в документации. Обратите внимание, что сам Domain может использоваться без Research.

Описание эксперимента

Давайте объясним, что именно мы имеем в виду, когда говорим об эксперименте. Это повторяющийся процесс вызова некоторых действий в зависимости от некоторых параметров. Более того, некоторые методы могут выполняться только на нескольких итерациях. Хорошим примером эксперимента может быть обучение модели с периодической проверкой (скажем, каждую эпоху). В этом случае итерации соответствуют шагам обучения, а действия - шагам обучения и вычислению показателей тестирования. Сформулированный таким образом эксперимент может возникнуть не только в области науки о данных.

Прежде чем мы покажем реальный пример с ML, давайте посмотрим на еще один игрушечный эксперимент с генератором итераций.

На первой итерации функция sequence создаст генератор с аргументами, указанными в add_generator. На каждой итерации генератор создает новый элемент, который будет использоваться методом power. Этот элемент хранится до тех пор, пока не будет создан следующий, поэтому мы можем поместить его в power, используя O('sequence') (просто напоминание об именованных выражениях: это то же самое, что и E(‘sequence’).output).

Количество итераций для каждого эксперимента указывается в run вызове. По умолчанию он равен 1, если исследование содержит только вызываемые объекты, и None, если оно имеет генераторы. None интерпретируется как бесконечность, и эксперимент будет продолжаться до тех пор, пока генератор в исследовании не будет исчерпан. Здесь продолжительность эксперимента зависит от length.

Мы не используем параметр save_to с sequence, поэтому его вывод не будет сохранен в результатах. В результате у нас будет следующий фрейм данных.

Здесь id - идентификатор эксперимента, столбцы start, b и length - для параметров из домена, power - столбец с сохраненным значением (которое мы определяем как save_to).

Обратите внимание, что здесь мы видим разное количество итераций экспериментов. Дело в том, что генератор будет исчерпан на второй или третьей итерации (зависит от length). По умолчанию все блоки будут выполнены на последней итерации, после чего эксперимент завершит свою работу. На последней итерации вывод генератора будет получен с предыдущей итерации.

Количество модулей (вызываемые объекты и генераторы, добавленные в Research, называются исполняемыми модулями) не ограничено. Порядок, в котором они добавляются, определяет порядок, в котором они выполняются на каждой итерации. Если количество итераций не указано, эксперимент будет выполняться до тех пор, пока не будет исчерпан один из генераторов в исследовании. Параметр when указывает количество итераций, когда должен выполняться модуль. По умолчанию все добавленные блоки выполняются на каждой итерации.

Здесь мы добавляем единицу к выводу power на последней итерации.

Параметр when указывает период выполнения (when=100 означает, что модуль будет выполняться каждые 100 итераций) или точные итерации ('%5' для пятой итерации). Также это может быть список таких значений.

У нас есть несколько строк для каждой итерации, но мы можем агрегировать результаты с помощьюresearch.results.to_df(pivot=False):

Теперь у нас есть столбцы name и value вместо нескольких столбцов для каждой переменной.

В дополнение к вызываемым объектам и генераторам вы можете добавлять в эксперимент классы.

Экземпляр класса MyCalc будет инициализирован в начале эксперимента с именем calc. Его атрибуты (вызываемые объекты и генераторы) могут использоваться с такими именами, как calc.power.

Параллельные эксперименты

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

Приблизительное время выполнения этой функции на моей машине без исследования составляет около 40 с. Мы дважды запустим этот вызываемый объект в исследовании, указав n_reps. Как мы уже говорили ранее, Research может запускать эксперименты с одним и тем же конфигом несколько раз. В нашем случае конфигурация пуста, но мы все еще можем использовать n_reps.

Время выполнения исследования 1мин 14с. Чтобы проводить эксперименты параллельно, просто определите workers в run методе.

Теперь у нас будет два параллельных рабочих, которые будут проводить эксперименты в рамках исследования. А полное время выполнения составляет всего 39,2 с, что вдвое быстрее.

Вы также можете указать дополнительные конфигурации воркеров, которые будут объединены с конфигурациями экспериментов из домена. Например, workers=[{'device': 0}, {'device': 1}] означает создание двух воркеров, и каждый воркер добавит в эксперимент свою собственную конфигурацию с индексом устройства GPU. Что касается графических процессоров, вы можете просто определить workers=2 и devices=[0, 1], и каждый рабочий начнет выполнение, установив переменную среды“CUDA_VISIBLE_DEVICES”, чтобы указать доступные графические процессоры (первый рабочий получит устройство с индексом 0, а второй с индексом 1).

Обработка результатов

До сих пор мы всегда проводили исследования с параметром dump_results=False. Если мы запустим исследование dump_results=True (по умолчанию), мы найдем в рабочем каталоге новую папку с именем research. Его структура выглядит так:

research
├── env
│   ├── commit.txt
│   ├── diff.txt
│   └── status.txt
├── experiments
│   └── 74a701b639792
│       ├── config
│       ├── experiment.log
│       └── results
│           └── power
│               └── 0
├── monitor.csv
├── research.dill
└── research.log

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

  • save_to параметр. Выходные данные модуля для текущей итерации будут сохранены в результатах с этим именем.
  • save метод исследования. Это более гибкий способ, потому что здесь вы можете сохранить не только вывод модуля, но и все, на что вы можете ссылаться с помощью именованных выражений, например, любые атрибуты добавленных экземпляров.
  • пользовательские вызовы. Атрибут full_path экземпляра Experiment (можно получить с помощью выражения EP()) - это относительный путь к папке с результатами эксперимента, которая будет использоваться в качестве вызываемого параметра.

Все результаты, сохраненные первыми двумя методами, будут помещены в подпапку results эксперимента. Во время эксперимента результаты выполнения накапливаются и сохраняются на диске в конце эксперимента. Свойство df и load метод research.results загрузит их все. Кроме того, методы load иto_df имеют параметры, и это способ фильтровать результаты и загружать только то, что требуется. Таким образом, вы можете загружать результаты для указанных итераций или конфигураций. Это может быть полезно, если результаты содержат тяжелые объекты, например, примеры прогнозов.

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

Остальное содержимое папки результатов требуется для загрузки объекта Research и получения информации о состоянии среды, в которой проводилось исследование.

  • В папке env хранится состояние репозитория git, соответствующего рабочему каталогу.
  • monitor.csv хранит информацию о выполнении
  • research.dill - сериализованный Research объект
  • research.log - это журнал всего исследования.

Кроме того, в каждой папке экспериментов есть свой config и собственный журнал.

Корни и ветви

Исследователи могут проводить эксперименты, описанные одним шаблоном, но с разными конфигурациями параметров параллельно. Однако иногда загрузка и обработка данных (как в случае с КТ-изображениями и сейсмическими данными) может занять больше времени, чем обучение самой модели. Если в экспериментах с разными конфигурациями есть общая деталь, ее можно вынести в отдельный блок и оценить один раз для нескольких экспериментов.

Будут выполнены два эксперимента следующим образом:

Результирующий фрейм данных будет следующим:

Как мы видим, статистика в value столбцах для разных экспериментов различается. Теперь давайте добавим root=True к load_data вызываемым и branches=2 к run:

В этом случае мы выполним load_data один раз для двух экспериментов, а затем его результат будет использован mean модулями в экспериментах (ветвях), которые будут выполняться в параллельных потоках.

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

Экземпляры, добавленные в исследование, также имеют параметр root и могут использоваться несколькими ветвями.

Заключение

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

  • добавить вызываемые объекты и генераторы в Research,
  • получить результат,
  • определить домены параметров,
  • проводить эксперименты параллельно,
  • сделать некоторые вызываемые объекты общими для нескольких экспериментов.

В следующих статьях мы более подробно расскажем, как использовать Research для проведения экспериментов с моделями.