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

Что не так со статическими рекомендациями?

Когда вам нужно ранжировать список «вам также могут понравиться» фильмы, может возникнуть вопрос контекстуальной двусмысленности. Хороший пример — фильм «Вспомнить все».

Вам нравится научная фантастика? Или только Шварценеггер? Или, может быть, что-то о Марсе? Или только фильмы из 90-х? Похожих фильмов в каждой области много, но всех в одном списке не бывает. Вам нужно выбрать.

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

Создание статических рекомендаций

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

Чтобы сделать краткий обзор идеи, лежащей в основе подхода матричной факторизации:

  • Вы создаете гигантскую разреженную матрицу MxN взаимодействий, где каждая строка — это пользователь (т. е. имеется M строк), а каждый столбец — элемент (т. е. имеется N столбцов). Когда пользователь взаимодействует с элементом, вы что-то помещаете в эту ячейку, например, оцениваете этот пользовательский набор для этого конкретного фильма.
  • Матрица разреженная и имеет множество дырок для фильмов, никогда не оцененных пользователями.
  • Затем вы выполняете матричную декомпозицию, математическую операцию, похожую на сжатие JPEG с потерями, но для матриц. Он разбивает исходную матрицу MxN на две меньшие: MxK для встраивания информации, специфичной для пользователя, и NxK для встраивания информации, специфичной для элемента.
  • Когда вы умножаете вложения, специфичные для пользователя, на специфичные для элемента, вы получите что-то похожее на исходную разреженную матрицу, но со всеми дырами, волшебным образом заполненными прогнозами того, как пользователь А оценит фильм Б.

Но у этих вложений также есть одна интересная особенность: похожие фильмы (на основании того, что они нравятся похожим пользователям) будут иметь похожие вложения! Таким образом, чтобы порекомендовать похожие фильмы для фильма A, вам нужно всего лишь найти K ближайших соседей его вложения.

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

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

  • Научите своего рекомендателя создавать вложения фильмов.
  • Запишите вложения в базу данных поиска kNN.
  • Сделайте поиск kNN для похожих фильмов.

Набор данных для эксперимента

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

  • 25 миллионов оценок от 162 тысяч посетителей
  • Метаданные о фильмах, включая ссылки TMDB/IMDB для получения более подробной информации.

Основное ограничение этого набора данных с точки зрения Learn-to-Rank заключается в том, что он имеет только явные отзывы пользователей: фильм оценивается по шкале от 1 до 5 баллов, и все. В случае, если вы создаете только рекомендатель фильмов, этого может быть достаточно, но в более практических случаях использования в реальной жизни, таких как рекомендации по поиску и электронной коммерции, вы редко можете получить достаточно явную обратную связь от посетителей, поэтому вам придется полагаться на более шумной неявной обратной связи, такой как клики и показы.

В наборе данных игровой площадки мы хотели иметь следующие важные моменты:

  • Положительная обратная связь по релевантности, например клики по элементам
  • Отрицательная обратная связь, когда посетитель отмечает элемент как не соответствующий его первоначальному намерению. Отрицательная обратная связь может быть явной (например, отзыв с 1 звездой) или неявной (например, просмотр товара и отсутствие клика). Учитывая проблему смещения позиции в обучающих данных, идеальным вариантом будет иметь доступ к полному исходному рейтингу, но он редко доступен во всех изученных нами наборах данных.
  • Метаданные о предметах по их названиям, таксономии: например, категории, цены, теги и т. д.

Мы не нашли доступных наборов данных, подходящих для нашего варианта использования (большинство из них имеют только две точки из трех), мы создали наш набор данных для игровой площадки:

  • Речь идет о фильмах, и большинство людей в мире время от времени их смотрят.
  • Метаданные фильма взяты из набора данных Grouplens с некоторыми дополнительными битами из TMDB.
  • Мы сгруппировали фильмы по категориям по тегам, таким как «Фэнтези», «Фильмы о войне» и т. д., на основе краудсорсинговых тегов из набора данных grouplens.

Этот подход решает пункты № 1 и № 3 в нашем списке проблем, но где взять негативные отзывы? Для решения этой проблемы мы решили использовать краудсорсинговый подход, поскольку существует множество платформ для маркировки данных, ориентированных на машинное обучение, таких как Amazon MTurk и Toloka.ai.

Идея эксперимента заключалась в том, чтобы показать наши кластеры фильмов разным людям и попросить их отметить фильмы, которые им понравились. Мы также сделали несколько приемов сэмплирования:

  • Для каждого кластера тегов мы взяли 25 случайных фильмов из топ-50 в случайном порядке. Это помогло нам сделать набор данных нечувствительным к проблеме смещения позиции, так как при случайном порядке фильмов каждый ход всегда будет отображаться в случайных позициях и не станет больше похожим, потому что он занял первое место в списке.
  • Один и тот же кластер фильмов был показан как минимум 10 разным людям. Ранжирование всегда было случайным для каждого человека.
  • Каждый волонтер должен был изучить как минимум 5 разных кластеров, поэтому информации о каждом конкретном посетителе будет достаточно.

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

