Listwise 排序

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

基本排序教學課程中,我們訓練了一個模型,可以預測使用者/電影配對的評分。該模型的訓練目標是盡可能減少預測評分的均方誤差。

然而,最佳化模型對個別電影的預測,不一定是訓練排序模型的最佳方法。我們不需要排序模型非常精確地預測分數。相反地,我們更關心模型產生符合使用者偏好排序的項目清單的能力。

我們可以最佳化模型對整個清單的排序,而不是最佳化模型對個別查詢/項目配對的預測。此方法稱為 listwise 排序

在本教學課程中,我們將使用 TensorFlow Recommenders 建構 listwise 排序模型。為此,我們將使用 TensorFlow Ranking 提供的排序損失和指標,TensorFlow Ranking 是一個專注於 learning to rank 的 TensorFlow 套件。

準備工作

如果您的執行階段環境中沒有 TensorFlow Ranking,您可以使用 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 資料集進行清單最佳化。若要執行 listwise 最佳化,我們需要存取每個使用者評分過的電影清單,但 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)>}

模型定義

我們將使用三種不同的損失函數訓練相同的模型

  • 均方誤差,
  • 成對 hinge 損失,以及
  • listwise ListMLE 損失。

這三種損失函數分別對應於 pointwise、pairwise 和 listwise 最佳化。

為了評估模型,我們使用標準化折扣累積增益 (NDCG)。NDCG 透過取得每個候選者實際評分的加權總和來衡量預測的排序。模型排名較低的電影評分將被進一步折扣。因此,能將高評分電影排在最前面的良好模型將具有較高的 NDCG 結果。由於此指標會將每個候選者的排名位置納入考量,因此它是 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),
    )

訓練模型

我們現在可以訓練這三個模型中的每一個。

epochs = 30

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

均方誤差模型

這個模型與基本排序教學課程中的模型非常相似。我們訓練模型以盡可能減少實際評分與預測評分之間的均方誤差。因此,此損失是針對每部電影單獨計算的,而訓練是 pointwise。

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 損失模型

透過盡可能減少成對 hinge 損失,模型會嘗試最大化模型對高評分項目和低評分項目的預測之間的差異:差異越大,模型損失越小。但是,一旦差異夠大,損失就會變為零,從而阻止模型進一步最佳化此特定配對,並使其專注於其他排名不正確的配對

此損失不是針對個別電影計算,而是針對成對電影計算。因此,使用此損失的訓練是 pairwise。

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>

Listwise 模型

TensorFlow Ranking 中的 ListMLE 損失表示清單最大概似估計。若要計算 ListMLE 損失,我們先使用使用者評分產生最佳排序。然後,我們使用預測分數計算每個候選者被最佳排序中低於它的任何項目超越排名的可能性。模型會嘗試盡可能減少此可能性,以確保高評分候選者不會被低評分候選者超越排名。您可以在論文 Position-aware ListMLE: A Sequential Learning Process 的第 2.2 節中深入瞭解 ListMLE 的詳細資訊。

請注意,由於可能性是針對候選者以及最佳排序中低於它的所有候選者計算的,因此損失不是 pairwise 而是 listwise。因此,訓練使用清單最佳化。

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 指標。此結果顯示如何使用 listwise 最佳化來訓練排序模型,並可能產生效能優於以 pointwise 或 pairwise 方式最佳化之模型的模型。