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

Приходят как-то на синк разработчик, тестировщик и time.sleep (1), а он им и говорит: ребята, мы что, в анекдоте?

→ Ну почти: они в подводке к статье про то, как тестировать мл системы, что бы не потерять $100k.
Приходят как-то на синк разработчик, тестировщик и time.sleep (1), а он им и говорит: ребята, мы что, в анекдоте?

→ Ну почти: они в подводке к статье про то, как тестировать мл системы, что бы не потерять $100k.

7cyqwsgthirkhah1blztjpxn0bc.jpeg

1. Зачем тестировать ML-системы?

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

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

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

r_bj6wsfkxycdrnegv6eblc63cu.jpeg

1. Microsoft Tay

bs_7zyhwknpwtnutojdn9dzkejq.png

В 2016 году Microsoft запустила чат-бота Tay на платформе Twitter. Tay был разработан для обучения и взаимодействия с пользователями, используя машинное обучение. Основная идея заключалась в том, чтобы бот обучался в реальном времени, анализируя ответы пользователей и генерируя собственные ответы. Однако, в течение всего 16 часов после запуска, Tay начал генерировать оскорбительные и неприемлемые сообщения, что привело к значительным репутационным потерям для Microsoft.

2. Tesla и ошибка в системе автопилота

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

2. И чем-же отличается от классического тестирования?

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

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

2.1 Особенности тестирования больших моделей

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

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

2.2 Testing vs. Evaluation: В чем разница?

Многие разработчики знакомы с этапом оценки (evaluation), который следует после обучения модели. Это когда мы обучаем модель и оцениваем её работу на тестовом или валидационном наборе данных. На этом этапе используются такие метрики, как precision, recall, ROC AUC и другие, чтобы понять, насколько хорошо модель справляется с задачей.

ML тестирование — это более глубокий процесс. Это включает в себя не только проверку метрик, но и оценку поведения модели и её внутренней логики. Например, можно запускать предтренировочные тесты ещё до начала обучения, чтобы убедиться, что все элементы работают корректно. Например, проверяем, что вероятности классификации действительно лежат в диапазоне от 0 до 1, или оцениваем реакцию модели на аномальные данные и редкие случаи.

Процесс тестирования машинного обучения

Процесс тестирования машинного обучения

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

2.3 Pre-train тестирование

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

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

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

def test_dataset_shape():
    df = load_dataset()
    
    # Ожидаемое количество строк и столбцов
    expected_rows = 5
    expected_columns = 4
    
    assert df.shape == (expected_rows, expected_columns), f"Ожидалось {expected_rows} строк и {expected_columns} столбцов, но получили {df.shape}"

def test_dataset_dtypes():
    df = load_dataset()
    
    expected_dtypes = {
        'user_id': np.int64,
        'item_id': np.int64,
        'rating': np.int64,
        'timestamp': np.int64
    }
    
    for column, expected_dtype in expected_dtypes.items():
        assert df[column].dtype == expected_dtype, f"Ожидался тип данных {expected_dtype} для столбца '{column}', но получили {df[column].dtype}"

def test_model_output():
    df = load_dataset()
    model = RecommenderModel()
    
    X = df[['user_id', 'item_id']]
    predictions = model.predict(X)
    
    # Проверка, что тип данных вывода — float
    assert predictions.dtype == np.float64, f"Ожидался тип данных float64, но получили {predictions.dtype}"
    
    # Проверка, что все значения в диапазоне от 0 до 1
    assert (predictions >= 0).all() and (predictions <= 1).all(), "Все предсказания должны быть в диапазоне от 0 до 1"

2.4 Post-train тестирование

В пост-тренировочных тестах мы проверяем поведение модели после её обучения. Эти тесты позволяют убедиться, что модель действительно «усвоила» логику, которую мы пытались ей передать.

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

def test_all_users_in_output(model, users):
    for user_id in users:
        recommendations = model.recommend(user_id)
        assert len(recommendations) > 0, f"Пользователь {user_id} должен получать хотя бы одну рекомендацию"

