Рекомендуемые фильмы: рейтинг

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Реальные рекомендательные системы часто состоят из двух этапов:

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

Мы сосредоточимся на втором этапе - рейтинге. Если вы заинтересованы в поисковой стадии, взгляните на наш поисковой учебник.

В этом уроке мы собираемся:

  1. Получите наши данные и разделите их на набор для обучения и тестирования.
  2. Реализуйте модель ранжирования.
  3. Подгоните и оцените.

Импорт

Давайте сначала уберем наш импорт.

pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
import os
import pprint
import tempfile

from typing import Dict, Text

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
import tensorflow_recommenders as tfrs

Подготовка набора данных

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

ratings = tfds.load("movielens/100k-ratings", split="train")

ratings = ratings.map(lambda x: {
    "movie_title": x["movie_title"],
    "user_id": x["user_id"],
    "user_rating": x["user_rating"]
})
2021-10-02 11:04:25.388548: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

Как и раньше, мы разделим данные, поместив 80% оценок в набор поездов и 20% в набор тестов.

tf.random.set_seed(42)
shuffled = ratings.shuffle(100_000, seed=42, reshuffle_each_iteration=False)

train = shuffled.take(80_000)
test = shuffled.skip(80_000).take(20_000)

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

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

movie_titles = ratings.batch(1_000_000).map(lambda x: x["movie_title"])
user_ids = ratings.batch(1_000_000).map(lambda x: x["user_id"])

unique_movie_titles = np.unique(np.concatenate(list(movie_titles)))
unique_user_ids = np.unique(np.concatenate(list(user_ids)))

Реализация модели

Архитектура

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

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

class RankingModel(tf.keras.Model):

  def __init__(self):
    super().__init__()
    embedding_dimension = 32

    # Compute embeddings for users.
    self.user_embeddings = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
        vocabulary=unique_user_ids, mask_token=None),
      tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
    ])

    # Compute embeddings for movies.
    self.movie_embeddings = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
        vocabulary=unique_movie_titles, mask_token=None),
      tf.keras.layers.Embedding(len(unique_movie_titles) + 1, embedding_dimension)
    ])

    # Compute predictions.
    self.ratings = tf.keras.Sequential([
      # Learn multiple dense layers.
      tf.keras.layers.Dense(256, activation="relu"),
      tf.keras.layers.Dense(64, activation="relu"),
      # Make rating predictions in the final layer.
      tf.keras.layers.Dense(1)
  ])

  def call(self, inputs):

    user_id, movie_title = inputs

    user_embedding = self.user_embeddings(user_id)
    movie_embedding = self.movie_embeddings(movie_title)

    return self.ratings(tf.concat([user_embedding, movie_embedding], axis=1))

Эта модель берет идентификаторы пользователей и названия фильмов и выводит прогнозируемый рейтинг:

RankingModel()((["42"], ["One Flew Over the Cuckoo's Nest (1975)"]))
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['42']
Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['42']
Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ["One Flew Over the Cuckoo's Nest (1975)"]
Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ["One Flew Over the Cuckoo's Nest (1975)"]
Consider rewriting this model with the Functional API.
<tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.03740937]], dtype=float32)>

Убыток и метрики

Следующий компонент - потери, используемые для обучения нашей модели. В TFRS есть несколько уровней потерь и задач, которые упрощают эту задачу.

В этом случае, мы будем использовать в Ranking объекта задачи: пломбируемая обертка , что пучки вместе функции потерь и вычисление метрики.

Мы будем использовать его вместе с MeanSquaredError потери Keras, чтобы предсказать рейтинги.

task = tfrs.tasks.Ranking(
  loss = tf.keras.losses.MeanSquaredError(),
  metrics=[tf.keras.metrics.RootMeanSquaredError()]
)

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

Полная модель

Теперь мы можем собрать все это вместе в модель. ОКФ предоставляет базовый класс модели ( tfrs.models.Model ) , который упорядочивает Bulding модели: все , что нам нужно сделать , это установить компоненты в __init__ методе и реализовать compute_loss метода, принимая в исходных функциях и возвращает значение потерь .

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

