推薦電影:排名

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

實際應用推薦系統通常由兩個階段組成

  1. 擷取階段負責從所有可能的候選項目中選取最初的數百個候選項目。此模型的主要目標是有效淘汰使用者不感興趣的所有候選項目。由於擷取模型可能要處理數百萬個候選項目,因此必須具備高運算效率。
  2. 排名階段會採用擷取模型的輸出結果,並加以微調,以選取最合適的少數建議。其任務是將使用者可能感興趣的項目範圍縮小到可能的候選項目候選清單。

我們將重點放在第二階段:排名。如果您對擷取階段感興趣,請參閱我們的擷取教學課程。

在本教學課程中,我們將:

  1. 取得我們的資料,並將其分割為訓練集和測試集。
  2. 實作排名模型。
  3. 進行擬合和評估。

匯入

我們先匯入必要的項目。

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
2022-12-14 12:17:03.715935: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 12:17:03.716032: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 12:17:03.716042: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
import tensorflow_recommenders as tfrs

準備資料集

我們將使用與擷取教學課程相同的資料。這次,我們也會保留評分:這些是我們嘗試預測的目標。

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"]
})
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/tensorflow/python/autograph/pyct/static_analysis/liveness.py:83: Analyzer.lamba_check (from tensorflow.python.autograph.pyct.static_analysis.liveness) is deprecated and will be removed after 2023-09-23.
Instructions for updating:
Lambda fuctions will be no more assumed to be used in the statement where they are used, or at least in the same block. https://github.com/tensorflow/tensorflow/issues/56089
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/tensorflow/python/autograph/pyct/static_analysis/liveness.py:83: Analyzer.lamba_check (from tensorflow.python.autograph.pyct.static_analysis.liveness) is deprecated and will be removed after 2023-09-23.
Instructions for updating:
Lambda fuctions will be no more assumed to be used in the statement where they are used, or at least in the same block. https://github.com/tensorflow/tensorflow/issues/56089

和之前一樣,我們將分割資料,將 80% 的評分放入訓練集,而 20% 放入測試集。

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)

我們也來找出資料中存在的唯一使用者 ID 和電影標題。

這很重要,因為我們需要能夠將類別特徵的原始值對應到模型中的嵌入向量。若要執行此操作,我們需要一個詞彙表,將原始特徵值對應到連續範圍內的整數:這可讓我們在嵌入表中查閱對應的嵌入。

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

實作模型

架構

排名模型不像擷取模型那樣面臨相同的效率限制,因此我們在架構選擇方面有更多自由。

由多個堆疊密集層組成的模型是排名任務相對常見的架構。我們可以按如下方式實作:

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

此模型會採用使用者 ID 和電影標題,並輸出預測評分

RankingModel()((["42"], ["One Flew Over the Cuckoo's Nest (1975)"]))
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs=['42']. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs=['42']. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs=["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. Received: inputs=["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.01534399]], dtype=float32)>

損失和指標

下一個元件是用於訓練模型的損失。TFRS 有多個損失層和任務,讓這一切變得更容易。

在此範例中,我們將使用 Ranking 任務物件:這是一個方便的包裝函式,可將損失函數和指標計算捆綁在一起。

我們將搭配 MeanSquaredError Keras 損失來使用,以便預測評分。

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

任務本身是一個 Keras 層,會將真實值和預測值作為引數,並傳回計算出的損失。我們將使用它來實作模型的訓練迴圈。

完整模型

我們現在可以將所有內容整合到模型中。TFRS 公開了一個基礎模型類別 (tfrs.models.Model),可簡化模型的建構:我們只需要在 __init__ 方法中設定元件,並實作 compute_loss 方法,以接收原始特徵並傳回損失值。

然後,基礎模型會負責建立適當的訓練迴圈來擬合我們的模型。

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)

擬合與評估

定義模型後,我們可以使用標準 Keras 擬合和評估常式來擬合和評估模型。

我們先例項化模型。

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

然後,隨機排序、批次處理和快取訓練和評估資料。

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

接著訓練模型

model.fit(cached_train, epochs=3)
Epoch 1/3
10/10 [==============================] - 4s 166ms/step - root_mean_squared_error: 2.0902 - loss: 4.0368 - regularization_loss: 0.0000e+00 - total_loss: 4.0368
Epoch 2/3
10/10 [==============================] - 0s 4ms/step - root_mean_squared_error: 1.1613 - loss: 1.3426 - regularization_loss: 0.0000e+00 - total_loss: 1.3426
Epoch 3/3
10/10 [==============================] - 0s 4ms/step - root_mean_squared_error: 1.1140 - loss: 1.2414 - regularization_loss: 0.0000e+00 - total_loss: 1.2414
<keras.callbacks.History at 0x7fd31445d490>

隨著模型訓練,損失會減少,而 RMSE 指標會改善。

最後,我們可以在測試集上評估模型

