Хабрахабр

Организуем ML-проект с помощью Ocean

image

Вступление

При старте нового проекта эти полезные знания помогают увереннее начать исследование, переиспользовать полезные методы и получить первые результаты быстрее. За годы разработки ML- и DL-проектов у нашей студии накопились и большая кодовая база, и много опыта, и интересные инсайты и выводы.

Это позволит эффективнее обучить новых сотрудников, ввести их в курс дела и погрузить в проект. Очень важно, чтобы все эти материалы были не только в головах разработчиков, но и в читаемом виде на диске.

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

  • Каждый проект был организован по-разному, особенно если их инициировали разные люди.
  • Недостаточно отслеживали, что делает код, как его запустить и кто его автор.
  • Не использовали виртуализацию в должной степени, зачастую мешая своим коллегам установкой существующих библиотек другой версии.
  • Забывались выводы, сделанные по графикам, которые осели и умерли в горé jupyter-тетрадок.
  • Теряли отчеты по результатам и прогрессу в проекте.

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

Вишенка на торте — логи проекта, которые агрегируются и превращаются в красивый сайт, автоматически собранный с помощью выполнения одной команды.

В статье мы расскажем на маленьком искусственном примере, из каких частей состоит Ocean и как его использовать.

Почему Ocean

Прежде всего нужно упомянуть cookiecutter-data-science (далее CDS) как идейного вдохновителя. В мире ML существуют и другие варианты, которые мы рассматривали. Начнем с хорошего: CDS не только предлагает удобную структуру проекта, но и рассказывает, как вести проект, чтобы всё было хорошо, — поэтому здесь мы рекомендуем отвлечься и посмотреть в оригинальной статье CDS основные ключевые идеи этого подхода.

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

Однако в процессе работы стали всплывать и минусы подхода CDS:

  • Папка data может разрастаться, но какой из скриптов или тетрадей порождает очередной файл — не до конца понятно. В большом количестве файлов легко запутаться. Не ясно, нужно ли в рамках реализации новой функциональности использовать какие-то файлы из существующих, так как нигде не хранится описание или документация по их предназначению.
  • В data не хватает подпапки features, в которую можно складировать признаки: посчитанные статистики, векторы и другие характеристики, из которых собирались бы разные конечные представления данных. Об этом уже замечательно написано в блог-посте.
  • src — другая папка-проблема. В ней есть функции, которые актуальны для всего проекта, например, подготовка и чистка данных модуля src.data. Но есть и модуль src.models, который содержит все модели от всех экспериментов, а их могут быть десятки. В итоге src очень часто обновляется, расширяясь совсем незначительными изменениями, а согласно философии CDS после каждого обновления необходимо пересобирать проект, а это тоже время..., — ну, вы поняли.
  • references представлен, но все ещё стоит открытый вопрос: кто, когда и в каком виде должен заносить туда материалы. А рассказать можно много по ходу проекта: какие работы проведены, каков их результат, каковы дальнейшие планы.

Эксперимент — хранилище всех данных, участвовавших в проверке некоторой гипотезы. Для решения вышеперечисленых проблем в Ocean представлена следующая сущность: эксперимент. Часть этих сведений можно трекать с помощью специальных утилит, например, MLFlow. Сюда можно отнести: какие данные использовались, какие данные (артефакты) получились в результате, версия кода, время начала и завершения эксперимента, исполняемый файл, параметры, метрики и логи. Однако структура экспериментов, которая представлена в Ocean, богаче и гибче.

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

<project_root> └── experiments ├── exp-001-Tree-models │ ├── config <- yaml-файлы с настройками │ ├── models <- сохраненные модели │ ├── notebooks <- ноутбуки для экспериментов │ ├── scripts <- скрипты, например, train.py или predict.py │ ├── Makefile <- для управления экспериментом из консоли │ ├── requirements.txt <- список зависимых библиотек │ └── log.md <- лог проведения эксперимента │ ├── exp-002-Gradient-boosting ...

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

Рассмотрим возможности нашего фреймворка на примере абстрактного ML/DL-проекта.

Workflow проекта

Инициализация

Итак, клиент — полиция Чикаго, — выгрузил нам данные и задачу: проанализировать преступления, совершенные в городе на протяжении 2011-2017 годов и сделать выводы.

Заходим в терминал и выполняем команду: Начинаем!

ocean project new -n Crimes

Смотрим на её структуру: Фреймворк создал соответствующую папку проекта crimes.