class MovielensModel(tfrs.models.Model):

  def __init__(self):
    super().__init__()
    self.ranking_model: tf.keras.Model = RankingModel()
    self.task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
      loss = tf.keras.losses.MeanSquaredError(),
      metrics=[tf.keras.metrics.RootMeanSquaredError()]
    )

  def call(self, features: Dict[str, tf.Tensor]) -> tf.Tensor:
    return self.ranking_model(
        (features["user_id"], features["movie_title"]))

  def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
    labels = features.pop("user_rating")

    rating_predictions = self(features)

    # The task computes the loss and the metrics.
    return self.task(labels=labels, predictions=rating_predictions)

Установка и оценка

После определения модели мы можем использовать стандартные процедуры подгонки и оценки Keras для подбора и оценки модели.

Давайте сначала создадим экземпляр модели.

model = MovielensModel()
model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))

Затем перемешайте, пакетируйте и кэшируйте данные обучения и оценки.

cached_train = train.shuffle(100_000).batch(8192).cache()
cached_test = test.batch(4096).cache()

Затем обучите модель:

model.fit(cached_train, epochs=3)
Epoch 1/3
10/10 [==============================] - 2s 26ms/step - root_mean_squared_error: 2.1718 - loss: 4.3303 - regularization_loss: 0.0000e+00 - total_loss: 4.3303
Epoch 2/3
10/10 [==============================] - 0s 8ms/step - root_mean_squared_error: 1.1227 - loss: 1.2602 - regularization_loss: 0.0000e+00 - total_loss: 1.2602
Epoch 3/3
10/10 [==============================] - 0s 8ms/step - root_mean_squared_error: 1.1162 - loss: 1.2456 - regularization_loss: 0.0000e+00 - total_loss: 1.2456
<keras.callbacks.History at 0x7f28389eaa90>

По мере обучения модели потери падают, а показатель RMSE улучшается.

Наконец, мы можем оценить нашу модель на тестовом наборе:

model.evaluate(cached_test, return_dict=True)
5/5 [==============================] - 2s 14ms/step - root_mean_squared_error: 1.1108 - loss: 1.2287 - regularization_loss: 0.0000e+00 - total_loss: 1.2287
{'root_mean_squared_error': 1.1108061075210571,
 'loss': 1.2062578201293945,
 'regularization_loss': 0,
 'total_loss': 1.2062578201293945}

Чем ниже показатель RMSE, тем точнее наша модель прогнозирует рейтинги.

Тестирование модели ранжирования

Теперь мы можем протестировать модель ранжирования, вычислив прогнозы для набора фильмов, а затем ранжировать эти фильмы на основе прогнозов:

test_ratings = {}
test_movie_titles = ["M*A*S*H (1970)", "Dances with Wolves (1990)", "Speed (1994)"]
for movie_title in test_movie_titles:
  test_ratings[movie_title] = model({
      "user_id": np.array(["42"]),
      "movie_title": np.array([movie_title])
  })

print("Ratings:")
for title, score in sorted(test_ratings.items(), key=lambda x: x[1], reverse=True):
  print(f"{title}: {score}")
Ratings:
M*A*S*H (1970): [[3.584712]]
Dances with Wolves (1990): [[3.551556]]
Speed (1994): [[3.5215874]]

Экспорт для обслуживания

Модель легко экспортируется для обслуживания:

tf.saved_model.save(model, "export")
2021-10-02 11:04:38.235611: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
WARNING:absl:Found untraced functions such as ranking_1_layer_call_and_return_conditional_losses, ranking_1_layer_call_fn, ranking_1_layer_call_fn, ranking_1_layer_call_and_return_conditional_losses, ranking_1_layer_call_and_return_conditional_losses while saving (showing 5 of 5). These functions will not be directly callable after loading.
INFO:tensorflow:Assets written to: export/assets
INFO:tensorflow:Assets written to: export/assets

Теперь мы можем загрузить его обратно и выполнить прогнозы:

loaded = tf.saved_model.load("export")

loaded({"user_id": np.array(["42"]), "movie_title": ["Speed (1994)"]}).numpy()
array([[3.5215874]], dtype=float32)

Следующие шаги

Приведенная выше модель дает нам хорошее начало для построения рейтинговой системы.

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

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

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