O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

Recomendadores multitarefa

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

No tutorial de recuperação básico nós construímos um sistema de recuperação usando relógios de filmes como sinais de interação positiva.

Em muitos aplicativos, no entanto, existem várias fontes ricas de feedback para recorrer. Por exemplo, um site de comércio eletrônico pode registrar as visitas do usuário às páginas do produto (sinal abundante, mas relativamente baixo), cliques na imagem, adição ao carrinho e, finalmente, compras. Ele pode até registrar sinais pós-compra, como avaliações e devoluções.

Integrar todas essas diferentes formas de feedback é fundamental para construir sistemas que os usuários adoram usar e que não otimizam para nenhuma métrica em detrimento do desempenho geral.

Além disso, construir um modelo conjunto para várias tarefas pode produzir melhores resultados do que construir vários modelos específicos de tarefas. Isso é especialmente verdadeiro quando alguns dados são abundantes (por exemplo, cliques) e alguns dados são esparsos (compras, devoluções, revisões manuais). Nesses cenários, um modelo de articulação pode ser capaz de usar representações aprendidas com a tarefa abundante para melhorar as suas previsões sobre a tarefa esparsos por meio de um fenômeno conhecido como aprendizado transferência . Por exemplo, este papel mostra que um modelo de previsão de classificações explícita do utilizador a partir inquéritos aos utilizadores esparsos podem ser substancialmente melhoradas através da adição de um auxiliar tarefa que utiliza dados de log clique abundantes.

Neste tutorial, vamos construir um recomendador multi-objetivo para Movielens, usando sinais implícitos (assistir filmes) e sinais explícitos (avaliações).

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 o conjunto de dados Movielens 100K.

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

# Select the basic features.
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"])
2021-10-02 12:05:52.001219: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

E repita nossos preparativos para construir vocabulários e dividir os dados em um trem e um conjunto de teste:

# Randomly shuffle data and split between train and 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)

movie_titles = movies.batch(1_000)
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)))

Um modelo multitarefa

Existem duas partes críticas para os recomendadores multitarefa:

  1. Eles otimizam para dois ou mais objetivos e, portanto, têm duas ou mais perdas.
  2. Eles compartilham variáveis ​​entre as tarefas, permitindo a aprendizagem por transferência.

Neste tutorial, vamos definir nossos modelos como antes, mas em vez de ter uma única tarefa, teremos duas tarefas: uma que prevê avaliações e outra que prevê assistências de filmes.

Os modelos de usuário e filme são como antes:

user_model = tf.keras.Sequential([
  tf.keras.layers.StringLookup(
      vocabulary=unique_user_ids, mask_token=None),
  # We add 1 to account for the unknown token.
  tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
])

movie_model = 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)
])

Porém, agora teremos duas tarefas. A primeira é a tarefa de classificação:

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

Seu objetivo é prever as classificações com a maior precisão possível.

A segunda é a tarefa de recuperação:

tfrs.tasks.Retrieval(
    metrics=tfrs.metrics.FactorizedTopK(
        candidates=movies.batch(128)
    )
)

Como antes, o objetivo dessa tarefa é prever quais filmes o usuário assistirá ou não.

Juntar as peças

Reunimos tudo em uma classe modelo.

O novo componente aqui é que - como temos duas tarefas e duas perdas - precisamos decidir a importância de cada perda. Podemos fazer isso atribuindo um peso a cada uma das perdas e tratando esses pesos como hiperparâmetros. Se atribuirmos um grande peso de perda à tarefa de classificação, nosso modelo se concentrará na previsão de classificações (mas ainda usará algumas informações da tarefa de recuperação); se atribuirmos um grande peso de perda à tarefa de recuperação, ela se concentrará na recuperação.

