- BrainTools - https://www.braintools.ru -
Модель оценивает риск отмены заказа продавцом. На проверочном наборе она выглядит убедительно. После запуска часть заказов получает неожиданно низкий риск, хотя код модели [1] и порог не менялись.
Разработчик открывает карточку одного заказа и видит: в обучающей строке у продавца две отмены за последние семь дней, а в запросе к модели во время оформления заказа не было ни одной.
Команда сначала проверяет версию модели. Но расхождение может возникнуть раньше, при сборке признака. Обучающая таблица была выгружена спустя неделю и содержала окончательный счётчик отмен. Сервис принимал решение в конкретный момент, когда часть отмен ещё не произошла или не поступила в систему.
Ниже — моделируемый сценарий, а не инцидент из действующего маркетплейса. На четырёх заказах разберём, как воспроизвести несовпадение, отделить разные причины и поставить проверку перед релизом. Числа заданы в исполняемом примере; качество реальной модели из них не следует.
Я Иннокентий Бодров, Product Leader с опытом [2] более 18 лет в технологических продуктах. В статье покажу, как сравнить обучающую строку с фактическим входом модели, отделить будущие события от поздно доставленных и превратить расхождение в проверяемое условие релиза.
Training–serving skew обычно называют несовпадение условий обучения [3] и применения модели. Google в «Rules of Machine Learning» [4] перечисляет несколько причин: различную обработку данных, изменение данных между обучением и применением и обратную связь от решений модели. В руководстве по мониторингу производственных ML-систем [5] отдельно названы несовпадение схемы входа и несовпадение рассчитанных признаков.
Эти случаи полезно разделять при расследовании:
|
Наблюдение |
Возможная причина |
Что сравнивать |
|---|---|---|
|
В сервисе нет поля или изменился тип |
Несовпадение схемы |
Названия, типы, долю пропусков, допустимые значения |
|
Поле есть, но для того же заказа значение другое |
Разные преобразования или момент расчёта |
Признак по идентификатору заказа и времени прогноза |
|
Значения признаков совпадают, но качество падает позже |
Изменение потока, связи признака с целью или самого процесса |
Распределения, метки после их появления, продуктовые метрики |
Разница распределений ещё не доказывает ошибку [6] в коде: поток продавцов мог измениться. И одинаковые распределения не гарантируют равенство строк. Если у двух заказов переставить значения признака местами, гистограмма останется прежней, а предсказания могут измениться. Поэтому агрегатный мониторинг дополняют сравнением тех же объектов в тот же момент принятия решения.
Пусть модель использует 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.
|
Заказ |
Продавец |
Прогноз |
Счётчик в поздней выгрузке |
Доступно при прогнозе |
|---|---|---|---|---|
|
|
|
10:00 |
2 |
0 |
|
|
|
12:00 |
2 |
1 |
|
|
|
14:00 |
2 |
2 |
|
|
|
12:00 |
1 |
0 |
Поздняя выгрузка отвечает на вопрос «сколько отмен мы знаем сейчас?». Для обучающей строки нужен другой вопрос: «сколько отмен было доступно тогда, когда модель должна была оценить заказ?». Это правило не зависит от того, учим мы дерево решений или нейросеть.
Если ранжировать продавца условным правилом счётчик >= 1, поздняя выгрузка изменит бинарное решение для O1 и O4. У O2 значение тоже расходится, хотя решение при таком пороге остаётся тем же. Для настоящей модели иного порога последствия надо пересчитать, а не выводить из этой таблицы.
Код на стандартной библиотеке Python намеренно не обучает модель. Он воспроизводит входной признак для четырёх заказов и показывает ошибку в подготовке обучающих строк. В рабочем расследовании вместо списка событий берут сохранённый журнал входов модели и версионированные источники событий.
from datetime import datetime, timedelta, timezone
UTC = timezone.utc
def 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"] <= end
for event in events
)
for order in orders:
offline = late_snapshot(order["seller"])
at_decision = as_seen_at_decision(order)
print(order["id"], offline, at_decision,
int(offline >= 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 0
O2 2 1 1 1
O3 2 2 1 1
O4 1 0 1 0
Пример выявляет сразу две ошибки. Для O1 поздняя выгрузка включает будущие отмены продавца. Для O4 событие уже произошло, но сервис получил его только после прогноза. Ограничения по occurred_at недостаточно: нужно знать время доступности записи. У O2 поздняя выгрузка добавляет второе событие, которое произошло до заказа, но поступило позже.
Здесь окно в семь суток задано как интервал [decision_at − 7 суток, decision_at). Правая граница исключена, чтобы событие с тем же временем, что и решение, не попадало в расчёт без явного правила порядка обработки. В реальном потоке потребуются точность timestamp, единая временная зона, стабильные идентификаторы и правило для повторно доставленных событий. Упрощённый код не решает эти задачи автоматически.
Тестовый набор должен содержать решения, для которых можно восстановить реальный вход модели. Минимальная строка: request_id, entity_id, decision_at, версия модели, версия вычисления признаков, значение признака на входе сервиса и источник для его исторического пересчёта. request_id защищает от ложного сопоставления двух заказов одного продавца.
Для каждого признака сравнение идёт в два шага:
Контракт: поле есть с обеих сторон; совпадают тип, единицы измерения, значение по умолчанию и правила обработки пропусков. Например, число отмен и доля отмен — разные признаки, даже если обе колонки названы похоже.
Парный пересчёт: для одних и тех же запросов восстанавливаем значение на decision_at, затем сравниваем его с сохранённым значением на входе модели. Сохраняем число несовпавших строк, долю несовпадений, величину разницы и распределение по источникам заказов.
Проверка offline == online на сегодняшней базе не годится для вчерашнего заказа: оба значения уже могут содержать исправления и поздние события. Нужен либо журнал входных признаков модели, либо исторический источник, который позволяет восстановить состояние на момент решения. Google рекомендует сохранять признаки, использованные при обслуживании запросов [4], и использовать их для проверки согласованности с обучением. Даже небольшая диагностическая выборка таких записей ценнее сравнения двух несвязанных гистограмм.
При этом журнал входов — не полная копия всех персональных данных «на всякий случай». Состав, срок хранения и доступ к нему определяют в рамках политики данных продукта. Для расследования часто хватает выбранных числовых признаков, идентификатора запроса и версий конвейера.
Для исторического набора нужен point-in-time join: значение признака выбирают относительно времени каждой обучающей строки. Документация Feast [7] показывает, как исторический запрос ищет подходящую запись признака назад от timestamp конкретной строки и ограничивает поиск TTL. Это устраняет типовую ошибку, когда к старому решению присоединяют сегодняшнее значение.
Но сама идея соединения по времени события ещё не доказывает, что признак был доступен сервису тогда. Если источник получает события с задержкой или исправляет прошлые записи, для точного воспроизведения нужен отдельный available_at либо сохранённая версия состояния, увиденная сервисом. В зависимости от системы событие может иметь и другие времена: создание у партнёра, приём в шину, запись в хранилище, материализацию в онлайн-хранилище. Для сравнения надо выбрать именно ту границу, после которой модель могла использовать значение.
Общая функция преобразования для обучения и обслуживания уменьшает риск, что одна ветка округляет или заполняет пропуски иначе. Руководство TensorFlow по препроцессингу [8] рекомендует для онлайн-прогнозов способы, при которых согласованные преобразования и рассчитанные при обучении статистики применяются при обслуживании. Это полезный подход, но он не вернёт прошлое состояние из источника, где история уже перезаписана.
Полезно зафиксировать контракт признака: определение, окно, точку отсчёта, источник, временную зону, допустимую задержку, правило для поздних событий и владельца. Тогда слово seller_cancellations_7d перестаёт скрывать несколько разных вычислений.
Для релизной проверки я бы разделил гейты по типам сбоя:
|
Гейт |
Пример условия |
Что он поймает |
|---|---|---|
|
Схема |
Нет новых обязательных полей и несовместимых типов |
Поломку контракта входа |
|
Построчное равенство |
На контрольных запросах нет необъяснённых расхождений критичных признаков |
Разный расчёт для тех же объектов |
|
Время доступности |
Исторический признак не использует запись после |
Будущую информацию и поздние события |
|
Покрытие |
Доля пропусков и устаревших значений не выше согласованного порога |
Сбой доставки и материализации |
|
Продуктовый контроль |
После появления меток качество и нагрузка на процесс остаются в границах |
Последствия для решений, которые гейты данных могли пропустить |
Порог для доли несовпадений нельзя взять из этой статьи. Для критичного признака, который определяет автоматическое решение, команда может потребовать точного совпадения на эталонном наборе. Для приблизительных счётчиков допустимую погрешность и задержку надо согласовать отдельно. Само наличие несовпадения не доказывает, что модель стала хуже; оно показывает, что офлайн-оценка описывает другой вход.
TensorFlow Data Validation [9] умеет проверять схему, статистические аномалии и различие распределений обучающих и обслуживающих данных. Авторы исследования Google о валидации данных для ML [10] описали применение системы в производственных конвейерах и случаи, где неожиданное поведение [11] данных требовало отдельной диагностики. Эти инструменты помогают заметить проблему на уровне набора. Построчный тест из нашего примера решает более узкую задачу: сверяет конкретное значение, на основании которого принято конкретное решение.
После исправления признака прежнюю офлайн-метрику нельзя автоматически считать актуальной. Следует заново собрать обучение и проверку с корректным историческим состоянием, выбрать порог на отдельной выборке и сравнить кандидата с текущей версией на тех же объектах. Затем проверить поток после запуска: реальные входные признаки, задержки событий, долю пропусков и качество, когда появятся метки. Если метки появляются только у части заказов, оценка качества потребует отдельной проверки отбора; журнал признаков эту проблему не решает.
В четырёх смоделированных заказах поздняя выгрузка расходится с доступным на момент прогноза значением в трёх строках. Условное бинарное решение меняется в двух. Для исправления мало заменить общий счётчик на фильтр по occurred_at: заказ O4 всё равно получил бы событие, которого сервис ещё не видел. Проверка должна восстановить и время события, и время его доступности.
Тот же подход применим к другим признакам: сумме платежей за сутки, числу обращений в поддержку, текущему тарифу клиента. Для каждого нужно спросить, что было известно системе в момент решения, сохранить фактический вход модели и сравнить его с исторической обучающей строкой. Тогда расхождение становится воспроизводимой ошибкой с идентификатором запроса, временем и значением признака, а не расплывчатой жалобой на «дрифт».
Построчное сравнение обучающего признака и сохранённого входа модели показывает расхождение, которое может скрыться за одинаковыми агрегатными распределениями.
Для исторического значения важны оба времени: когда событие произошло и когда система получила его. Фильтра по времени события недостаточно для поздней доставки.
Проверка перед релизом должна фиксировать равенство критичных признаков, допустимую задержку и долю пропусков на одних и тех же объектах.
После исправления признака офлайн-оценку собирают заново. В проде отдельно наблюдают входные данные и качество после появления меток.
Если модель хорошо проходит офлайн-тесты, но ошибается в продакшене, важно проверить не только алгоритм, но и путь данных до него. Научитесь отслеживать задержки доставки событий, контролировать состояние конвейеров данных и вовремя замечать проблемы, которые влияют на входные признаки модели. Это поможет находить причины нестабильных прогнозов до того, как они начнут влиять на продукт.
Разобраться с инструментами наблюдаемости и надёжности конвейеров данных помогут бесплатные открытые уроки:
30 ноября, 20:00. «Grafana на практике: от настройки дашбордов до автоматизации». Записаться [12]
9 декабря, 20:00. «Apache Kafka в Observability: как построить надёжный конвейер данных». Записаться [13]
Больше открытых уроков ищите в дайджесте OTUS. [14]
Автор: innokentyBo
Источник [15]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/38281
URLs in this post:
[1] код модели: https://otus.pw/DUPm/
[2] опытом: http://www.braintools.ru/article/6952
[3] обучения: http://www.braintools.ru/article/5125
[4] Google в «Rules of Machine Learning»: https://developers.google.com/machine-learning/guides/rules-of-ml/
[5] мониторингу производственных ML-систем: https://developers.google.com/machine-learning/crash-course/production-ml-systems/monitoring
[6] ошибку: http://www.braintools.ru/article/4192
[7] Документация Feast: https://docs.feast.dev/getting-started/concepts/point-in-time-joins
[8] Руководство TensorFlow по препроцессингу: https://www.tensorflow.org/tfx/guide/tft_bestpractices
[9] TensorFlow Data Validation: https://www.tensorflow.org/tfx/data_validation/get_started
[10] исследования Google о валидации данных для ML: https://research.google/pubs/data-validation-for-machine-learning/
[11] поведение: http://www.braintools.ru/article/9372
[12] Записаться: https://otus.pw/pGA4/
[13] Записаться: https://otus.pw/KaR1/
[14] дайджесте OTUS.: https://otus.pw/Vk19/
[15] Источник: https://habr.com/ru/companies/otus/articles/1091202/?utm_campaign=1091202&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.