Программная инженерия для науки о данных

Переосмысление непрерывной интеграции для науки о данных

Широко распространенная практика разработки программного обеспечения заслуживает отдельного внимания в нашей области.

Прелюдия: практика разработки программного обеспечения в области науки о данных

По мере того как наука о данных и машинное обучение получают все большее распространение в отрасли, практики понимают, что развертывание продуктов для обработки данных сопряжено с высокими (и часто неожиданными) затратами на обслуживание. Как утверждают Скалли и соавторы в своей известной статье:

(системы машинного обучения) имеют все проблемы обслуживания традиционного кода плюс дополнительный набор специфических проблем машинного обучения.

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

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

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

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

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

Резюме

  1. Структурируйте свой конвейер на несколько задач, каждая из которых сохраняет промежуточные результаты на диск
  2. Реализуйте конвейер таким образом, чтобы его можно было параметризовать.
  3. Первый параметр должен отбирать необработанные данные, чтобы обеспечить быстрое сквозное выполнение для тестирования.
  4. Второй параметр должен изменить расположение артефактов, чтобы разделить тестовую и производственную среды.
  5. При каждом нажатии служба CI запускает модульные тесты, которые проверяют логику внутри каждой задачи.
  6. Затем выполняется конвейер с выборкой данных, и интеграционные тесты проверяют целостность промежуточных результатов.

Что такое непрерывная интеграция?

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

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

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

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

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

Непрерывная интеграция для науки о данных: идеальный рабочий процесс

Как я упоминал в прелюдии, последнее задание в конвейере данных часто привлекает наибольшее внимание (например, обученная модель в конвейере машинного обучения). Неудивительно, что существующие статьи о CI для науки о данных / машинного обучения также сосредоточены на этом; но чтобы эффективно применять структуру CI, мы должны мыслить в терминах всей вычислительной цепочки: от получения необработанных данных до доставки продукта данных. Неспособность признать, что конвейер данных имеет более богатую структуру, заставляет специалистов по данным слишком много внимания уделять самому концу и игнорировать качество кода в остальных задачах.

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

Давайте конкретизируем ситуацию с описанием предлагаемого рабочего процесса:

  1. Специалист по данным вносит изменения в код (например, изменяет одну из задач в конвейере)
  2. Нажатие запускает службу CI для сквозного запуска конвейера и тестирования каждого сгенерированного артефакта (например, один тест может подтвердить, что все строки в таблице customers имеют непустое значение customer_id)
  3. Если тесты пройдены, следует проверка кода.
  4. Если изменения одобрены рецензентом, код объединяется
  5. Каждое утро «производственный» конвейер (последняя фиксация в основной ветке) проходит от начала до конца и отправляет отчет бизнес-аналитикам.

Такой рабочий процесс имеет два основных преимущества:

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

Этот рабочий процесс - то, что инженеры-программисты делают в традиционных программных проектах. Я называю это идеальным рабочим процессом, потому что это то, что мы сделали бы, если бы могли выполнить сквозную конвейерную обработку в разумные сроки. Это неверно для многих проектов из-за масштабов данных: если нашему конвейеру требуются часы для сквозного выполнения, невозможно запускать его каждый раз, когда мы вносим небольшое изменение. Вот почему мы не можем просто применить стандартный рабочий процесс CI (шаги с 1 по 4) к Data Science. Мы внесем несколько изменений, чтобы сделать это возможным для проектов, в которых время выполнения является проблемой.

Тестирование программного обеспечения

CI позволяет разработчикам непрерывно интегрировать изменения кода путем запуска автоматических тестов: если какой-либо из тестов завершается неудачно, фиксация отклоняется. Это гарантирует, что у нас всегда будет рабочий проект в основной ветке.

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

  1. Люди могут создавать новые учетные записи, используя электронную почту и пароль
  2. Пароли можно восстановить, отправив сообщение на зарегистрированный адрес электронной почты.
  3. Пользователи могут входить в систему, используя ранее сохраненные учетные данные

Как только инженер напишет код для поддержки такой функциональности (или даже раньше!), Он / она убедится, что код работает, написав несколько тестов, которые выполнят тестируемый код и проверит его поведение, как ожидалось:

Но модульное тестирование - не единственный тип тестирования, как мы увидим в следующем разделе.