Набор данных с открытым исходным кодом и доступен на GitHub. Пример метаданных элемента:

И помимо метаданных, у нас также есть фактические события ранжирования:

Персонализация

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

Но как мы можем включить информацию об отзывах в рейтинг? Текущие рекомендательные модели, основанные на подходе матричной факторизации [ссылка здесь], по-прежнему могут включать в формулу некоторые метаданные пользователя и элемента, но за этим стоят некоторые технические проблемы:

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

Обычный подход к этой проблеме состоит в том, чтобы разделить весь рейтинг на два отдельных шага:

  1. Сгенерируйте набор статических рекомендаций в качестве первоначальных кандидатов: они могут быть не на 100% релевантными, но этот шаг в основном сосредоточен на скорости и запоминаемости. Другими словами, в списке могут быть ложные срабатывания.
  2. Переоцените набор рекомендаций-кандидатов, используя различные более медленные алгоритмы, ориентированные на точность, которые на практике должны перемещать более важные элементы выше.

Такой подход называется многоступенчатым ранжированием и широко используется в отрасли. Хорошим примером такого подхода является Amazon:

Существует 3 отдельных рейтинговых прохода по начальному набору кандидатов:

Инженерные проблемы

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

Согласно нашим внутренним опросам и некоторым публичным статьям (например, знаменитому из Airbnb), переход от статического ранжирования к динамическому требует больших вложений в стек инженерии данных:

  • Надежное отслеживание активности посетителей: вы должны собирать множество событий телеметрии, фиксирующих каждое взаимодействие посетителя, например показы и клики.
  • Аналитика: все события телеметрии должны очищаться и обрабатываться в режиме реального времени.
  • Разработка функций: обновление функций с отслеживанием состояния, таких как «вам понравился фильм этого жанра в прошлом», является еще одной проблемой, поскольку для обучения машинному обучению требуется автономный пакетный доступ к полной истории этих функций. Тем не менее, для логического вывода требуется доступ к последним значениям с малой задержкой. Поэтому для обоих шаблонов доступа к данным требуется два отдельных конвейера обработки.

Крупные компании, такие как Uber, Airbnb и Amazon, могут позволить себе внедрить такие системы обработки данных, но более мелкие игроки часто не могут: вкладывать годы человеческого труда в индивидуальное решение с неясными бизнес-перспективами может быть слишком рискованно. И прямо сейчас нет готовых к использованию инструментов с открытым исходным кодом, автоматизирующих это.

Мы, авторы этой статьи, тоже столкнулись с этими препятствиями на пути к персонализации в реальном времени: так родился Метаранк. Мы заметили, что многие из этих специально созданных систем (поскольку мы принимали участие в их реализации не раз) имеют много общих элементов:

  • В большинстве случаев базовая модель данных телеметрии посетителей очень похожа: есть показы, клики и метаданные элемента. К каждому из этих типов событий может быть присоединено несколько настраиваемых полей. Мы были вдохновлены моделью структуры событий электронной коммерции segment.io и попытались реализовать аналогичный, но гораздо более узкий набор типов событий.
  • Извлечение признаков также часто имеет много общего: парсеры UA/GeoIP/Referer, глобальные и временные счетчики событий, отслеживание профилей посетителей.
  • Часть инженерии данных, по нашему мнению, в последние годы постепенно сближается с решениями на основе хранилища функций.

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

Отображение событий

Metarank ожидает строго типизированный набор входящих событий как для обучения модели, так и для онлайн-вывода: показы, клики и метаданные. Пример события метаданных:

Как видите, мы преобразовали исходное событие из набора данных Ranklens в специальное для Metarank со следующими важными моментами:

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

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

Рейтинги

Но нам также нужно отправлять отзывы посетителей о показах, что может показаться необычным. Зачем нам это нужно? Причина, по которой существует требование иметь данные о рейтинге/впечатлениях, — позже мы собираемся научить базовую модель машинного обучения тому, какой элемент действительно актуален, а какой нет. Таким образом, в целом, подход Learn-to-Rank заключается в обучении своего рода бинарного классификатора, которому затем задается вопрос: «Учитывая элементы A и B, какой из них должен быть оценен выше?»

Если у вас есть данные о показах с информацией о том, что элементы A-B-C-D были показаны, а затем был нажат элемент C, то вы можете сделать предположение, что посетитель просмотрел элементы A+B+C, но на самом деле релевантным из этой группы является только элемент C. Итак, вы учите модель тому, что C должен иметь более высокий рейтинг, чем A и B.

