Это моя первая статья, которую я когда-либо писал на Medium или любом другом блоге. Моя основная мотивация начать писать не для того, чтобы прославиться (для этого я должен скорее показывать видео, где я играю в видеоигры и играю хуже, чем мой 3-летний сын), а скорее для того, чтобы помочь себе вспомнить вещи и, надеюсь, также помочь вам понять основные понятия с помощью объяснений и кода. Из этого вы уже можете догадаться, что будет код, в основном написанный на Python 3.7, и может быть немного математики, когда это необходимо. В примерах кода, размещенных здесь, я всегда стараюсь, чтобы импорты были близки к исполняемому коду, просто потому, что я ненавижу, когда я не знаю, откуда берется функция super_awesome(1,2,'you'). Вы можете найти весь код, возможно немного отличающийся по структуре и формату, в моем репозитории github. Итак, хватит блаблы, давайте перейдем к теме.

Свет Введение

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

  • Методы, основанные на содержании, которые пытаются описать пользователей или элементы на основе статических характеристик, таких как возраст, пол, место жительства, теги, характеристики элементов и т. д.
  • Совместныеметоды, которые пытаются описать пользователей и элементы на основе их динамического взаимодействия.

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

  • Такие пользователи, как вы, любят Mario Kart и Warcraft, так что вам это тоже может понравиться (подход на основе пользователей).
  • Предмет Mario Kart похож на Fifa 2020 и Smash Bros Brawl, поскольку вам понравился Mario Kart, вам также могут понравиться Fifa 2020 и Smash Bros Brawl (подход на основе предметов).

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

Подход

В основе совместной фильтрации лежит взаимодействие пользователей с элементами. Взаимодействия показывают, что пользователь использовал предмет, и, возможно, его мнение об этом предмете. Это мнение может быть представлено как явная оценка, например, от 1 до 5 звезд, или как неявная оценка, например, пользователь перестал смотреть фильм через 10 % времени, выглядит так, будто фильм ей не понравился, следовательно, 0 звезд, или пользователь смотрел этот фильм 3 раза за полнометражку, похоже, ей это нравится, поэтому 5 звезд.

Учитывая это, наша цель — найти способ ответить на вопрос: «Какова оценка (мнение) пользователя i с индексом uᵢ о товаре j с индексом i». ⱼ. Знание ответа на этот вопрос позволит вам предлагать элементы своим пользователям, просто беря те элементы, которые имеют наивысший оценочный рейтинг. С «математической» точки зрения, мы хотим изучить отображение f, которое с учетом пары пользовательский индекс, элемент-индекс возвращает соответствующий рейтинг: f(uᵢ, iⱼ) → rating_ij. Следовательно, пары "пользователь-индекс", "элемент-индекс" являются нашими переменными-предикторами X, а рейтинги - нашей целевой переменной y.

Данные

Без данных мы ничего не можем сделать, поэтому давайте сначала сосредоточимся на этом. Данные, которые мы здесь используем, — это знаменитый набор данных Movielens 100k, который содержит 100 000 оценок от 1 до 5 от ~1000 пользователей примерно к 1700 фильмам. В моем репозитории github вы найдете вспомогательный класс под названием MovieLens100K для скачивания, загрузки и форматирования данных так, как нам нужно.

Поскольку все мы хорошие специалисты по данным, мы разделяем данные на данные для обучения и данные для тестирования, прежде чем начать с ними работать. Здесь я использовал обычное разделение 80% обучения и 20% тестирования. В коде это дается как

import numpy as np
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from recommender.utils import MovieLens100K
data = MovieLens100K("../data/")
x_train, x_test, y_train, y_test = train_test_split(
    np.array(
        [
            data.ratings.userId.cat.codes,
            data.ratings.movieId.cat.codes,
        ]
    ).T,
    data.ratings.rating,
    train_size=0.8,
    random_state=42,
)

Картирование