Уровни тестирования

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

Модульное тестирование

Фрагмент, который я показал в предыдущем разделе, называется модульным тестом. Модульные тесты проверяют, что один модуль работает. Строгое определение unit не существует, но часто оно эквивалентно вызову одной процедуры. В нашем случае мы тестируем create_account процедуру.

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

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

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

Интеграционное тестирование

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

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

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

Эффективное тестирование

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

Эффективный тест должен отвечать четырем требованиям:

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

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

2. Входные данные отражают реальный ввод пользователя

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

3. Соответствующий тестовый оракул

Как мы упоминали в предыдущем разделе, тестовый оракул - это наш критерий «годен / не годен». Чем проще и меньше процедура тестирования, тем легче ее придумать. Если мы не проверяем правильный результат, наш тест бесполезен. Наш тест на create_account подразумевает, что проверка таблицы пользователей в базе данных является подходящим способом оценки нашей функции.

4. Разумное время выполнения

Пока выполняются тесты, разработчик должен ждать, пока не вернутся результаты. Если тестирование идет медленно, нам придется ждать долгое время, что может привести к тому, что разработчики просто проигнорируют систему CI в целом. Это приводит к тому, что изменения кода накапливаются, что значительно затрудняет отладку (легче найти ошибку, когда мы изменили 5 строк, чем когда мы изменили 100).

Эффективное тестирование конвейеров данных

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

Модульное тестирование конвейеров данных

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

Скажем, наша задача add_product_information выполняет некоторую очистку данных перед объединением продаж с продуктами:

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

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

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

Интеграционное тестирование конвейеров данных

Наш конвейер обрабатывает входные и выходные данные до тех пор, пока не будет получен окончательный результат. Этот поток может прерваться, если ожидания ввода задачи не соответствуют действительности (например, имена столбцов), более того, каждое преобразование данных кодирует определенные предположения, которые мы делаем в отношении данных. Интеграционные тесты помогают нам убедиться, что выходные данные проходят через конвейер правильно.

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

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

Состояние системы

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

Проверить оракул

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

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

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

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

Задачи конвейера не обязательно должны быть процедурами Python, они часто представляют собой сценарии SQL, и вы должны тестировать их таким же образом. Например, вы можете проверить, что в определенном столбце нет нулей, с помощью следующего запроса:

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

Реалистичные входные данные и время работы

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

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

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

В приведенном выше фрагменте предполагается, что мы можем представить наш конвейер как «объект конвейера» и вызвать его с параметрами. Это очень мощная абстракция, которая делает ваш конвейер гибким для выполнения при различных настройках. После успешного выполнения задачи следует запустить соответствующий интеграционный тест. Например, предположим, что мы хотим протестировать нашу add_product_information процедуру, наш конвейер должен вызывать следующую функцию после выполнения такой задачи:

Обратите внимание, что мы передаем путь к данным в качестве аргумента функции, это позволит нам легко переключить путь для загрузки данных. Это важно, чтобы участки трубопровода не мешали друг другу. Например, если у вас несколько веток git, вы можете упорядочить артефакты по веткам в папке с именем /data/{branch-name}; если вы используете сервер совместно с коллегой, каждый из них может сохранить свои артефакты в /data/{username}.

Если вы работаете со сценариями SQL, вы можете применить тот же шаблон тестирования:

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

В нашей библиотеке Ploomber поддерживается параметризованные конвейеры и выполнение тестов при выполнении задачи.

Компромисс при тестировании в Data Science

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

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

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

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

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

Отладка конвейеров данных

Когда тесты терпят неудачу, пора отлаживать. Наша первая линия защиты - это ведение журнала: всякий раз, когда мы запускаем наш конвейер, мы должны генерировать соответствующий набор записей журнала, чтобы мы могли его просмотреть. Я рекомендую вам взглянуть на модуль logging в стандартной библиотеке Python, который предоставляет гибкую структуру для этого (не используйте print для ведения журнала). Хорошей практикой является сохранение файла с журналами каждого запуска конвейера.

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

Серии упорядоченных задач, входными данными которых являются необработанные наборы данных, промежуточные задачи создают преобразованные наборы данных (сохраняются на диск), а конечная задача создает продукт данных.

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

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

