Вступление
За годы разработки 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, богаче и гибче.
Модуль одного эксперимента выглядит следующим образом:
<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)
Чтобы ваши коллеги были в курсе, что вы сделали и могут ли ваши результаты быть ими использованы, нужно прокомментировать это в логе: файле log.md. Структура лога (по сути являющегося привычным markdown-файлом) выглядит следующим образом:
Цветом выделены части, которые заполнены от руки. Основная мета эксперимента (светло-сливовый цвет) — автор и объяснение своей задачи, результата, к которому эксперимент идет. Ссылки на данные, как взятые, так и порожденные в процессе (зеленый цвет), помогают следить за файлами данных и понимать, кто, в рамках чего и зачем их использует. В самом логе (желтый цвет) рассказывается итог работ, выводы и рассуждения. Все эти данные позже станут наполнением сайта проектного лога.
Дальше— этап EDA (Exploratory Data Analysis — “разведывательный анализ данных”). Возможно, его будут проводить разные люди, и, конечно, нам понадобятся результаты в виде отчетов и графиков в последствии. Эти доводы повод создать новый эксперимент. Выполняем:
ocean exp new -n Eda -a ivanov
В папке notebooks эксперимента создаем тетрадь 001-EDA.ipynb. Полный код приводить не имеет смысла, но он и не нужен, например, вашим коллегам. Зато нужны графики и выводы. В тетради выходит много кода, и она сама по себе не то, что хочется показывать клиенту. Поэтому наши находки и инсайты запишем в файл log.md, а картинки графиков сохраним в references.
Вот, например, карта безопасных районов Чикаго, если судьба вас занесет туда:
Она как раз была получена в тетради и перенесена в 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):
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. И наш модуль с комментариями там уже появился:
Модели
Следующий шаг — построение модели. Выполняем:
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, мы будем только рады, если вы поучаствуете в улучшении проекта.