Это моя первая статья, которую я когда-либо писал на 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элементов, которые могут заинтересовать пользователя, все, что нам нужно сделать, это
- Выборка случайного пользователя
- Возьмите все фильмы, которые пользователь еще не посмотрел/не оценил, так как мы не хотим раздражать его, предлагая то, что он уже знает.
- Предсказать рейтинги этих фильмов, используя наше сопоставление
- Отсортируйте оценки в порядке убывания и верните первые 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 за их код и записи.