Почему модель может испортиться, даже если код и веса не менялись
Одна из самых неприятных особенностей ML-систем в том, что модель может деградировать без единой правки в репозитории. Код тот же, артефакт тот же, сервис отвечает как раньше, а качество постепенно становится хуже. Частая причина — data drift. Это ситуация, когда распределение входных данных в проде начинает отличаться от того, на чем модель обучалась.
Для инженера это очень важный сдвиг мышления: проблема иногда живет не внутри модели, а в том мире, который подается ей на вход.
Как drift удобно описать формально
$$ P_{train}(X) \neq P_{prod}(X) $$Data drift означает, что распределение признаков в продакшене стало другим по сравнению с обучающей выборкой. Даже если модель осталась прежней, ее рабочая среда изменилась.
P_{train}(X)— распределение признаков на обучающей выборкеP_{prod}(X)— распределение признаков на продовых данныхX— вектор входных признаков
Эта запись полезна тем, что убирает магию. Drift — это не ощущение, что «что-то пошло не так», а конкретное изменение входного распределения.
Почему это так опасно для модели
Потому что любая модель по сути учится на некотором мире примеров. Если мир меняется, даже хорошая закономерность может перестать работать. Пользователи меняют поведение, рынок меняет структуру спроса, логирование начинает писать новые категории, партнеры присылают данные в другой шкале — и модель постепенно оказывается в незнакомом пространстве. Особенно больно это для тонких признаковых конструкций и чувствительных моделей.
При этом drift не всегда сразу виден по бизнес-метрике. Иногда сначала меняются только входные признаки, а падение качества проявляется позже.
Как посмотреть на это на Python
import pandas as pd # создаем train и prod срезы одного и того же признака
train = pd.Series([10, 12, 11, 13, 12, 14], name='avg_check') # историческое распределение признака на train
prod = pd.Series([18, 20, 19, 22, 21, 23], name='avg_check') # новое распределение того же признака в проде
summary = pd.DataFrame({ # собираем простую сводку по двум срезам данных
'train_mean': [train.mean()], # среднее исторического распределения
'prod_mean': [prod.mean()], # среднее текущего продового распределения
'train_std': [train.std()], # разброс на обучении
'prod_std': [prod.std()], # разброс в проде
})
print(summary.round(3)) # сравниваем, насколько сильно входной мир сдвинулсяЭто очень простой индикатор, но даже он помогает увидеть суть: drift начинается с того, что модель получает уже не тот тип данных, под который была настроена. А значит, monitoring должен смотреть не только на предсказания, но и на входы.