Путешествие по инструментам

Когда я начал работать исследователем машинного обучения в slashwhy (ранее SALT AND PEPPER Software) на постоянной основе, у меня был опыт разработки в Unity с C #, а также на Python. В течение нескольких месяцев наша команда выросла до 5 человек, и вскоре нам пришлось столкнуться с несколькими очень важными вопросами о том, как мы работаем и организовываемся. Вкратце: мы почти всегда работаем над проектами глубокого обучения с широким кругом клиентов из промышленного сектора, начиная от работы с большими машинными данными до оптического распознавания и контроля качества. Таким образом, наша работа всегда предполагает тесное взаимодействие с нашими клиентами: понимание данных, проблемы и желаемое значение, которое должно быть достигнуто с помощью машинного обучения.

Мы должны были задать себе следующие вопросы:

  • Как мы пишем код и структурируем наши проекты?
  • Как мы управляем нашими экспериментами по машинному обучению?
  • Как мы сообщаем о своей работе нашим клиентам?
  • Как мы гарантируем качество нашего кода и результатов?

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

Как мы пишем код и структурируем наши проекты?

В Jupyter или не в Jupyter?

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

Gitlab уже использовался почти всеми сотрудниками нашей компании, поэтому контроль версий не представлял проблемы. С другой стороны, структура кода и проекта была.
Обсуждение того, как мы могли бы правильно структурировать наш рабочий процесс, быстро подняло вопрос о записных книжках Jupyter и о том, как обрабатывать их в нашей повседневной жизни. Глядя на то, насколько популярными стали записные книжки и Google Collab, вы можете спросить: «Почему бы мне просто не использовать записные книжки Jupyter для всего?». По нашему опыту, записные книжки Jupyter отлично подходят для исследовательской работы, такой как первоначальное исследование данных, и даже для имитации быстрых прототипов. Они просты в использовании и позволяют отслеживать, что вы делаете со своими данными. Но как только ваш проект разрастается и такие вещи, как развертывание Docker, повторное использование кода и тестирование, входят в стадию, они быстро превращаются в ужасный беспорядок. Они также плохо работают с контролем версий, поскольку логика ваших алгоритмов запутана с синтаксисом представления, что создает беспорядок при проверке (особенно если кто-то забывает очистить вывод перед фиксацией).

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

src/
├── train.py
├── inference.py
├── data_processing/
│   └── data_loader.py
├── utils/
│   └── ...
├── models/
│   └── model.py
└── visualization/
    └── explore.ipynb
.gitignore
README.md
requirements.txt
run.py

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

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

Хотя мы решили не использовать записные книжки Jupyter для нашей общей процедуры кодирования, мы все же сохранили исследовательскую записную книжку для первоначального исследования данных как часть нашего шаблона.

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

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

Как мы управляем нашими экспериментами по машинному обучению?

К этому моменту у нас была четкая структура кода в наших проектах и ​​даже руководство по стилю, в котором мы договорились о том, какие правила кодирования использовать. Чего нам по-прежнему не хватало, так это способа систематической организации нашей основной работы - экспериментов машинного обучения.

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

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

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

Мы быстро решили попробовать и превратили одну из наших рабочих станций машинного обучения в сервер MongoDB. Децентрализованная структура проекта с множеством разных скриптов оказалась здесь преимуществом, поскольку sacred очень хорошо интегрируется в эту структуру. За короткое время мы запустили наши первые проекты при священной поддержке. Теперь у нас был центральный сервер с очень структурированной и организованной системой отслеживания - мы решили использовать Omniboard в качестве средства просмотра базы данных.

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

Файлы конфигурации гиперпараметров

При использовании sacred обычно есть словарь гиперпараметров как часть вашего основного обучающего сценария, который используется для переопределения параметров в вызовах функций. Хотя нам очень понравилась эта идея, она не совсем соответствовала нашей структуре отдельных скриптов для каждой части нашего кода, поэтому мы добавили скрипт config.py в наш новый шаблон проекта. Этот сценарий конфигурации содержит конфигурацию по умолчанию, к которой можно получить доступ через вызов функции, а также функцию переопределения конфигурации, которая может создать список пермутированных конфигураций из нескольких значений для одного и того же гиперпараметра. Если, например, кто-то хочет исследовать влияние различных значений скорости обучения на производительность обучения, простой ввод списка из нескольких различных значений в конфигурации создает и возвращает список из нескольких словарей конфигурации, которые, в свою очередь, можно использовать для обучения сетке.