model.evaluate(cached_test, return_dict=True)
5/5 [==============================] - 2s 9ms/step - root_mean_squared_error: 1.1009 - loss: 1.2072 - regularization_loss: 0.0000e+00 - total_loss: 1.2072
{'root_mean_squared_error': 1.100862741470337,
 'loss': 1.1866925954818726,
 'regularization_loss': 0,
 'total_loss': 1.1866925954818726}

RMSE 指標越低,表示我們的模型在預測評分方面越準確。

測試排名模型

現在,我們可以透過計算一組電影的預測,然後根據預測對這些電影進行排名,來測試排名模型

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:
Dances with Wolves (1990): [[3.539769]]
M*A*S*H (1970): [[3.5356772]]
Speed (1994): [[3.4501984]]

匯出以進行服務

模型可以輕鬆匯出以進行服務

tf.saved_model.save(model, "export")
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314460fa0>, 140544556312496), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314460fa0>, 140544556312496), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314497c10>, 140544555465184), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314497c10>, 140544555465184), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 256), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31449fa90>, 140544555465504), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 256), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31449fa90>, 140544555465504), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314452af0>, 140544555464064), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314452af0>, 140544555464064), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256, 64), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd3144a9760>, 140544555331632), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256, 64), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd3144a9760>, 140544555331632), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448f490>, 140544555331952), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448f490>, 140544555331952), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 1), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314493d60>, 140544555334032), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 1), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314493d60>, 140544555334032), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(1,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448deb0>, 140544555334272), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(1,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448deb0>, 140544555334272), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314460fa0>, 140544556312496), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314460fa0>, 140544556312496), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314497c10>, 140544555465184), {}).
INFO:tensorflow:Unsupported signature for serialization: ((IndexedSlicesSpec(TensorShape([None, 32]), tf.float32, tf.int64, tf.int32, TensorShape([None])), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314497c10>, 140544555465184), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 256), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31449fa90>, 140544555465504), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 256), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31449fa90>, 140544555465504), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314452af0>, 140544555464064), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314452af0>, 140544555464064), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256, 64), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd3144a9760>, 140544555331632), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(256, 64), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd3144a9760>, 140544555331632), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448f490>, 140544555331952), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448f490>, 140544555331952), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 1), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314493d60>, 140544555334032), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(64, 1), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd314493d60>, 140544555334032), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(1,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448deb0>, 140544555334272), {}).
INFO:tensorflow:Unsupported signature for serialization: ((TensorSpec(shape=(1,), dtype=tf.float32, name='gradient'), <tensorflow.python.framework.func_graph.UnknownArgument object at 0x7fd31448deb0>, 140544555334272), {}).
WARNING:absl:Found untraced functions such as ranking_1_layer_call_fn, ranking_1_layer_call_and_return_conditional_losses, _update_step_xla while saving (showing 3 of 3). These functions will not be directly callable after loading.
INFO:tensorflow:Assets written to: export/assets
INFO:tensorflow:Assets written to: export/assets

我們現在可以將其載回並執行預測

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

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

將模型轉換為 TensorFLow Lite

雖然 TensorFlow Recommenders 主要設計用於執行伺服器端推薦,您仍然可以將訓練後的排名模型轉換為 TensorFLow Lite,並在裝置上執行 (以提升使用者隱私權和降低延遲時間)。

converter = tf.lite.TFLiteConverter.from_saved_model("export")
tflite_model = converter.convert()
open("converted_model.tflite", "wb").write(tflite_model)
2022-12-14 12:17:24.837136: W tensorflow/compiler/mlir/lite/python/tf_tfl_flatbuffer_helpers.cc:362] Ignored output_format.
2022-12-14 12:17:24.837175: W tensorflow/compiler/mlir/lite/python/tf_tfl_flatbuffer_helpers.cc:365] Ignored drop_control_dependency.
544480

模型轉換完成後,您可以像執行一般 TensorFlow Lite 模型一樣執行它。請查看 TensorFlow Lite 文件以瞭解詳情。

interpreter = tf.lite.Interpreter(model_path="converted_model.tflite")
interpreter.allocate_tensors()

# Get input and output tensors.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# Test the model.
if input_details[0]["name"] == "serving_default_movie_title:0":
  interpreter.set_tensor(input_details[0]["index"], np.array(["Speed (1994)"]))
  interpreter.set_tensor(input_details[1]["index"], np.array(["42"]))
else:
  interpreter.set_tensor(input_details[0]["index"], np.array(["42"]))
  interpreter.set_tensor(input_details[1]["index"], np.array(["Speed (1994)"]))

interpreter.invoke()

rating = interpreter.get_tensor(output_details[0]['index'])
print(rating)
[[3.450199]]
INFO: Created TensorFlow Lite XNNPACK delegate for CPU.

後續步驟

上述模型為我們建構排名系統提供了一個良好的起點。

當然,建構實際可用的排名系統需要付出更多努力。

在大多數情況下,排名模型可以透過使用更多功能 (而不僅僅是使用者和候選項目 ID) 來大幅改善。若要瞭解如何做到這一點,請參閱側邊功能教學課程。

仔細瞭解值得最佳化的目標也很重要。若要開始建構可最佳化多個目標的推薦系統,請參閱我們的多工教學課程。