Classement par liste

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

Dans le tutoriel classement de base , nous avons formé un modèle qui peut prédire les évaluations pour paires utilisateur / film. Le modèle a été formé pour minimiser l'erreur quadratique moyenne des cotes prédites.

Cependant, l'optimisation des prédictions du modèle sur des films individuels n'est pas nécessairement la meilleure méthode pour entraîner des modèles de classement. Nous n'avons pas besoin de modèles de classement pour prédire les scores avec une grande précision. Au lieu de cela, nous nous soucions davantage de la capacité du modèle à générer une liste ordonnée d'éléments qui correspond à l'ordre des préférences de l'utilisateur.

Au lieu d'optimiser les prédictions du modèle sur des paires requête/élément individuelles, nous pouvons optimiser le classement du modèle d'une liste dans son ensemble. Cette méthode est appelée listwise classement.

Dans ce didacticiel, nous utiliserons les recommandations TensorFlow pour créer des modèles de classement par liste. Pour ce faire, nous allons utiliser le classement des pertes et des mesures fournies par tensorflow Classement , un ensemble de tensorflow qui met l' accent sur l' apprentissage de rang .

Préliminaires

Si tensorflow Le classement est pas disponible dans votre environnement d'exécution, vous pouvez l' installer à l' aide pip :

pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
pip install -q tensorflow-ranking

Nous pouvons alors importer tous les packages nécessaires :

import pprint

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/pkg_resources/__init__.py:119: PkgResourcesDeprecationWarning: 0.18ubuntu0.18.04.1 is an invalid version and will not be supported in a future release
  PkgResourcesDeprecationWarning,
import tensorflow_ranking as tfr
import tensorflow_recommenders as tfrs
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow_addons/utils/ensure_tf_install.py:67: UserWarning: Tensorflow Addons supports using Python ops for all Tensorflow versions above or equal to 2.4.0 and strictly below 2.7.0 (nightly versions are not supported). 
 The versions of TensorFlow you are currently using is 2.7.0 and is not supported. 
Some things might work, some things might not.
If you were to encounter a bug, do not file an issue.
If you want to make sure you're using a tested and supported configuration, either change the TensorFlow version or the TensorFlow Addons's version. 
You can find the compatibility matrix in TensorFlow Addon's readme:
https://github.com/tensorflow/addons
  UserWarning,

Nous continuerons à utiliser le jeu de données MovieLens 100K. Comme auparavant, nous chargeons les ensembles de données et ne conservons que l'ID utilisateur, le titre du film et les fonctionnalités d'évaluation de l'utilisateur pour ce didacticiel. Nous faisons également un peu de ménage pour préparer notre vocabulaire.

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

ratings = ratings.map(lambda x: {
    "movie_title": x["movie_title"],
    "user_id": x["user_id"],
    "user_rating": x["user_rating"],
})
movies = movies.map(lambda x: x["movie_title"])

unique_movie_titles = np.unique(np.concatenate(list(movies.batch(1000))))
unique_user_ids = np.unique(np.concatenate(list(ratings.batch(1_000).map(
    lambda x: x["user_id"]))))

Prétraitement des données

Cependant, nous ne pouvons pas utiliser directement l'ensemble de données MovieLens pour l'optimisation de liste. Pour effectuer une optimisation par liste, nous devons avoir accès à une liste de films que chaque utilisateur a évalués, mais chaque exemple de l'ensemble de données MovieLens 100K ne contient que l'évaluation d'un seul film.

Pour contourner ce problème, nous transformons l'ensemble de données afin que chaque exemple contienne un identifiant d'utilisateur et une liste de films évalués par cet utilisateur. Certains films de la liste seront mieux classés que d'autres ; le but de notre modèle sera de faire des prédictions qui correspondent à cet ordre.

Pour ce faire, nous utilisons la tfrs.examples.movielens.movielens_to_listwise fonction d'aide. Il prend l'ensemble de données MovieLens 100K et génère un ensemble de données contenant des exemples de liste comme indiqué ci-dessus. Les détails de mise en œuvre se trouvent dans le code source .

tf.random.set_seed(42)

# Split between train and tests sets, as before.
shuffled = ratings.shuffle(100_000, seed=42, reshuffle_each_iteration=False)

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

# We sample 50 lists for each user for the training data. For each list we
# sample 5 movies from the movies the user rated.
train = tfrs.examples.movielens.sample_listwise(
    train,
    num_list_per_user=50,
    num_examples_per_list=5,
    seed=42
)
test = tfrs.examples.movielens.sample_listwise(
    test,
    num_list_per_user=1,
    num_examples_per_list=5,
    seed=42
)

Nous pouvons inspecter un exemple à partir des données d'entraînement. L'exemple comprend un identifiant d'utilisateur, une liste de 10 identifiants de films et leurs évaluations par l'utilisateur.

for example in train.take(1):
  pprint.pprint(example)
{'movie_title': <tf.Tensor: shape=(5,), dtype=string, numpy=
array([b'Postman, The (1997)', b'Liar Liar (1997)', b'Contact (1997)',
       b'Welcome To Sarajevo (1997)',
       b'I Know What You Did Last Summer (1997)'], dtype=object)>,
 'user_id': <tf.Tensor: shape=(), dtype=string, numpy=b'681'>,
 'user_rating': <tf.Tensor: shape=(5,), dtype=float32, numpy=array([4., 5., 1., 4., 1.], dtype=float32)>}

Définition du modèle

Nous allons entraîner le même modèle avec trois pertes différentes :

  • erreur quadratique moyenne,
  • perte de charnière par paire, et
  • une perte ListMLE listwise.

Ces trois pertes correspondent à une optimisation par points, par paires et par listes.

Pour évaluer le modèle que nous utilisons normalisé les réduit le gain cumulatif (NDCG) . Le NDCG mesure un classement prévu en prenant une somme pondérée de la note réelle de chaque candidat. Les notes des films qui sont classés plus bas par le modèle seraient davantage réduites. En conséquence, un bon modèle qui classe les films les mieux notés en haut aurait un résultat NDCG élevé. Étant donné que cette métrique prend en compte la position classée de chaque candidat, il s'agit d'une métrique par liste.

class RankingModel(tfrs.Model):

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

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

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

    # Compute predictions.
    self.score_model = 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)
    ])

    self.task = tfrs.tasks.Ranking(
      loss=loss,
      metrics=[
        tfr.keras.metrics.NDCGMetric(name="ndcg_metric"),
        tf.keras.metrics.RootMeanSquaredError()
      ]
    )

  def call(self, features):
    # We first convert the id features into embeddings.
    # User embeddings are a [batch_size, embedding_dim] tensor.
    user_embeddings = self.user_embeddings(features["user_id"])

    # Movie embeddings are a [batch_size, num_movies_in_list, embedding_dim]
    # tensor.
    movie_embeddings = self.movie_embeddings(features["movie_title"])

    # We want to concatenate user embeddings with movie emebeddings to pass
    # them into the ranking model. To do so, we need to reshape the user
    # embeddings to match the shape of movie embeddings.
    list_length = features["movie_title"].shape[1]
    user_embedding_repeated = tf.repeat(
        tf.expand_dims(user_embeddings, 1), [list_length], axis=1)

    # Once reshaped, we concatenate and pass into the dense layers to generate
    # predictions.
    concatenated_embeddings = tf.concat(
        [user_embedding_repeated, movie_embeddings], 2)

    return self.score_model(concatenated_embeddings)

  def compute_loss(self, features, training=False):
    labels = features.pop("user_rating")

    scores = self(features)

    return self.task(
        labels=labels,
        predictions=tf.squeeze(scores, axis=-1),
    )

Former les modèles

Nous pouvons maintenant entraîner chacun des trois modèles.

epochs = 30

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

Modèle d'erreur quadratique moyenne

Ce modèle est très similaire au modèle dans le tutoriel classement de base . Nous entraînons le modèle pour minimiser l'erreur quadratique moyenne entre les notes réelles et les notes prédites. Par conséquent, cette perte est calculée individuellement pour chaque film et l'apprentissage est ponctuel.

mse_model = RankingModel(tf.keras.losses.MeanSquaredError())
mse_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
mse_model.fit(cached_train, epochs=epochs, verbose=False)
<keras.callbacks.History at 0x7f64791a5d10>

Modèle de perte de charnière par paire

En minimisant la perte de charnière par paire, le modèle essaie de maximiser la différence entre les prédictions du modèle pour un élément hautement coté et un élément faiblement coté : plus cette différence est grande, plus la perte du modèle est faible. Cependant, une fois que la différence est suffisamment importante, la perte devient nulle, empêchant le modèle d'optimiser davantage cette paire particulière et le laissant se concentrer sur d'autres paires mal classées.

Cette perte n'est pas calculée pour des films individuels, mais plutôt pour des paires de films. Par conséquent, l'entraînement utilisant cette perte se fait par paires.

hinge_model = RankingModel(tfr.keras.losses.PairwiseHingeLoss())
hinge_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
hinge_model.fit(cached_train, epochs=epochs, verbose=False)
<keras.callbacks.History at 0x7f647914f190>

Modèle listwise

La ListMLE perte de tensorflow EXPRIME classement liste estimation du maximum de vraisemblance. Pour calculer la perte ListMLE, nous utilisons d'abord les évaluations des utilisateurs pour générer un classement optimal. Nous calculons ensuite la probabilité que chaque candidat soit surclassé par tout élément en dessous de lui dans le classement optimal en utilisant les scores prédits. Le modèle essaie de minimiser cette probabilité pour s'assurer que les candidats les mieux notés ne sont pas surclassés par les candidats les moins bien notés. Vous pouvez en savoir plus sur les détails de ListMLE à la section 2.2 du document conscient Position ListMLE: un processus d' apprentissage séquentiel .

Notez que puisque la vraisemblance est calculée par rapport à un candidat et à tous les candidats en dessous de lui dans le classement optimal, la perte n'est pas par paire mais par liste. Par conséquent, la formation utilise l'optimisation de liste.

listwise_model = RankingModel(tfr.keras.losses.ListMLELoss())
listwise_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
listwise_model.fit(cached_train, epochs=epochs, verbose=False)
<keras.callbacks.History at 0x7f647b35f350>

Comparer les modèles

mse_model_result = mse_model.evaluate(cached_test, return_dict=True)
print("NDCG of the MSE Model: {:.4f}".format(mse_model_result["ndcg_metric"]))
1/1 [==============================] - 0s 405ms/step - ndcg_metric: 0.9053 - root_mean_squared_error: 0.9671 - loss: 0.9354 - regularization_loss: 0.0000e+00 - total_loss: 0.9354
NDCG of the MSE Model: 0.9053
hinge_model_result = hinge_model.evaluate(cached_test, return_dict=True)
print("NDCG of the pairwise hinge loss model: {:.4f}".format(hinge_model_result["ndcg_metric"]))
1/1 [==============================] - 0s 457ms/step - ndcg_metric: 0.9058 - root_mean_squared_error: 3.8330 - loss: 1.0180 - regularization_loss: 0.0000e+00 - total_loss: 1.0180
NDCG of the pairwise hinge loss model: 0.9058
listwise_model_result = listwise_model.evaluate(cached_test, return_dict=True)
print("NDCG of the ListMLE model: {:.4f}".format(listwise_model_result["ndcg_metric"]))
1/1 [==============================] - 0s 432ms/step - ndcg_metric: 0.9071 - root_mean_squared_error: 2.7224 - loss: 4.5401 - regularization_loss: 0.0000e+00 - total_loss: 4.5401
NDCG of the ListMLE model: 0.9071

Des trois modèles, le modèle formé à l'aide de ListMLE a la métrique NDCG la plus élevée. Ce résultat montre comment l'optimisation par liste peut être utilisée pour former des modèles de classement et peut potentiellement produire des modèles plus performants que les modèles optimisés par points ou par paires.