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

Это вторая часть из трех частей. Предлагаю вам прочитать часть 1 для лучшего понимания:



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

  • Организация тестирования в исходном коде
  • Применимые тесты с примерами (практические)
  • Освоение выполнения тестов с помощью Pytest (практическое)
  • Ожидаемый сбой и условный пропуск
  • Автоматизация тестирования с помощью git action (практический) (часть 3)
  • Помимо утверждения: настройка и разборка (практическое) (часть 3)
  • Издевательство (практическое) (часть 3)

Организация тестирования в исходном коде

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

├── ./README.md
├── ./app.py
├── ./datasets
│   ├── ./datasets/test.csv
│   └── ./datasets/train.csv
├── ./notebook
│   └── ./notebook/insurance-prediction.ipynb
├── ./src
│   ├── ./src/__init__.py
│   ├── ./src/artefacts
│   │   ├── ./src/artefacts/__init__.py
│   │   ├── ./src/artefacts/lgbm_model.pkl
│   │   └── ./src/artefacts/one_hot_encoder.pkl
│   └── ./src/utils
│       ├── ./src/utils/__init__.py
│       ├── ./src/utils/feature_engineering.py
│       ├── ./src/utils/load_artefact.py
│       ├── ./src/utils/make_prediction.py
│       ├── ./src/utils/preprocessor.py
│       └── ./src/utils/user_input.py
└── ./tests
    ├── ./tests/__init__.py
    ├── ./tests/artefacts
    │   ├── ./tests/artefacts/__init__.py
    │   ├── ./tests/artefacts/lgbm_model.pkl
    │   └── ./tests/artefacts/one_hot_encoder.pkl
    └── ./tests/utils
        ├── ./tests/utils/__init__.py
        ├── ./tests/utils/test_feature_engineering.py
        ├── ./tests/utils/test_load_artefact.py
        ├── ./tests/utils/test_make_prediction.py
        ├── ./tests/utils/test_preprocessor.py
        └── ./tests/utils/test_user_input.py

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

Заявление Assert

Утверждения - это проверки, возвращающие статус "Истина" или "Ложь". с помощью оператора assert мы можем проверить, совпадает ли результат функции или модуля с ожидаемыми значениями. Обычно в Pytest, если утверждение не выполняется в тестовом методе, выполнение этого метода останавливается там. Оставшийся код в этом методе тестирования не выполняется, и Pytest продолжит выполнение следующего метода тестирования. Это поведение можно изменить для некоторых особых случаев, но я сохраню это объяснение в следующих двух разделах. Оператор assert иногда может завершиться ошибкой из-за неправильного допущения аргументов, особенно с числами с плавающей запятой. "Читать далее!"

Применимые тесты с примерами

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

  • Тестирует вводимые пользователем данные
  • Тесты сохраненных артефактов
  • Тесты предварительной обработки данных
  • Тесты на разработку функций
  • Типовой поведенческий тест

Тесты на ввод данных пользователем. Первое, что нужно сделать в конвейере машинного обучения, - это прием данных как на этапе обучения, так и на этапе вывода. с помощью этого теста мы хотим убедиться, что пользовательские данные находятся в ожидаемом формате с ожидаемым диапазоном значений. То, как мы напишем этот тест, зависит от режима прогнозирования, который мы принимаем, например, одиночное или пакетное прогнозирование, чтобы упомянуть некоторые из них. В этом проекте мы приняли пакетное прогнозирование, при котором пользователь может загрузить файл .csv, содержащий несколько записей для прогнозирования модели. Модуль, отвечающий за прием пользовательских данных, - это «user_input.py», и несколько тестов, которые можно написать, показаны во фрагменте кода ниже:

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

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

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

Тесты предварительной обработки данных: Эта часть может быть сложной, так как успешная работа этого модуля зависит от многих факторов. Каждый вход в компонент конвейера машинного обучения зависит от выходных данных предыдущего компонента как такового, поэтому становится трудно отлаживать, особенно при работе с тесно связанными инструментами компонентов конвейера. Приведенный ниже фрагмент тестового кода будет улучшен в третьей части этой серии, где мы будем обсуждать «установку и разборку» с использованием фикстур Pytest.

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

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

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

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

Освоение выполнения тестов с помощью Pytest

Теперь, когда мы написали и организовали тесты в тестовом костюме / папке, мы можем запустить тест, чтобы проверить, какая функция прошла или не прошла. Сначала все функции завершатся ошибкой, потому что нам еще предстоит реализовать код src, поскольку наш подход к проекту - TDD. В Pytest общий способ запуска тестов - сменить каталог в терминале или командной строке на каталог тестов и запустить команду «pytest». С помощью этой команды Pytest рекурсивно перейдет в поддерево каталога папки tests, чтобы идентифицировать файлы, начинающиеся с «test», как тестовые модули. В модулях он определяет классы, начинающиеся с «Test» как тестовый класс, и переходит к идентификации функций, начинающихся с «test »в тестовом классе как модульный тест. Он собирает эти модульные тесты и запускает их все. наше первое выполнение теста должно выглядеть так, как показано ниже:

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

На рисунке выше показано, что все тесты, выполненные Pytest, прошли с некоторой полезной информацией, которую стоит учесть.

  1. Для выполнения было собрано 9 юнит-тестов.
  2. Из 9 тестов 2 взяты из test_feature_engineering.py с зелеными точками, обозначающими пройденные тесты.
  3. 9 тестов прошли, время выполнения составило 1,97 секунды.

Результат будет другим, когда есть неудачные тесты, пример, показанный ниже:

Из выходных данных выше видно, что неудавшийся модульный тест происходит в функции test_pred, которую можно найти в модуле make_prediction. . Тег «E» указывает на режим ошибки, и местоположение ошибки в функции можно увидеть непосредственно перед «E ».

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

  • Pytest -x: этот флаг останавливает Pytest после первого неудачного теста.

  • Pytest -k: с помощью этого тега мы можем запускать тесты с использованием ключевых слов. тег принимает строковый аргумент для поиска указанного шаблона. Это запустит только тесты с этим шаблоном, например
pytest -k TestGetUserInput
  • Запуск теста с использованием идентификатора узла. Это позволяет запустить подмножество тестов. например, мы можем захотеть запустить только тесты в определенном модуле или классе, как показано ниже:
pytest <test path>::<test class>::<unit test>
example:
pytest /Users/user/Documents/dev/projects/TDD-in-MLOps/tests/utils/test_preprocessor.py::test_map_text_to_number

"Читать далее !!!"

Ожидаемый сбой и условный пропуск

Поскольку мы используем TDD, все тесты сначала обречены на неудачу, потому что мы реализовали тесты до исходного кода. С Pytest мы можем предотвратить ложные срабатывания при тестировании, используя декоратор «xfail», чтобы указать на ожидаемый сбой. о тестах, отмеченных этим декоратором, сообщается "xfailed", а не о ложной тревоге.

Этот тест помечается как «xpassed» после правильной реализации функции, но по-прежнему имеет пометку «xfail».

Другой связанный декоратор - skipif, который можно использовать для пропуска теста на основе определенного логического выражения. "читать далее!!!"

Подведение итогов

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

  • Покрытие кода и CI с Трэвисом (часть 3)
  • Автоматизация тестирования с помощью git action (практический) (часть 3)
  • Помимо утверждения: настройка и разборка (практическое) (часть 3)
  • Издевательство (практическое) (часть 3)

Проверьте репозиторий Project на GitHub и не забудьте пометить его по ссылке ниже:



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

Свяжитесь со мной в Twitter и LinkedIn

Приветствую