Привет! С вами Ярослав Хныков — senior ML engineer в Авито. В статье расскажу, как мы повысили разнообразие и релевантность рекомендаций на главной странице.
Покажу, как появляется выдача с однотипными рекомендациями, чем здесь помогает простой «блендер» категорий и как мы прокачали его с помощью модели интересов пользователя, основанной на трансформерах. В конце — результаты A/B-тестов, метрики и рекомендации, которые вы сможете забрать к себе в продукт.
Статья будет особенно интересна специалистам, которые работают с рекомендательными системами.

Содержание
Рекомендации на главной Авито: как всё устроено и какие возникают проблемы
Рекомендации на главной странице — это бесконечная персональная лента объявлений. Через неё проходит примерно 50% всех просмотров и 30% общего числа контактов покупателей с продавцами. Это первая точка входа и место, где легко «залипнуть», — но только если лента не превращается в однообразный поток похожих карточек.
Как выглядит ML-архитектура на главной Авито:
двухуровневая система: кандидатогенерация → ранжирование;
5 кандидатогенераторов: 1 работает офлайн и реагирует на последние действия пользователя с задержкой в несколько часов, а 4 действуют онлайн и подстраиваются мгновенно;
финальное ранжирование CatBoost с богатым набором фич: от простых счётчиков до скорингов трансформенных моделей.

Часто такой системы достаточно, чтобы показать действительно качественные рекомендации. Но есть проблема — «склейка» однотипных карточек объявлений.
Если мы просто отсортируем предложения по скору ранжирования и отдадим это в качестве выдачи, пользователь столкнётся с достаточно однообразной лентой.
Дело в том, что ранжирующая модель скорит объявления независимо, поэтому похожие предложения получают очень близкие скоры и после сортировки «склеиваются» в блоки. При тысячах кандидатов на входе ранжирования весь топ легко забивается одной-двумя категориями. Из-за этого снижается суммарная полезность ленты.
Представьте, что вам на первой позиции показали Айфон, а на второй позиции вам показали тот же Айфон, но другого цвета. Тогда вся добавочная ценность второго предложения заключается только в смене цвета. Некоторые предложения могут и вовсе повторяться, а пользователю может быть интересен не только Айфон.
Что получаем в итоге:
Убывающая полезность: второй и последующие подряд Айфоны дают мизерную добавочную ценность.
Узкое покрытие интересов: другие потенциально релевантные объявления, которые могут закрыть потребности пользователя, не пробиваются в топ.
Снижение «интриги»: пользователь видит одно и то же. В итоге однотипная выдача может побудить его уйти из сервиса.
Чтобы улучшить ситуацию, мы решили дополнить систему выдачи рекомендаций дополнительным «слоем», направленным на повышение разнообразия.
Внедрили блендер категорий на основе интересов пользователя
Его цель — не «сломать» релевантность, а предотвратить склейку однотипных карточек.

Давайте обсудим принцип его работы:
1. Группируем объявления по категориям, сохраняя внутри категорий порядок, который задаётся моделью ранжирования. Для примера я взял три раздела: Питомцы, Недвижимость, Авто.
2. Считаем интерес пользователя к категориям. Интерес — это счётчик событий с затуханием по времени. Для его расчёта мы забираем историю пользователя из онлайн-хранилища истории. Каждому типу события назначаем вес в зависимости от его важности. Например, добавление в избранное — более важный показатель пользовательского интереса, чем клик.
Далее мы складываем эти веса, дополнительно добавляя экспоненциальный дисконт по времени. Таким образом, отдаём приоритет более свежим событиям, чтобы быстрее реагировать на смену пользовательских предпочтений.

3. Сэмплируем категорию из распределения категорийных интересов и забираем лучшее из доступных объявлений в ней.
4. Повторяем до заполнения ленты.
Разнообразие можно регулировать сглаживанием распределения — чем сильнее сглаживание, тем больше разнообразия получим на выходе.
Благодаря такому простому механизму мы из блочно-однотипной выдачи получили достаточно разнообразную выдачу по категориям.

Результаты A/B-теста:
Давайте посмотрим, что это даёт пользователю. Мы запустили простой A/B-тест, где сравнили базовую систему рекомендаций до и после применения блендера. В результате получили:
+2,5% пользователей, совершивших контакт с продавцом. На масштабах Авито — это сотни тысяч дополнительных покупателей в сутки.
+4% пользователей, которые совершили контакт в новой для себя категории.
Рост кросс-категорийности обеспечен тем, что мы начинаем чаще показывать в топе менее очевидные, но всё ещё релевантные для пользователя категории.
Ограничения формулы интересов
Пока улучшали формулу, столкнулись с определёнными ограничениями:
Она не работает для новых категорий. Интерес определён лишь для тех категорий, с которыми пользователь уже взаимодействовал. Новым для пользователя категориям приходится присваивать константный ненулевой вес, не учитывая их реальную популярность.
Не строит связи между категориями. Например, пользователь искал хомячка и даже уже успел купить его. Но блендер не «понимает», что пора показать клетки и корм. Комплементарность не моделируется.
Медленно переключает контекст. Если пользователь резко сместил фокус, интерес к новой теме долго «догоняет» старую из-за инерции накопления событий.
Сложно встраивать дополнительные факторы. Например, мы хотели также учитывать источник события. Действия с поисковой выдачей — более явный сигнал пользовательского интереса, так как пользователь сам предварительно указал свою потребность в поисковой строке. Однако встраивание дополнительного фактора сводится к дополнительному подбору веса, что не очень удобно.
Мы решили разработать ML-модель интересов пользователя, чтобы обойти ограничения блендера. Заменили ручную формулу на модель, предсказывающую распределение интересов по категориям — то есть сразу дающую «правильные» веса для блендера с учётом контекста.
Расскажу, как строился весь процесс.
Собрали датасет для обучения ML-модели
Ориентируемся на реальные моменты посещения главной. Что защищает нас от различных ликов.
В качестве инпута берём историю пользователя на момент посещения главной.
В качестве таргета берём пропорции целевых событий: добавление в избранное и контактные события по категориям в следующие 7 дней. С этим гиперпараметром можно экспериментировать, мы, например, остановились на компромиссном варианте — неделя.
Если взять слишком короткий промежуток, модель будет менее склонна предсказывать комплиментарные категории. Если же, наоборот, слишком длинный — модель начнёт скатываться в популярное, теряя связь с историей пользователя.

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

