newsmode
search
Меню
arrow_back Назад

Building a Strong Baseline Recommender in PyTorch, on a Laptop

auto_awesomeКраткое саммари

Юджин Ян (Ziyou Yan) описывает построение сильной базовой рекомендательной системы на PyTorch, работающей на обычном ноутбуке с 16 ГБ ОЗУ. В качестве данных используется датасет Amazon (май 1996 – июль 2014) от профессора Julian McAuley из UC San Diego — разделы электроники (420 тыс. товаров, 4 млн пар) и книг (1,95 млн товаров, 26,6 млн пар), причём данные крайне разрежены (99,99%). Вместо классической матричной факторизации в памяти автор делает её итеративно, пара за парой, с негативным сэмплированием, регуляризацией L2 и косинусным отжигом learning rate за 5 эпох. На бинарных метках достигается ROC-AUC 0,8083, на непрерывных — около 0,92–0,93, но кривые precision-recall показывают «обрыв смерти» около порога 0,5. Добавление смещения (bias) немного снижает AUC-ROC, но сглаживает кривые и делает модель более пригодной для продакшена. Главный вывод — нельзя оценивать модель только по числовым метрикам; графики кривых раскрывают её поведение гораздо лучше, и это лишь базлайн перед применением графовых и NLP-методов в следующей статье.

Building a Strong Baseline Recommender in PyTorch, on a Laptop

Построение сильной базовой рекомендательной системы на PyTorch, на ноутбуке

[ recsys deeplearning python 🛠 ] · 14 min read

[ recsys deeplearning python 🛠 ] · 14 мин чтения

P.S. Looking for the code for this? Available on github: recsys-nlp-graph

P.S. Ищете код к этой статье? Доступен на github: recsys-nlp-graph

Recommender systems (“recsys”) are a fairly old topic that started back in the 1990s. The key idea—we can use opinions and behaviours of users to suggest and personalize relevant and interesting content for them.

Рекомендательные системы («recsys») — довольно старая тема, появившаяся ещё в 1990-х. Ключевая идея в том, что мы можем использовать мнения и поведение пользователей, чтобы предлагать и персонализировать для них релевантный и интересный контент.

Adding yet another post on the standard item-user collaborative filtering wouldn’t contribute much to the hundreds, if not thousands, of posts available.

Ещё одна публикация о стандартной item-user коллаборативной фильтрации вряд ли что-то добавила бы к сотням, если не тысячам, уже существующих статей.

Thankfully, this is not one of those.

К счастью, эта статья — не из таких.

Overview

Обзор

A web search on recommender systems surfaces articles on “collaborative filtering”, “content-based”, “user-item matrix”, etc.

Поиск по теме рекомендательных систем выдаёт статьи о «коллаборативной фильтрации», «контентных методах», «матрице пользователь-товар» и тому подобном.

Since then, there has been much progress applying newer techniques for recsys. Unfortunately, easily readable articles or blogs on them are few and far between.

С тех пор появилось много прогресса в применении более новых методов для recsys. К сожалению, понятных статей или блогов о них крайне мало.

Thus, in this pair of articles, I’ll be sharing about seven different implementations of recsys.

Поэтому в этой паре статей я расскажу о семи различных реализациях recsys.

The articles cover the end-to-end, from data acquisition and preparation, and (classic) matrix factorization. It also includes applying techniques from graphs and natural language processing. A result comparison of the different approaches on real-world data will also be discussed (I hope you love learning curves.)

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

Data Acquisition

Получение данных

We can’t build a recsys without data (well, we can’t build most machine learning systems without data).

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

Thankfully, Professor Julian McAuley, Associate Professor at UC San Diego has a great collection of data sets (e.g., product reviews, meta data, social networks). For this post, we’ll be using the Amazon dataset (May 1996 – July 2014), specifically, the electronics and books datasets.

К счастью, профессор Julian McAuley, доцент UC San Diego, собрал прекрасную коллекцию датасетов (например, отзывы о товарах, метаданные, социальные сети). В этой статье мы будем использовать датасет Amazon (май 1996 – июль 2014), а именно разделы электроники и книг.

Parsing json

Парсинг json

All of the datasets are in json format and require parsing into a format that’s easier to work with (e.g., tabular). Some of these datasets can be pretty large, with the largest having 142.8 million rows and requiring 20gb on disk.