Отладка сценариев SQL сложнее, поскольку у нас нет отладчиков, как в Python. Я рекомендую сохранять сценарии SQL разумного размера: как только сценарий SQL становится слишком большим, вам следует рассмотреть возможность разбивки его на две отдельные задачи. Организация кода SQL с использованием WITH помогает повысить удобочитаемость и может помочь отладить сложные операторы:

Если вы обнаружите ошибку в сценарии SQL, организованном таким образом, вы можете заменить последний оператор SELECT на что-то вроде SELECT * FROM customers_subset, чтобы просмотреть промежуточные результаты.

Запуск интеграционных тестов в продакшене

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

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

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

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

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

Новый взгляд на рабочий процесс

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

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

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

Детали реализации

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

Модульное тестирование

Для логики модульного тестирования внутри каждой задачи конвейера данных мы можем использовать существующие инструменты. Очень рекомендую использовать pytest. Для базового использования требуется небольшая кривая обучения; когда вы освоитесь с ним, я бы порекомендовал вам изучить другие его функции (например, приспособления). Стать опытным пользователем любого фреймворка для тестирования дает большие преимущества, поскольку вы тратите меньше времени на написание тестов и максимизируете их эффективность для выявления ошибок. Продолжайте практиковаться, пока написание тестов не станет естественным первым шагом перед написанием фактического кода. Этот метод написания тестов в первую очередь называется Разработка через тестирование (TDD).

Запуск интеграционных тестов

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

Наша библиотека Ploomber поддерживает все функции, необходимые для реализации этого рабочего процесса: представление вашего конвейера как DAG, разделение сред разработки / тестирования / производства, параметризация конвейеров, запуск тестовых функций при выполнении задачи, интеграция с отладчиком Python, среди других функций. .

Внешние системы

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

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

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

Это часто невозможно в проектах данных. Если мы используем большой внешний сервер для ускорения вычислений, у нас, скорее всего, есть только эта система (например, кластер Hadoop в масштабах компании), и насмехаться над ней невозможно. Один из способов решить эту проблему - хранить артефакты конвейера в разных «средах». Например, если вы используете большую аналитическую базу данных для своего проекта, храните производственные артефакты в prod схеме, а тестовые артефакты в test схеме. Если вы не можете создавать схемы, вы также можете добавить префиксы ко всем своим таблицам и представлениям (например, prod_customers и test_customers). Параметризация конвейера может помочь вам легко переключать схемы / суффиксы.

CI-сервер

Для автоматизации выполнения тестирования вам понадобится CI-сервер. Всякий раз, когда вы нажимаете на репозиторий, CI-сервер будет запускать тесты для новой фиксации. Доступно множество вариантов, проверьте, есть ли у компании, в которой вы работаете, уже служба CI. Если его нет, вы не получите автоматизированный процесс, но все равно можете реализовать его на полпути, выполняя тесты локально при каждой фиксации.

Расширение: конвейер машинного обучения.

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

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

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

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

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

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

Оценка модели как часть рабочего процесса CI

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

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

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

Тестирование алгоритма обучения

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

Это то, что делает любая среда машинного обучения (scikit-learn, keras и т. Д.), Поскольку они должны гарантировать, что улучшения текущих реализаций не нарушат их. В большинстве случаев, если вы не работаете с алгоритмом, требующим очень большого количества данных, это не приведет к проблеме времени выполнения, потому что вы можете выполнить модульное тестирование своей реализации с синтетическим / игрушечным набором данных. Та же самая логика применима к любым препроцессорам обучения (например, к масштабированию данных).

Тестирование вашего учебного процесса

На практике обучение не является одноэтапной процедурой. Первым шагом является загрузка ваших данных, затем вы можете выполнить некоторую заключительную очистку, такую ​​как удаление идентификаторов или категориальных функций горячего кодирования. После этого вы передаете данные в многоступенчатый обучающий конвейер, который включает в себя разделение, предварительную обработку данных (например, стандартизацию, PCA и т. Д.), Настройку гиперпараметров и выбор модели. На любом из этих шагов что-то может пойти не так, особенно если у вашего конвейера есть сильно настроенные процедуры.