Выбрали ML-модель
В качестве модели решили использовать Transformer Encoder, потому что он:
гибкий с точки зрения добавления фичей и экспериментов с таргетами;
хорошо учитывает последовательный сигнал — важно в нашей задаче;
имеет довольно низкую задержку (latency) для использования в real-time: применение небольшого трансформера на CPU укладывается в 10–20 мс.

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

Позиция: используем стандартные обучаемые позиционные эмбеддинги. Единственная тонкость заключается в том, что мы кодируем историю от последнего события к первому. Мы знаем, что последние события — самые важные в истории пользователя, поэтому хотим сохранить для них некоторую инвариантность.
Возраст события: здесь мы хотели сохранить непрерывность из прежней формулы интересов, поэтому использовали следующий подход.
Возраст события в секундах (∆T) линейно интерполируется в диапазон: [-1, 1], где 1 — текущий момент, а -1 — некоторый момент в прошлом. В нашем случае — 2 месяца назад. В результате получается такая формула:
Это значение затем пропускается через небольшой MLP для обучения нелинейной функции затухания, а его вывод проецируется в общую размерность модели.

Чтобы закодировать событие полностью, нам достаточно всё сложить: эмбеддинги позиции, возраста и атрибуты. Далее нормируем результат и подаём в Transformer Encoder.

Основные параметры модели:
Параметр |
Значение |
Размерность вектора |
64 |
Максимальная длина истории |
512 событий |
Количество слоёв энкодера |
2 |
Число параметров |
Чуть больше 1 миллиона |
Функция активации |
HardSwish — даёт 1.5× ускорения на CPU |
Функция потерь |
Cross Entropy Loss по распределению категорий |
Сгладили предикты новой модели и запустили A/B-тест
Из-за того, что в обучающих данных покупатели за целевой период часто активны лишь в одной-двух категориях, предсказания модели получаются немного вырожденными.
Чтобы лента не становилась из-за этого однообразной, мы используем температурный софтмакс и подбираем температуру на реальных данных.

Температуру мы подбираем через имитацию продового трафика, чтобы выровняться по релевантности на реальных пользовательских выдачах.
Что увидели по метрикам
Валидация:
в рамках офлайн-валидации мы увидели рост метрик ранжирования категорий на 5–7% по сравнению с формулой-эвристикой, которую использовали ранее;
модель научилась строить более логичные связи между категориями. Например, после просмотра микрофона она определяет интерес к Музыкальным инструментам:

А просмотр футбольных бутс в категории Обувь говорит ей, что пользователю может быть интересен «Спорт и отдых»:

кроме того, модель корректно учитывает источники событий: данные из поиска оказывают большее влияние, чем из ленты рекомендаций.
A/B-тест:
Дополнительно к эвристике: +1% пользователей, совершивших контакт.
+0,6% пользователей, совершивших контакт в новой категории.
– 2,5% скрытий рекомендации с причиной «несоответствие категории» — при бóльшем разнообразии.
Метрики нас порадовали, и мы раскатили модель в продакшен.
Вся статья кратко
Давайте повторим:
Мы начали с простой проблемы: однотипная выдача на главной странице снижала полезность ленты и «сжимала» пространство пользовательских интересов.
На первом шаге мы внедрили блендер категорий — он дал ощутимый прирост: больше контактов, больше кросс-категорийных взаимодействий и заметно более разнообразная лента.
-
Затем, чтобы уйти от ограничений ручной формулы интересов и научиться учитывать контекст, связи между категориями и источник событий, мы перешли на модель интересов на базе Transformer Encoder.
Она компактная, работает на CPU в real-time, корректно кодирует историю пользователя и предсказывает распределение категорий, которое значительно улучшает работу блендера.
Итоги A/B-тестов подтвердили эффект:
рост контактов и взаимодействий в новых категориях;
снижение скрытий «несоответствие категории» при большем разнообразии;
улучшение офлайн-метрик ранжирования на 5–7%.
Ну и несколько важных уроков:
В реальном продукте важна не только средняя релевантность ваших рекомендаций, но и то, как вы представляете их пользователю.
В рекомендациях важно работать не только над релевантностью, но и над тем, как именно выдача собирается и показывается пользователю.
Простые решения дают быстрый прирост, а усложнение модели оправдано только тогда, когда даёт устойчивый выигрыш в метриках и качестве продукта.
Ещё больше контента на тему data science — в канале: «Доска AI-объявлений». Заходите, там интересно.
Комментарии (2)

Dakar
09.12.2025 16:27А вы уже догадались убирать из рекомендаций заказанное/купленное?
И может не надо кидаться с рекомендациями по результатам одного открытого объявления? Так случайно или мимоходом откроешь какую-нибудь фигню и потом долго любуешься на поток такой фигни.
coden12
Почему то иногда поиск принудительно сужается до какого-то раздела и из корневого не доступен. Переход в корневой можно вручную осуществить только в поисковой строке браузера.