Recommander des films : classement

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Les systèmes de recommandation du monde réel sont souvent composés de deux étapes :

  1. L'étape de récupération est chargée de sélectionner un ensemble initial de centaines de candidats parmi tous les candidats possibles. L'objectif principal de ce modèle est d'éliminer efficacement tous les candidats qui ne intéressent pas l'utilisateur. Étant donné que le modèle de récupération peut traiter des millions de candidats, il doit être efficace en termes de calcul.
  2. L'étape de classement prend les sorties du modèle de récupération et les affine pour sélectionner la meilleure poignée possible de recommandations. Sa tâche consiste à réduire l'ensemble d'éléments susceptibles d'intéresser l'utilisateur à une liste restreinte de candidats probables.

Nous allons nous concentrer sur la deuxième étape, le classement. Si vous êtes intéressé à l'étape de récupération, jetez un oeil à notre recherche tutoriel.

Dans ce tutoriel, nous allons :

  1. Obtenez nos données et divisez-les en un ensemble d'entraînement et de test.
  2. Mettre en place un modèle de classement.
  3. Ajustez-le et évaluez-le.

Importations

Éliminons d'abord nos importations.

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

Préparation du jeu de données

Nous allons utiliser les mêmes données que la récupération tutoriel. Cette fois, on va aussi garder les notes : ce sont les objectifs qu'on essaie de prévoir.

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

Comme précédemment, nous allons diviser les données en mettant 80% des notes dans l'ensemble de train et 20% dans l'ensemble de test.

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)

Déterminons également les identifiants d'utilisateur uniques et les titres de films présents dans les données.

Ceci est important car nous devons être en mesure de mapper les valeurs brutes de nos caractéristiques catégorielles aux vecteurs intégrés dans nos modèles. Pour ce faire, nous avons besoin d'un vocabulaire qui mappe une valeur de caractéristique brute à un entier dans une plage contiguë : cela nous permet de rechercher les plongements correspondants dans nos tables de plongement.

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)))

Implémentation d'un modèle

Architecture

Les modèles de classement ne sont pas confrontés aux mêmes contraintes d'efficacité que les modèles de recherche, nous avons donc un peu plus de liberté dans nos choix d'architectures.

Un modèle composé de plusieurs couches denses empilées est une architecture relativement courante pour le classement des tâches. Nous pouvons l'implémenter comme suit :

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))

Ce modèle prend les identifiants des utilisateurs et les titres des films, et génère une évaluation prédite :

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)>

Perte et métriques

Le composant suivant est la perte utilisée pour entraîner notre modèle. TFRS a plusieurs couches et tâches de perte pour rendre cela facile.

Dans ce cas, nous utilisons le Ranking objet tâche: un emballage pratique qui regroupe ensemble la fonction de perte et calcul métrique.

Nous allons l' utiliser en même temps que la l' MeanSquaredError perte Keras afin de prédire les évaluations.

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

La tâche elle-même est une couche Keras qui prend vrai et prédit comme arguments et renvoie la perte calculée. Nous l'utiliserons pour implémenter la boucle d'entraînement du modèle.

Le modèle complet

Nous pouvons maintenant mettre le tout ensemble dans un modèle. TFRS expose une classe de modèle de base ( tfrs.models.Model ) qui rationalise les modèles bulding: tout ce que nous devons faire est de mettre en place les composants dans la __init__ méthode, et mettre en œuvre la compute_loss méthode, en prenant les fonctions premières et de retourner une valeur de perte .

Le modèle de base s'occupera ensuite de créer la boucle d'entraînement appropriée pour s'adapter à notre modèle.

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)

Ajustement et évaluation

Après avoir défini le modèle, nous pouvons utiliser les routines d'ajustement et d'évaluation Keras standard pour ajuster et évaluer le modèle.

Commençons par instancier le modèle.

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

Ensuite, mélangez, regroupez et cachez les données d'entraînement et d'évaluation.

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

Ensuite, entraînez le modèle :

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>

Au fur et à mesure que le modèle s'entraîne, la perte diminue et la métrique RMSE s'améliore.

Enfin, nous pouvons évaluer notre modèle sur l'ensemble de test :

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}

Plus la métrique RMSE est basse, plus notre modèle est précis pour prédire les notes.

Tester le modèle de classement

Nous pouvons maintenant tester le modèle de classement en calculant des prédictions pour un ensemble de films, puis classer ces films en fonction des prédictions :

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]]

Exportation pour diffusion

Le modèle peut être facilement exporté pour servir :

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

Nous pouvons maintenant le recharger et effectuer des prédictions :

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

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

Prochaines étapes

Le modèle ci-dessus nous donne un bon départ vers la construction d'un système de classement.

Bien sûr, créer un système de classement pratique nécessite beaucoup plus d'efforts.

Dans la plupart des cas, un modèle de classement peut être considérablement amélioré en utilisant plus de fonctionnalités plutôt que de simples identifiants d'utilisateur et de candidat. Pour savoir comment faire, jetez un oeil à côté fonctionnalités tutoriel.

Une bonne compréhension des objectifs à optimiser est également nécessaire. Pour commencer la construction d' un recommender qui optimise les objectifs multiples, un coup d' oeil à notre multitâches tutoriel.