class MovielensModel(tfrs.models.Model):

  def __init__(self, rating_weight: float, retrieval_weight: float) -> None:
    # We take the loss weights in the constructor: this allows us to instantiate
    # several model objects with different loss weights.

    super().__init__()

    embedding_dimension = 32

    # User and movie models.
    self.movie_model: tf.keras.layers.Layer = 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)
    ])
    self.user_model: tf.keras.layers.Layer = 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)
    ])

    # A small model to take in user and movie embeddings and predict ratings.
    # We can make this as complicated as we want as long as we output a scalar
    # as our prediction.
    self.rating_model = tf.keras.Sequential([
        tf.keras.layers.Dense(256, activation="relu"),
        tf.keras.layers.Dense(128, activation="relu"),
        tf.keras.layers.Dense(1),
    ])

    # The tasks.
    self.rating_task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
        loss=tf.keras.losses.MeanSquaredError(),
        metrics=[tf.keras.metrics.RootMeanSquaredError()],
    )
    self.retrieval_task: tf.keras.layers.Layer = tfrs.tasks.Retrieval(
        metrics=tfrs.metrics.FactorizedTopK(
            candidates=movies.batch(128).map(self.movie_model)
        )
    )

    # The loss weights.
    self.rating_weight = rating_weight
    self.retrieval_weight = retrieval_weight

  def call(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
    # We pick out the user features and pass them into the user model.
    user_embeddings = self.user_model(features["user_id"])
    # And pick out the movie features and pass them into the movie model.
    movie_embeddings = self.movie_model(features["movie_title"])

    return (
        user_embeddings,
        movie_embeddings,
        # We apply the multi-layered rating model to a concatentation of
        # user and movie embeddings.
        self.rating_model(
            tf.concat([user_embeddings, movie_embeddings], axis=1)
        ),
    )

  def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:

    ratings = features.pop("user_rating")

    user_embeddings, movie_embeddings, rating_predictions = self(features)

    # We compute the loss for each task.
    rating_loss = self.rating_task(
        labels=ratings,
        predictions=rating_predictions,
    )
    retrieval_loss = self.retrieval_task(user_embeddings, movie_embeddings)

    # And combine them using the loss weights.
    return (self.rating_weight * rating_loss

            + self.retrieval_weight * retrieval_loss)

Modelo especializado em classificação

Dependendo dos pesos que atribuímos, o modelo codificará um equilíbrio diferente das tarefas. Vamos começar com um modelo que considera apenas classificações.

model = MovielensModel(rating_weight=1.0, retrieval_weight=0.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
cached_train = train.shuffle(100_000).batch(8192).cache()
cached_test = test.batch(4096).cache()
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 7s 335ms/step - root_mean_squared_error: 2.0903 - factorized_top_k/top_1_categorical_accuracy: 3.8750e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0026 - factorized_top_k/top_10_categorical_accuracy: 0.0055 - factorized_top_k/top_50_categorical_accuracy: 0.0296 - factorized_top_k/top_100_categorical_accuracy: 0.0590 - loss: 4.0315 - regularization_loss: 0.0000e+00 - total_loss: 4.0315
Epoch 2/3
10/10 [==============================] - 3s 322ms/step - root_mean_squared_error: 1.1531 - factorized_top_k/top_1_categorical_accuracy: 3.5000e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0026 - factorized_top_k/top_10_categorical_accuracy: 0.0055 - factorized_top_k/top_50_categorical_accuracy: 0.0298 - factorized_top_k/top_100_categorical_accuracy: 0.0593 - loss: 1.3189 - regularization_loss: 0.0000e+00 - total_loss: 1.3189
Epoch 3/3
10/10 [==============================] - 3s 317ms/step - root_mean_squared_error: 1.1198 - factorized_top_k/top_1_categorical_accuracy: 4.2500e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0026 - factorized_top_k/top_10_categorical_accuracy: 0.0056 - factorized_top_k/top_50_categorical_accuracy: 0.0302 - factorized_top_k/top_100_categorical_accuracy: 0.0598 - loss: 1.2479 - regularization_loss: 0.0000e+00 - total_loss: 1.2479
5/5 [==============================] - 3s 158ms/step - root_mean_squared_error: 1.1130 - factorized_top_k/top_1_categorical_accuracy: 5.0000e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0030 - factorized_top_k/top_10_categorical_accuracy: 0.0050 - factorized_top_k/top_50_categorical_accuracy: 0.0298 - factorized_top_k/top_100_categorical_accuracy: 0.0596 - loss: 1.2336 - regularization_loss: 0.0000e+00 - total_loss: 1.2336
Retrieval top-100 accuracy: 0.060.
Ranking RMSE: 1.113.

O modelo se sai bem na previsão de classificações (com um RMSE de cerca de 1,11), mas tem um desempenho ruim em prever quais filmes serão assistidos ou não: sua precisão em 100 é quase 4 vezes pior do que um modelo treinado exclusivamente para prever relógios.

Modelo especializado em recuperação

Agora, vamos tentar um modelo que se concentra apenas na recuperação.

model = MovielensModel(rating_weight=0.0, retrieval_weight=1.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 4s 314ms/step - root_mean_squared_error: 3.7237 - factorized_top_k/top_1_categorical_accuracy: 0.0013 - factorized_top_k/top_5_categorical_accuracy: 0.0104 - factorized_top_k/top_10_categorical_accuracy: 0.0217 - factorized_top_k/top_50_categorical_accuracy: 0.1027 - factorized_top_k/top_100_categorical_accuracy: 0.1817 - loss: 69818.0298 - regularization_loss: 0.0000e+00 - total_loss: 69818.0298
Epoch 2/3
10/10 [==============================] - 3s 310ms/step - root_mean_squared_error: 3.7495 - factorized_top_k/top_1_categorical_accuracy: 0.0029 - factorized_top_k/top_5_categorical_accuracy: 0.0189 - factorized_top_k/top_10_categorical_accuracy: 0.0383 - factorized_top_k/top_50_categorical_accuracy: 0.1688 - factorized_top_k/top_100_categorical_accuracy: 0.2925 - loss: 67473.2884 - regularization_loss: 0.0000e+00 - total_loss: 67473.2884
Epoch 3/3
10/10 [==============================] - 3s 307ms/step - root_mean_squared_error: 3.7648 - factorized_top_k/top_1_categorical_accuracy: 0.0031 - factorized_top_k/top_5_categorical_accuracy: 0.0224 - factorized_top_k/top_10_categorical_accuracy: 0.0461 - factorized_top_k/top_50_categorical_accuracy: 0.1884 - factorized_top_k/top_100_categorical_accuracy: 0.3156 - loss: 66329.2507 - regularization_loss: 0.0000e+00 - total_loss: 66329.2507
5/5 [==============================] - 1s 161ms/step - root_mean_squared_error: 3.7730 - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0095 - factorized_top_k/top_10_categorical_accuracy: 0.0216 - factorized_top_k/top_50_categorical_accuracy: 0.1252 - factorized_top_k/top_100_categorical_accuracy: 0.2350 - loss: 31085.0693 - regularization_loss: 0.0000e+00 - total_loss: 31085.0693
Retrieval top-100 accuracy: 0.235.
Ranking RMSE: 3.773.

Obtemos o resultado oposto: um modelo que se sai bem na recuperação, mas mal na previsão de classificações.

Modelo de junta

Agora vamos treinar um modelo que atribua pesos positivos a ambas as tarefas.

model = MovielensModel(rating_weight=1.0, retrieval_weight=1.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 4s 314ms/step - root_mean_squared_error: 2.5007 - factorized_top_k/top_1_categorical_accuracy: 0.0015 - factorized_top_k/top_5_categorical_accuracy: 0.0108 - factorized_top_k/top_10_categorical_accuracy: 0.0219 - factorized_top_k/top_50_categorical_accuracy: 0.1031 - factorized_top_k/top_100_categorical_accuracy: 0.1811 - loss: 69811.8260 - regularization_loss: 0.0000e+00 - total_loss: 69811.8260
Epoch 2/3
10/10 [==============================] - 3s 308ms/step - root_mean_squared_error: 1.2097 - factorized_top_k/top_1_categorical_accuracy: 0.0027 - factorized_top_k/top_5_categorical_accuracy: 0.0183 - factorized_top_k/top_10_categorical_accuracy: 0.0368 - factorized_top_k/top_50_categorical_accuracy: 0.1641 - factorized_top_k/top_100_categorical_accuracy: 0.2879 - loss: 67481.2734 - regularization_loss: 0.0000e+00 - total_loss: 67481.2734
Epoch 3/3
10/10 [==============================] - 3s 308ms/step - root_mean_squared_error: 1.1200 - factorized_top_k/top_1_categorical_accuracy: 0.0034 - factorized_top_k/top_5_categorical_accuracy: 0.0226 - factorized_top_k/top_10_categorical_accuracy: 0.0447 - factorized_top_k/top_50_categorical_accuracy: 0.1867 - factorized_top_k/top_100_categorical_accuracy: 0.3153 - loss: 66297.9311 - regularization_loss: 0.0000e+00 - total_loss: 66297.9311
5/5 [==============================] - 1s 163ms/step - root_mean_squared_error: 1.1312 - factorized_top_k/top_1_categorical_accuracy: 9.0000e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0084 - factorized_top_k/top_10_categorical_accuracy: 0.0225 - factorized_top_k/top_50_categorical_accuracy: 0.1249 - factorized_top_k/top_100_categorical_accuracy: 0.2344 - loss: 31062.8213 - regularization_loss: 0.0000e+00 - total_loss: 31062.8213
Retrieval top-100 accuracy: 0.234.
Ranking RMSE: 1.131.

O resultado é um modelo com desempenho quase tão bom em ambas as tarefas quanto em cada modelo especializado.

Embora os resultados aqui não mostrem um benefício claro de precisão de um modelo conjunto neste caso, o aprendizado multitarefa é, em geral, uma ferramenta extremamente útil. Podemos esperar melhores resultados quando podemos transferir conhecimento de uma tarefa com muitos dados (como cliques) para uma tarefa esparsa de dados intimamente relacionada (como compras).