Recomendar filmes: classificação

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Os sistemas de recomendação do mundo real costumam ser compostos de dois estágios:

  1. O estágio de recuperação é responsável por selecionar um conjunto inicial de centenas de candidatos de todos os candidatos possíveis. O principal objetivo desse modelo é eliminar com eficiência todos os candidatos nos quais o usuário não está interessado. Como o modelo de recuperação pode lidar com milhões de candidatos, ele deve ser computacionalmente eficiente.
  2. O estágio de classificação pega os resultados do modelo de recuperação e os ajusta para selecionar o melhor punhado possível de recomendações. Sua tarefa é restringir o conjunto de itens nos quais o usuário pode estar interessado em uma lista de prováveis ​​candidatos.

Vamos nos concentrar na segunda etapa, a classificação. Se você estiver interessado na fase de recuperação, ter um olhar para a nossa recuperação tutorial.

Neste tutorial, vamos:

  1. Obtenha nossos dados e divida-os em um conjunto de treinamento e teste.
  2. Implemente um modelo de classificação.
  3. Ajustar e avaliar.

Importações

Vamos primeiro tirar nossas importações do caminho.

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 o conjunto de dados

Vamos usar os mesmos dados como a recuperação de tutorial. Desta vez, também vamos manter as classificações: são estes os objetivos que pretendemos prever.

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 os dados colocando 80% das avaliações no conjunto de trem e 20% no conjunto de teste.

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)

Também vamos descobrir ids de usuário e títulos de filmes exclusivos presentes nos dados.

Isso é importante porque precisamos ser capazes de mapear os valores brutos de nossos recursos categóricos para inserir vetores em nossos modelos. Para fazer isso, precisamos de um vocabulário que mapeie um valor de recurso bruto para um inteiro em um intervalo contíguo: isso nos permite consultar os embeddings correspondentes em nossas tabelas de embedding.

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 um modelo

Arquitetura

Os modelos de classificação não enfrentam as mesmas restrições de eficiência dos modelos de recuperação e, portanto, temos um pouco mais de liberdade na escolha das arquiteturas.

Um modelo composto de várias camadas densas empilhadas é uma arquitetura relativamente comum para tarefas de classificação. Podemos implementá-lo da seguinte maneira:

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 usa IDs de usuário e títulos de filmes e produz uma classificação 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)>

Perda e métricas

O próximo componente é a perda usada para treinar nosso modelo. TFRS tem várias camadas de perda e tarefas para tornar isso fácil.

Neste caso, vamos fazer uso do Ranking objeto de tarefa: um invólucro de conveniência que agrupa a função de perda e de cálculo da métrica.

Vamos usá-lo juntamente com o MeanSquaredError perda Keras, a fim de prever os ratings.

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

A tarefa em si é uma camada Keras que considera verdadeiro e previsto como argumentos e retorna a perda calculada. Usaremos isso para implementar o loop de treinamento do modelo.

O modelo completo

Agora podemos colocar tudo junto em um modelo. TFRS expõe uma classe de modelo base ( tfrs.models.Model ) que simplifica modelos bulding: tudo o que precisamos fazer é configurar os componentes no __init__ método, e implementar o compute_loss método, tendo em recursos brutos e retornar um valor de perda .

O modelo básico se encarregará de criar o loop de treinamento apropriado para se ajustar ao nosso 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)

Ajustando e avaliando

Depois de definir o modelo, podemos usar rotinas de ajuste e avaliação Keras padrão para ajustar e avaliar o modelo.

Vamos primeiro instanciar o modelo.

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

Em seguida, embaralhe, agrupe e armazene em cache os dados de treinamento e avaliação.

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

Em seguida, treine o 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>

Conforme o modelo treina, a perda diminui e a métrica RMSE melhora.

Finalmente, podemos avaliar nosso modelo no conjunto de teste:

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}

Quanto mais baixa for a métrica RMSE, mais preciso será o nosso modelo nas classificações de previsão.

Testando o modelo de classificação

Agora podemos testar o modelo de classificação calculando previsões para um conjunto de filmes e, em seguida, classificar esses filmes com base nas previsões:

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

Exportando para servir

O modelo pode ser facilmente exportado 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

Agora podemos carregá-lo de volta e realizar previsões:

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

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

Próximos passos

O modelo acima nos dá um começo decente para a construção de um sistema de classificação.

Claro, fazer um sistema de classificação prático requer muito mais esforço.

Na maioria dos casos, um modelo de classificação pode ser substancialmente aprimorado usando mais recursos, em vez de apenas identificadores de usuário e candidato. Para ver como fazer isso, ter um olhar para o lado apresenta tutorial.

Uma compreensão cuidadosa dos objetivos que vale a pena otimizar também é necessária. Para começar a construir um recommender que otimiza múltiplos objetivos, ter um olhar para o nosso multitask tutorial.