Привет! Меня зовут Илья Шамов, я работаю в AI-центре Тинькофф и занимаюсь рекомендательными системами. Сегодня я расскажу, зачем делать умную ленту в социальной сети для инвесторов, как работают рекомендательные системы и как выбор целевой переменной влияет на ранжирование. Разберемся в устройстве рекомендательных систем, посмотрим, как таргет влияет на вид умной ленты, и узнаем, как дойти от MVP до промышленного решения.

Что такое Тинькофф Пульс и зачем там умная лента

Есть приложение «Тинькофф Инвестиции», в котором можно торговать акциями, облигациями и другими активами. Нажимая на любую ценную бумагу, мы видим несколько страниц:

  • обзор деталей бумаги;

  • биржевой стакан;

  • и главное для нашего рассказа — Пульс. 

Туда попадают все посты, где пользователи упомянули ценную бумагу с помощью тикера. Получается лента, которая формируется в хронологическом порядке.

Так выглядит Пульс
Так выглядит Пульс

Сейчас у Тинькофф Инвестиций несколько миллионов пользователей. В Пульсе сотни тысяч активных читателей и писателей, и ежедневно там появляются десятки тысяч постов. Наши пользователи читают примерно 20 километров текста в час. 

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

Потребности у всех разные. Кто-то хочет узнать аналитику рынка от компетентных инвесторов. Кто-то — посмотреть мемы на инвестиционную тематику. А кому-то хочется погрустить или порадоваться вместе с другими. Мы захотели сделать ленту интересной и полезной для каждого.

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

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

Ликбез по рекомендательным системам

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

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

  • Колонки слева — предсказания модели, насколько каждый объект будет релевантным для пользователя. Оценки можно считать вероятностями того, что пользователь отреагирует на тему.

  • Колонки справа — история просмотров пользователя. Обозначим единицей темы, на которые он реагировал (например, поставил лайк), а нулем — темы, которые он проигнорировал.

Рисунок 1
Рисунок 1

Простые методы решения задачи

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

Классификация

Обучаем модель предсказывать, поставит пользователь лайк на такой объект или нет. Модель будет рассчитывать вероятность обоих вариантов, и для принятия бинарного решения выберет наиболее вероятный. Тогда у каждого поста будет «вероятность» быть лайкнутым. По ней мы бы и отсортировали список объектов.

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

Возвращаясь к задаче с постами Пульса, мы могли бы предсказывать, что пользователь повзаимодействует с постом — лайкнет или прокомментирует его.

Ранжирование

При такой постановке задачи модели нужно принимать решение о том, какой объект предпочтительнее другого. Функции ранжирования могут быть разными:

  • Pointwise. Модель оценивает релевантность объекта самого по себе. Классификация — частный случай этого типа.

  • Pairwise. Модель попарно рассматривает объекты на каждом шаге оптимизации алгоритма, чтобы у менее релевантного объекта скор был ниже, чем у более релевантного.

  • Listwise. Модель на каждом шаге оптимизации рассматривает весь список объектов, чтобы напрямую оптимизировать выбранную метрику качества ранжирования. Это позволяет не только оценивать релевантность объектов, но и оптимизировать порядок просмотра: какой пост выгоднее показать раньше, а какой — позже.

Коллаборативная фильтрация

Простой и эффективный метод решения задачи рекомендаций. Суть подхода в предположении, что похожим людям нравятся похожие объекты. Для измерения схожести составляют матрицу размера количество пользователей×количество объектов. Тогда значение на пересечении строки i и столбца j будет реакцией одного на другое.

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

У этого подхода есть минусы. В реальности пользователь не может повзаимодействовать со всеми объектами, потому что их слишком много. Значит, и реакций будет мало. Матрица получится разреженной. Тогда и результат скалярного произведения чаще всего будет равняться нулю. Чем больше объектов и пользователей, тем более разрежена матрица и тем больше ресурсов требуется на вычисление.

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

Виды реакции пользователя

Отклик пользователя на рекомендации может быть разным. Проще всего попросить оценить предложенный контент — поставить лайк. Это называется явным откликом.

Плюсы и минусы явного отклика:

+ Ясна реакция пользователя.

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

Есть противоположный вид отклика — неявный. Он косвенно подтверждает, что пользователю нравится объект.

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

Плюсы и минусы неявного отклика:

