Трехмесячный конкурс рекомендательных систем 2019 ACM RecSys Challenge наконец-то подошел к концу. Мы, RosettaAI, в итоге заняли 4-е место среди 1564 команд в частной таблице лидеров, заняв последнее место в призовой зоне! Большое спасибо профессору Линю и нашим замечательным товарищам по команде из Национального Тайваньского университета и Техасского университета в Далласе. Без отличной командной работы невозможно достичь такого результата.

Абстрактный

В этом году на конкурсе ACM Recsys Challenge, организованном на сайте trivago.com, аналитикам данных было предложено создать систему рекомендаций, основанную на сессиях и контекстно-зависимую, для решения задачи ранжирования. Учитывая последовательность взаимодействий с пользователем, цель состоит в том, чтобы расположить список отелей таким образом, чтобы, заняв первое место в списке, было больше вероятности того, что он будет отключен, как показано на следующих рисунках.

Наш подход включает три разные модели: нейронные сети, LightGBM и XGBoost. Я отвечал за всю процедуру обработки данных, реализацию и проектирование нейронных сетей, а также большую часть функциональной инженерии. Это сообщение в блоге резюмирует наш подход. Я кратко проиллюстрирую наборы данных, функцию потерь, архитектуру нейронных сетей и проектирование функций.

Наборы данных

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

Каждая строка представляет действие пользователя, тип которого записывается в столбце action_type. Соответствующий ему столбец ссылка сообщает нам, с каким объектом это действие выполнялось. Например, в первой строке поиск пункта назначения соответствует Барселона, Испания. Это означает, что пользователь вводит Барселона, Испания в поле назначения. Целочисленное значение в справочном столбце представляет собой ID отелей. Наиболее важным действием является элемент щелчка, подразумевающий, что пользователь щелкает ссылку, ведущую на внешний сайт. Только в строках, связанных с этим действием, показы и цены не равны нулю. Оба этих столбца хранят список целых чисел, указывающий список отелей, отображаемых пользователю, и цену каждого отеля, соответственно. В этих строках элемента щелчка столбец ссылка может рассматриваться как целевой. Цель состоит в том, чтобы отсортировать список показов таким образом, чтобы цель располагалась в начале списка.

Метрика оценки для этой задачи - Средний взаимный рейтинг (MRR):

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

Функция потерь

С первого взгляда на эту задачу я подумал об использовании потери ранжирования, которую, как я полагаю, выберет большинство команд, например, Байесовское персонализированное ранжирование (BPR) или LambdaRank . Однако в конце концов я решил рассматривать это как проблему бинарной классификации, разбивая каждый элемент в списке оттисков на отдельную выборку. Потеря определяется как двоичная кросс-энтропия (BCE). Я сделал это по двум причинам:

  1. Меня вдохновила эта статья, опубликованная Youtube еще в 2017 году. Мне показалось, что вариант использования их прогнозов второго этапа напоминает контекст этой задачи.
  2. Эти данные напомнили мне о предыдущем конкурсе, который я проводил, Avito Demand Prediction, который проводил Avito на Kaggle (мы были в 5 местах от зоны золотой медали). Я вспомнил, как много лучших бомбардиров использовали проигрыш BCE, хотя это и не было проблемой бинарной классификации. Кажется, что если выходные данные соответствуют такому вероятностному распределению, BCE является надежной функцией потерь для правильного обучения модели.

Архитектура нейронной сети

Моя нейронная сеть основана на ранее упомянутых Youtube paper и DeepFM.

Функции можно разделить на три части: непрерывные функции, категориальные функции и последовательные категориальные функции.

Непрерывная функция

Обработка непрерывных функций проста. Единственное, о чем нужно знать, - это то, что непрерывные функции должны быть правильно масштабированы, чтобы нейронные сети могли лучше учиться. В нашей реализации лучше всего работают MinMaxScaler от Sklearn и RankGaussScaler из этого удивительного ядра Kaggle.

Категориальный признак

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

Последовательный категориальный признак

Аналогичным образом, последовательные категориальные функции также кодируются и внедряются с помощью того же метода. Следует отметить, что одни и те же функции имеют одинаковый вес встраивания. Например, на приведенном выше рисунке синий, красный и зеленый полосы ниже получены из одной и той же матрицы встраивания элементов. Две из этих функций применяются двунаправленным ГРУ для моделирования временной динамики в каждой из последовательностей. Затем они объединяются с помощью Max Pooling, чтобы стать векторами той же размерности, что и другие категориальные функции.

Взаимодействие с функциями

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

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

Идея основана на следующих уравнениях:

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

Функциональная инженерия

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

Прогнозируемая позиция следующего взаимодействия

Это основано на позиции позиции последнего шага и позиции второго последнего шага в текущем списке показов. Он рассчитывается следующим образом:

predicted_pos = last_pos + (last_pos- second_last_pos)

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

Разница во времени

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

Время истекло

Агрегируя разницу во времени между каждым шагом, мы вычисляем (максимальное / среднее / суммарное) количество времени, которое пользователь тратит на каждый элемент в показах.

Заключение

В целом, мы получили массу удовольствия от этой задачи. Хотя конкуренция была до смешного жесткой, особенно в последние несколько недель, результаты оказались неплохими. Еще раз искренняя признательность всем нашим замечательным товарищам по команде и всем, кто давал нам советы на этом пути. Спасибо всем за то, что сделали это возможным. Репозиторий GitHub для нашего решения можно найти здесь. Если у вас есть какие-либо вопросы или комментарии, оставьте их ниже. Если вам нужны пробные версии продукта или возможности сотрудничества, свяжитесь со мной напрямую через [email protected].