Привет! Меня зовут Саша Михеев, и я работаю в Авито над развитием персонализации пользователей. Делаю так, чтобы покупатели видели объявления, которые могут их заинтересовать. Расскажу, как мы внедряли трансформеры, чтобы улучшить рекомендации для пользователей.
Статья будет полезна data scientist- и ML-инженерам, ML-Ops-специалистам и продакт-менеджерам.

Предыдущая версия системы рекомендаций
Мы всегда старались показывать пользователям только релевантные объявления, чтобы увеличить вероятность сделки. Одна из последних версий системы рекомендаций работала в три шага:
Шаг 1. Кандидатогенерация — подбирали объявления-кандидатов для рекомендаций из специализированных моделей кандидатогенераторов.
Шаг 2. Все кандидаты попадали в рекомендательный ранкер, который ранжировал их с точки зрения релевантности пользователю. Для этого использовали скоры кандидатогенераторов и различные кликовые фичи. Например, популярность объявления и интерес пользователей к категории.
Шаг 3. Формировалась финальная выдача, в которой применяли различные бизнес-логики, например, смешивали объявления из разных категорий. Всё работало следующим образом. Сначала мы случайно и взвешенно сэмплировали категории для каждого слота в выдаче. В качестве веса использовали интерес пользователя к данной категории, который предсказывали специальной моделью. Когда определялась категория для слота, туда попадало самое релевантное объявление с точки зрения ранкера.
В Авито много кандидатогенераторов разных типов, например, коллаборативные, графовые, нейросетевые. Одни из основных — нейросетевые модели — item2vec и user2vec.
item2vec создавала эмбеддинги для объявлений. Если покупатель взаимодействовал с несколькими объявлениями в рамках одной сессии, item2vec создавала для них близкие эмбеддинги. Это позволяло рекомендовать пользователю другие объявления, семантически близкие к тем, с которыми он взаимодействовал.
Подробнее о принципах работы item2vec можно почитать в статье: «Как мы используем item2vec для рекомендаций похожих товаров».
user2vec создавала эмбеддинг пользователя. Он состоял из взвешенной суммы эмбеддингов объявлений, с которыми он взаимодействовал. При этом вес со временем убывал, то есть больший вес имели объявления, которыми пользователь интересовался недавно. Подход работал хорошо и быстро, но у него были недостатки:
Фактически модель аппроксимировала интерес пользователя линейной комбинацией уже просмотренных объявлений, что ограничивало её выразительность и не позволяло учитывать сложные нелинейные зависимости, смену намерения и контекст взаимодействий.
Например, если пользователь интересовался велосипедами, линейная модель в основном предлагала другие велосипеды и хуже справлялась с рекомендацией сопутствующих товаров — покрышек, смазок или аксессуаров. Даже если они были логически связаны с намерениями покупателя.
Мы не учитывали порядок пользовательских действий. Да, у недавних действий вес больше, но явной зависимости нет.
Модель не учитывала поисковые запросы пользователя.
Новый подход к системе рекомендаций
Хотелось создать модель, которая учтёт эти проблемы, будет генерировать эмбеддинги пользователей и находить кандидатов в реальном времени. Для этой цели хорошо подошла двухбашенная архитектура.