+ Легко собрать со всех пользователей, потому что они оставляют фидбэк неосознанно.

− Неоднозначность. Например, пользователь мог посмотреть десятичасовое видео до конца, потому что оно было скучным и он уснул.

пример неявного отклика - процент просмотра видео
пример неявного отклика - процент просмотра видео

Оценка качества рекомендаций

Обучив рекомендательную модель предсказывать выбранный отклик, нужно оценить ее качество. Предположим, что модель оценила объекты, как показано на рисунке 1. 

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

Поэтому нужно решить, сколько объектов рекомендовать. В случае задачи с постами в Пульсе — сколько постов мы выведем в топ, чтобы пользователь увидел их на одном экране, как только зайдет в ленту. Проблема выбора количества объектов важная — с ней сталкиваются все, кто занимается ранжированием. Чтобы решить ее, можно измерять метрики только по топу K рекомендаций.

Метрики Precision и Recall вам, скорее всего, знакомы из классического машинного обучения: это точность и полнота того, что мы предсказали.

расчёт метрик precision и recall
расчёт метрик precision и recall

Для оценки ранжирования можно модифицировать эти метрики. Считать, какая часть рекомендованного списка попала в интересы пользователя (Precision@k) и насколько полно интересы охвачены списком (Recall@k). В нашем игрушечном примере Precision@1 = 1, Precision@3 = 1/3, Precision@4 = 1/2. Recall@1= 1/2, Recall@3 = 1/2, Recall@4 = 1.

Можно заметить, что эти метрики показывают, попадает ли вообще модель в интересы пользователя. Но не показывают, насколько хорошо отсортирован список. Чтобы узнать это, понадобятся другие метрики ранжирования.

Самая простая метрика ранжирования — Mean Average Precision. Разберем это название по словам. Мы уже знаем, что Precision — это точность. Average Precision мы посчитаем по формуле ниже.

\frac{1}{m} \sum_{i=1}^K P(i) * rel(i)

P(i)— precision — показывает, взаимодействовал ли в онлайне пользователь с объектом, который мы порекомендовали. rel(i)— relevance — это то, на сколько модель оценила важность i-ого объекта для пользователя. Average Precision — это кумулятивная (то есть суммирование идет именно по всем возможным длинам списка рекомендаций от 1 до топа k) сумма вероятности всех объектов, которые пользователь на самом деле лайкнет.

А Mean Average Precision — это усредненный по всем m-пользователям Average Precision. Таким образом, в отличие от precision@k и recall@k, метрика отображает не только релевантность, но и учет порядка каждого объекта в топе списка, и считается не по одному юзеру, а усредняется по всем.

Как мы искали умную ленту в Пульсе

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

Первые наши предположения были довольно наивными. Предскажем простое взаимодействие — лайк.

Представим пользователя как набор признаков:

  • доходность;

  • размер портфеля;

  • частота лайков.

Пост представим так:

  • длина поста;

  • количество упомянутых тикеров;

  • количество эмодзи;

  • количество абзацев;

  • количество реакций.

По простым статистикам пользователя и поста предсказывать лайки сложно. Лента начинает выводить в топ популярные, уже залайканные посты. Она почти одинаковая для разных пользователей. Такие ленты можно встретить в небольших проектах. Рекомендовать самые популярные объекты часто бывает выгодной стратегией. Но такие рекомендации однообразны и быстро устаревают.

Чтобы персонализировать выдачу, мы добавили контентные признаки постов:

  • тип текста (новостной, аналитический, эмоциональный и так далее);

  • тип картинки (скриншот, мем и так далее).

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

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

Обученная предсказывать лайки по этим признакам модель стала ранжировать выше длинные посты, часто с картинками. Пользователи ставят лайки на такие посты, но не дочитывают до конца. Кроме того, такие посты иногда бывают кликбейтными. То есть часто интригующий заголовок может завлечь пользователя в длинный текст.

Пример ранжирования ленты с таргетом реакций
Пример ранжирования ленты с таргетом реакций

В выступлении о ленте Яндекс Дзена Андрей Зимовнов рассказывал, что такой таргет приводит к появлению в топе кликбейтных постов. Специфика этой социальной сети в большом размере текстов и разнообразии тем у авторов. Наша новая модель столкнулась с этой же проблемой. Несмотря на то что проблема разрешима, мы отказались от модели с этим таргетом, так как лента Пульса решает другую бизнес-задачу. Мы стали искать таргет дальше.