crimes ├── crimes <- src-модуль с переиспользуемым кодом, одноименный с проектом ├── config <- настройки, актуальные во всем проекте ├── data <- данные ├── demos <- демо для заказчика ├── docs <- Sphinx-документация ├── experiments <- эксперименты ├── notebooks <- ноутбуки для EDA ├── Makefile <- простые команды для запуска из консоли ├── log.md <- проектный лог ├── README.md └── setup.py

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

make package

Это относится и ко всем дальнейшим примерам. Это баг: если make-команды не хотят выполняться, то добавьте к ним флажок -B, например “make -B package”.

Логи и эксперименты

Начинаем работу с того, что данные клиента, — в нашем случае файл crimes.csv, — мы помещаем в папку data/raw.

Эти данные можно использовать для визуализации. На сайте Чикаго есть карты с разделениями города на посты (“beats” — наименьшая по размеру локация, за которой закреплена одна патрульная машина), секторы (“sectors”, состоят из 3-5 постов), участки (“districts”, состоят из 3 секторов), административных районов (“wards”) и, наконец, общественные зоны (“community area”). В то же время json-файлы с координатами полигонов-участков каждого из типа не являются данными, присланными заказчиком, поэтому мы помещаем их в data/external.

Все просто: рассматриваем отдельную задачу как отдельный эксперимент. Далее нужно ввести понятие эксперимента. Это стоит поместить в эксперимент. Нужно распарсить/выкачать данные и подготовить их для использования в дальнейшем? Отдельный эксперимент. Подготовить много визуализации и отчетов? Ну, вы поняли. Проверить гипотезу, подготовив модель?

Для создание нашего первого эксперимента из папки проекта выполняем:

ocean exp new -n Parsing -a ivanov

Теперь в папке crimes/experiments появилась новая папка с именем exp-001-Parsing, её структура приведена выше.

Для этого создаем ноутбук в соответствующей папке notebooks. После этого надо посмотреть на данные. Внутри мы подготовим данные для последующей работы. В Surf мы придерживаемся именования “номер ноутбука — название”, и созданный ноутбук будет называться 001-Parse-data.ipynb.

Код подготовки данных

import numpy as np
import pandas as pd
pd.options.display.max_columns = 100 # Используем наш проект как источник полезного кода:
from crimes.coordinator import Coordinator
coord = Coordinator()
coord.data_raw.contents()
> ['/opt/jupyterhub/notebooks/aolferuk/crimes/data/raw/crimes.csv'] # Синтаксический сахар для загрузки файлов:
df = coord.data_raw.join('crimes.csv').load()
df['Date'] = pd.to_datetime(df['Date'])
df['Updated On'] = pd.to_datetime(df['Updated On'])
df['Location X'] = np.nan
df['Location Y'] = np.nan
df.loc[df.Location.notnull(), 'Location X'] = df.loc[df.Location.notnull(), 'Location'].apply(lambda x: eval(x)[0])
df.loc[df.Location.notnull(), 'Location Y'] = df.loc[df.Location.notnull(), 'Location'].apply(lambda x: eval(x)[1])
df.drop('Location', axis=1, inplace=True)
df['month'] = df.Date.apply(lambda x: x.month)
df['day'] = df.Date.apply(lambda x: x.day)
df['hour'] = df.Date.apply(lambda x: x.hour) # Синтаксический сахар для сериализации файлов:
coord.data_interim.join('crimes.pkl').save(df)

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

log.md

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

Возможно, его будут проводить разные люди, и, конечно, нам понадобятся результаты в виде отчетов и графиков в последствии. Дальше— этап EDA (Exploratory Data Analysis — “разведывательный анализ данных”). Выполняем: Эти доводы повод создать новый эксперимент.

ocean exp new -n Eda -a ivanov

Полный код приводить не имеет смысла, но он и не нужен, например, вашим коллегам. В папке notebooks эксперимента создаем тетрадь 001-EDA.ipynb. В тетради выходит много кода, и она сама по себе не то, что хочется показывать клиенту. Зато нужны графики и выводы. Поэтому наши находки и инсайты запишем в файл log.md, а картинки графиков сохраним в references.

Вот, например, карта безопасных районов Чикаго, если судьба вас занесет туда:

chicagoMap

Она как раз была получена в тетради и перенесена в references.

В логе добавлена следующая запись:

19.02.2019, 18:15
EDA conclusion: * The most common and widely spread crimes are theft (including burglary), battery and criminal damage done with firearms.
* In 1 case out of 4 the suspect will be set free after detention. [!Criminal activity in different beats of the city](references/beats_activity.jpg) Actual exploration you can check in [the notebook](notebooks/001-Eda.ipynb)

