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

Рекомендации на главной Авито: как всё устроено и какие возникают проблемы [2]
Внедрили блендер категорий на основе интересов пользователя [3]
Рекомендации на главной странице — это бесконечная персональная лента объявлений. Через неё проходит примерно 50% всех просмотров и 30% общего числа контактов покупателей с продавцами. Это первая точка входа и место, где легко «залипнуть», — но только если лента не превращается в однообразный поток похожих карточек.
Как выглядит ML-архитектура на главной Авито:
двухуровневая система: кандидатогенерация → ранжирование;
5 кандидатогенераторов: 1 работает офлайн и реагирует на последние действия пользователя с задержкой в несколько часов, а 4 действуют онлайн и подстраиваются мгновенно;
финальное ранжирование CatBoost с богатым набором фич: от простых счётчиков до скорингов трансформенных моделей.
Часто такой системы достаточно, чтобы показать действительно качественные рекомендации. Но есть проблема — «склейка» однотипных карточек объявлений.
Если мы просто отсортируем предложения по скору ранжирования и отдадим это в качестве выдачи, пользователь столкнётся с достаточно однообразной лентой.
Дело в том, что ранжирующая модель скорит объявления независимо, поэтому похожие предложения получают очень близкие скоры и после сортировки «склеиваются» в блоки. При тысячах кандидатов на входе ранжирования весь топ легко забивается одной-двумя категориями. Из-за этого снижается суммарная полезность ленты.
Представьте, что вам на первой позиции показали Айфон, а на второй позиции вам показали тот же Айфон, но другого цвета. Тогда вся добавочная ценность второго предложения заключается только в смене цвета. Некоторые предложения могут и вовсе повторяться, а пользователю может быть интересен не только Айфон.
Что получаем в итоге:
Убывающая полезность: второй и последующие подряд Айфоны дают мизерную добавочную ценность.
Узкое покрытие интересов: другие потенциально релевантные объявления, которые могут закрыть потребности [10] пользователя, не пробиваются в топ.
Снижение «интриги»: пользователь видит одно и то же. В итоге однотипная выдача может побудить его уйти из сервиса.
Чтобы улучшить ситуацию, мы решили дополнить систему выдачи рекомендаций дополнительным «слоем», направленным на повышение разнообразия.
Его цель — не «сломать» релевантность, а предотвратить склейку однотипных карточек.
Давайте обсудим принцип его работы:
1. Группируем объявления по категориям, сохраняя внутри категорий порядок, который задаётся моделью ранжирования. Для примера я взял три раздела: Питомцы, Недвижимость, Авто.
2. Считаем интерес [11] пользователя к категориям. Интерес — это счётчик событий с затуханием по времени. Для его расчёта мы забираем историю пользователя из онлайн-хранилища истории. Каждому типу события назначаем вес в зависимости от его важности. Например, добавление в избранное — более важный показатель пользовательского интереса, чем клик.
Далее мы складываем эти веса, дополнительно добавляя экспоненциальный дисконт по времени. Таким образом, отдаём приоритет более свежим событиям, чтобы быстрее реагировать [12] на смену пользовательских предпочтений.
3. Сэмплируем категорию из распределения категорийных интересов и забираем лучшее из доступных объявлений в ней.
4. Повторяем [13] до заполнения ленты.
Разнообразие можно регулировать сглаживанием распределения — чем сильнее сглаживание, тем больше разнообразия получим на выходе.
Благодаря такому простому механизму мы из блочно-однотипной выдачи получили достаточно разнообразную выдачу по категориям.
Результаты A/B-теста:
Давайте посмотрим, что это даёт пользователю. Мы запустили простой A/B-тест, где сравнили базовую систему рекомендаций до и после применения блендера. В результате получили:
+2,5% пользователей, совершивших контакт с продавцом. На масштабах Авито — это сотни тысяч дополнительных покупателей в сутки.
+4% пользователей, которые совершили контакт в новой для себя категории.
Рост кросс-категорийности обеспечен тем, что мы начинаем чаще показывать в топе менее очевидные, но всё ещё релевантные для пользователя категории.
Пока улучшали формулу, столкнулись с определёнными ограничениями:
Она не работает для новых категорий. Интерес определён лишь для тех категорий, с которыми пользователь уже взаимодействовал. Новым для пользователя категориям приходится присваивать константный ненулевой вес, не учитывая их реальную популярность.
Не строит связи между категориями. Например, пользователь искал хомячка и даже уже успел купить его. Но блендер не «понимает», что пора показать клетки и корм. Комплементарность не моделируется.
Медленно переключает контекст. Если пользователь резко сместил фокус, интерес к новой теме долго «догоняет» старую из-за инерции накопления событий.
Сложно встраивать дополнительные факторы. Например, мы хотели также учитывать источник события. Действия с поисковой выдачей — более явный сигнал пользовательского интереса, так как пользователь сам предварительно указал свою потребность в поисковой строке. Однако встраивание дополнительного фактора сводится к дополнительному подбору веса, что не очень удобно.
Мы решили разработать ML-модель интересов пользователя, чтобы обойти ограничения блендера. Заменили ручную формулу на модель, предсказывающую распределение интересов по категориям — то есть сразу дающую «правильные» веса для блендера с учётом контекста.
Расскажу, как строился весь процесс.
Ориентируемся на реальные моменты посещения главной. Что защищает нас от различных ликов.
В качестве инпута берём историю пользователя на момент посещения главной.
В качестве таргета берём пропорции целевых событий: добавление в избранное и контактные события по категориям в следующие 7 дней. С этим гиперпараметром можно экспериментировать, мы, например, остановились на компромиссном варианте — неделя.
Если взять слишком короткий промежуток, модель будет менее склонна предсказывать комплиментарные категории. Если же, наоборот, слишком длинный — модель начнёт скатываться в популярное, теряя связь с историей пользователя.
Исключаем просмотры из таргета, чтобы не обучаться под кликбейтные для пользователя категории.
Для генерализации модели берём не только моменты посещения главной, но и моменты запроса поисковой выдачи. События, в которых пользователь получает выдачу, мы называем генераторными.
Оставляем только первое целевое событие по каждому объявлению в качестве дополнительного препроцессинга. Так как не хотим предсказывать какие-то слишком очевидные сигналы.
Например, если мы знаем, что пользователь уже проконтактировал с продавцом по объявлению о продаже машины, то мы не хотим предсказывать, что потенциальный покупатель может после этого зайти в чат ещё раз, чтобы уточнить детали.
Для валидации разбиваем наши генераторные события по глобальному таймстемпу и дополнительно оставляем временной промежуток между обучением [14] модели и валидацией, чтобы излишне оптимистично не смотреть на метрики.
В качестве модели решили использовать Transformer Encoder, потому что он:
гибкий с точки зрения [15] добавления фичей и экспериментов с таргетами;
хорошо учитывает последовательный сигнал — важно в нашей задаче;
имеет довольно низкую задержку (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 по распределению категорий |
Из-за того, что в обучающих данных покупатели за целевой период часто активны лишь в одной-двух категориях, предсказания модели получаются немного вырожденными.
Чтобы лента не становилась из-за этого однообразной, мы используем температурный софтмакс и подбираем температуру на реальных данных.
Температуру мы подбираем через имитацию продового трафика, чтобы выровняться по релевантности на реальных пользовательских выдачах.
Валидация:
в рамках офлайн-валидации мы увидели рост метрик ранжирования категорий на 5–7% по сравнению с формулой-эвристикой, которую использовали ранее;
модель научилась строить более логичные связи между категориями. Например, после просмотра микрофона она определяет интерес к Музыкальным инструментам:
А просмотр футбольных бутс в категории Обувь говорит ей, что пользователю может быть интересен «Спорт и отдых»:
кроме того, модель корректно учитывает источники событий: данные из поиска оказывают большее влияние, чем из ленты рекомендаций.
A/B-тест:
Дополнительно к эвристике: +1% пользователей, совершивших контакт.
+0,6% пользователей, совершивших контакт в новой категории.
– 2,5% скрытий рекомендации с причиной «несоответствие категории» — при бóльшем разнообразии.
Метрики нас порадовали, и мы раскатили модель в продакшен.
Давайте повторим:
Мы начали с простой проблемы: однотипная выдача на главной странице снижала полезность ленты и «сжимала» пространство пользовательских интересов.
На первом шаге мы внедрили блендер категорий — он дал ощутимый прирост: больше контактов, больше кросс-категорийных взаимодействий и заметно более разнообразная лента.
Затем, чтобы уйти от ограничений ручной формулы интересов и научиться учитывать контекст, связи между категориями и источник событий, мы перешли на модель интересов на базе Transformer Encoder.
Она компактная, работает на CPU в real-time, корректно кодирует историю пользователя и предсказывает распределение категорий, которое значительно улучшает работу блендера.
Итоги A/B-тестов подтвердили эффект:
рост контактов и взаимодействий в новых категориях;
снижение скрытий «несоответствие категории» при большем разнообразии;
улучшение офлайн-метрик ранжирования на 5–7%.
Ну и несколько важных уроков:
В реальном продукте важна не только средняя релевантность ваших рекомендаций, но и то, как вы представляете их пользователю.
В рекомендациях важно работать не только над релевантностью, но и над тем, как именно выдача собирается и показывается пользователю.
Простые решения дают быстрый прирост, а усложнение модели оправдано только тогда, когда даёт устойчивый выигрыш в метриках и качестве продукта.
Ещё больше контента на тему data science — в канале: «Доска AI-объявлений» [16]. Заходите, там интересно.
Автор: Yaroslav_Khnykov
Источник [17]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/22966
URLs in this post:
[1] Авито: https://clc.to/LV3FBg
[2] Рекомендации на главной Авито: как всё устроено и какие возникают проблемы: #section1
[3] Внедрили блендер категорий на основе интересов пользователя: #section2
[4] Ограничения формулы интересов: #section3
[5] Собрали датасет для обучения ML-модели: #section4
[6] Выбрали ML-модель: #section5
[7] Сгладили предикты новой модели и запустили A/B-тест: #section6
[8] Что увидели по метрикам: #section7
[9] Вся статья кратко: #section8
[10] потребности: http://www.braintools.ru/article/9534
[11] интерес: http://www.braintools.ru/article/4220
[12] реагировать: http://www.braintools.ru/article/1549
[13] Повторяем: http://www.braintools.ru/article/4012
[14] обучением: http://www.braintools.ru/article/5125
[15] зрения: http://www.braintools.ru/article/6238
[16] «Доска AI-объявлений»: https://t.me/+UIIbsrA8OjU5ZDRi
[17] Источник: https://habr.com/ru/companies/avito/articles/974682/?utm_campaign=974682&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.