Всем привет! Меня зовут Ярослав Хныков, я DS Tech Lead в Авито. Наша команда отвечает за рекомендации на главной странице Авито.
В статье расскажу, как мы шли от CatBoost к нейросетевому ранжированию: какие данные брали, почему первые АБ-тесты уходили в минус и что дало резкий прирост качества.
Будет полезно ML-инженерам и тимлидам, которые работают с ранжированием и рекомендательными системами.
В этой статье

Как устроена лента рекомендаций Авито
Рекомендации на главной — первая точка касания пользователя с площадкой. На них приходится примерно половина всех просмотров объявлений и около 30% контактов покупателей с продавцами.
Устроено всё привычно. Когда приходит пользователь:
1️⃣ Обращаемся к движкам кандидатогенерации — каждый нацелен на свой пользовательский сигнал — и из сотен миллионов объявлений получаем несколько тысяч самых релевантных.
2️⃣ Обогащаем их признаками объявления и признаками пользователя, затем ранжируем.
3️⃣ Отправляем ранжированную выдачу на слой диверсификации, который даёт финальное разнообразие ленты. Про это я писал в статье «Нейросетевая модель интересов пользователя».
Почему CatBoost перестал давать рост
CatBoost появился в рекомендациях Авито в 2020 году. Вначале это была простая модель, но к 2026 году она сильно развилась.
|
|
2020 |
2026 |
|
Датасет |
80 тыс. выдач |
больше 1 млн выдач |
|
Таргет |
бинарный: был ли просмотр |
составной, из целевых действий |
|
Лосс |
классификационный |
ранжирующий |
|
Признаки |
19 простых счётчиков |
около 200: признаки над эмбеддингами и нейросетевые предсказания |
Несмотря на это, мы упёрлись в три ограничения:
🔴 Эффект «насыщения». Пробовали брать кратно больше данных, но качество не росло.
🔴 Эмбеддинги использовались не полностью. Появляется много мощных эмбеддингов для айтемов и пользователей. Но прямо подать их в модель нельзя, приходилось сжимать в dot-product’ы и счётчики — это субоптимально.
🔴 Ручные признаки стали узким местом. Чтобы подавать в CatBoost новый сигнал, нужно генерить много ручных признаков. Это неудобно, и здесь мы снова теряем часть информации.
💡 Нейросети позволяют обойти часть этих ограничений, поэтому мы посмотрели в их сторону. Предположение простое: есть много данных и хорошие эмбеддинги — обучим модель, и за счёт скейлинга она обыграет CatBoost. Забегая вперёд: получилось, но далеко не с первой попытки.
Как готовили данные для нейросети
За основу взяли датасет CatBoost — буквально показы объявлений на главной и таргет, который собирается из действий после показа. Сразу масштабировали его в 10 раз: до 10 миллионов выдач. Мы верили, что потенциал нейросети раскроется только на больших данных.
Дальше начинается то, чего с CatBoost делать не приходилось: весь препроцессинг у него под капотом, а для нейросети признаки нужно готовить руками. Это серьёзный недостаток.
Вот как мы с ним живём:
Категориальные признаки. Для каждой фичи находим все возможные уникальные значения и маппим их в целочисленные индексы.
В модели каждая фича смотрит в свою обучаемую эмбеддинг-таблицу по этому индексу. Трюки с хешированием нам не нужны: категориальных фичей немного и кардинальность у каждой невысокая.

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

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

Пять итераций от MVP до прода
Модель №1. Быстро запустились. Мы решили, что в 2026 году нет смысла имплементировать всё самостоятельно, и попросили ИИ-агента набросать первую версию. Он сразу реализовал сложный энкодинг фичей и завёз DCNv2 со смесью экспертов и low-rank аппроксимациями. Обучили на бинарную кросс-энтропию — результаты оказались ужасными.
Модель сильно проигрывала CatBoost, и из-за начальной сложности мы не могли понять, что пошло не так: проблема в препроцессинге или в модели? Оценить вклад каждой компоненты было нечем.
Но на первом этапе для нас было важнее не качество модели — сразу обойти CatBoost, который развивался годами, было бы сложно, — а возможность применять её онлайн.
Для первых экспериментов мы использовали ONNX Runtime прямо внутри нашего сервиса на Go и поняли, что отскорить нейронкой все 5 000 кандидатов со всеми признаками и эмбеддингами не сможем.
Так появилась схема, которая живёт у нас до сих пор: 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% пользователей с контактами
Мы всё ещё заметно проигрывали, но офлайн улучшился — и онлайн потянулся за ним. Важный для нас шаг: значит, гипотезы можно проверять офлайн, и проверять их надо последовательно.
Модель №3. Встроили эмбеддинги. Мы поддержали проекцию для каждой версии эмбеддинга и задизайнили слой попарных интеракций, внутри которого считали всевозможные dot-product’ы попавших туда векторов.
Расчёт был на то, что модель выучит такие проекции, при которых эти произведения — например, между разными версиями юзерских и айтемных эмбедов — станут осмысленными и понесут сигнал релевантности.
🏁 И это сработало неплохо:
-
В офлайне модель впервые обошла CatBoost: +0,7% NDCG
-
Целевая метрика — количество пользователей с контактами — снова упала, на 1,8%
-
Позитивный сигнал: пользователей с целевыми действиями, куда помимо контактов входит, например, добавление в избранное, стало больше на 0,7%
Тут можно было расстроиться и решить, что офлайн у нас не сходится с онлайном — но всё оказалось проще.
🏁 Смотрим офлайн-метрики в бизнес-разрезах:
-
−1,9% NDCG по контактам
-
+0,8% по целевым действиям
Контакты мы всё ещё ранжируем хуже CatBoost, а тотальная метрика растёт за счёт лучшего ранжирования добавлений в избранное — их в датасете намного больше.
Вывод: смотрите на метрики в разрезах, иначе тотал замаскирует проблему с главной бизнес-метрикой.
Модель №4. Отбалансировали контакты. Оказалось, что если выкинуть хитрый слой попарных интеракций и подавать эмбеддинги напрямую в голову, тотальное качество не улучшается, но разрез по контактам растёт.
Дополнительно внедрили в лосс веса, которые сильнее штрафуют за неправильное ранжирование контактов. Game changer’ом это не стало, но офлайн-метрики баланса чуть улучшило.
🏁 В четвёртом А/Б модель наконец сравнялась с CatBoost:
-
−0,6% пользователей с контактами
-
+0,6% с целевыми действиями
Контакты вышли в серую зону. Мы построили нейросеть, но сильного скачка качества не было.
В этом же АБ проверили ещё один подход: та же модель, но в топ-300 мы смешали ранги CatBoost и нейроранкера, чтобы получить финальное ранжирование. Приём известный по соревнованиям: модели разной природы хорошо компенсируют ошибки друг друга, и при блендинге общее качество, как правило, растёт.
🏁 Метрики потянулись:
-
+0,7% пользователей с контактами
-
+0,9% пользователей с целевыми действиями из рекомендаций
-
+0,4% пользователей с целевыми действиями по всему Авито
Результат скромный, но такое изменение мы решили раскатить. У него была и важная инженерная подоплёка. Коэффициенты мы осознанно не подбирали — хотели сохранить вклад каждой модели одинаковым, иначе получилась бы сильная завязка на конкретные версии. А так CatBoost и нейроранкер можно развивать независимо: улучшаешь любую из них — сразу видишь продуктовый эффект, а удачные идеи по фичам и датасетам переносятся из одной модели в другую. И всё это время у нас остаётся стабильный продакшн-вариант.
Модель №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