Пусть U = {u₁, …, u_n-users} будет набором всех представлений пользователей с uᵢ представляет собой k-мерный действительный вектор, представляющий пользователя i, и пусть I= {i₁, …, i_n-items} — набор всех представлений элементов, где iⱼ — k-мерный действительный вектор, представляющий элемент j. Кроме того, пусть Ub = {ub₁, …, ub_n-users} будет набором всех пользовательских предубеждений, где ubᵢ является реальным числом для пользователя i, и пусть Ib= {ib₁, …, ib_n-items} — набор всех смещений элементов, где ibⱼ — действительное число для элемента j. Отображение пользователя, индексированного i, и элемента, индексированного j, в соответствующий рейтинг определяется как

f(i,j) = рейтингᵢⱼ=шкала(точка(uᵢ, iⱼ) + ubᵢ + ibⱼ),

с дополнительной вспомогательной функцией s, которая определяется как

s(x) = сигмоид(x) *(max_rating-min_rating) + min_rating,

где max_rating и min_rating являются соответствующими диапазонами рейтингов. (Извините за дерьмовое форматирование, но я надеюсь, что вы его поняли). Таким образом, это просто точечный продукт между двумя представлениями и членами смещения, и результат подвергается масштабирующей функции. Скалярное произведение является причиной того, что 2 представления должны иметь одинаковую размерность k, иначе мы не смогли бы вычислить скалярное произведение напрямую без дополнительного сопоставления. Теперь у нас есть данные и отображение. Чего не хватает, так это того, как найти U, I, Ub и Ib?

Обучение

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

Вложение - это в основном вектор действительных чисел, описывающий сущность. Таким образом, мы можем использовать их как наши k-мерные векторы в U и I, а также как наши одномерные векторы, то есть числа, >Ub и Ib. Здесь я использую Keras для изучения вложений. Для этого вам нужно знать только кардинальность сущности, т. е. количество отдельных пользователей/элементов и желаемое измерение вложений, наше k. U, I, Ub и Ib представляют собой размерные весовые матрицы (мощности xk), хранящиеся в вложение слоев. На выводе, учитывая индекс i в качестве входных данных, слой внедрения просто возвращает соответствующий k-мерный столбец из своей матрицы.

Итак, собрав все это вместе, у нас есть 4 слоя встраивания, точечный продукт с отображением на основе условий смещения и масштабирование. В Кодексе это читается как

from dataclasses import dataclass
from typing import Optional, Tuple

import tensorflow.keras.backend as K
from tensorflow import Tensor
from tensorflow.keras.layers import (
    Embedding,
    Input,
    Dot,
    Lambda,
    Activation,
    Add,
)
from tensorflow.keras.models import Model


@dataclass
class SqueezLayer:
    axis: int

    def __call__(self, x: Tensor) -> Tensor:
        return Lambda(lambda xx: K.squeeze(xx, axis=self.axis))(x)


@dataclass
class ScalingLayer:
    min: float
    max: float
    
    def __call__(self, x: Tensor) -> Tensor:
        return Lambda(
            lambda in_: in_ * (self.max - self.min) + self.min)(
            Activation("sigmoid")(x)
        )


def recommender_model(
    n_users: int,
    n_movies: int,
    emb_dim: int,
    min_max: Optional[Tuple[float, float]] = None,
    use_bias: bool = False,
) -> Model:
    users = Input(shape=(1,))
    u_e = Embedding(input_dim=n_users, output_dim=emb_dim)(users)

    movies = Input(shape=(1,))
    m_e = Embedding(input_dim=n_movies, output_dim=emb_dim)(movies)

    ratings = Dot(axes=2)([u_e, m_e])

    if use_bias:
        u_bias = Embedding(input_dim=n_users, output_dim=1)(users)
        m_bias = Embedding(input_dim=n_movies, output_dim=1)(movies)
        ratings = Add()([ratings, u_bias, m_bias])
    
    ratings = SqueezeLayer(axis=1)(ratings)
    ratings = (
        ratings
        if min_max is None
        else ScalingLayer(*min_max)(ratings)
    )
    return Model(inputs=[users, movies], outputs=ratings)