def test_specific_user_recommendation_count(model, specific_user_id):
    recommendations = model.recommend(specific_user_id, top_n=5)
    assert len(recommendations) == 5, f"Пользователь {specific_user_id} должен получить ровно 5 рекомендаций"

def test_cold_user_recommendations(model, cold_user_id):
    recommendations = model.recommend(cold_user_id, top_n=5)
    popular_items = model.get_popular_items(top_n=5)
    assert set(recommendations) == set(popular_items), f"Для холодного пользователя {cold_user_id} должны быть рекомендованы популярные товары"

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

3. Окей, а какие тесты бывают?

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

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

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

p2biax7bk3ll_bre7sewdzwt1my.png

3.1.1 Unit tests

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

vayaenvkvakzrwot7buwdiz9yv8.png

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

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

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

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

@pytest.mark.parametrize('angle', [30, 45])
def test_detect_rotated_image(angle):
    img = cv2.imread('fixtures/car.jpg')

    rotated = rotate_image(img, angle=angle)
    detector = Detector()
    coords, detected_angle = detector(rotated)
    assert detected_angle == angle

Этот тест гарантирует, что алгоритм распознавания корректно определяет угол поворота объекта на изображении. Мы используем декоратор @pytest.mark.parametrize, чтобы запускать тест с разными углами и убедиться в корректной работе функции при различных входных данных.

3.1.2 Fixture

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

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

Пример теста с использованием фикстуры для функции crop_image:

import cv2
import numpy as np

def test_crop_image_with_fixture():
    img = cv2.imread('fixtures/full_img.jpg')
    
    cropped = crop_image(img, (10, 10, 10, 10))
    expected = cv2.imread('fixtures/cropped_img.jpg')
    np.testing.assert_equal(cropped, expected)

В этом тесте:

  • Загрузка исходного изображения: мы используем заранее подготовленный файл full_img.jpg из папки fixtures.

  • Обрезка изображения: функция crop_image принимает исходное изображение и координаты обрезки.

  • Сравнение с ожидаемым результатом: мы загружаем заранее подготовленное обрезанное изображение cropped_img.jpg и сравниваем его с результатом, полученным от функции crop_image.

3.2.1 Integration tests

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

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

0dijsk4cdhyfmbnk1vwtmwc7foi.png

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

def test_save_image(client):
    request_data = {'image_path': '/path/to/image.jpg'}
    
    response = client.post('/detect', request_data)
    assert response['success'] is True
    assert response['result_path'].endswith('.jpg')

В этом тесте:

  • Подготовка данных запроса: Создается словарь request_data с путем к изображению.

  • Отправка запроса к API: Клиент отправляет POST-запрос к эндпоинту /detect.

  • Проверка успешности ответа: Проверяется, что обработка изображения прошла успешно.

  • Проверка формата результата: Проверяется, что путь к сохраненному результату имеет правильное расширение.

3.2.2 Mocks and patches

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

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

Пример использования mocks и patches:

from unittest import mock

def get_angle():
    return 42

new_mock = mock.Mock(return_value=0)

with mock.patch('__main__.get_angle', new_mock):
    print(get_angle())
    print(new_mock.call_count)

3.3 Smoke tests

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

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

Пример smoke теста для сервиса с object detection:

import pytest
from my_object_detection_service import app

@pytest.fixture
def client():
    with app.test_client() as client:
        yield client

def test_smoke_detect_object(client):
    request_data = {'image_path': 'fixtures/test_image.jpg'}
    
    response = client.post('/detect', json=request_data)
    
    assert response.status_code == 200
    assert response.json['success'] is True
    
    assert 'coords' in response.json
    assert isinstance(response.json['coords'], list)
    assert len(response.json['coords']) > 0

3.4.1 Mutation tests

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

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

Пример mutation теста:

import pytest

def is_object_at_angle(detected_angle, target_angle):
    return detected_angle != target_angle
    
@pytest.mark.parametrize("detected_angle, target_angle, expected", [
    (30, 30, True),
    (45, 30, False),
])
def test_is_object_at_angle(detected_angle, target_angle, expected):
    assert is_object_at_angle(detected_angle, target_angle) == expected