-
одна башня генерирует эмбеддинги объявлений, которые складываются в кэш-хранилище, откуда можно быстро их достать;
-
вторая создаёт эмбеддинг пользователя из истории его действий. Это последовательность, поэтому мы применили архитектуру трансформера.
Актуальность конкретного объявления определяется величиной скалярного произведения между эмбеддингами из двух башен.
Особенности данных
Мы оперируем большим объёмом данных. Это сотни миллионов объявлений в датасете, а также сотни миллионов поисковых запросов и действий каждый день: клики, просмотры, добавления в избранное.
Все действия пользователей поделили на 2 основных типа:
-
поисковые запросы;
-
клики, добавления в избранное и другие активности с объявлениями.
Нам требовалось закодировать эти типы действий с помощью параметров объявления или поиска, чтобы получить эмбеддинги каждого, и потом подавать их в трансформер.
Выделили четыре типа параметров (фичей):
-
бинарные: например, наличие или отсутствие доставки, частное лицо или компания;
-
категорийные: например, категория, локация товара;
-
числовые: например, цена, рейтинг продавца, радиус поиска;
-
последовательные: например, заголовок, описание и инфомодельные параметры для объявления, текст запроса для поиска.
Инфомодельные параметры
Инфомодель — это набор всех категорийных характеристик для всех объявлений в виде пары ключ-значение. Например, для категории Недвижимость такой парой может быть «тип объекта — квартира», для автомобилей — «марка — Жигули».
Мы отбираем 10 000 самых популярных категорийных характеристик и ставим им в соответствие эмбеддинг для каждой характеристики. Далее эмбеддинг всех категорийных характеристик объявления агрегируем также, как для текста.
Деление на группы понадобилось, потому что мы хотели сделать разные размерности эмбеддингов для каждой. Логично, что в заголовке объявления из группы последовательных фичей находится больше информации, чем, например, для бинарных (есть ли для объявления доставка или нет). Исходя из этого мы понимаем, что размерность эмбеддинга для последовательных фичей логично сделать больше.
Для каждой из групп была своя nn.Embedding матрица, в которой находились все фичи, то есть внутри всех групп была сквозная индексация.
Например, для категорийных фичей внутри матрицы находились эмбеддинги для ID-категории, ID-локации и остальных категорийных признаков. Это позволяло избежать лишних итераций перебора фичей внутри форварда модели. Так мы ускорили обучение, поскольку фичей было много.
Числовые фичи. Мы кодируем эмбеддингами такой же размерности, как для категорийных фичей, чтобы не уменьшить вклад числовых.
Встаёт вопрос, как кодировать числовые признаки? Можно, например, разбить числовой диапазон признака на бины — это интервалы для группировки значений. Для каждого бина поставить в соответствие эмбеддинг. Если значение фичи попадает в какой-то бин, мы берём его эмбеддинг.
В таком подходе есть проблема
Если у нас два разных значения фичи попали в один бин, то у них получаются одинаковые эмбеддинги. Поэтому мы делаем иначе. Привязываем эмбеддинги не к самим бинам, а к их границам. В результате получаем значение эмбеддинга для конкретной фичи в виде взвешенной суммы эмбеддингов границ бина, где веса отражают близость до границ.
Текстовые фичи. Разбиваем текст на токены и для каждого ставим в соответствие эмбеддинг. В результате эмбеддинг текста — это сумма эмбеддингов токенов.
Обучающие сэмплы и данные для модели
Один обучающий пример (сэмпл) представляет собой пару из действия пользователя в его истории и объявления, с которым произошло таргет-действие. При формировании обучающих данных мы учитываем разные сценарии пользовательского поведения.
Мы хотим моделировать краткосрочные интересы: рекомендовать пользователю объявления, семантически похожие на те, с которыми он взаимодействовал недавно. Например, если пользователь просматривал объявления с животными, модель должна рекомендовать объявления с похожими животными. Также мы хотим учитывать и долгосрочные интересы пользователя.
Для этого между последним событием в истории пользователя и таргетным объявлением вводится случайный временной отступ — от нескольких минут до нескольких дней. Такой подход позволяет моделировать различные поведенческие сценарии, снижает переобучение на локальный контекст и делает задачу для модели более сложной и реалистичной.
В результате модель учится одновременно:
-
эффективно использовать недавний контекст;
-
учитывать устойчивые долгосрочные предпочтения пользователя.