Из-за размеров тензоров мы должны добавить слой сжатия, который просто удаляет ненужную ось размерности 1 из тензора. Как видите, код структурирован таким образом, что вы можете экспериментировать с различными моделями. Установив для min_max значение None, вы можете отключить функцию масштабирования, а с помощью флага use_bias вы можете отключить условия смещения. Не стесняйтесь видеть, как эти дополнения влияют на производительность модели.

Наконец, код, который собирает все вместе,

n_users = data.ratings.userId.nunique()
n_movies = data.ratings.movieId.nunique()
k = 50  # As suggest by Jeremy Howard 
min_max = (data.ratings.rating.min(), data.ratings.rating.max())
mdl = recommender_model(n_users, n_movies, k, min_max,use_bias=True)

mdl.compile("adam", loss="mse")
print(mdl.summary())

history = mdl.fit(
    np.split(x_train, 2, axis=1),
    y_train,
    batch_size=64,
    epochs=5,
    verbose=1,
    validation_data=(np.split(x_test, 2, axis=1), y_test),
)

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

Рекомендация

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

  1. Выборка случайного пользователя
  2. Возьмите все фильмы, которые пользователь еще не посмотрел/не оценил, так как мы не хотим раздражать его, предлагая то, что он уже знает.
  3. Предсказать рейтинги этих фильмов, используя наше сопоставление
  4. Отсортируйте оценки в порядке убывания и верните первые n записей этого списка.
from typing import Tuple

import numpy as np
import pandas as pd
from tensorflow.keras.models import Model

from recommender.utils import MovieLens100K

DATA = MovieLens100K("../data/")
class Recommender:
    def __init__(self, mdl: Model):
        self._mdl = mdl
        self._movies = pd.Series(
            dict(
                enumerate(DATA.ratings.movieId.cat.categories)
            )
        )

    def __call__(self, user, n_suggestions=10):
        user_rows = DATA.ratings[DATA.ratings.userId == user.values]
        unknown_movies = self._movies[
            ~self._movies.isin(user_rows.movieId)
        ]
        ratings = self._mdl.predict(
            [
                np.full_like(
                    unknown_movies,
                    user.cat.codes
                ),
                unknown_movies.index.values,
            ]
        ).flatten()


        index = ratings.argsort()[: -(n_suggestions + 1) : -1]
        recommends = unknown_movies.iloc[index].astype(int).values
        return DATA.movies.loc[recommends], ratings[index]
recommender = Recommender(mdl)
n = 5
for i in range(10):
    user = DATA.ratings.userId.sample(1)
    print(f"The {n} movies we suggest are")
    print(recommender(user, n))

Вот и все, ура, мой первый пост почти готов.

В качестве примечания: использование оценочных оценок в качестве основы для рекомендаций — это лишь один из возможных подходов. Другой вариант — взять вложения как функции, представляющие пользователей, найти K наиболее похожих пользователей на основе этих функций, например, с помощью KNN, вычислить средние оценки, которые пользователи дали элементам, и выбрать те, которые получили наилучшие оценки, в качестве рекомендации (пользователь- основанный подход). Или мы могли бы выбрать элемент, который больше всего понравился пользователю, взять , найти n элементов, наиболее похожих с точки зрения их встраивания в этот элемент, и рекомендовать их (подход, основанный на элементах). Или, или, или, … Существует множество способов использования вложений, не только для рекомендаций, но я оставляю это как потенциальную будущую публикацию, если я когда-нибудь сделаю это снова.

Заключительные слова

Если вы следовали за моей статьей до сюда, спасибо вам за это. Если вам это понравилось или нет, пожалуйста, дайте мне знать, почему, чтобы я мог улучшить свое письмо в целом, а также эту статью в частности. Не забудьте посмотреть полный код на github, здесь снова ссылка https://github.com/Shawe82/recommender101. И еще раз спасибо fastai и John Wittenauer за их код и записи.