Всем привет! Меня зовут Аня Шатшнайдер, я старший BI-разработчик в команде антифрода. Мы пользуемся сотнями ИИ-моделей, чтобы бороться с действиями недобросовестных пользователей. Но иногда модели работают не так эффективно, как следует. Поэтому моя команда решила найти аутсайдеров и передать их на доработку DS-инженерам. Расскажу, как мы пересматривали подход к оценке моделей.
Статья будет полезна аналитикам и DS, которые работают с несколькими ML-моделями в проде и хоть раз озадачились вопросом, как сравнивать их между собой.
Статья будет полезна аналитикам и DS, которые работают с несколькими ML-моделями в проде и хоть раз озадачились вопросом, как сравнивать их между собой.
Наши модели работают в разных сервисах, пишут логи в различных форматах, одни выдают скоры, другие флаги 0/1, а в конце этой цепочки стоит система санкций и обеления, если действие пользователя ошибочно оценили как недопустимое.
Изменения в антифроде мы запускаем быстро, поскольку это помогает опережать недобросовестных пользователей. В результате за всё время накопилось много моделей, из-за чего простой вопрос «какая из них вносит наибольший вклад, и нет ли дублей?» превращается в отдельное исследование и съедает заметную часть аналитического ресурса.
Мы не могли найти лучшую модель
Давайте сразу обозначим контекст. В антифроде Авито модель — это часть длинной цепочки: Предсказание модели → запрос на санкцию → обеление, то есть возможная отмена санкции → применение → прохождение санкции (пользователь сталкивается с ограничением: например, ему нужно пройти дополнительную проверку) → блокировка (или нет).
Каждое звено этой цепочки раньше было автономным, со своими таблицами, логами и дашбордами, из-за чего мы испытывали ряд трудностей.
Не получалось связать модель и её последствия. В логах не было сквозного идентификатора. Модель предсказала фрод — окей, это где-то записалось. Потом к пользователю применилась санкция — это записалось в другом месте. Само решение принималось корректно, но проанализировать работу было сложно.
Не было быстрого способа надёжно сопоставить, что вот эта конкретная санкция — следствие этого предсказания той модели. Мы проводили анализ вручную, что занимало много времени, из-за чего быстро измерять вклад каждой модели на потоке мы не могли. В некоторых сервисах логирования не хватало: часть срабатываний вообще не фиксировалась, потому что агрегация была на пользователя и логировалась только одно срабатывание за период.
Обеления ломали любую наивную метрику. Чтобы не наказывать пользователей зря, у нас работает механизм обелений: в некоторых случаях санкция не применяется, даже если модель настаивает. Это хорошо для пользователей, но плохо для аналитики — без сквозного ID связать обеление с предсказанием конкретной модели тоже было сложно.
Нет понимания, как учитывать в аналитике пересечения. Отправить пользователя на санкцию может не одна модель, а сразу несколько. В аналитических отчётах это не учитывалось, поскольку каждая оценивалась отдельно. В итоге на действия одного и того же пользователя могли среагировать две модели, и обе выглядели эффективными, хотя по факту они просто дублировали решение друг друга.
AUC не работает. Это площадь под ROC-кривой — стандартная метрика качества классификатора. Она показывает, насколько хорошо ваша модель способна отличать один класс от другого. Но классический ROC-AUC требует таргета: мы должны знать, реально ли пользователь был фродером. А после запуска модели в прод у нас этого знания нет, потому что получить таргет можно только через ручную разметку, что довольно дорого. Значит, офлайн-метрики качества классификации нам мало что скажут про реальную пользу модели.
В итоге аналитики на недели уходили в ручные расследования, у каждого был свой подход, и честно сравнить модели между собой было практически невозможно. Некоторые из них были неэффективными, но никто об этом не знал, потому что не было данных.
Переформулировали задачу и выделили слои работы: данные, метрики и визуализация
Изначально задача пришла к нам в таком виде: «Разработать инструмент для оценки эффективности моделей».
Сначала мы попробовали решить её в лоб и быстро упёрлись в то, что дашборд поверх поломанных логов не лечит поломанные логи. Если в данных нет связи между моделью и её последствиями, никакая визуализация не поможет.
Поэтому задачу мы переформулировали: построить сквозную систему оценки моделей от логирования до метрики, которая покажет не только количество срабатываний, но и дальнейшие шаги в цепочке.
К этой задаче мы подошли с нестандартным подходом:
1️⃣ Заранее зафиксировали измеримый результат. Ещё до старта договорились, что считаем успехом — health score дашбордов не ниже жёлтого, а оценка от пользователей — 4,5+ (минимум 5 оценок).
Health score — это метрика здоровья разных аналитических объектов — дашбордов, датасетов и витрин внутри Авито. Каждый объект находится в зелёной, жёлтой или красной зоне в зависимости от его состояния. Для дашбордов зона зависит от значения health score по шкале от 1 до 100. Чем выше скор, тем лучше:
🟢 — выше 80, значит, это здоровый объект.
🟡 — от 60 до 80 означает, что дашборду нужны небольшие доработки.
🔴 — ниже 60 значит, что дашборд надо серьёзно переработать.
Сверхрезультат — зелёный health score, мы нашли важные инсайты и запланировали на их основе работы в следующем периоде. Критерий согласовали заранее, а потом честно замерили опросом стейкхолдеров прямо в Redash.
2️⃣ Вовлекли ключевого стейкхолдера в разработку. Мы добавили в контрибьюторы ключевого стейкхолдера — DS-лида и договорились, что он регулярно участвует в разработке вместе с нами. Работали по принципу: «Скажи мне — и я забуду, покажи мне — и я запомню, вовлеки меня — и я пойму». Для BI-задач это нетипичная практика, но именно она помогла попасть в реальные потребности команд.
Дальше работа распалась на три слоя в строгом порядке: сначала данные, затем метрика, и только в конце визуализация.
Слой данных из сквозных ID и новых витрин
У нас не было идентификаторов и было неясно, что и где именно не работает. Поэтому мы начали работу именно с внедрения ID.
Ввели единый идентификатор запроса на санкцию — punisher_request_id. Это UUID, который проходит через всю цепочку модель → обеление → санкция и позволяет связать между собой звенья, которые раньше находились в разных таблицах. Этот же ID мы добавили в уже существующие витрины с обелениями, санкциями и скорами. Так быстро и просто соединили всё в единое целое.
Переработанное логирование. В одном из сервисов со скоринговыми моделями заодно отработали технический долг — переделали схему логирования. Теперь всё стало аккуратнее:
Теперь в логах по каждому срабатыванию есть ID пользователя, название модели, скор, флаг тестового режима, требуемая санкция и её параметры, а также ID отправки на санкцию.
📗 Вывод 1: сквозной ID — обязательный элемент, если у вас есть многошаговый процесс и каждому событию нужен идентификатор, который будет связывать эти шаги. Без него в будущем будет невозможно связать причину со следствием.
Реализовали 7 новых витрин в DWH на Trino. Это каркас, на котором держится остальная аналитика и отчётность:
Добавили одно событие в ClickStream — «Запрос в ОРР об отправке пользователя на санкцию». Теперь каждый такой запрос фиксируется независимо от его результата.
Главное, связали все действия моделей. Теперь по одному punisher_request_id можно восстановить всю цепочку: «Модель X предсказала фрод, отправила в ОРР запрос на санкции Y и Z, пользователь был обелён для Y, а санкция Z в итоге применилась».
Главное, связали все действия моделей. Теперь по одному punisher_request_id можно восстановить всю цепочку: «Модель X предсказала фрод, отправила в ОРР запрос на санкции Y и Z, пользователь был обелён для Y, а санкция Z в итоге применилась».
📗 Вывод 2: аналитика начинается с логов, а не с дашборда. Сколько бы красивых графиков вы ни нарисовали поверх кривых данных, они останутся графиками поверх кривых данных. Мы потратили значительную часть времени именно на слой с логами и не жалеем.
📗 И сразу вывод 3: В антифроде, да и не только, модель живёт в связке с тем, что происходит после предсказания: обелениями, санкциями, прохождением. Оценивать имеет смысл всю цепочку, а не только «угадала или не угадала».
Слой MES из метрик эффективности моделей
Когда данные, наконец, стали связными, появилась возможность нормально считать. ROC-AUC мы использовать не можем, потому что таргета нет. Значит, надо смотреть, что реально произошло с пользователем после срабатывания модели. Так появилась MES (Model Effectiveness Score) — сводная метрика из шести компонентов:
MES = Σ(Компонент × Вес) / 6
Чем выше MES, тем эффективнее модель в связке с санкцией.
Каждый компонент закрывает одну проблему из первой части статьи:
Три конверсии (в санкцию, прохождение или блокировку) — реальная оценка вместо ROC-AUC. Они показывают, доводит ли модель дело до конца. Ведь есть вероятность, что она просто выдаст флаг и на этом успокоится.
Конверсия на прохождение санкции пользователем получила вес 2, потому что важно понимать, насколько она останавливает потенциальных недобросовестных пользователей.
False Positive Rate — модель не должна зарабатывать очки за счёт того, что наказывает всех подряд. Мы постоянно работаем над снижением ложных отправок пользователей на санкции.
Уникальность и степень неуникальности показывает, нет ли дублирования работы других моделей. Если модель срабатывает только там, где уже сработали пять других, её вклад невелик, даже если конверсии на бумаге красивые.
Три конверсии (в санкцию, прохождение или блокировку) — реальная оценка вместо ROC-AUC. Они показывают, доводит ли модель дело до конца. Ведь есть вероятность, что она просто выдаст флаг и на этом успокоится.
Конверсия на прохождение санкции пользователем получила вес 2, потому что важно понимать, насколько она останавливает потенциальных недобросовестных пользователей.
False Positive Rate — модель не должна зарабатывать очки за счёт того, что наказывает всех подряд. Мы постоянно работаем над снижением ложных отправок пользователей на санкции.
Уникальность и степень неуникальности показывает, нет ли дублирования работы других моделей. Если модель срабатывает только там, где уже сработали пять других, её вклад невелик, даже если конверсии на бумаге красивые.
📗 Вывод 4: Частота срабатываний ≠ полезность. Модель, которая 1000 раз триггерится на одном пользователе, не в 1000 раз лучше модели, сработавшей один раз. Учитывайте уникальность срабатываний как по пользователю, так и между моделями.
Слой дашбордов из 10 штук
Все доски находятся в Redash — это стандартный стек для Авито из Trino и Redash. Всего получилось 10 дашбордов, объединённых в один отчёт. Я специально сделала их много, потому что один универсальный дашборд на всё обычно превращается в кашу, которую никто не читает.
Бонус: Мы получили системный инструмент и единый язык, на котором команды из разных сервисов теперь могут обсуждать эффективность своих моделей. И это, пожалуй, оказалось самым ценным.
Что получилось в итоге
👉 Модели из разных сервисов и команд теперь можно сравнивать по одной метрике, с учётом нескольких факторов, а не только конверсии.
👉 8 неэффективных моделей нашлись сразу. Одну взяли в переработку одновременно с выходом отчёта, ещё 4 уже отключили или переработали, а остальные запланированы на следующие периоды.
👉 Теперь MES подсвечивает проблемные модели. Именно на неё смотрят при решениях о переработке.
👉 7 витрин живут отдельной жизнью. Их используют в дашбордах, а также в обычных задачах аналитиков и DS. Оказалось, что нормальные логи нужны всем.
👉 Ручной анализ схлопнулся, и там, где раньше уходили дни, теперь хватает одного отчёта.
👉 Мы достигли и перевыполнили цель. Итоговая оценка дашбордов от пользователей — 5.0, health score — зелёный, а среди моделей нашлись те, что мы отправили в переработку. Это даже выше планки, которую мы ставили себе на старте.
Если у вас есть свой опыт построения таких систем — расскажите в комментариях, особенно интересны истории, где пришлось отказаться от AUC в пользу продуктовых метрик. И задавайте вопросы, с радостью отвечу.
А если вы хотите узнать больше о работе аналитиков в Авито — подписывайтесь на канал «Коммуналка аналитиков»