Пример рекомендации из Яндекс Дзена
Пример рекомендации из Яндекс Дзена
Пример поста с привлекательным лидом
Пример поста с привлекательным лидом

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

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

Мы выбрали мерой залипания то, что пользователь читал пост на одно среднеквадратическое отклонение дольше, чем в среднем читали остальные пользователи. Таким образом, брали около 16% наиболее заинтересованных просмотров поста. Большее отсечение оставит недостаточно положительных примеров для обучения модели. Меньшее сотрет границу между долгим и средним просмотром поста. Для решения задачи методом классификации такой таргет нужно бинаризовать.

Когда мы обучили залипательную модель, ситуация изменилась в противоположную сторону. В топ начали выходить короткие и зачастую провокационные посты. Часто это были простые вопросы к аудитории, например «Стоит ли покупать $TSLA сейчас?» Или посты с мемами. Такие посты часто вызывают холивар в комментариях.

Пример ранжирования ленты с залипательным таргетом
Пример ранжирования ленты с залипательным таргетом

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

Пример популярного поста во ВКонтакте
Пример популярного поста во ВКонтакте

Мы решили попробовать взвешенный таргет. Он позволит учитывать и лайки, и залипание на постах, и другие метрики вовлеченности. Также можно влиять на выраженность каждой метрики в итоговом ранжировании. Сейчас мы тестируем такой подход.

На внутреннем тесте мы собрали в основном теплые отзывы коллег
На внутреннем тесте мы собрали в основном теплые отзывы коллег

Метрики качества алгоритма легко посчитать офлайн на датасете. Но они не отражают реакцию пользователей, потому что им не были показаны эти рекомендации. Ее можно наблюдать в таких онлайн-метриках, как средняя глубина сессии и количество сессий за сутки. Онлайн-метрики невозможно посчитать без запуска алгоритма на аудиторию. А зависимость между офлайн- и онлайн-метриками не всегда прямая. Например, когда мы повышаем MAP, средняя глубина просмотра ленты не всегда растет. Поэтому мы принялись за эксперименты. Данила Савенков в своем выступлении о таргетах умной ленты подробно рассказывал, как со временем автоматизировать такой перебор.

Как эволюционировал сервис

В рассказах о продуктах часто показана история успеха. Здесь же хочется показать историю развития от MVP «на коленке» до текущего решения.

Шаг первый

Изначально нужно было сравнить простую модель с существующей эвристикой. На этом этапе нерационально вкладывать силы в реализацию микросервисов, спецификацию контрактов запросов и прочие радости качественной разработки.

Многие модели машинного обучения легко сериализовать. Например, решающие деревья могут быть переведены в набор if’ов на любом языке. Для встраивания в существующий Java-бэкенд приложения модель сконвертирована в исполняемый файл. Это очень негибкое решение, потому что при изменении списка признаков или обновлении модели придется менять файл и выкатывать новый релиз приложения.

На диаграмме ниже — общая архитектура такого сервиса в виде UML-диаграммы развертывания. Слева устройство клиента, в центре — бэкенд-сервер, обращающийся к компоненту с ML-моделью. Отдельно справа — обучение модели и работа с хранилищем признаков и логов Пульса.

Шаг второй

На следующей стадии эволюции мы выделили отдельный сервис ранжирования на стороне команды рекомендаций.

Также мы начали применять ML-модели для извлечения признаков из публикуемых постов. Например, для извлечения типов текста и картинки. Для этого завели отдельный сервис, отмеченный на диаграмме как Feature Extractor. Он получает из Kafka новые посты, извлекает признаки и складывает их в хранилище.

Теперь при запросе ленты сервер Пульса собирал последние посты и их признаки из хранилища, а затем отправлял их в сервис ранжирования. Это дало возможность обновлять ML-модель в любой момент и проводить A/B-тесты между моделями.

В итоге получилось масштабируемое и удобное, но слишком запутанное решение. Без пояснений на эту диаграмму смотреть не хочется. На ней даже стрелки пересекаются.

Шаг третий

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

Мы забрали получение признаков на свою сторону и добавили кэширование. Теперь сервер все так же ходит в сервис с ML-моделью, но отправляет только идентификаторы постов и пользователей. Стрелки больше не пересекаются, и диаграмма выглядит прилично.

Заключение

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

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

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

Комментарии (0)