Другая важная причина, по которой данные ранжирования важны, — это информация о позиции: люди, как правило, гораздо чаще нажимают на первые элементы в списке. В электронной коммерции около 50% кликов приходится на топ-5 результатов. Тогда щелчок по позиции 1 на самом деле не дает вам много информации о том, был ли этот элемент вообще релевантным. Но если посетитель прокрутил список до конца и только там нашел что-то достаточно релевантное, чтобы сделать клик — тогда этот клик — чистое золото с точки зрения ценности.

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

Пример события показа о ранжировании выглядит следующим образом:

Основные моменты:

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

Оценки релевантности в нашем случае использования рекомендаций фильмов могут быть, например, косинусным сходством между вложениями фильмов из моделей матричной факторизации. В случае поиска это могут быть баллы BM25 от ElasticSearch. Теоретически в некоторых случаях эти оценки могут быть нулевыми (например, ранжирование инвентаря в определенной категории предметов).

Взаимодействия

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

Наиболее важные вещи, которые следует учитывать:

  • Те же поля id/timestamp/user/session.
  • Существует поле показа, связывающее клик с показом родительского рейтинга.
  • Впечатлений может быть несколько. В нашем случае это только клики, но в случае электронной коммерции это также может включать события добавления в корзину и покупки.
  • Вы также можете прикрепить к нему контекстно-зависимые поля.

Сопоставление событий с функциями машинного обучения

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

Конфиг Metarank состоит из двух основных частей:

  1. Типы взаимодействия и их веса
  2. Список экстракторов признаков и их конфигурация

Так как у нас только клики, то это должно выглядеть довольно просто:

Каждому определению экстрактора признаков требуется имя (в нашем примере — такое же, как у поля) и тип. Экстрактор числовых признаков также должен знать следующее:

  • Источник: где он должен искать значение. В нашем случае это должно быть поле voice_avg в событии метаданных. Но технически вы можете получить поля также из показов и событий взаимодействия.
  • В какой объем он должен быть включен.

Обзор

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

  • Арендатор: функция распространяется на весь интернет-магазин в целом, например, на температуру воздуха снаружи.
  • Элемент: возьмите идентификатор элемента для журнала изменений функции. Пример: цена товара.
  • Пользователь: используйте идентификатор пользователя в качестве уникального идентификатора, например: количество прошлых сеансов.
  • Сеанс: используйте идентификатор сеанса как уникальный идентификатор, например: количество товаров в корзине прямо сейчас.

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

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

Более сложные функции

С помощью подхода, описанного в предыдущем разделе, мы можем скопировать множество полей событий метаданных в виде необработанных функций: количество голосов, бюджет фильма, год выпуска, время выполнения. Но простое копирование постоянных полей из событий метаданных — не самое замечательное, что может сделать Metarank.

Возьмем жанры кино. Это набор строк с низкой кардинальностью, и было бы здорово сделать из них однократное кодирование. Мы можем сделать это следующим образом:

Итак, здесь мы определили набор из 15 лучших жанров из набора данных, и с помощью экстрактора «строки» Metarank закодирует его в набор нулей и единиц.

Например, в «Истории игрушек 2» есть жанры «Анимация», «Семейный», «Комедия». Анимация — № 12, семья — № 9, а комедия — № 1 (с индексацией, начинающейся с нуля), поэтому это будет вектор из 15 двоичных значений.

[0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0].

Точно так же мы можем также закодировать некоторые более типичные случаи информации о посетителях, такие как поле User-Agent:

Он работает так же, как «строковый» энкодер. Тем не менее, с предопределенным набором возможных случаев для мобильных устройств/настольных компьютеров/планшетов: он анализирует поле User-Agent, сопоставляет его с базой данных эталонных значений и обнаруживает возможную платформу посетителя. Тот же трюк можно проделать с GeoIP поверх обнаруженных городов и поля Referer для обнаружения источника трафика.

Функции с отслеживанием состояния

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

Таким образом, каждый раз, когда посетитель совершает клик, мы увеличиваем счетчик этих кликов, определяемый идентификатором сеанса посетителя. Мы можем проделать тот же трюк с подсчетом кликов, происходящих в рамках продукта (то есть, сколько кликов было сделано по элементу всеми посетителями), но глобальный счетчик не имеет никакого смысла, так как он должен быть ограничен по времени: 100 кликов. за последние 5 дней — это не то же самое, что 100 кликов за последний год.

Но есть экстрактор функций window_count, решающий именно эту проблему:

Таким образом, этот экстрактор функций создаст циклический буфер из 60 счетчиков (каждый для определенного дня из 60 запрошенных) и будет увеличивать дневной счетчик для каждого захваченного события. И каждый 1 час он будет складывать эти 60 дневных счетчиков в вектор с 4 значениями количества событий, захваченных для каждого настроенного временного окна.

Если мы можем отслеживать счетчики событий разных типов в скользящем окне, то, разделив два из этих счетчиков друг на друга, мы можем получить классные вещи, такие как CTR скользящего окна и коэффициенты конверсии! Существует экстрактор скорости, делающий именно это:

Много YAML, но он будет делить количество кликов на количество покупок, эффективно вычисляя конверсию. Существует также специальный тип синтетического события взаимодействия, называемого «проверить», поэтому, если вы нажмете, чтобы проверить рейтинг, вы получите CTR!

Профилирование клиентов

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

  • Вам нравился определенный жанр/режиссер/актер в прошлом
  • Возможно, вам также может понравиться новый фильм с высоким пересечением жанров/режиссеров/актеров с вашими прошлыми любимыми фильмами.

Таким образом, вы можете настроить такое же поведение следующим образом:

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

Подготовка исторических данных

Итак, на данный момент мы описали схему входящих событий как для исторических событий, так и для событий в реальном времени для метаданных, ранжирования и взаимодействий. В настоящее время Metarank поддерживает только файлы входных данных в формате JSONL:

  • Каждое событие должно быть отформатировано как одна строка
  • Порядок событий не важен; все события будут явно отсортированы по отметке времени
  • Входных файлов может быть несколько, и каждый файл будет обрабатываться параллельно.
  • Файлы можно читать из HDFS, S3 и локальных файловых систем.

Начальная загрузка данных

Задание начальной загрузки будет обрабатывать ваши входящие события на основе файла конфигурации и создавать несколько выходных частей:

  1. набор данных — числовые значения признаков, не зависящие от бэкенда, для всех кликов в наборе данных.
  2. функции — моментальный снимок последних значений функций, который следует использовать на этапе вывода позже.
  3. точка сохранения — точка сохранения Apache Flink для беспрепятственного продолжения обработки онлайн-событий после задания начальной загрузки.

Запустите следующую команду с помощью Metarank CLI и укажите расположение файлов events.json.gz и config.yml в качестве ее параметров:

Обучение модели машинного обучения

Когда задание Bootstrap завершено, вы можете обучить модель, используя файл config.yml и выходные данные задания Bootstrap. Задание «Обучение» проанализирует входные данные, проведет фактическое обучение и создаст файл модели:

Загрузить последние значения функций

Metarank использует Redis для вывода (обработка данных в режиме реального времени для онлайн-персонализации), поэтому вам необходимо загрузить туда текущие версии значений функций после задания Bootstrap. Используйте URL-адрес вашего экземпляра Redis в качестве параметра хоста:

Этот шаг является необязательным, так как на следующем шаге есть способ запустить метаранг локально и полностью в памяти, но без какого-либо сохранения.

Вывод

Запустите службу Metarank REST API для обработки событий обратной связи и повторного ранжирования в режиме реального времени. По умолчанию Metarank будет доступен на localhost:8080, и вы можете отправлять события обратной связи на http://‹ip›:8080/feedback и получать персонализированный рейтинг от http://‹ip› :8080/ранг.

API отзывов и выводов

Прямо сейчас вы можете отправлять события повторного ранжирования в конечную точку /rank следующим образом. Учитывая запрос с последними фильмами о Терминаторе:

Отправляем его и получаем ответ:

curl -H “Content-Type: application/json” \
-XPOST -d @req.json http://localhost:8080/rank|jq .items

Итак, мы вернули наши фильмы о Терминаторе, но в другом порядке. Мы также можем отправить образец события клика в событие /feedback и проверить, как изменился рейтинг.

Заключительные замечания

Основной целью проекта Metarank всегда был способ упростить рутинные задачи проектирования данных и функций в тех случаях, когда вы ожидаете получить результаты в стиле Парето: достичь 80% результатов (имея настоящую персонализацию в реальном времени) всего с 20% усилий (с базовыми преобразованиями JSON и написанием некоторой конфигурации YAML).

Метаранк не панацея и имеет массу недостатков:

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

Но в любом случае, чтобы решить эти проблемы, мы просим вас оставить отзыв:

  • Если предлагаемая модель данных слишком ограничена или не может быть сопоставлена ​​с вашей внутренней моделью, не стесняйтесь открыть проблему GitHub с описанием варианта использования.
  • Metarank предназначен для работы в облаке и работает внутри k8s, но, поскольку установки kubernetes часто довольно самоуверенны, если вы столкнетесь с какими-либо препятствиями в инфраструктуре — также сообщите о них.
  • Ядро ML Metarank имеет открытый исходный код, поэтому мы также приветствуем вклады: как код, так и не код — это здорово.

Чтобы поболтать о Metarank, ранжировании и персонализации, присоединяйтесь к специальному Slack на metarank.ai/slack и не забудьте подписаться на Metarank на Github, чтобы быть в курсе последних событий.