А если оставить ссылку на тетрадь, то она будет конвертирована в html-формат и сохранена как отдельная страничка сайта. Обратите внимание: график оформлен просто как вставка изображения в md-файл.

Чтобы его собрать из логов экспериментов, выполняем следующую команду на уровне проекта:

ocean log new

После этого создается папка crimes/project_log, и index.html в нём — лог проекта.

Поэтому с помощью Ocean можно сразу сделать архив с копией сайта, чтобы было удобно её скачать и открыть на локальном компьютере или отправить по почте. Это баг: при отображении в Jupyter сайт внедряется как iframe для пущей безопасности, в связи с чем шрифты не отображаются корректно. Вот так:
ocean log archive [-n NAME] [-p PASSWORD]

Документация

Создадим функцию в файле crimes/my_cool_module.py и задокументируем её. Посмотрим на формирование документации с помощью Sphinx. Обратите внимание, что в Sphinx используется reStructured Text-формат (RST):

my_cool_module.py

def my_super_cool_random(max_value): ''' Returns a random number from [0; max_value) interval. Considers the number to be taken from uniform distribution. :param max_value: Maximum value that defines range. :returns: Random number. ''' return 4 # Good enough to begin with

А дальше все очень просто: на уровне проекта выполняем команду формирования документации, и готово:

ocean docs new

Ocean берет на себя сканирование каталога ваших исходных кодов, по ним строит индекс для Sphinx, и только потом сам Sphinx берется за работу. Вопрос из зала: Почему, если мы собирали проект через make, собирать документацию приходится через ocean?
Ответ: процесс генерации документации — не только выполнение команды Sphinx, которую можно поместить в make.

И наш модуль с комментариями там уже появился: Готовая html-документация ждет вас по пути crimes/docs/_build/html/index.html.

genDoc

Модели

Выполняем: Следующий шаг — построение модели.

ocean exp new -n Model -a ivanov

Файл train.py — заготовка для будущего процесса обучения. И на этот раз взглянем на то, что лежит в папке scripts внутри эксперимента. В файле уже приведен boilerplate-код, который делает сразу несколько вещей.

  • Функция обучения принимает несколько путей к файлам:
    • К файлу конфигурации, в который разумно вынести параметры модели, параметры обучения и прочие опции, которыми удобно управлять снаружи, не вникая в код.
    • К файлу с данными.
    • Путь к директории, в которой необходимо сохранить итоговый дамп модели.
  • Трекает метрики, полученные в процессе обучения, в mlflow. На все, что было затрекано, можно посмотреть через UI mlflow, выполнив команду make dashboard в папке эксперимента.
  • Отправляет оповещение в ваш Телеграм о том, что процесс обучения завершен. Для реализации этого механизма использован бот Alarmerbot. Чтобы это заработало, нужно сделать совсем немного: отправить боту команду /start, а затем перенести токен, выданный ботом, в файл crimes/config/alarm_config.yml. Строка может иметь такой вид:
    ivanov: a5081d-1b6de6-5f2762
  • Управляется из консоли.

Все организовано для того, чтобы процесс обучения или получения предсказаний любой модели был легко организован сторонним разработчиком, не знакомым с деталями реализации вашего эксперимента. Зачем управлять нашим скриптом из консоли? В нем есть заготовка команды train, и вам остается только правильно расставить пути к перечисленным выше требуемым файлам конфигурации, а в значении параметра username перечислить всех желающих получать Telegram-оповещения. Чтобы все кусочки паззла сошлись, после оформления train.py нужно оформить Makefile. В частности, работает алиас all, который отправит оповещение всем членам команды.

Как только все готово, наш эксперимент стартует с помощью команды make train, просто и изящно.

Создавать и удалять их в рамках эксперимента очень легко: На случай, если хочется применить чужие нейросети, вполне помогут виртуальные окружения (venv).

  • ocean env new создаст новое окружение. Оно не только активно по умолчанию, но еще и создает дополнительное ядро (kernel) для тетрадей и проведения дальнейших исследований. Называться оно будет так же, как и название эксперимента.
  • ocean env list отобразит список ядер.
  • ocean env delete удалит созданное в эксперименте окружение.

Чего не хватает?

  • Ocean не дружит с conda (потому что мы ее не используем).
  • Шаблон проекта только на английском.
  • Проблема локализации пока относится и к сайту: построение проектного лога предполагает, что все логи на английском.

Заключение

Исходный код проекта лежит здесь.

Больше информации вы можете почерпнуть в README в репозитории Ocean. Если вы заинтересовались — здорово!

И как обычно говорят в таких случаях, contributions are welcome, мы будем только рады, если вы поучаствуете в улучшении проекта.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть