Логотип Data Secrets
Аватар пользователя
обложка-1

RecTools – OpenSource библиотека для рекомендательных систем

28.04.2024

Если вы когда-либо работали с рекомендательными системами, то знаете, что все необходимые и самые часто используемые инструменты разбросаны по разным библиотекам. Более того, каждая из таких библиотек имеет много уникальных особенностей, к которым нужно приноровиться (например, разные форматы данных на вход). 

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

К такому же выводу, видимо, пришли ребята из МТС – и выкатили в опенсурс RecTools. Это библиотека, где собраны самые часто используемые модели для рекомендательных систем. Также с её помощью можно максимально просто и быстро оценивать необходимые метрики.  

Сегодня мы решили разобраться, что RecTools умеет, и как с этим работать.

Кстати, кроме сайта у нас есть телеграм-канал: https://t.me/data_secrets. Туда мы публикуем самые свежие новости, мемы и наши полезные ML-находки. Подписывайтесь, чтобы не пропустить!

Также нам помогли усовершенствовать эту статью сами разработчики RecTools, за что мы выражаем им большую благодарность :)

картинка

Установка

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

pip install rectools

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

Как установить расширение:

pip install rectools[extension-name]

Если хотите установить все расширения сразу:

pip install rectools[all]

Как подавать данные

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

Формат данных на входПо факту от пользователя требуется обычная таблица, где каждая строка отражает одно взаимодействие: в первом столбце id юзера, во втором — айтема, а в третьем — скор взаимодействия (например, купил/ не купил). Если есть данные по времени, их тоже можно добавить. 

Из особенностей: имена столбцов фиксированы. Поэтому колонки нужно будет переименовать с помощью класса Columns. Затем остается просто обернуть все в Dataset и готово:

from rectools import Columns

ratings = pd.read_csv(
"ml-1m/ratings.dat",
sep="::",
engine="python",
header=None,
names=[Columns.User, Columns.Item, Columns.Weight, Columns.Datetime],
)

# Prepare a dataset to build a model
dataset = Dataset.construct(ratings)

Модели библиотеки

В библиотеке реализовано единое API для всех широко известных моделей: Implicit ItemKNN, ALS, SVD, Lightfm, DSSM и пр. То есть вся разнообразная механика инструментов оказывается спрятанной под капотом, а сами модели работают из коробки – с помощью единых методов fit – recommend. 

Модель "популярное"

Это база. Алгоритм работает просто: на основе данных делает выводы о том, с какими продуктами пользователи чаще всего взаимодействуют (смотрят или покупают) и рекомендует их всем. Конечно, особой положительной репутацией метод не обладает, но его можно улучшить с помощью простого трюка. Нужно всего лишь рекомендовать с некоторой рандомизацией. Например, каждый раз брать 10 случайных айтемов из топ-100 самых популярных. Называется reranking diversity. 

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

Короче, если ищете, с чего начать – начните с популярного. В RecTools реализовать модельку можно вот так

from rectools.models import PopularModel

# Fit model and generate recommendations for all users

model = PopularModel()
model.fit(dataset)
recos = model.recommend(
users=ratings[Columns.User].unique(),
dataset=dataset,
k=10,
filter_viewed=True,
)

rjl

Матричные разложения

Матричные разложения – это такие методы коллаборативной фильтрации. Вкратце: это прогнозирования действий пользователя на основе действий других пользователей с похожим поведением. Представим, что у нас есть некоторая матрица user-item. Строки — пользователи, столбцы — айтемы. На пересечении пользователя и айтема стоит число, отражающее взаимодействие.

c[tvfНа основе такой матрицы мы можем описать поведение пользователей и характеристики айтемов. Для этого используются методы спектрального разложения. Один из них – алгебраическое разложение матриц, SVD (singular value decomposition). В основе — разложение исходной матрицы в произведение 3 других:

Здесь U и S – матрицы представлений пользователей и айтемов соответственно. Используя эти матрицы, далее мы можем получать рекомендации. 

В RecTools этот инструмент можно использовать с помощью класса PureSVDModel:

from rectools.models import PureSVDModel

# Fit model and generate recommendations for all users
model = PureSVDModel()
model.fit(dataset)
recos = model.recommend(
users=ratings[Columns.User].unique(),
dataset=dataset,
k=10,
filter_viewed=True,
)

Алгоритм достаточно прозрачный, но у него есть недостатки. Во-первых, он не интерпретируемый, так как предсказания делаются на основе скрытых представлений. Во-вторых, вычисление таких разложений весьма трудоемко. В-третьих, SVD решает задачу разложения исходной матрицы напрямую, пытаясь восстановить все пропущенные значения нулями. И является идеальным решением такой задачи с математической точки зрения. Но по факту пропущенные значения в матрице взаимодействий - это необязательно нули (юзеры могли просто не успеть про-взаимодействовать с релевантными айтемами). 

Поэтому на практике чаще используется ALS (alternating least squares) – это эвристический приближенный алгоритм для разложения матриц. На выходе снова получаем эмбеддинги юзеров и айтемов, с помощью которых делаем рекомендации.

Использование этого алгоритма из волшебной коробки RecTools немного отличается от предыдущих алгоритмов. Так как эта имплементация – просто обертка для модели из implicit, на вход объекту класса нужно подать Base model (подробнее тут), внутри которой уже можно указать необходимые параметры, вот так:

from rectools.models import ImplicitALSWrapperModel
from implicit.als import AlternatingLeastSquares

model = ImplicitALSWrapperModel(
AlternatingLeastSquares(
factors=64,
regularization=0.01,
alpha=1,
random_state=2023,
use_gpu=False,
iterations=15))

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

Implicit KNN

Помимо обертки для модели ALS из библиотеки implicit, в RecTools также представлен wrapper для модели item-to-item KNN recommender оттуда же. Суть такова: ищем в матрице по методу ближайших соседей айтемы, максимально похожие на айтемы из истории взаимодействий юзера, и именно их ему рекомендуем.


from rectools.models import ImplicitItemKNNWrapperModel
from implicit.nearest_neighbours import TFIDFRecommender

model = ImplicitItemKNNWrapperModel(
model=TFIDFRecommender(K=5)
)

model.fit(dataset)

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

LightFM

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

Воспользоваться моделью по-прежнему просто, только не забудьте установить библиотеку lightfm с помощью pip install lightfm и соответствующе расширение для RecTools. 

from rectools.models import LightFMWrapperModel
from lightfm import LightFM

model = LightFMWrapperModel(
# внутри модели указываем параметр no_components
# это размезность эмбеддингов, которые выучит модель
model=LightFM(no_components = 30)
)

model.fit(dataset)
recos = model.recommend(
users=ratings[Columns.User].unique(),
dataset=dataset,
k=10,
filter_viewed=True,
)

Подробнее про алгоритм можно прочитать в оригинальной статье 2015 года.

DSSM

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

То есть, нам нужна модель, которая моделирует релевантность с помощью скалярного произведения эмбеддингов. Одна из таких моделей – DSSM – была придумана в 2013 году исследователями из Microsoft. Идея этой простой нейросети такая: давайте сделаем две "башни" с полносвязными слоями и нелинейностями: одна башня будет для юзеров, вторая для айтемов.dssmНа выходе каждая из этих башен будет выдавать эмбеддинги юзера и айтема, а косинус угла между этими векторами будет моделировать релевантность айтема юзеру.

В RecTools есть имплементация и этого алгоритма. Авторы реализовали его сами с помощью фреймворка PyTorch Lightning. Чтобы воспользоваться моделью, не забудьте установить нужное разрешение. 

from rectools.models import DSSMModel

model = DSSMModel(dataset,
max_epochs = 10,
batch_size = 64
)
model.fit(dataset)

Добавление атрибутов в модели

Ну, а что, если мы хотим учитывать не только взаимодействия пользователей и объектов, но и какие-то их отличительные признаки –  атрибуты? Понятно ведь, что часто одинаковые товары нравятся людям одного возраста, пола, профессии и пр. А для объектов атрибутами могут стать производитель, цена и многое другое, в зависимости от того, что именно мы рекомендуем.

В RecTools есть функционал, который позволяет добавлять фичи пользователей и айтемов во многие перечисленные в предыдущем разделе модели (iALS, LightFM, DSSM, PopularInCategory).

Для начала нужно приготовить данные о юзерах и/или айтемах специальным образом. Во-первых, все признаки нужно "вытянуть" в одну колонку value, пометив каждое значение названием фичи, к которой оно относится.

# загружаем данные
users = pd.read_csv(
"ml-1m/users.dat",
sep="::",
engine="python",
header=None,
names=[Columns.User, "sex", "age", "occupation"],
)

# отбираем только тех юзеров, которые есть в таблице взаимодействий
users = users.loc[users["user_id"].isin(ratings["user_id"])].copy()

# вытягиваем фичи в одну колонку и метим
user_features_frames = []
for feature in ["sex", "age", "occupation"]:
feature_frame = users.reindex(columns=["user_id", feature])
feature_frame.columns = ["id", "value"]
feature_frame["feature"] = feature
user_features_frames.append(feature_frame)
user_features = pd.concat(user_features_frames)

resПосле остается заново обернуть основной датасет в специальный класс, при этом указав значения аргументов, которые отвечают за фичи. Если у вас представлены только числовые фичи, то можно поставить make_dense_user_features=True. Иначе стоит придать этому аргументу значение False – это будет соответствовать sparse-формату представления данных. 

sparse_features_dataset = Dataset.construct(
ratings,
# датасет фич
user_features_df=user_features,
# к этим фичам будет применен One Hot Encoding
cat_user_features=["sex", "age"],
# для `sparse` формата
make_dense_user_features=False
)

После этого мы снова готовы скармливать датасет моделям (здесь никаких изменений в коде нет). Например:

model = ImplicitALSWrapperModel(AlternatingLeastSquares(10, num_threads=32))
model.fit(sparse_features_dataset)

Метрики

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

"Мы решили использовать подход, который позволяет считать метрики на основе табличных данных. Это похоже на Single Instruction Multiple Data. То есть это инструкции в самом процессоре, которые могут применять одну и ту же простую операцию (типа сложения и умножения) к нескольким блокам данных. Это занимает меньше кода, сохраняет интерпретируемость, так ещё и работает в сотни раз быстрее."

Вот так вы можете посчитать, например, классическую ndcg для ваших рекомендаций: 

ndcg = NDCG(k=10, log_base=3)

print("NDCG: ", ndcg.calc(reco=recos, interactions=df_test))
# 0.068

Помимо этой метрики, в библиотеке также представлены: Accuracy, F1Beta, IntraListDiversity, MAP, MCC, MRR, MeanInvUserFreq, Precision, Recall, Serendipity

Заключение

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

Библиотека постоянно дорабатывается и оптимизируется. Например на последних бенчмарках зафиксировано ускорение инференса для LightFM в 10-25 раз, а также прирост качества для iALS с фичами на 50%.

g1g2В общем, не поскупитесь – поставьте ребятам звездочку на GitHub :)

Дополнительные материалы

Если хотите глубже погрузиться в тему, вам будут полезны следующие ресурсы: