Тестирование серии ML

Эффективное тестирование машинного обучения (часть I)

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

Обновление: Часть II уже вышла!

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

Проблемы при тестировании проектов ML

Тестирование проектов машинного обучения — сложная задача. Обучение модели — это длительная задача, которая может занять несколько часов и имеет недетерминированный результат, что противоположно тому, что нам нужно для тестирования программного обеспечения: быстрые и детерминированные процедуры. Год назад я опубликовал пост о тестировании проектов с интенсивным использованием данных, чтобы сделать непрерывную интеграцию возможной. Позже я превратил этот пост в блоге в выступление и представил его на PyData 2020. Но такая предыдущая работа охватывала только общие аспекты тестирования конвейеров данных и не учитывала тестирование моделей машинного обучения.

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

В посте я использую термины конвейер и задача. Задача — это единица работы (обычно функция или скрипт); например, одна задача может быть сценарием, загружающим необработанные данные, а другая может очищать такие данные. С другой стороны, конвейер — это просто серия задач, выполняемых в заранее определенном порядке. Мотивация создания пайплайнов, состоящих из небольших задач, состоит в том, чтобы сделать наш код более удобным в сопровождении и более простым для тестирования; это согласуется с целью нашей платформы с открытым исходным кодом — помочь специалистам по данным создавать более удобные в сопровождении проекты с использованием Jupyter. В следующих разделах вы увидите пример кода Python; мы используем pytest, pandas и Ploomber.

Если вы хотите знать, когда выйдет вторая часть, подпишитесь на нашу рассылку новостей, следите за нами в Twitter или LinkedIn.

Части конвейера машинного обучения

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

  1. Конвейер создания признаков. Серия вычислений для обработки необработанных данных и сопоставления каждой точки данных с вектором признаков. Обратите внимание, что мы используем этот компонент при обучении и отбывании наказания.
  2. Обучающая задача. Берет обучающий набор и создает файл модели.
  3. Файл модели. Результат обучающей задачи. Это один файл, содержащий модель с изученными параметрами. Кроме того, он может включать препроцессоры, такие как масштабирование или горячее кодирование.
  4. Конвейер обучения. Инкапсулирует логику обучения: получение необработанных данных, создание функций и обучение моделей.
  5. Конвейер обслуживания (также известный как конвейер вывода). Инкапсулирует логику обслуживания: получает новое наблюдение, генерирует функции, пропускает функции через модель и возвращает прогноз.

Что может пойти не так?

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

Конвейер создания функций

  1. Не удается запустить конвейер (например, проблемы с настройкой, неработающий код).
  2. Невозможно воспроизвести ранее сгенерированный обучающий набор.
  3. Pipeline производит некачественные обучающие данные.

Учебное задание

  1. Невозможно обучить модель (например, отсутствующие зависимости, неработающий код).
  2. Выполнение обучающей задачи с высококачественными данными приводит к созданию моделей низкого качества.

Файл модели

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

Конвейер обслуживания

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

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

Стратегия тестирования

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

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

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

  1. Дымовое тестирование. Чтобы убедиться, что наш код работает, мы запускаем его на каждом git push.
  2. Интеграционное тестирование и модульное тестирование. Тестирование выходных данных задачи и преобразование данных.
  3. Изменения в распределении и конвейер обслуживания. Протестируйте изменения в распределении данных и проверьте, можем ли мы загрузить файл модели и предсказать.
  4. Перекос между обучением и обслуживанием. Проверьте согласованность логики обучения и обслуживания.
  5. Качество модели. Проверка качества модели.

Краткое введение в тестирование с помощью pytest

Если вы использовали pytest ранее, вы можете пропустить этот раздел.

Тесты — это короткие программы, которые проверяют, работает ли наш код. Например:

Тест — это функция, которая запускает некоторый код и подтверждает его вывод. Например, в предыдущем файле есть два теста: test_add и test_substract, организованные в файл с именем test_math.py; обычно для каждого модуля используется один файл (например, test_math.py проверяет все функции в модуле math.py). Тестовые файлы обычно находятся в каталоге tests/:

Фреймворки для тестирования, такие как pytest, позволяют вам собирать все ваши тесты, выполнять их и сообщать, какие из них провалились, а какие прошли успешно:

Типичная структура проекта выглядит так:

src/ содержит задачи пайплайна вашего проекта и другие служебные функции. exploratory/ включает исследовательские блокноты, а ваши тесты помещаются в каталог tests/. Код в src/ должен быть доступен для импорта из двух других каталогов. Самый простой способ добиться этого — упаковать свой проект. В противном случае вам придется возиться с sys.path или PYTHONPATH.

Как ориентироваться в примере кода

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

Проект реализует пайплайн с использованием Ploomber, нашего фреймворка с открытым исходным кодом. Следовательно, вы можете увидеть спецификацию конвейера в файле pipeline.yaml. Чтобы увидеть, какие команды мы используем для тестирования конвейера, откройте .github/workflows/ci.yml, это файл конфигурации действий GitHub, который сообщает GitHub запускать определенные команды для каждого git push.

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

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

Уровень 1. Проверка дыма

Пример кода доступен здесь

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

Документирование зависимостей

Список внешних зависимостей — это нулевой шаг при запуске любого программного проекта, поэтому убедитесь, что вы задокументировали все зависимости, необходимые для запуска вашего проекта, при создании виртуальной среды. Например, при использовании pip ваш файл requirements.txt может выглядеть так:

После создания виртуальной среды создайте еще один файл (requirements.lock.txt), чтобы зарегистрировать установленные версии всех зависимостей. Вы можете сделать это с помощью команды pip freeze > requirements.lock.txt (выполните ее после запуска pip install -r requirements.txt), которая генерирует что-то вроде этого:

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

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

Тестирование конвейеров создания функций

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

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

Обратите внимание, что это базовый тест; мы не проверяем вывод конвейера! Однако этот простой тест позволяет нам проверить, работает ли код. Очень важно запускать этот тест всякий раз, когда мы выполняем git push. Если вы используете GitHub, вы можете сделать это с помощью GitHub Actions, другие платформы git имеют аналогичные функции.

Тестирование обучающей задачи

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

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

Уровень 2. Интеграционное и модульное тестирование

Пример кода доступен здесь

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

  1. Убедитесь, что данные, используемые для обучения модели, соответствуют минимальному уровню качества.
  2. Отдельно протестируйте те части вашего кода, которые имеют точно определенное поведение.

Давайте обсудим первую цель.

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

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

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

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

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

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

Вот реализация интеграционного теста в нашем тестовом репозитории.

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

Внутри каждой задачи в вашем конвейере (например, внутри clean) у вас, скорее всего, будут меньшие подпрограммы; такие части вашего кода должны быть написаны как отдельные функции и протестированы (т. е. добавлены тесты в каталог tests/).

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

В отличие от общей процедуры clean, transform.chest_pain_type имеет явное, объективно определенное поведение: она должна сопоставлять целые числа с соответствующими удобочитаемыми значениями. Мы можем перевести это в модульный тест, указав входные данные и ожидаемые выходные данные.

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

Вот реализация модульного теста в репозитории примеров.

Рекомендации

Далее

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

Если вы хотите узнать, когда выйдет вторая часть, подпишитесь на нашу рассылку, следите за нами в Twitter или LinkedIn.

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

Первоначально опубликовано на ploomber.io.