Recomendar películas: clasificación

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

Los sistemas de recomendación del mundo real a menudo se componen de dos etapas:

  1. La etapa de recuperación es responsable de seleccionar un conjunto inicial de cientos de candidatos entre todos los posibles candidatos. El objetivo principal de este modelo es eliminar de manera eficiente a todos los candidatos que no le interesan al usuario. Dado que el modelo de recuperación puede estar tratando con millones de candidatos, tiene que ser computacionalmente eficiente.
  2. La etapa de clasificación toma los resultados del modelo de recuperación y los ajusta para seleccionar el mejor puñado de recomendaciones posibles. Su tarea es reducir el conjunto de elementos que pueden interesar al usuario a una lista corta de posibles candidatos.

Nos vamos a centrar en la segunda etapa, la clasificación. Si usted está interesado en la etapa de recuperación, echar un vistazo a nuestra recuperación tutorial.

En este tutorial, vamos a:

  1. Obtenga nuestros datos y divídalos en un conjunto de prueba y entrenamiento.
  2. Implementar un modelo de clasificación.
  3. Ajústelo y evalúelo.

Importaciones

Primero saquemos nuestras importaciones del camino.

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

Preparando el conjunto de datos

Vamos a utilizar los mismos datos que la recuperación de tutorial. Esta vez, también vamos a mantener las calificaciones: estos son los objetivos que estamos tratando de predecir.

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

Como antes, dividiremos los datos colocando el 80% de las calificaciones en el conjunto de trenes y el 20% en el conjunto de prueba.

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)

También averigüemos identificadores de usuario únicos y títulos de películas presentes en los datos.

Esto es importante porque necesitamos poder mapear los valores brutos de nuestras características categóricas para incrustar vectores en nuestros modelos. Para hacer eso, necesitamos un vocabulario que asigne un valor de característica sin procesar a un número entero en un rango contiguo: esto nos permite buscar las incrustaciones correspondientes en nuestras tablas de incrustaciones.

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

Implementando un modelo

Arquitectura

Los modelos de clasificación no enfrentan las mismas restricciones de eficiencia que los modelos de recuperación, por lo que tenemos un poco más de libertad en nuestra elección de arquitecturas.

Un modelo compuesto por múltiples capas densas apiladas es una arquitectura relativamente común para clasificar tareas. Podemos implementarlo de la siguiente manera:

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

Este modelo toma identificadores de usuario y títulos de películas, y genera una calificación prevista:

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

Pérdidas y métricas

El siguiente componente es la pérdida utilizada para entrenar nuestro modelo. TFRS tiene varias capas de pérdidas y tareas para facilitar esta tarea.

En este caso, vamos a hacer uso de la Ranking objeto de tarea: una envoltura de conveniencia que agrupa juntos la función de pérdida y el cómputo métrico.

La usaremos junto con el MeanSquaredError pérdida Keras con el fin de predecir las votaciones.

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

La tarea en sí es una capa de Keras que toma verdadero y predicho como argumentos, y devuelve la pérdida calculada. Lo usaremos para implementar el ciclo de entrenamiento del modelo.

El modelo completo

Ahora podemos ponerlo todo junto en un modelo. TfRs expone una clase de modelo de base ( tfrs.models.Model ), que simplifica los modelos de bulding: todo lo que tenemos que hacer es configurar los componentes en el __init__ método, y aplicar el compute_loss método, disfruta de las características primas y devolver un valor de pérdida .

El modelo base se encargará de crear el ciclo de entrenamiento apropiado para ajustarse a nuestro modelo.

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)

Ajuste y evaluación

Después de definir el modelo, podemos usar rutinas estándar de ajuste y evaluación de Keras para ajustar y evaluar el modelo.

Primero creemos una instancia del modelo.

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

Luego, mezcle, agrupe y almacene en caché los datos de capacitación y evaluación.

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

Luego entrena el modelo:

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>

A medida que se entrena el modelo, la pérdida disminuye y la métrica RMSE mejora.

Finalmente, podemos evaluar nuestro modelo en el conjunto de prueba:

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}

Cuanto menor sea la métrica RMSE, más preciso será nuestro modelo para predecir las calificaciones.

Probando el modelo de clasificación

Ahora podemos probar el modelo de clasificación calculando predicciones para un conjunto de películas y luego clasificar estas películas según las predicciones:

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

Exportar para servir

El modelo se puede exportar fácilmente para 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

Ahora podemos volver a cargarlo y realizar predicciones:

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

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

Próximos pasos

El modelo anterior nos da un comienzo decente hacia la construcción de un sistema de clasificación.

Por supuesto, hacer un sistema de clasificación práctico requiere mucho más esfuerzo.

En la mayoría de los casos, un modelo de clasificación se puede mejorar sustancialmente utilizando más funciones en lugar de solo identificadores de usuarios y candidatos. Para ver cómo hacer eso, tener una mirada en el lado de las características tutorial.

También es necesaria una comprensión cuidadosa de los objetivos que vale la pena optimizar. Para empezar a trabajar en la construcción de un sistema de recomendación que optimiza múltiples objetivos, echar un vistazo a nuestra multitarea tutorial.