3.4.2 Property-based tests

Property-based тесты проверяют, что определённые свойства или инварианты системы остаются верными для широкого диапазона входных данных. В отличие от обычных тестов, которые проверяют конкретные случаи, property-based тесты генерируют множество случайных входных данных и проверяют, что система ведет себя корректно для всех них.

Для нашего сервиса можно использовать property-based тесты, чтобы убедиться, что функция поворота изображения всегда возвращает результат в пределах допустимого угла от 0 до 360 градусов, независимо от входных данных. Это позволяет проверить устойчивость и корректность системы в условиях большого разнообразия данных.

import pytest
from hypothesis import given
from hypothesis.strategies import integers

def rotate_angle(angle, rotation):
    return (angle + rotation) % 360

@given(integers(min_value=0, max_value=360), integers(min_value=-720, max_value=720))
def test_rotate_angle(angle, rotation):
    result = rotate_angle(angle, rotation)
    assert 0 <= result < 360

В этом примере:

  • Используем библиотеку hypothesis, которая позволяет генерировать случайные значения для тестирования.

  • Функция rotate_angle корректно обрабатывает углы, чтобы они всегда оставались в пределах от 0 до 360 градусов.

  • Декоратор @given генерирует случайные значения для angle и rotation, а тест проверяет, что результат всегда находится в пределах допустимого диапазона.

Property-based тесты полезны для проверки устойчивости и корректности системы в условиях большого разнообразия входных данных, помогая находить скрытые ошибки и edge cases.

4. Best practices

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

4.1. Coverage tests

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

\text{test coverage} = \frac{\text{N of lines of code covered by tests}}{\text{total N of lines of code}}

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

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

Практические советы:

  • Используйте покрытие кода как ориентир для нахождения необхваченных тестами участков, но не стремитесь к 100% покрытию без необходимости.

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

  • Рассматривайте coverage как одну из метрик в совокупности с другими подходами тестирования, такими как unit, integration, smoke и mutation tests.

4.2. Add test when bug happens

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

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

4.3. Run test in CI/CD pipeline

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

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

Пример YAML конфигурации для GitHub Actions показывает, как можно

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.x'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest

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

4.4. Assertions vs. Runtime Checks: Почему стоит выбирать одно над другим

В процессе разработки и тестирования программного обеспечения, особенно в динамически типизированных языках, таких как Python, важно различать использование assertions и runtime checks. В то время как утверждения (assertions) полезны для тестирования, в рабочем окружении предпочтительно использовать проверки условий (conditional checks). Почему? Давайте разберемся.

Зачем использовать runtime checks вместо assertions?

assertions — это мощный инструмент, который разработчики часто используют для проверки кода во время тестирования. Однако есть несколько причин, почему в рабочем окружении предпочтительнее использовать проверки условий:

  • Оптимизация: При запуске Python с опцией -O (оптимизация) все утверждения (assert) игнорируются. Это может привести к тому, что критические проверки не будут выполняться в продакшене, что потенциально может привести к неожиданным ошибкам.

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

  • Принцип Python: Явное лучше, чем неявное: Согласно философии Python (Zen of Python), код должен быть явным, а не неявным. Использование явных проверок условий вместо утверждений делает код более прозрачным и понятным.

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

# Предпочтительный способ
if not input_data.shape == (100, 100, 3):
    logger.error(f"Got data shaped {input_data.shape} as input, expected (100, 100, 3)")
    raise SpecificRuntimeError("Some proper message")

# Утверждение (assert)
assert input_data.shape == (100, 100, 3)

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

Что мы должны проверять в runtime?

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

  • Формат входных данных: Например, для изображений — количество каналов, формат (BCHW, BHWC, HWC и т.д.)

  • Тип входных данных: Например, float32 vs uint8.

  • Диапазон входных данных: Например, 0…255 или 0…1 для изображений.

  • Другие специфичные для домена константы и инварианты: Например, проверка, что определитель матрицы вращения равен 1 для функции вращения изображения.