Все датасеты представлены в формате json, и их нужно распарсить в более удобный для работы формат (например, табличный). Некоторые из этих датасетов могут быть довольно большими: самый крупный содержит 142,8 млн строк и занимает 20 ГБ на диске.

Here’s an example of how a json entry for a single product looks like (what we’re interested in is the related field).

Вот пример того, как выглядит json-запись для одного товара (нас интересует поле related).

{  "asin": "0000031852", "title": "Girls Ballet Tutu Zebra Hot Pink", "price": 3.17, "imUrl": "http://ecx.images-amazon.com/images/I/51fAmVkTbyL._SY300_.jpg", "related”: { "also_bought":[ "B00JHONN1S", "B002BZX8Z6", "B00D2K1M3O", ... "B007R2RM8W" ], "also_viewed":[  "B002BZX8Z6", "B00JHONN1S", "B008F0SU0Y", ... "B00BFXLZ8M" ], "bought_together":[  "B002BZX8Z6" ] }, "salesRank": {  "Toys & Games":211836 }, "brand": "Coxlures", "categories":[  [ "Sports & Outdoors", "Other Sports", "Dance" ] ] }

{ "asin": "0000031852", "title": "Girls Ballet Tutu Zebra Hot Pink", "price": 3.17, "imUrl": "http://ecx.images-amazon.com/images/I/51fAmVkTbyL._SY300_.jpg", "related”: { "also_bought":[ "B00JHONN1S", "B002BZX8Z6", "B00D2K1M3O", ... "B007R2RM8W" ], "also_viewed":[ "B002BZX8Z6", "B00JHONN1S", "B008F0SU0Y", ... "B00BFXLZ8M" ], "bought_together":[ "B002BZX8Z6" ] }, "salesRank": { "Toys & Games":211836 }, "brand": "Coxlures", "categories":[ [ "Sports & Outdoors", "Other Sports", "Dance" ] ] }

Needless to say, we’re not going to be able to load this fully into ram on a regular laptop with 16gb ram (which I used for this exercise).

Само собой, мы не сможем полностью загрузить это в оперативную память обычного ноутбука с 16 ГБ ОЗУ (который я использовал для этого упражнения).

To parse the json to csv, I iterated through the json file row by row, converted the json into comma-delimited format, and wrote it out to CSV. (Note: This requires that we know the full keys of the json beforehand).

Чтобы преобразовать json в csv, я проходил по json-файлу строка за строкой, конвертировал json в формат с разделением запятыми и записывал в CSV. (Примечание: для этого нужно заранее знать полный набор ключей json.)

At the end, we have product metadata in nice tabular format. This includes columns for title and description, brand, categories, price, and related products.

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

Getting product relationships

Получение связей между товарами

To get the product relationships, we need the data in the “related” field. This contains relationships on “also bought”, “also viewed”, “bought together”, etc.

Чтобы получить связи между товарами, нам нужны данные из поля «related». Оно содержит связи типа «also bought» (также покупали), «also viewed» (также смотрели), «bought together» (купили вместе) и так далее.

Extracting this requires a bit of extra work as it is contained in a dictionary of lists. Did I mention that it’s also in string format?

Извлечение этого требует немного дополнительной работы, поскольку оно хранится в словаре списков. Я уже упоминал, что это ещё и в строковом формате?

To get the relationships, we take the following steps:

Чтобы получить связи, мы выполняем следующие шаги:

  • Evaluate the string and convert to dictionary format
  • Get the associated product IDs for each relationship
  • Explode it so each product-pair is a single row with a single relationship
  • Вычислить (eval) строку и преобразовать её в формат словаря. Получить связанные идентификаторы товаров для каждой связи. Развернуть (explode) так, чтобы каждая пара товаров была отдельной строкой с одной связью.

    In the end, what we have is a skinny three column table, with columns for product 1, product 2, and relationships (i.e., also bought, also viewed, bought together). At this point, each product pair can have multiple relationships between them.

    В итоге у нас получается узкая таблица из трёх колонок: товар 1, товар 2 и связь (то есть also bought, also viewed, bought together). На этом этапе каждая пара товаров может иметь несколько связей между собой.

    Here’s a sample:

    Вот пример:

    product1 | product2 | relationship -------------------------------------- B001T9NUFS | B003AVEU6G | also_viewed 0000031895 | B002R0FA24 | also_viewed B007ZN5Y56 | B005C4Y4F6 | also_viewed 0000031909 | B00538F5OK | also_bought B00CYBULSO | B00B608000 | also_bought B004FOEEHC | B00D9C32NI | bought_together

    product1 | product2 | relationship -------------------------------------- B001T9NUFS | B003AVEU6G | also_viewed 0000031895 | B002R0FA24 | also_viewed B007ZN5Y56 | B005C4Y4F6 | also_viewed 0000031909 | B00538F5OK | also_bought B00CYBULSO | B00B608000 | also_bought B004FOEEHC | B00D9C32NI | bought_together

    Converting relationships into a single score

    Преобразование связей в единый скор

    Now that we have product-pairs and their relationships, we’ll need to convert them into some kind of single score/weight.

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

    For this exercise, I’ll simplify and assume the relationships are bidirectional (i.e., symmetric in direction). (Note: Product-pair relationships can be asymmetric; people who buy phones would also buy a phone case, but not vice versa).

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

    How do we convert those relationship types into numeric scores?

    Как нам преобразовать эти типы связей в числовые скоры?

    One simple way is to assign 1.0 if a product-pair have any/multiple relationships, and 0.0 otherwise.

    Один простой способ — присвоить 1.0, если у пары товаров есть любая/несколько связей, и 0.0 в противном случае.

    However, this would ignore the various relationship types—should a product pair with an “also bought” relationship have the same score as one that has an “also viewed” relationship?

    Однако это игнорировало бы различные типы связей — должна ли пара товаров со связью «also bought» иметь тот же скор, что и пара со связью «also viewed»?

    To address this, weights were assigned based on relationships (bought together = 1.2, also bought = 1.0, also viewed = 0.5) and summed across all unique product pairs. This results in a single numeric score for each product pair (sample below).

    Чтобы учесть это, веса были назначены в зависимости от связи (bought together = 1.2, also bought = 1.0, also viewed = 0.5) и просуммированы по всем уникальным парам товаров. В результате для каждой пары товаров получается единый числовой скор (пример ниже).

    product1 | product2 | weight -------------------------------- B001T9NUFS | B003AVEU6G | 0.5 0000031895 | B002R0FA24 | 0.5 B007ZN5Y56 | B005C4Y4F6 | 0.5 0000031909 | B00538F5OK | 1.0 B00CYBULSO | B00B608000 | 1.0 B004FOEEHC | B00D9C32NI | 1.2

    product1 | product2 | weight -------------------------------- B001T9NUFS | B003AVEU6G | 0.5 0000031895 | B002R0FA24 | 0.5 B007ZN5Y56 | B005C4Y4F6 | 0.5 0000031909 | B00538F5OK | 1.0 B00CYBULSO | B00B608000 | 1.0 B004FOEEHC | B00D9C32NI | 1.2

    For the electronics dataset, there’re 418,749 unique products and 4,005,262 product-pairs (sparsity = 4 million / 420k ** 2 = 0.9999). For books, we have 1,948,370 unique products, and 26,595,848 product pairs (sparsity=0.9999).

    Для датасета электроники насчитывается 418 749 уникальных товаров и 4 005 262 пары товаров (разреженность = 4 млн / 420 тыс. ** 2 = 0.9999). Для книг у нас 1 948 370 уникальных товаров и 26 595 848 пар товаров (разреженность = 0.9999).

    Yeap, recommendation data is very sparse.

    Да, данные для рекомендаций очень разрежены.

    Train-validation split

    Разбиение на обучение и валидацию

    The data sets were randomly split into 2/3 train and 1/3 validation. This can be done with sklearn.train_val_split or random indices. Easy-peasy.

    Датасеты были случайным образом разделены на 2/3 для обучения и 1/3 для валидации. Это можно сделать с помощью sklearn.train_val_split или случайных индексов. Проще простого.

    Done, amirite?

    Готово, верно?

    Unfortunately, not.

    К сожалению, нет.

    You might have noticed that our dataset of product-pairs consists only of positive cases. Thus, for our validation set (and the training set during training time), we need to create negative samples.

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

    Creating negative samples in an efficient manner is not so straightforward.

    Создавать негативные сэмплы эффективным способом не так-то просто.

    Assuming we have 1 million positive product-pairs, to create a validation set with 50:50 positive-negative pairs would mean 1 million negative samples.

    Предположим, у нас есть 1 миллион положительных пар товаров; чтобы создать валидационный набор с соотношением положительных и негативных пар 50:50, потребуется 1 миллион негативных сэмплов.

    If we randomly (and naïvely) sample from our product pool to create these negative samples, it would mean random sampling 2 million times. Calling random these many times takes very long—believe me, I (naïvely) tried it.

    Если мы будем случайно (и наивно) сэмплировать из нашего пула товаров для создания этих негативных сэмплов, это означало бы случайное сэмплирование 2 миллиона раз. Вызов random столько раз занимает очень много времени — поверьте, я (наивно) это пробовал.

    To get around this, I applied this hack:

    Чтобы обойти это, я применил такой хак:

  • Put the set of products in an array
  • Shuffle the array
  • Поместить множество товаров в массив. Перемешать массив.

    For each iteration, take a slice of the array at the iteration x 2

    На каждой итерации брать срез массива по номеру итерации × 2

  • If iteration = 0, product-pair = array[0: 0+2]
  • If iteration = 1, product-pair = array[2:, 2+2]
  • If iteration = 10, product-pair = array[20: 20+2]
  • Once the array is exhausted, (re)shuffle and repeat
  • Если итерация = 0, пара товаров = array[0: 0+2]. Если итерация = 1, пара товаров = array[2:, 2+2]. Если итерация = 10, пара товаров = array[20: 20+2]. Когда массив исчерпан, (пере)перемешать и повторить.

    This greatly reduced the amount of random operations required and was about 100x faster than the naïve approach.

    Это значительно сократило количество необходимых случайных операций и оказалось примерно в 100 раз быстрее наивного подхода.

    Collaborating filtering (how it is commonly known)

    Коллаборативная фильтрация (как её обычно называют)

    Collaborative filtering uses information on user behaviours, activities, or preferences to predict what other users will like based on item or user similarity. In contrast, content filtering is based solely on item metadata (i.e., brand, price, category, etc.).

    Коллаборативная фильтрация использует информацию о поведении, активности или предпочтениях пользователей, чтобы предсказать, что понравится другим пользователям, на основе сходства товаров или пользователей. В отличие от неё, контентная фильтрация основана исключительно на метаданных товара (то есть бренде, цене, категории и т. д.).

    Most collaborative filtering articles are on user-item collaborative filtering (more specifically, matrix factorization). And there’s always an (obligatory) user-item matrix (Fig. 1).

    Большинство статей о коллаборативной фильтрации посвящены коллаборативной фильтрации пользователь-товар (точнее, матричной факторизации). И там всегда присутствует (обязательная) матрица пользователь-товар (Рис. 1).

    Figure 1. Cliché representation of collaborative filtering

    Рисунок 1. Клишированное изображение коллаборативной фильтрации

    However, if we don’t have user data (like in this case), can this still work?

    Однако если у нас нет данных о пользователях (как в нашем случае), может ли это всё равно работать?

    Why not?

    Почему бы и нет?

    Instead of a user-item matrix, we have an item-item matrix. We then learn latent factors for each item and determine how strongly they’re related to other items.

    Вместо матрицы пользователь-товар у нас есть матрица товар-товар. Затем мы обучаем латентные факторы для каждого товара и определяем, насколько сильно они связаны с другими товарами.

    One way to do this is to load the entire item-item matrix (in memory) and apply something like Python’s surprise or SparkML’s Alternating Least Squares.

    Один из способов сделать это — загрузить всю матрицу товар-товар (в память) и применить что-то вроде Python-библиотеки surprise или Alternating Least Squares из SparkML.

    However, this can be very resource intensive if you have to load the entire dataset into memory or apply some distributed learning approach.

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

    Our datasets are very sparse (99.99% empty). Can we make use of the sparsity to make it more resource efficient? (Hint: Yes, we can.)

    Наши датасеты очень разрежены (на 99,99% пусты). Можем ли мы воспользоваться этой разреженностью, чтобы сделать всё более ресурсоэффективным? (Подсказка: да, можем.)

    Implementation 1: Matrix Factorization (iteratively pair by pair)

    Реализация 1: Матричная факторизация (итеративно, пара за парой)

    One way to reduce the memory footprint is to perform matrix factorization product-pair by product-pair, without fitting it all into memory. Let’s discuss how to implement this in PyTorch.

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

    First, we load the product-pairs (just the pairs, not the entire matrix) into an array. To iterate through all samples, we just need to iterate through the array.

    Сначала мы загружаем пары товаров (только пары, а не всю матрицу) в массив. Чтобы пройти по всем сэмплам, нам нужно лишь пройтись по массиву.

    Next, we need to create negative product-pair samples. Here’s the approach taken to generate negative samples:

    Далее нам нужно создать негативные сэмплы пар товаров. Вот подход, использованный для генерации негативных сэмплов:

  • For each product pair (e.g., 001-002), we take the product on the left (i.e., 001) and pair it with n (e.g., five) negative product samples to form negative product-pairs
  • The negative samples are based on the set of products available, with the probability of each product being sampled proportional to their frequency of occurrence in the training set
  • Для каждой пары товаров (например, 001-002) мы берём товар слева (то есть 001) и объединяем его с n (например, пятью) негативными сэмплами товаров, образуя негативные пары товаров. Негативные сэмплы берутся из множества доступных товаров, причём вероятность того, что товар будет выбран, пропорциональна частоте его появления в обучающем наборе.

    Thus, each (positive) product-pair (i.e., 001-002) will also have five related negative product-pairs during training.

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

    Continous labels

    Непрерывные метки

    For each product-pair, we have numeric scores based on some weightage (bought together = 1.2, also bought = 1.0, also viewed = 0.5). Let’s first train on these continuous labels. This is similar to working with explicit ratings.

    Для каждой пары товаров у нас есть числовые скоры на основе некоторого взвешивания (bought together = 1.2, also bought = 1.0, also viewed = 0.5). Давайте сначала обучимся на этих непрерывных метках. Это похоже на работу с явными рейтингами.

    Binary labels

    Бинарные метки

    To do it in the binary case (such as with implicit feedback), actual scores greater than 0 are converted to 1. Then, a final sigmoid layer is added to convert the score to between 0 – 1. Also, we use a loss function like binary cross entropy (BCE).

    Чтобы сделать это в бинарном случае (например, с неявной обратной связью), фактические скоры больше 0 преобразуются в 1. Затем добавляется финальный сигмоидный слой, чтобы преобразовать скор в значение между 0 и 1. Также мы используем функцию потерь вроде бинарной кросс-энтропии (BCE).

    Regularization

    Регуляризация

    Regularization—no doubt it’s key in machine learning. How can we apply it?

    Регуляризация — без сомнения, она ключевая в машинном обучении. Как нам её применить?

    One approach is adding L2 regularization to ensure embeddings weights don’t grow too large. We do this by by adding a cost of sum(embedding.weights**2) * C (regularization param) to the total loss.

    Один из подходов — добавление L2-регуляризации, чтобы веса эмбеддингов не росли слишком сильно. Мы делаем это, добавляя к суммарным потерям штраф sum(embedding.weights**2) * C (параметр регуляризации).

    At a high level, for each pair:

    На высоком уровне для каждой пары:

  • Get the embedding for each product
  • Multiply embeddings and sum the resulting vector (this is the prediction)
  • Reduce the difference between predicted score and actual score (via gradient descent and a loss function like mean squared error or BCE)
  • Получить эмбеддинг для каждого товара. Перемножить эмбеддинги и просуммировать получившийся вектор (это и есть предсказание). Сократить разницу между предсказанным и фактическим скором (через градиентный спуск и функцию потерь вроде среднеквадратичной ошибки или BCE).

    Here’s some pseudo-code on how iterative matrix factorization would work:

    Вот немного псевдокода о том, как работала бы итеративная матричная факторизация:

    Training Schedule

    Расписание обучения

    For the training schedule, we run it over 5 epochs with cosine annealing. For each epoch, learning rate starts high (0.01) and drops rapidly to a minimum value near zero, before being reset for to the next epoch (Fig. 2).

    Для расписания обучения мы запускаем его на 5 эпох с косинусным отжигом (cosine annealing). В каждой эпохе learning rate начинается с высокого значения (0.01) и быстро падает до минимального, близкого к нулю, после чего сбрасывается к следующей эпохе (Рис. 2).

    Figure 2. Cosine Annealing learning rate schedule

    Рисунок 2. Расписание learning rate с косинусным отжигом

    Results

    Результаты

    For the smaller electronics dataset (420k unique products with 4 million pairs), it took 45 minutes for 5 epochs—doesn’t seem too bad.

    Для меньшего датасета электроники (420 тыс. уникальных товаров с 4 млн пар) 5 эпох заняли 45 минут — кажется, не так уж плохо.

    Training on binary labels (1 vs. 0), we get a ROC-AUC of 0.8083. Not bad considering that random choice leads to ROC-AUC of 0.5.

    Обучаясь на бинарных метках (1 против 0), мы получаем ROC-AUC 0.8083. Неплохо, учитывая, что случайный выбор даёт ROC-AUC 0.5.

    From the learning curve (Fig. 3) below, you’ll see that one epoch is sufficient to achieve close to optimal ROC-AUC. Interestingly, each time the learning rate is reset, the model seems to need to “relearn” the embeddings from scratch (AUC-ROC drops to near 0.5).

    По кривой обучения (Рис. 3) ниже видно, что одной эпохи достаточно, чтобы достичь близкого к оптимальному ROC-AUC. Интересно, что каждый раз, когда learning rate сбрасывается, модели, похоже, приходится «переобучать» эмбеддинги с нуля (AUC-ROC падает почти до 0.5).

    Figure 3. AUC-ROC across epochs for matrix factorization; Each time learning rate is reset, the model seems to ”forget”, causing AUC-ROC to revert to ~0.5. Also, a single epoch seems sufficient.

    Рисунок 3. AUC-ROC по эпохам для матричной факторизации; каждый раз при сбросе learning rate модель словно «забывает», из-за чего AUC-ROC возвращается к ~0.5. Также одной эпохи, похоже, достаточно.

    However, if we look at the precision-recall curves below (Fig. 4), you see that at around 0.5 we hit the “cliff of death”. If we estimate the threshold slightly too low, precision drops from close to 1.0 to 0.5; slightly too high and recall is poor.

    Однако если посмотреть на кривые precision-recall ниже (Рис. 4), видно, что примерно на 0.5 мы натыкаемся на «обрыв смерти». Если оценить порог чуть-чуть ниже, precision падает почти с 1.0 до 0.5; чуть выше — и recall становится плохим.

    Figure 4. Precision recall curves for Matrix Factorization (binary labels).

    Рисунок 4. Кривые precision-recall для матричной факторизации (бинарные метки).

    Using the continuous labels, performance seems significantly better (AUC-ROC = 0.9225) but we see the same cliff of death (Fig. 5). Nonetheless, this does suggest that explicit (i.e., scale) ratings can perform better than implicit ratings.

    При использовании непрерывных меток производительность кажется значительно лучше (AUC-ROC = 0.9225), но мы видим тот же обрыв смерти (Рис. 5). Тем не менее это говорит о том, что явные (то есть шкальные) рейтинги могут работать лучше неявных.

    Figure 5. Precision recall curves for Matrix Factorization (continuous labels).

    Рисунок 5. Кривые precision-recall для матричной факторизации (непрерывные метки).

    TL;DR: While we use continuous labels for training, the output layer is a sigmoid that return scores from 0 - 1.

    TL;DR: хотя для обучения мы используем непрерывные метки, выходной слой — это сигмоида, которая возвращает скоры от 0 до 1.


    Why use continuous labels? Because they’re able to better distinguish the strength of product-pair relationships. Here’s a breakdown on binary vs. continuous labels.

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

    Binary labels: A pair of products have a label of 1 if they have any, or multiple, relationships (e.g., also viewed, also bought, bought together); zero otherwise.

    Бинарные метки: пара товаров имеет метку 1, если у них есть любая или несколько связей (например, also viewed, also bought, bought together); ноль в противном случае.

    This means that a pair of products which only have the also viewed will have the same label as a pair of products which have all three relationships (i.e., also viewed, also bought, bought together). While this approach can tell us if a pair of products have, or do not have, a relationship, it doesn’t distinguish the strength of the relationship.

    Это значит, что пара товаров, у которой есть только also viewed, получит ту же метку, что и пара товаров, у которой есть все три связи (то есть also viewed, also bought, bought together). Хотя этот подход может сказать нам, есть ли у пары товаров связь или нет, он не различает силу связи.

    Continuous labels: To distinguish between the strengths of relationships, we use the following labels (instead of 1 or 0) - bought together = 1.2, also bought = 1.0, also viewed = 0.5.

    Непрерывные метки: чтобы различать силу связей, мы используем следующие метки (вместо 1 или 0) — bought together = 1.2, also bought = 1.0, also viewed = 0.5.

    As a result, binary labels have values of 0 or 1.0 while continuous labels have values of 0, 0.5, 1.0, 1,2.

    В результате бинарные метки имеют значения 0 или 1.0, тогда как непрерывные метки имеют значения 0, 0.5, 1.0, 1.2.


    Regardless of the type of label, we get predictions from a simple sigmoid, and predictions will range from 0 - 1. (See code for matrix factorization on binary labels and continuous labels). Nonetheless, we use binary cross entropy loss for binary labels, and mean square error loss for continuous labels.

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

    Given that the final layer for both binary and continuous labels is a sigmoid, we can use ROC AUC for both, making the results for both approaches comparable.

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

    This baseline model only captures relationships between each product. But what if a product is generally popular or unpopular?

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

    We can add biases. Implementation wise, it’s a single number for each product.

    Мы можем добавить смещения (bias). С точки зрения реализации это одно число для каждого товара.

    Implementation 2: Matrix Factorization with Bias

    Реализация 2: Матричная факторизация со смещением

    If we just observe the AUC-ROC metric, adding bias doesn’t seem to help, where AUC-ROC decreases from 0.8083 to 0.7951 on binary labels, and from 0.9335 to 0.8319 on continuous labels.

    Если смотреть только на метрику AUC-ROC, добавление смещения, похоже, не помогает: AUC-ROC снижается с 0.8083 до 0.7951 на бинарных метках и с 0.9335 до 0.8319 на непрерывных метках.

    However, if we examine the precision-recall curves, adding bias reduces the steepness of the curves where they intersect, making it more production-friendly (i.e., putting it into production). Here are the curves for both binary (Fig. 6) and continuous labels (Fig. 7).

    Однако если рассмотреть кривые precision-recall, добавление смещения уменьшает крутизну кривых в точке их пересечения, делая модель более пригодной для продакшена (то есть для запуска в продакшен). Вот кривые для бинарных (Рис. 6) и непрерывных меток (Рис. 7).

    Figure 6. Precision recall curves for Matrix Factorization + bias (binary labels).

    Рисунок 6. Кривые precision-recall для матричной факторизации + смещение (бинарные метки).

    Figure 7. Precision recall curves for Matrix Factorization + bias (continuous labels).

    Рисунок 7. Кривые precision-recall для матричной факторизации + смещение (непрерывные метки).

    It may seem alarming that AUC-ROC on continuous labels drops sharply (0.9335 to 0.8319), but it shouldn’t be—it’s an artefact of how AUC-ROC is measured.

    Может показаться тревожным, что AUC-ROC на непрерывных метках резко падает (с 0.9335 до 0.8319), но не стоит этого пугаться — это артефакт того, как измеряется AUC-ROC.

    In the ROC curve for the continuous labels without bias (Fig. 8a), false positives only start to occur after about 0.75 of true positives are identified. Beyond the threshold of 0.5, all false positives are introduced (i.e., precision curve cliff of death in Fig. 5). The ROC curve and AUC-ROC metric doesn’t make this very observable and the AUC-ROC appears significantly better (but it really isn’t).

    На ROC-кривой для непрерывных меток без смещения (Рис. 8a) ложные срабатывания начинают появляться только примерно после того, как идентифицировано около 0.75 истинных срабатываний. За порогом 0.5 вводятся все ложные срабатывания (то есть обрыв смерти кривой precision на Рис. 5). ROC-кривая и метрика AUC-ROC делают это не очень заметным, и AUC-ROC кажется значительно лучше (хотя на самом деле это не так).

    Figure 8a (left) and 8b (right). ROC curves with and without bias.

    Рисунок 8a (слева) и 8b (справа). ROC-кривые со смещением и без него.

    On the larger books dataset, it took about 22 hours for 5 epochs—this approach seems to scale more than linearly based on the number of product-pairs (i.e., scales badly). Unfortunately, the AUC-ROC stays around 0.5 during training.

    На большем датасете книг 5 эпох заняли около 22 часов — этот подход, похоже, масштабируется более чем линейно в зависимости от количества пар товаров (то есть масштабируется плохо). К сожалению, AUC-ROC во время обучения остаётся около 0.5.

    Key takeaways

    Ключевые выводы

    The (iterative) matrix factorization approach seems to do okay for a baseline, achieving decent AUC-ROC of ~0.8.

    Подход (итеративной) матричной факторизации, похоже, неплохо справляется в роли базлайна, достигая приличного AUC-ROC около 0.8.

    While the baseline approach (without bias) does well, it suffers from sharp cliffs on the precision curve. Adding bias improves on this significantly.

    Хотя базовый подход (без смещения) работает хорошо, он страдает от резких обрывов на кривой precision. Добавление смещения значительно улучшает это.

    More importantly, we learnt that one shouldn’t just look at numeric metrics to understand how a machine learning model is doing. Plotting the curves can be very useful to better understand how your model performs, especially if it’s classification-based and you need to arbitrarily set some threshold.

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

    What’s next?

    Что дальше?

    Wait, isn’t this still matrix factorization, albeit in an iterative, less memory-intensive form? Where’s the graph and NLP approaches? You tricked me!

    Постойте, разве это всё ещё не матричная факторизация, пусть и в итеративной, менее памятоёмкой форме? Где же подходы с графами и NLP? Ты меня обманул!

    Yes, the current post only covers the baseline, albeit in an updated form.

    Да, текущая статья охватывает только базлайн, пусть и в обновлённой форме.

    Before trying any new-fangled techniques, we have to first establish a baseline, which is what this post does.

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

    In the next post, we’ll apply graph and NLP techniques to the problem of recommender systems.

    В следующей статье мы применим методы графов и NLP к задаче рекомендательных систем.

    P.S. Looking for the code for this? Available on github: recsys-nlp-graph

    P.S. Ищете код к этой статье? Доступен на github: recsys-nlp-graph

    References

    Источники

    McAuley, J., Targett, C., Shi, Q., & Van Den Hengel, A. (2015, August). Image-based recommendations on styles and substitutes. In Proceedings of the 38th International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 43-52). ACM.

    McAuley, J., Targett, C., Shi, Q., & Van Den Hengel, A. (2015, August). Image-based recommendations on styles and substitutes. In Proceedings of the 38th International ACM SIGIR Conference on Research and Development in Information Retrieval (pp. 43-52). ACM.

    If you found this useful, please cite this write-up as:

    Если эта статья оказалась полезной, пожалуйста, цитируйте её так:

    Yan, Ziyou. (Jan 2020). Building a Strong Baseline Recommender in PyTorch, on a Laptop. eugeneyan.com. https://eugeneyan.com/writing/recommender-systems-baseline-pytorch/.

    Yan, Ziyou. (Jan 2020). Building a Strong Baseline Recommender in PyTorch, on a Laptop. eugeneyan.com. https://eugeneyan.com/writing/recommender-systems-baseline-pytorch/.

    or

    или

    @article{yan2020matrix, title = {Building a Strong Baseline Recommender in PyTorch, on a Laptop}, author = {Yan, Ziyou}, journal = {eugeneyan.com}, year = {2020}, month = {Jan}, url = {https://eugeneyan.com/writing/recommender-systems-baseline-pytorch/} }

    @article{yan2020matrix, title = {Building a Strong Baseline Recommender in PyTorch, on a Laptop}, author = {Yan, Ziyou}, journal = {eugeneyan.com}, year = {2020}, month = {Jan}, url = {https://eugeneyan.com/writing/recommender-systems-baseline-pytorch/} }



    Join 11,800+ readers getting updates on machine learning, RecSys, LLMs, and engineering.

    Присоединяйтесь к 11 800+ читателям, получающим обновления о машинном обучении, RecSys, LLM и инженерии.