Ранжирование рекомендаций на главной Авито: как мы перешли от CatBoost к нейросетям. CatBoost.. CatBoost. recsys.. CatBoost. recsys. Машинное обучение.. CatBoost. recsys. Машинное обучение. нейросети.. CatBoost. recsys. Машинное обучение. нейросети. Ранжирование.. CatBoost. recsys. Машинное обучение. нейросети. Ранжирование. рекомендательные системы.. CatBoost. recsys. Машинное обучение. нейросети. Ранжирование. рекомендательные системы. рекомендации.. CatBoost. recsys. Машинное обучение. нейросети. Ранжирование. рекомендательные системы. рекомендации. трансформеры.

Всем привет! Меня зовут Ярослав Хныков, я DS Tech Lead в Авито. Наша команда отвечает за рекомендации на главной странице Авито.

В статье расскажу, как мы шли от CatBoost к нейросетевому ранжированию: какие данные брали, почему первые АБ-тесты уходили в минус и что дало резкий прирост качества.

Будет полезно ML-инженерам и тимлидам, которые работают с ранжированием и рекомендательными системами.

В этой статье

Ранжирование рекомендаций на главной Авито: как мы перешли от CatBoost к нейросетям - 1

Как устроена лента рекомендаций Авито

Рекомендации на главной — первая точка касания пользователя с площадкой. На них приходится примерно половина всех просмотров объявлений и около 30% контактов покупателей с продавцами.

Устроено всё привычно. Когда приходит пользователь: 

1️⃣ Обращаемся к движкам кандидатогенерации — каждый нацелен на свой пользовательский сигнал — и из сотен миллионов объявлений получаем несколько тысяч самых релевантных. 

2️⃣ Обогащаем их признаками объявления и признаками пользователя, затем ранжируем. 

3️⃣ Отправляем ранжированную выдачу на слой диверсификации, который даёт финальное разнообразие ленты. Про это я писал в статье «Нейросетевая модель интересов пользователя».

ML-архитектура рекомендаций

ML-архитектура рекомендаций

Почему CatBoost перестал давать рост

CatBoost появился в рекомендациях Авито в 2020 году. Вначале это была простая модель, но к 2026 году она сильно развилась.

2020

2026

Датасет

80 тыс. выдач

больше 1 млн выдач

Таргет

бинарный: был ли просмотр

составной, из целевых действий

Лосс

классификационный

ранжирующий

Признаки

19 простых счётчиков

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

Несмотря на это, мы упёрлись в три ограничения:

🔴 Эффект «насыщения». Пробовали брать кратно больше данных, но качество не росло.

🔴 Эмбеддинги использовались не полностью. Появляется много мощных эмбеддингов для айтемов и пользователей. Но прямо подать их в модель нельзя, приходилось сжимать в dot-product’ы и счётчики — это субоптимально.

🔴 Ручные признаки стали узким местом. Чтобы подавать в CatBoost новый сигнал, нужно генерить много ручных признаков. Это неудобно, и здесь мы снова теряем часть информации.

💡 Нейросети позволяют обойти часть этих ограничений, поэтому мы посмотрели в их сторону. Предположение простое: есть много данных и хорошие эмбеддинги — обучим модель, и за счёт скейлинга она обыграет CatBoost. Забегая вперёд: получилось, но далеко не с первой попытки.

Тут еще больше контента

Как готовили данные для нейросети

За основу взяли датасет CatBoost — буквально показы объявлений на главной и таргет, который собирается из действий после показа. Сразу масштабировали его в 10 раз: до 10 миллионов выдач. Мы верили, что потенциал нейросети раскроется только на больших данных.

Дальше начинается то, чего с CatBoost делать не приходилось: весь препроцессинг у него под капотом, а для нейросети признаки нужно готовить руками. Это серьёзный недостаток. 

Вот как мы с ним живём:

Категориальные признаки. Для каждой фичи находим все возможные уникальные значения и маппим их в целочисленные индексы.

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

Ранжирование рекомендаций на главной Авито: как мы перешли от CatBoost к нейросетям - 4

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

Поэтому мы используем квантильное кодирование: предварительно считаем для каждой фичи 128 бинов по квантилям, а когда приходит новое значение, смотрим, между какими бинами оно находится, и рассчитываем вес — насколько близко оно к левому или правому бину.

В датасете храним простую тройку: индекс левого бина, индекс правого и вес. Внутри модели интерполируем номер бина и нормализуем его на количество бинов для этой фичи. Получается число от 0 до 1, при этом сохраняется монотонная связь с исходным признаком: растёт признак — растёт закодированное значение.

Ранжирование рекомендаций на главной Авито: как мы перешли от CatBoost к нейросетям - 5

Эмбеддинги. Хранить сырые вектора в датасете накладно. Здесь выручает наблюдение, что один и тот же айтем повторяется между выдачами — он может встречаться в десятках и сотнях тысяч.

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