Тестировать ваш обучающий конвейер сложно, потому что нет очевидного тестового оракула. Мой совет - постарайтесь сделать ваш конвейер как можно более простым, используя существующие реализации (в scikit-learn есть потрясающие инструменты для этого), чтобы уменьшить количество кода для тестирования.

На практике мне показалось полезным определить критерии теста относительно предыдущих результатов. Если при первом обучении модели я получил точность X, то я сохраняю это число и использую его как ссылку. Последующие эксперименты должны завершиться неудачей в пределах разумного диапазона от X: внезапное падение или повышение производительности вызывает предупреждение для просмотра результатов вручную. Иногда это хорошая новость, это означает, что производительность улучшается из-за работы новых функций, иногда это плохая новость: внезапный прирост производительности может происходить из-за утечки информации, а внезапное падение - из-за неправильной обработки данных или случайного отбрасывания строк / столбцов.

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

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

Следующий рубеж: компакт-диск для науки о данных

CI позволяет нам интегрировать код за короткие циклы, но это не конец истории. В какой-то момент нам нужно развернуть наш проект, здесь на помощь приходят Непрерывная доставка и Непрерывное развертывание.

Первым шагом к развертыванию является выпуск нашего проекта. При выпуске все необходимые файлы (например, исходный код, файлы конфигурации и т. Д.) Помещаются в формат, который можно использовать для установки нашего проекта в производственной среде. Например, для выпуска пакета Python требуется загрузить наш код в указатель пакетов Python.

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

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

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

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

Но непрерывная доставка для машинного обучения - это управляемый процесс. После того, как фиксация прошла все тесты с образцом данных (CI), другой процесс запускает конвейер с полным набором данных и сохраняет окончательный набор данных в хранилище объектов (этап CD 1).

Затем процедура обучения загружает артефакты и находит оптимальные модели, настраивая гиперпараметры для каждого выбранного алгоритма. Наконец, он сериализует лучшую спецификацию модели (то есть алгоритм и его лучшие гиперпараметры) вместе с отчетами об оценке (этап 2 CD). Когда все будет готово, мы смотрим на отчеты и выбираем модель для развертывания.

В предыдущем разделе мы обсудили, как включить оценку модели в рабочий процесс CI. Предлагаемое решение ограничено требованиями к времени работы CI; после завершения первого этапа процесса CD мы можем включить более надежное решение, обучив последнюю лучшую спецификацию модели с полным набором данных, это позволит отловить ошибки, вызывающие падение производительности в данный момент, вместо того, чтобы ждать второго этапа чтобы закончить, учитывая, что у него гораздо большее время работы. Рабочий процесс CD выглядит так:

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

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

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

Заключительное слово

По мере того, как процессы CI / CD для Data Science начнут развиваться и стандартизироваться, мы начнем видеть новые инструменты, упрощающие внедрение. В настоящее время многие специалисты по данным даже не рассматривают CI / CD как часть своего рабочего процесса. В некоторых случаях они просто не знают об этом, в других, потому что для эффективного внедрения CI / CD требуется сложный процесс настройки для переназначения существующих инструментов. Специалистам по данным не следует беспокоиться о настройке службы CI / CD, им следует просто сосредоточиться на написании своего кода, тестировании и продвижении.

Помимо инструментов CI / CD, специально предназначенных для проектов данных, нам также нужны инструменты управления конвейером данных для стандартизации разработки конвейера. За последние пару лет я увидел много новых проектов, к сожалению, большинство из них сосредоточено на таких аспектах, как планирование или масштабирование, а не на опыте пользователя, что имеет решающее значение, если мы хотим, чтобы такие методы разработки программного обеспечения, как модульность и тестирование, были приняли все специалисты по анализу данных. По этой причине мы создали Ploomber, чтобы помочь специалистам по обработке данных легко и постепенно внедрять передовые методы разработки.

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

Я считаю, что CI - это самый важный недостающий элемент в программном стеке Data Science: у нас уже есть отличные инструменты для выполнения AutoML, развертывания модели одним щелчком мыши и мониторинга модели. CI сократит разрыв, позволяя командам уверенно и непрерывно обучать и развертывать модели.

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

Нашли ошибку в этом посте? "Нажмите здесь, чтобы дать нам знать".

Изначально размещено на ploomber.io