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

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

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

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

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

Я Иннокентий Бодров, Product Leader с опытом более 18 лет в технологических продуктах. В статье покажу, как сравнить обучающую строку с фактическим входом модели, отделить будущие события от поздно доставленных и превратить расхождение в проверяемое условие релиза.

Где именно возникает расхождение

Training–serving skew обычно называют несовпадение условий обучения и применения модели. Google в «Rules of Machine Learning» перечисляет несколько причин: различную обработку данных, изменение данных между обучением и применением и обратную связь от решений модели. В руководстве по мониторингу производственных ML-систем отдельно названы несовпадение схемы входа и несовпадение рассчитанных признаков.

Эти случаи полезно разделять при расследовании:

Наблюдение

Возможная причина

Что сравнивать

В сервисе нет поля или изменился тип

Несовпадение схемы

Названия, типы, долю пропусков, допустимые значения

Поле есть, но для того же заказа значение другое

Разные преобразования или момент расчёта

Признак по идентификатору заказа и времени прогноза

Значения признаков совпадают, но качество падает позже

Изменение потока, связи признака с целью или самого процесса

Распределения, метки после их появления, продуктовые метрики

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

Один признак и четыре решения

Пусть модель использует seller_cancellations_7d — число отмен продавца за семь суток до оформления заказа. Для каждого заказа есть decision_at, время прогноза. У события отмены есть два времени:

  • occurred_at: когда отмена произошла;

  • available_at: когда запись стала доступна сервису признаков.

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

В тестовом наборе два продавца. У продавца S1 отмены произошли в 11:00 и 11:30; первая стала доступна в 11:02, вторая — только в 13:00. У S2 отмена произошла в 09:00, но запись пришла в 13:00. Заказы оценивались в 10:00, 12:00 и 14:00.

Заказ

Продавец

Прогноз

Счётчик в поздней выгрузке

Доступно при прогнозе

O1

S1

10:00

2

0

O2

S1

12:00

2

1

O3

S1

14:00

2

2

O4

S2

12:00

1

0

Поздняя выгрузка отвечает на вопрос «сколько отмен мы знаем сейчас?». Для обучающей строки нужен другой вопрос: «сколько отмен было доступно тогда, когда модель должна была оценить заказ?». Это правило не зависит от того, учим мы дерево решений или нейросеть.

Если ранжировать продавца условным правилом счётчик >= 1, поздняя выгрузка изменит бинарное решение для O1 и O4. У O2 значение тоже расходится, хотя решение при таком пороге остаётся тем же. Для настоящей модели иного порога последствия надо пересчитать, а не выводить из этой таблицы.

Воспроизводим ошибку без ML-библиотек

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

from datetime import datetime, timedelta, timezoneUTC = timezone.utcdef time(hour, minute=0): return datetime(2026, 9, 1, hour, minute, tzinfo=UTC)orders = [ {"id": "O1", "seller": "S1", "decision_at": time(10)}, {"id": "O2", "seller": "S1", "decision_at": time(12)}, {"id": "O3", "seller": "S1", "decision_at": time(14)}, {"id": "O4", "seller": "S2", "decision_at": time(12)},]events = [ {"seller": "S1", "occurred_at": time(11), "available_at": time(11, 2)}, {"seller": "S1", "occurred_at": time(11, 30), "available_at": time(13)}, {"seller": "S2", "occurred_at": time(9), "available_at": time(13)},]def late_snapshot(seller): """Ошибочная выгрузка: итоговый счётчик на дату извлечения.""" return sum(event["seller"] == seller for event in events)def as_seen_at_decision(order): """События за 7 суток, доступные сервису к моменту решения.""" end = order["decision_at"] start = end - timedelta(days=7) return sum( event["seller"] == order["seller"] and start <= event["occurred_at"] < end and event["available_at"] = 1), int(at_decision >= 1))assert [as_seen_at_decision(order) for order in orders] == [0, 1, 2, 0]assert [late_snapshot(order["seller"]) for order in orders] == [2, 2, 2, 1]

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

