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

image


Вступление

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

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

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


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

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

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

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


Почему Ocean

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

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

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


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

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

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


    └── 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)

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

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)

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

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

ocean log new

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


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


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

Посмотрим на формирование документации с помощью Sphinx. Создадим функцию в файле crimes/my_cool_module.py и задокументируем её. Обратите внимание, что в 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


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

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

genDoc


Модели

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

ocean exp new -n Model -a ivanov

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


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

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

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

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


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


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


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


Заключение

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

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

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

© Habrahabr.ru