Classificação Listwise

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

No tutorial classificação básica , treinamos um modelo que pode predizer notas de pares de utilizador / filme. O modelo foi treinado para minimizar o erro quadrático médio das classificações previstas.

No entanto, otimizar as previsões do modelo em filmes individuais não é necessariamente o melhor método para treinar modelos de classificação. Não precisamos de modelos de classificação para prever as pontuações com grande precisão. Em vez disso, nos preocupamos mais com a capacidade do modelo de gerar uma lista ordenada de itens que correspondem à ordem de preferência do usuário.

Em vez de otimizar as previsões do modelo em pares de consulta / item individuais, podemos otimizar a classificação do modelo de uma lista como um todo. Este método é chamado listwise classificação.

Neste tutorial, usaremos os Recomendadores do TensorFlow para construir modelos de classificação listwise. Para fazer isso, vamos fazer uso do ranking perdas e métricas fornecidas por TensorFlow Ranking , um pacote TensorFlow que se concentra em aprender a classificação .

Preliminares

Se TensorFlow ranking não está disponível em seu ambiente de tempo de execução, você pode instalá-lo usando pip :

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

Podemos então importar todos os pacotes necessários:

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 a usar o conjunto de dados MovieLens 100K. Como antes, carregamos os conjuntos de dados e mantemos apenas o ID do usuário, o título do filme e os recursos de avaliação do usuário para este tutorial. Também fazemos algumas tarefas domésticas para preparar nosso vocabulário.

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é-processamento de dados

No entanto, não podemos usar o conjunto de dados MovieLens para otimização de lista diretamente. Para realizar a otimização listwise, precisamos ter acesso a uma lista de filmes que cada usuário classificou, mas cada exemplo no conjunto de dados MovieLens 100K contém apenas a classificação de um único filme.

Para contornar isso, transformamos o conjunto de dados de forma que cada exemplo contenha um ID de usuário e uma lista de filmes avaliados por esse usuário. Alguns filmes da lista terão uma classificação mais alta do que outros; o objetivo do nosso modelo será fazer previsões que correspondam a essa ordem.

Para fazer isso, usamos o tfrs.examples.movielens.movielens_to_listwise função auxiliar. Ele pega o conjunto de dados MovieLens 100K e gera um conjunto de dados contendo exemplos de lista conforme discutido acima. Os detalhes de implementação pode ser encontrada no código fonte .

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 inspecionar um exemplo dos dados de treinamento. O exemplo inclui um ID de usuário, uma lista de 10 IDs de filme e suas classificações pelo usuário.

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

Definição de modelo

Vamos treinar o mesmo modelo com três perdas diferentes:

  • erro médio quadrático,
  • perda de dobradiça par a par, e
  • uma perda ListMLE listwise.

Essas três perdas correspondem à otimização pontual, emparelhada e listwise.

Para avaliar o modelo que usamos normalizada descontado ganho acumulado (NDCG) . O NDCG mede uma classificação prevista tomando uma soma ponderada da classificação real de cada candidato. As classificações de filmes com classificação inferior pelo modelo receberiam mais descontos. Como resultado, um bom modelo que classifica os filmes de alta classificação no topo teria um resultado NDCG alto. Uma vez que essa métrica leva em consideração a posição de classificação de cada candidato, é uma métrica listwise.

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

Treinando os modelos

Agora podemos treinar cada um dos três modelos.

epochs = 30

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

Modelo de erro quadrático médio

Este modelo é muito semelhante ao modelo do tutorial classificação básica . Treinamos o modelo para minimizar o erro quadrático médio entre as classificações reais e as classificações previstas. Portanto, essa perda é calculada individualmente para cada filme e o treinamento é pontual.

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 perda de dobradiça pareada

Ao minimizar a perda de dobradiça par a par, o modelo tenta maximizar a diferença entre as previsões do modelo para um item com classificação alta e um item com classificação baixa: quanto maior for a diferença, menor será a perda do modelo. No entanto, uma vez que a diferença é grande o suficiente, a perda torna-se zero, impedindo o modelo de otimizar ainda mais este par específico e permitindo que ele se concentre em outros pares que estão classificados incorretamente

Essa perda não é calculada para filmes individuais, mas sim para pares de filmes. Portanto, o treinamento usando essa perda é pareado.

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

O ListMLE perda de TensorFlow expressa Graduação listar estimativa da probabilidade máxima. Para calcular a perda de ListMLE, primeiro usamos as classificações do usuário para gerar uma classificação ideal. Em seguida, calculamos a probabilidade de cada candidato ser superado por qualquer item abaixo dele na classificação ideal usando as pontuações previstas. O modelo tenta minimizar essa probabilidade para garantir que candidatos com classificação elevada não sejam superados por candidatos com classificação baixa. Você pode aprender mais sobre os detalhes de ListMLE na secção 2.2 do documento ListMLE Posição-aware: Processo A Sequential Aprendizagem .

Observe que, uma vez que a probabilidade é calculada com relação a um candidato e todos os candidatos abaixo dele na classificação ideal, a perda não é em pares, mas em lista. Portanto, o treinamento usa a otimização de lista.

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

Dos três modelos, o modelo treinado usando ListMLE tem a métrica NDCG mais alta. Este resultado mostra como a otimização listwise pode ser usada para treinar modelos de classificação e pode potencialmente produzir modelos com desempenho melhor do que os modelos otimizados de forma pontual ou pareada.