В ноутбуке модель точна, в API ошибается: сохранить preprocessing вместе с моделью

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

Артефакт предсказания начинается до модели

Все преобразования, статистика которых получена при fit, должны сопровождать модель: масштабирование, заполнение пропусков, кодирование категорий. Pipeline выполняет их в том же порядке при predict. Для API контрактом становится сырой набор полей, который принимает первый шаг пайплайна. Это не отменяет проверки типов и бизнес-значений, но убирает отдельную ручную копию preprocessing в коде сервиса.

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

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X = np.array([[0.0], [1.0], [2.0], [8.0], [9.0], [10.0]])
y = np.array([0, 0, 0, 1, 1, 1])
service = make_pipeline(StandardScaler(), LogisticRegression()).fit(X, y)
raw = np.array([[2.0]])
correct = service.predict_proba(raw)[0, 1]
wrong = service[-1].predict_proba(raw)[0, 1]
print("pipeline:", round(correct, 3), "classifier only:", round(wrong, 3))
assert correct < 0.5 < wrong

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

Когда preprocessing уже выполняется в SQL или отдельном feature-сервисе, всё равно нужен единый контракт преобразований. Например, категория «новый клиент» может кодироваться в сервисе как строка, а при обучении как целое число. Если весь путь нельзя поместить в sklearn Pipeline, сохраните версию внешнего преобразования рядом с моделью и проверяйте контрольные сырые записи после него. Главное — сравнивать не только итоговый прогноз, но и значения признаков перед последним estimator: это сокращает поиск причины при расхождении.

Что хранить рядом с пайплайном

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

Проверочный запрос лучше хранить в сыром виде: ровно такой JSON, какой получает API, с ожидаемым ответом и версией артефакта. Если сравнивать только уже преобразованную матрицу, тест обойдёт наиболее уязвимый участок. Для категориальных полей проверьте незнакомое значение; для числовых — пропуск и единицы измерения. Никакой успешный health-check, который просто загрузил объект в память, не доказывает, что преобразования действительно вызваны при предсказании.

В курсе «Data Scientist: от данных до деплоя моделей» работа с признаками продолжается подготовкой модели к деплою и тестированием inference API. Посмотрите программу курса, чтобы собрать и проверить этот контур целиком.

Продолжить на практике

  • Data Scientist: от данных до деплоя моделей ↗

    Практический курс по Data Science: от постановки ML-задачи и работы с данными до inference API, CI/CD и мониторинга моделей в продакшне. Вы пройдёте EDA, feature engineering, обучение и оценку моделей, experiment tracking, error analysis и deployment — чтобы собирать полноценный рабочий контур вокруг ML-решения.