Очереди экспериментов и обучение сетке

Когда мы начали использовать священный, он был очень сосредоточен на проведении одиночных экспериментов. Уже можно было просто поставить прогон в очередь вместо того, чтобы запускать его немедленно, но это означало вводить разные конфигурации для каждого такого прогона. Имея под рукой наш скрипт config.py, мы таким образом расширили run.py, чтобы использовать список различных конфигураций и автоматически создать из него очередь в нашей базе данных. Многопоточный цикл обучения с выделенным доступом к графическому процессору для каждого учебного прогона позволил нам создать обучение в большой сетке и запустить его автоматически без какого-либо дополнительного контроля со стороны человека.

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

docker/
├── Dockerfile
├── .dockerignore
└── main.py
src/
├── train.py
├── inference.py
├── config.py
├── data_processing/
│   └── data_loader.py
├── utils/
│   └── ...
├── models/
│   └── model.py
└── visualization/
    └── explore.ipynb
.gitignore
README.md
requirements.txt
run.py

Как видите, туда же проникла папка развертывания докеров. Если весь код аккуратно разделен на отдельные сценарии, это упрощает его повторное использование для таких задач, как развертывание Docker - или, по сути, любого развертывания.

Как мы гарантируем качество нашего кода и результатов?

С появлением Sacred у нас теперь была четкая структура проекта, рекомендации по стилю кода и система управления экспериментами. Однако нам все еще не хватало чего-то очень фундаментального - непрерывного тестирования.

Всякий раз, когда я читаю статьи о машинном обучении, обычно написанные людьми с опытом работы на Python, я замечаю, что, несмотря на большой объем информации об обработке данных или тонкой настройке нейронной сети, качество кода редко является проблемой. Это нормально, если вы работаете в чисто научной среде и в основном просто пишете код для себя. Для нас было просто недостаточно писать код и надеяться на лучшее, тестируя его вручную.

Таким образом, мы добавили в наш новый шаблон проекта еще один раздел - тесты. Мы решили использовать unittest, классический пакет Python для тестирования вашего кода. А потом мы начали писать тесты.

Чтобы сделать нашу жизнь намного проще, мы используем GitLab Continous Integration (CI) для автоматического запуска наших тестов после каждой фиксации.

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

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

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

Как мы сообщаем о своей работе нашим клиентам?

Для обмена промежуточными результатами с нашими клиентами у нас до этого были в основном две стратегии: 1 - быстро создать несколько изображений и слайдов, объясняющих наше текущее состояние, или 2 - напрямую использовать записную книжку Jupyter. Это не оптимально, поскольку мы хотим сосредоточить наши встречи на результатах и ​​иметь более удобный способ взаимодействия с данными (вместо изменения кода и повторного запуска ячеек). Также довольно громоздко переносить рабочие алгоритмы из структуры записной книжки в чистые сценарии Python, которые составляют нашу оставшуюся структуру кода. Поэтому мы искали разные инструменты.

К счастью, мы нашли Streamlit и сразу убедились в его функциональности. Streamlit позволяет создавать небольшие веб-приложения с минимумом дополнительного кода. Для нас это было выгодно по следующим причинам:

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

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

Наша окончательная и текущая структура ДНЯО теперь выглядит так:

docker/
├── Dockerfile
├── .dockerignore
└── main.py
src/
├── train.py
├── inference.py
├── config.py
├── data_processing/
│   └── data_loader.py
├── utils/
│   └── ...
├── models/
│   └── model.py
└── visualization/
    └── explore.py
tests/
├── data/
│   └── [project relevant data goes here for testing]
├── unit/
│   └── test_data_loader.py
└── integration/
    └── test_model.py
.gitignore
gitlab-ci.yml
README.md
requirements.txt
run.py

Как все это сочетается?

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

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