Ещё мы удаляем из истории события, которые привели к действию пользователя с таргет-объявлением, если они находятся в одной сессии с ним. Например, если это покупка телефона, удаляем из истории просмотр объявлений с этим телефоном, добавление в избранное и другие действия из сессии с покупкой.
Данные в модель подаём батчами из двух основных блоков: таргет-объявление и пользовательская история. История делится на две подпоследовательности: действия с объявлениями и поисковые запросы — это нужно, чтобы закодировать их разными энкодерами. Чтобы не потерять информацию о порядке действий в подпоследовательностях, для каждого из них мы передаём также индексы из исходной истории.
Исходная история действий пользователей — история до того, как её разбили на подпоследовательности.
Архитектура модели
Для каждой группы признаков мы конкатенируем эмбеддинги отдельных фичей, чтобы получить эмбеддинг группы. Далее эмбеддинг проходит через DCN (Deep Cross Network), которая последовательно создаёт кросс-фичи различных порядков: первый слой — попарные взаимодействия, второй — взаимодействия третьего порядка и так далее, в зависимости от глубины сети.
Потом конкатенируем эмбеддинги групп между собой и пропускаем через DCN ещё раз. Получаем новый эмбеддинг, который проходит через энкодер объявлений или поиска. Таким образом получаем конечный эмбеддинг объекта.
Пользовательская часть модели — трансформер, в который мы подаём две подпоследовательности объявлений и поисков, а также индексы действий пользователя в исходной истории. Чтобы учесть последовательность действий, используем позиционные эмбеддинги.
Профиль пользователя мы представляем с помощью CLS-эмбеддинга из последовательности событий. Поэтому эмбеддинг для пользователя должен содержать существенно больше информации, чем для отдельного объявления. Однако ёмкость у них одинаковая.
Чтобы выровнять информационную ёмкость представлений пользователя и объявления, мы перешли к кодированию пользовательского профиля несколькими эмбеддингами. Для каждого эмбеддинга пользователя вычисляем скалярное произведение с эмбеддингом объявления, а полученные значения агрегируем в итоговый скор.
Обучаем модель подбирать подходящие объявления для пользователя. Для этого требовалось максимизировать скалярное произведение между эмбеддингами пользователя и релевантных ему объявлений и минимизировать для нерелевантных. Вот как решали эту задачу.
Релевантные объявления подаём в батче вместе с историей — это позитивы.
Нерелевантные — все остальные объявления, несоответствующие данному пользователю в текущем батче — это негативы.
Заметили, что чем больше негативов в батче, тем выше его качество. Поэтому стали сэмплировать случайные объявления из общей базы и использовать их в качестве дополнительных негативов. Это оказалось рациональным решением с точки зрения ресурсов RAM и GPU-памяти. Дополнительно смогли учесть объявления, которые нечасто встречаются в истории действий пользователя.
В итоге получились две матрицы: для эмбеддингов пользователя и объявлений. При их перемножении получается матрица скоров.
На главной диагонали находятся скоры, соответствующие релевантным объявлениям, которые мы хотим максимизировать, и, таким образом, показывать их пользователям чаще. Остальные скоры нужно минимизировать. Для этого можно использовать подходы бинарной или мультиклассовой классификаций.
Бинарный подход (SigLIP) формулируется как максимизация логарифма сигмоиды от логитов для позитивных пар и логарифма сигмоиды от логитов, умноженных на −1, для негативных пар.
Таким образом модель одновременно учится увеличивать сходство для релевантных пар и уменьшать его для нерелевантных. Этот вариант соответствует функции потерь SigLIP loss.
Мультиклассовая классификация (InfoNCE) реализуется как максимизация логарифма softmax для позитивных пар, где в знаменателе softmax учитываются соответствующий позитив и набор негативных примеров. Увеличение числа негативов, как правило, приводит к улучшению качества эмбеддингов, поэтому мы дополнительно экспериментировали с расширением пула негативов за счёт их обмена между батчами.
Оказалось, что SigLIP с сигмоидой и обменом негативами работает с той же вычислительной скоростью, что и InfoNCE без обмена. При этом с InfoNCE метрики получались немного выше, поэтому в дальнейшем мы остановились на этом варианте функции потерь.
Вычислительная инфраструктура и масштаб обучения. Первоначально обучение модели проводилось на конфигурации из четырёх видеокарт (80 ГБ). Эффективный размер батча, агрегированный по всем GPU, составлял порядка 30 000 сэмплов.
В дальнейшем обучение перенесли на один сервер с восемью GPU, что позволило увеличить эффективный размер батча до ≈130 000 сэмплов. При этом на каждой GPU использовалось порядка 16 000 негативных примеров, расширяющих пул негативов.
Датасет за два месяца был порядка 10 млрд сэмплов.
Работа модели в проде и хранилища данных
Все этапы препроцессинга и инференса реализованы с использованием фреймворка Aqueduct, который распределяет эти этапы между отдельными процессами и обеспечивает эффективное взаимодействие между ними.
Препроцессинг выполняется на CPU, а для инференса на каждом инференс-сервере используется одна видеокарта. Весь инференс логически разделён на две части:
Инференс эмбеддингов объявлений. На вход части для объявлений поступают параметры объявлений, а на выходе формируются их эмбеддинги. Полученные эмбеддинги сохраняются в Redis-кэше, откуда их можно быстро извлечь.
Инференс эмбеддингов пользователя. На вход пользовательской части подаётся история пользователя в виде эмбеддингов объявлений, полученных из Redis-кэша, а также параметры поиска, которые кодируются пользовательской частью модели. На выходе формируется эмбеддинг пользователя в режиме реального времени.
Для работы модели нужно несколько хранилищ данных, вот основные:
Для пользовательской истории. Содержит информацию обо всех действиях пользователя. Эти данные используются, чтобы сформировать входные данные в пользовательскую часть инференс-сервера.
Для эмбеддингов объявлений. При создании нового объявления или обновлении существующего свежие данные передаются в часть инференс-сервера, отвечающую за объявления. В результате обновляются эмбеддинги в Redis-кэше векторного хранилища.
ANN-индекс Sphinx. Обновлённые эмбеддинги объявлений дополнительно отправляются в поисковый ANN-индекс Sphinx, где используются для поиска ближайших соседей.
Применение модели и результаты
Модель используется в двух основных сценариях.
Кандидатогенерация. На вход пользовательской части инференс-сервера поступает история пользователя, на основе которой формируется эмбеддинг пользователя. Далее этот эмбеддинг отправляется в ANN-индекс Sphinx, где выполняется поиск эмбеддингов наиболее подходящих объявлений. Чем больше скалярное произведение между эмбеддингами пользователя и объявления, тем выше релевантность. Найденные предложения формируют набор кандидатов.
Формирование признаков для рекомендательного ранкера. Мы получаем кандидатов не только из нашего кандидатогенератора, но и из других источников. Для всех объявлений-кандидатов из Redis извлекаются эмбеддинги, после чего вычисляется скалярное произведение между эмбеддингами пользователя и объявлений. Это значение используется в качестве дополнительной фичи объявлений в рекомендательном ранкере.
Мы обучили две модели для двух сценариев.
Первая модель — мультикатегорийная. Во время обучения использовали в датасете данные из всех категорий, она хорошо показала себя в качестве признака ранжирования.
Мы получили прирост +2,3% целевых действий покупателей на главной странице. По результатам SHAP-анализа в ранкере эта фича стала топ-1.
Вторая — модель обучена для одной категории и хорошо выступила в роли кандидатогенератора. Мы обучили её в категории «Личные вещи» и получили +1,1% целевых действий на главной странице, а в личных вещах + 8,1%.
Больше новостей, лайфхаков и кейсов от DS-инженеров Авито в канале «Доска AI-объявлений». Подписывайтесь
Автор: justalge