O1 2 0 1 0O2 2 1 1 1O3 2 2 1 1O4 1 0 1 0

Пример выявляет сразу две ошибки. Для O1 поздняя выгрузка включает будущие отмены продавца. Для O4 событие уже произошло, но сервис получил его только после прогноза. Ограничения по occurred_at недостаточно: нужно знать время доступности записи. У O2 поздняя выгрузка добавляет второе событие, которое произошло до заказа, но поступило позже.

Здесь окно в семь суток задано как интервал [decision_at − 7 суток, decision_at). Правая граница исключена, чтобы событие с тем же временем, что и решение, не попадало в расчёт без явного правила порядка обработки. В реальном потоке потребуются точность timestamp, единая временная зона, стабильные идентификаторы и правило для повторно доставленных событий. Упрощённый код не решает эти задачи автоматически.

Что сравнить в действующей системе

Тестовый набор должен содержать решения, для которых можно восстановить реальный вход модели. Минимальная строка: request_id, entity_id, decision_at, версия модели, версия вычисления признаков, значение признака на входе сервиса и источник для его исторического пересчёта. request_id защищает от ложного сопоставления двух заказов одного продавца.

Для каждого признака сравнение идёт в два шага:

  1. Контракт: поле есть с обеих сторон; совпадают тип, единицы измерения, значение по умолчанию и правила обработки пропусков. Например, число отмен и доля отмен — разные признаки, даже если обе колонки названы похоже.

  2. Парный пересчёт: для одних и тех же запросов восстанавливаем значение на decision_at, затем сравниваем его с сохранённым значением на входе модели. Сохраняем число несовпавших строк, долю несовпадений, величину разницы и распределение по источникам заказов.

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

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

Как вернуть обучению состояние на момент решения

Для исторического набора нужен point-in-time join: значение признака выбирают относительно времени каждой обучающей строки. Документация Feast показывает, как исторический запрос ищет подходящую запись признака назад от timestamp конкретной строки и ограничивает поиск TTL. Это устраняет типовую ошибку, когда к старому решению присоединяют сегодняшнее значение.

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

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

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

Что автоматизировать перед релизом

Для релизной проверки я бы разделил гейты по типам сбоя:

Гейт

Пример условия

Что он поймает

Схема

Нет новых обязательных полей и несовместимых типов

Поломку контракта входа

Построчное равенство

На контрольных запросах нет необъяснённых расхождений критичных признаков

Разный расчёт для тех же объектов

Время доступности

Исторический признак не использует запись после decision_at

Будущую информацию и поздние события

Покрытие

Доля пропусков и устаревших значений не выше согласованного порога

Сбой доставки и материализации

Продуктовый контроль

После появления меток качество и нагрузка на процесс остаются в границах

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

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

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

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

Что показывает маленький тест

В четырёх смоделированных заказах поздняя выгрузка расходится с доступным на момент прогноза значением в трёх строках. Условное бинарное решение меняется в двух. Для исправления мало заменить общий счётчик на фильтр по occurred_at: заказ O4 всё равно получил бы событие, которого сервис ещё не видел. Проверка должна восстановить и время события, и время его доступности.

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

Выводы

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

  • Для исторического значения важны оба времени: когда событие произошло и когда система получила его. Фильтра по времени события недостаточно для поздней доставки.

  • Проверка перед релизом должна фиксировать равенство критичных признаков, допустимую задержку и долю пропусков на одних и тех же объектах.

  • После исправления признака офлайн-оценку собирают заново. В проде отдельно наблюдают входные данные и качество после появления меток.

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

Разобраться с инструментами наблюдаемости и надёжности конвейеров данных помогут бесплатные открытые уроки:

  • 30 ноября, 20:00. «Grafana на практике: от настройки дашбордов до автоматизации». Записаться

  • 9 декабря, 20:00. «Apache Kafka в Observability: как построить надёжный конвейер данных». Записаться

Больше открытых уроков ищите в дайджесте OTUS.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев