Clasificación por lista

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

En el tutorial clasificación básica , hemos formado un modelo que puede predecir notas de pares usuario / película. El modelo se entrenó para minimizar el error cuadrático medio de las calificaciones previstas.

Sin embargo, optimizar las predicciones del modelo en películas individuales no es necesariamente el mejor método para entrenar modelos de clasificación. No necesitamos modelos de clasificación para predecir puntuaciones con gran precisión. En cambio, nos preocupamos más por la capacidad del modelo para generar una lista ordenada de elementos que coincida con el orden de preferencia del usuario.

En lugar de optimizar las predicciones del modelo en pares de consulta / elemento individuales, podemos optimizar la clasificación del modelo de una lista como un todo. Este método es llamado por lista clasificación.

En este tutorial, usaremos los recomendadores de TensorFlow para crear modelos de clasificación por listas. Para ello, vamos a hacer uso de la clasificación de las pérdidas y las métricas proporcionadas por TensorFlow Clasificación , un paquete TensorFlow que se centra en el aprendizaje de su rango .

Preliminares

Si TensorFlow Clasificación no está disponible en el entorno de ejecución, puede instalarlo usando pip :

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

Luego podemos importar todos los paquetes necesarios:

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,

Continuaremos usando el conjunto de datos MovieLens 100K. Como antes, cargamos los conjuntos de datos y solo conservamos la identificación del usuario, el título de la película y las características de calificación del usuario para este tutorial. También hacemos un poco de limpieza para preparar nuestro vocabulario.

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

Preprocesamiento de datos

Sin embargo, no podemos usar el conjunto de datos MovieLens para la optimización de listas directamente. Para realizar la optimización por lista, necesitamos tener acceso a una lista de películas que cada usuario ha calificado, pero cada ejemplo en el conjunto de datos MovieLens 100K contiene solo la calificación de una sola película.

Para evitar esto, transformamos el conjunto de datos para que cada ejemplo contenga una identificación de usuario y una lista de películas calificadas por ese usuario. Algunas películas de la lista se clasificarán más arriba que otras; el objetivo de nuestro modelo será realizar predicciones que coincidan con este orden.

Para ello, se utiliza el tfrs.examples.movielens.movielens_to_listwise función auxiliar. Toma el conjunto de datos MovieLens 100K y genera un conjunto de datos que contiene ejemplos de listas como se discutió anteriormente. Los detalles de implementación se encuentran en el código fuente .

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
)

Podemos inspeccionar un ejemplo de los datos de entrenamiento. El ejemplo incluye una identificación de usuario, una lista de 10 identificaciones de películas y sus calificaciones por parte del usuario.

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

Definición de modelo

Entrenaremos el mismo modelo con tres pérdidas diferentes:

  • error medio cuadrado,
  • pérdida de bisagra por pares, y
  • una pérdida ListMLE listwise.

Estas tres pérdidas corresponden a la optimización puntual, por pares y por lista.

Para evaluar el modelo que utilizamos normalizaron con descuento ganancia acumulada (NDCG) . NDCG mide una clasificación prevista tomando una suma ponderada de la clasificación real de cada candidato. Las calificaciones de las películas que están clasificadas más bajas por el modelo se descontarían más. Como resultado, un buen modelo que ubique las películas de alta calificación en la parte superior tendría un resultado NDCG alto. Dado que esta métrica tiene en cuenta la posición clasificada de cada candidato, es una métrica por lista.

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

Entrenando a los modelos

Ahora podemos entrenar cada uno de los tres modelos.

epochs = 30

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

Modelo de error cuadrático medio

Este modelo es muy similar al modelo en el tutorial clasificación básica . Entrenamos el modelo para minimizar el error cuadrático medio entre las calificaciones reales y las predichas. Por lo tanto, esta pérdida se calcula individualmente para cada película y el entrenamiento es puntual.

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>

Modelo de pérdida de bisagra por pares

Al minimizar la pérdida de bisagra por pares, el modelo intenta maximizar la diferencia entre las predicciones del modelo para un artículo con una calificación alta y un artículo con una calificación baja: cuanto mayor es la diferencia, menor es la pérdida del modelo. Sin embargo, una vez que la diferencia es lo suficientemente grande, la pérdida se vuelve cero, lo que impide que el modelo optimice aún más este par en particular y le permita enfocarse en otros pares que están clasificados incorrectamente.

Esta pérdida no se calcula para películas individuales, sino para parejas de películas. Por tanto, el entrenamiento que utiliza esta pérdida es por parejas.

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>

Modelo Listwise

El ListMLE pérdida de TensorFlow expresa Lista de clasificación estimación de máxima verosimilitud. Para calcular la pérdida de ListMLE, primero usamos las calificaciones de los usuarios para generar una clasificación óptima. Luego calculamos la probabilidad de que cada candidato sea superado por cualquier elemento por debajo de él en la clasificación óptima utilizando las puntuaciones previstas. El modelo intenta minimizar dicha probabilidad para garantizar que los candidatos con calificaciones altas no sean superados por los candidatos con calificaciones bajas. Usted puede aprender más acerca de los detalles de ListMLE en la sección 2.2 del documento ListMLE Posición-consciente: Un Proceso de Aprendizaje secuencial .

Tenga en cuenta que, dado que la probabilidad se calcula con respecto a un candidato y todos los candidatos por debajo de él en la clasificación óptima, la pérdida no es por pares sino por listas. Por lo tanto, el entrenamiento utiliza la optimización de listas.

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>

Comparando los modelos

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

De los tres modelos, el modelo entrenado con ListMLE tiene la métrica NDCG más alta. Este resultado muestra cómo la optimización por listas se puede utilizar para entrenar modelos de clasificación y potencialmente puede producir modelos que funcionen mejor que los modelos optimizados de manera puntual o por pares.