Пример проверки свойства в runtime:

def rotate_image(image, rotation_matrix):
    det = np.linalg.det(rotation_matrix)
    if np.abs(det - 1) > 1e-6:
        raise ValueError(f'Rotation matrix {rotation_matrix.tolist()} is scaled')
    ...

4.5. Positive and negative tests

При тестировании важно учитывать оба подхода — позитивные и негативные тесты. Позитивные тесты (positive tests) проверяют, что система работает корректно в «оптимистичных» сценариях, когда все данные валидны и все идет по плану. Они демонстрируют, что система способна правильно выполнять основные функции и предоставлять ожидаемые результаты при нормальных условиях.

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

oz9tmyumuxvmkyo6f7hazvdhhca.png

Примеры:

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

def test_process_image_valid_input():
    img = load_image('fixtures/valid_image.jpg')
    result = process_image(img)
    assert result is not None
    assert 'object_detected' in result

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

def test_process_image_invalid_input():
    invalid_data = "This is not an image"
    with pytest.raises(ValueError):
        process_image(invalid_data)

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

4.6. Dont mock ml models in unit tests

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

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

from transformers import AutoConfig, AutoModelForSequenceClassification

model_name = "valhalla/distilbart-mnli-12-1"
config = AutoConfig.from_pretrained("valhalla/distilbart-mnli-12-1")
model = AutoModelForSequenceClassification.from_config(config)
assert model.classification_head.out_proj.out_features == 3

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

def test_dispatch_model_bnb(self):
    """Tests that `dispatch_model` quantizes int8 layers"""
    from huggingface_hub import hf_hub_download
    from transformers import AutoConfig, AutoModel, BitsAndBytesConfig
    from transformers.utils.bitsandbytes import replace_with_bnb_linear

    with init_empty_weights():
        model = AutoModel.from_config(AutoConfig.from_pretrained("bigscience/bloom-560m"))

    quantization_config = BitsAndBytesConfig(load_in_8bit=True)
    model = replace_with_bnb_linear(
        model, modules_to_not_convert=["lm_head"], quantization_config=quantization_config
    )

    model_path = hf_hub_download("bigscience/bloom-560m", "pytorch_model.bin")

    model = load_checkpoint_and_dispatch(
        model,
        checkpoint=model_path,
        device_map="balanced",
    )

    assert model.h[0].self_attention.query_key_value.weight.dtype == torch.int8
    assert model.h[0].self_attention.query_key_value.weight.device.index == 0

    assert model.h[(-1)].self_attention.query_key_value.weight.dtype == torch.int8
    assert model.h[(-1)].self_attention.query_key_value.weight.device.index == 1

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

4.7. Exact Numbers Are Bad

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

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

import numpy as np

def test_model_accuracy():
    expected_accuracy = 0.85
    tolerance = 0.01  # Допустимая погрешность
    model_accuracy = compute_model_accuracy()
    assert np.isclose(model_accuracy, expected_accuracy, atol=tolerance)

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

4.8. Logging

Логирование — важная часть стратегии мониторинга и диагностики системы. Хорошее логирование позволяет:

  • Получать оповещения о сбоях.

  • Понимать, что именно сломалось и почему.

  • Анализировать тренды за длительные периоды.

  • Сравнивать поведение системы в разных версиях или условиях экспериментов (например, при A/B тестировании).

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

Однако логировать стоит только те события, которые добавляют полезный контекст. Например, простое сообщение «Rotated the image» не дает никакой информации о контексте операции. Вместо этого используйте логи с более детализированной информацией:

Пример плохого логирования:

logger.info("Rotated the image")

Такое логирование не предоставляет никакого контекста и не помогает понять, что именно произошло.

Пример хорошего логирования:

logger.info(f"Rotated the image loaded from {image_path} for angle {angle}")

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

Используйте логирование для:

  • Оповещения о сбоях.

  • Понимания того, что сломалось и почему.

  • Изучения трендов на длительных интервалах времени.

  • Сравнения поведения системы в разных версиях и условиях экспериментов.

Дополнительно

© Habrahabr.ru