Ранжирование рекомендаций на главной Авито: как мы перешли от CatBoost к нейросетям - 6

Пять итераций от MVP до прода

Модель №1. Быстро запустились. Мы решили, что в 2026 году нет смысла имплементировать всё самостоятельно, и попросили ИИ-агента набросать первую версию. Он сразу реализовал сложный энкодинг фичей и завёз DCNv2 со смесью экспертов и low-rank аппроксимациями. Обучили на бинарную кросс-энтропию — результаты оказались ужасными. 

Модель сильно проигрывала CatBoost, и из-за начальной сложности мы не могли понять, что пошло не так: проблема в препроцессинге или в модели? Оценить вклад каждой компоненты было нечем.

Но на первом этапе для нас было важнее не качество модели — сразу обойти CatBoost, который развивался годами, было бы сложно, — а возможность применять её онлайн. 

Для первых экспериментов мы использовали ONNX Runtime прямо внутри нашего сервиса на Go и поняли, что отскорить нейронкой все 5 000 кандидатов со всеми признаками и эмбеддингами не сможем.

Модель 1: быстрый старт

Модель 1: быстрый старт

Так появилась схема, которая живёт у нас до сих пор: CatBoost работает на L2 и отдаёт топ-300 лучших айтемов, а нейроранкер на L3 меняет порядок внутри этого топа. 

Мы используем раннее связывание кандидатов и признаков — по опыту оно ощутимо мощнее позднего, которое применяем в других моделях. Но у раннего связывания есть цена: посчитать его для очень большого пула кандидатов не получится, поэтому нейроранкер и работает только с топ-300, которые предварительно отобрал CatBoost.

На latency это тоже сказалось: инференс модели добавлял около 30 мс — не так много. Позже мы перенесли модель в Triton на GPU: инференс там быстрее, хотя появляются накладные расходы на передачу тензоров по сети. В сумме по всему пайплайну это добавило порядка 6% к latency.

🏁 Первый А/Б-тест показал ожидаемое: 

  • −3% NDCG в офлайне 

  • −6% пользователей с контактами из рекомендаций в онлайне

Зато мы проверили продакшн-контур, закрыли интеграционные баги, получили первый бейзлайн и пошли его улучшать.

Модель №2. Убрали лишнюю сложность. Начали с максимально простой архитектуры: вещественные и категориальные признаки, а поверх них — обычный MLP. На наших данных такая модель показала себя лучше, чем более сложная архитектура из предыдущей итерации.

Заодно поменяли лосс. Бинарная кросс-энтропия оптимизирует скор каждого кандидата отдельно, а в рекомендациях важен не скор конкретного кандидата, а итоговый порядок всей выдачи. Поэтому перешли на listwise-лосс. В статьях он встречается под названием ListMLE. 

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

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

🏁 Результаты второго А/Б:

  •  −1,5% NDCG

  •  −3% пользователей с контактами

Мы всё ещё заметно проигрывали, но офлайн улучшился — и онлайн потянулся за ним. Важный для нас шаг: значит, гипотезы можно проверять офлайн, и проверять их надо последовательно.

Модель 2: убрали лишнюю сложность

Модель 2: убрали лишнюю сложность

Модель №3. Встроили эмбеддинги. Мы поддержали проекцию для каждой версии эмбеддинга и задизайнили слой попарных интеракций, внутри которого считали всевозможные dot-product’ы попавших туда векторов.

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

🏁 И это сработало неплохо: 

  • В офлайне модель впервые обошла CatBoost: +0,7% NDCG

  • Целевая метрика — количество пользователей с контактами — снова упала, на 1,8%

  • Позитивный сигнал: пользователей с целевыми действиями, куда помимо контактов входит, например, добавление в избранное, стало больше на 0,7%

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

🏁 Смотрим офлайн-метрики в бизнес-разрезах:

  • −1,9% NDCG по контактам

  • +0,8% по целевым действиям

Контакты мы всё ещё ранжируем хуже CatBoost, а тотальная метрика растёт за счёт лучшего ранжирования добавлений в избранное — их в датасете намного больше. 

Вывод: смотрите на метрики в разрезах, иначе тотал замаскирует проблему с главной бизнес-метрикой.

Модель 3: важно смотреть на разрезы

Модель 3: важно смотреть на разрезы

Модель №4. Отбалансировали контакты. Оказалось, что если выкинуть хитрый слой попарных интеракций и подавать эмбеддинги напрямую в голову, тотальное качество не улучшается, но разрез по контактам растёт.

Дополнительно внедрили в лосс веса, которые сильнее штрафуют за неправильное ранжирование контактов. Game changer’ом это не стало, но офлайн-метрики баланса чуть улучшило.

🏁 В четвёртом А/Б модель наконец сравнялась с CatBoost: 

  • −0,6% пользователей с контактами

  • +0,6% с целевыми действиями

Контакты вышли в серую зону. Мы построили нейросеть, но сильного скачка качества не было.

В этом же АБ проверили ещё один подход: та же модель, но в топ-300 мы смешали ранги CatBoost и нейроранкера, чтобы получить финальное ранжирование. Приём известный по соревнованиям: модели разной природы хорошо компенсируют ошибки друг друга, и при блендинге общее качество, как правило, растёт.

🏁 Метрики потянулись: 

  • +0,7% пользователей с контактами 

  • +0,9% пользователей с целевыми действиями из рекомендаций

  • +0,4% пользователей с целевыми действиями по всему Авито 

Результат скромный, но такое изменение мы решили раскатить. У него была и важная инженерная подоплёка. Коэффициенты мы осознанно не подбирали — хотели сохранить вклад каждой модели одинаковым, иначе получилась бы сильная завязка на конкретные версии. А так CatBoost и нейроранкер можно развивать независимо: улучшаешь любую из них — сразу видишь продуктовый эффект, а удачные идеи по фичам и датасетам переносятся из одной модели в другую. И всё это время у нас остаётся стабильный продакшн-вариант.

Также отбалансировали loss: добавили веса и повысили значимость контактов. Изменение не дало значимого роста total NDCG, но улучшило баланс action/contact

Также отбалансировали loss: добавили веса и повысили значимость контактов. Изменение не дало значимого роста total NDCG, но улучшило баланс action/contact

Модель №5. Добавили историю пользователя в трансформер. История пользователя — вероятно, ключевой сигнал для рекомендаций. Один и тот же айтем может быть и хорошим, и плохим в зависимости от того, что человек недавно смотрел и чем интересовался. А у нас теперь есть нейросеть — ей можно скормить последовательность событий и получить представление каждого кандидата, которое зависит от истории.

Схема модели 5

Схема модели 5

Сначала мы отфильтровали историю до полезных целевых действий и в качестве бейзлайна ограничили её ёмкость 50 событиями. Клики добавлять не стали: они могли бы вытеснить из истории более значимые сигналы пользовательского интента.

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

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

Для кандидатов используем только айтемные эмбеддинги: у них нет типа события, источника и позиции в пользовательской истории.

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

История и кандидаты в одном трансформере

История и кандидаты в одном трансформере

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

Кандидаты получают контекст истории

Кандидаты получают контекст истории

Теперь предсказание для каждого кандидата зависит не только от него самого, но и от конкурентного контекста: какие ещё объявления есть в выдаче, среди каких альтернатив модель выбирает. Магии здесь нет — это обычный self-attention с маской.

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

Масштаб модели сейчас скромный — около 2,5 млн параметров. В ближайшие полгода выходить на сотни миллионов не планируем: сначала хотим выжать максимум из архитектуры.

Жми сюда!

Что получилось в онлайне

В офлайн-метриках мы видели сильный рост:

  •  +7,2% NDCG по целевым действиям

  • +4,9% по контактам

Он перешёл в онлайн:

  • +4,3% пользователей с контактами из рекомендаций

  • +3,3% пользователей с целевыми действиями

  • +0,8% пользователей, совершивших сделку

Последнее — самое важное. Эффект пророс в общую метрику Авито. Это одно из самых эффективных изменений в рекомендациях за последние годы.

История дала скачок качества

История дала скачок качества

Три вывода, которые можно взять

💡 Нейросети дают заметный эффект, если их правильно готовить. Придётся масштабировать датасет, настраивать признаки, подбирать лосс и адаптировать архитектуру под ваши данные — ручной работы здесь больше, чем с CatBoost.

💡 Офлайн-метрики должны вести к онлайн-результату. Для нейросетей это особенно важно: они гибкие, поэтому появляется много степеней свободы и гипотез, которые хочется проверить. Все гипотезы в онлайне проверить невозможно. Поэтому на ранних этапах важно понять, есть ли мэтч между офлайном и онлайном, — а если нет, разбираться в датасетах и особенностях метрик.

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

Вся статья кратко

CatBoost упёрся в потолок: больше данных перестало давать прирост, а эмбеддинги и новые сигналы требовали всё больше ручной работы.

Переход на нейросети пришлось делать постепенно: от простого MLP и ListMLE до эмбеддингов, блендинга с CatBoost и полноценного нейроранкера.

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

В онлайне модель дала +4,3% пользователей с контактами из рекомендаций и +0,8% пользователей, совершивших сделку.

Мы пишем про Data Science не только на Хабре, но и в телеграме. Переходите в канал «Доска AI-объявлений» — там много интересного про DS в Авито.

Кликни здесь и узнаешь

Автор: Yaroslav_Khnykov

Источник