列表式排序

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

基本排序教程 中,我们训练了一个可以预测用户/电影对评分的模型。该模型经过训练,可以最大程度地减少预测评分的均方误差。

但是,优化模型对单个电影的预测并不一定是训练排序模型的最佳方法。我们不需要排序模型来预测具有很高精度的分数。相反,我们更关心模型生成与用户偏好排序匹配的项目排序列表的能力。

我们可以优化模型对整个列表的排序,而不是优化模型对单个查询/项目对的预测。这种方法称为列表式排序

在本教程中,我们将使用 TensorFlow 推荐器构建列表式排序模型。为此,我们将使用 TensorFlow 排名 提供的排名损失和指标,这是一个专注于 学习排序 的 TensorFlow 包。

准备工作

如果 TensorFlow 排名在您的运行时环境中不可用,您可以使用 pip 安装它

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

然后,我们可以导入所有必要的包

import pprint

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
import tensorflow_ranking as tfr
import tensorflow_recommenders as tfrs

我们将继续使用 MovieLens 100K 数据集。与之前一样,我们将加载数据集,并仅保留用户 ID、电影标题和用户评分特征以用于本教程。我们还将进行一些整理工作以准备我们的词汇表。

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

数据预处理

但是,我们不能直接使用 MovieLens 数据集进行列表优化。要执行列表式优化,我们需要访问每个用户已评级的电影列表,但 MovieLens 100K 数据集中的每个示例仅包含单个电影的评分。

为了解决这个问题,我们转换数据集,以便每个示例包含用户 ID 和该用户评级的电影列表。列表中的一些电影将比其他电影排名更高;我们的模型的目标是做出与这种排序匹配的预测。

为此,我们使用 tfrs.examples.movielens.movielens_to_listwise 辅助函数。它接受 MovieLens 100K 数据集并生成包含如上所述的列表示例的数据集。实现细节可以在 源代码 中找到。

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
)

我们可以检查训练数据中的一个示例。该示例包含用户 ID、10 个电影 ID 列表以及用户对它们的评分。

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

模型定义

我们将使用三种不同的损失训练相同的模型

  • 均方误差,
  • 成对铰链损失,以及
  • 列表式 ListMLE 损失。

这三种损失分别对应于逐点、成对和列表式优化。

为了评估模型,我们使用 归一化折现累积增益 (NDCG)。NDCG 通过对每个候选者的实际评分进行加权求和来衡量预测的排名。模型排名较低的电影的评分将被更多地折现。因此,一个好的模型,它将高评分电影排在最前面,将具有较高的 NDCG 结果。由于此指标考虑了每个候选者的排名位置,因此它是一个列表式指标。

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

训练模型

现在,我们可以训练这三个模型中的每一个。

epochs = 30

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

均方误差模型

此模型与 基本排序教程 中的模型非常相似。我们训练模型以最小化实际评分和预测评分之间的均方误差。因此,此损失是针对每部电影单独计算的,训练是逐点的。

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 0x7f185010fc40>

成对铰链损失模型

通过最小化成对铰链损失,模型试图最大化模型对高评分项目和低评分项目的预测之间的差异:差异越大,模型损失越低。但是,一旦差异足够大,损失就会变为零,阻止模型进一步优化这个特定的对,并让它专注于其他排名错误的对

此损失不是针对单个电影计算的,而是针对电影对计算的。因此,使用此损失进行的训练是成对的。

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 0x7f187342cd90>

列表式模型

来自 TensorFlow 排名的 ListMLE 损失表示列表最大似然估计。为了计算 ListMLE 损失,我们首先使用用户评分生成最佳排名。然后,我们使用预测分数计算每个候选者被最佳排名中任何低于它的项目超越的可能性。模型试图最小化这种可能性,以确保高评分候选者不会被低评分候选者超越。您可以在论文 位置感知 ListMLE:一个顺序学习过程 的第 2.2 节中了解更多关于 ListMLE 的详细信息。

请注意,由于可能性是针对候选者和最佳排名中所有低于它的候选者计算的,因此损失不是成对的,而是列表式的。因此,训练使用列表优化。

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 0x7f17f4098880>

比较模型

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 390ms/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 426ms/step - ndcg_metric: 0.9058 - root_mean_squared_error: 3.8341 - loss: 1.0179 - regularization_loss: 0.0000e+00 - total_loss: 1.0179
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 416ms/step - ndcg_metric: 0.9071 - root_mean_squared_error: 2.7252 - loss: 4.5401 - regularization_loss: 0.0000e+00 - total_loss: 4.5401
NDCG of the ListMLE model: 0.9071

在这三个模型中,使用 ListMLE 训练的模型具有最高的 NDCG 指标。此结果表明,列表式优化如何用于训练排序模型,并且可能产生比以逐点或成对方式优化的模型性能更好的模型。