![]() |
![]() |
![]() |
![]() |
真實世界的推薦系統通常由兩個階段組成
- 檢索階段負責從所有可能的候選項目中選取最初數百個候選項目。此模型的主要目標是有效率地篩除使用者不感興趣的所有候選項目。由於檢索模型可能處理數百萬個候選項目,因此必須具備高運算效率。
- 排名階段會採用檢索模型的輸出結果,並進行微調,以選取最佳的少數推薦項目。其任務是將使用者可能感興趣的項目範圍縮小到可能的候選項目簡短清單。
在本教學課程中,我們將重點放在第一階段「檢索」。如果您對排名階段感興趣,請參閱我們的排名教學課程。
檢索模型通常由兩個子模型組成
- 查詢模型:使用查詢功能計算查詢表示法 (通常是固定維度的嵌入向量)。
- 候選項目模型:使用候選項目功能計算候選項目表示法 (大小相等的向量)
然後將這兩個模型的輸出相乘,得出查詢候選項目關聯性分數,分數越高表示候選項目與查詢之間的匹配程度越高。
在本教學課程中,我們將使用 MovieLens 資料集建構及訓練這類雙塔模型。
我們將會:
- 取得我們的資料,並將其分割為訓練集和測試集。
- 實作檢索模型。
- 擬合及評估模型。
- 匯出模型以進行有效率的服務,方法是建構近似最近鄰 (ANN) 索引。
資料集
MovieLens 資料集是明尼蘇達大學 GroupLens 研究群組的經典資料集。其中包含一組使用者對電影的評分,是推薦系統研究的重要資料。
資料可以兩種方式處理:
- 可以解釋為表示使用者觀看 (和評分) 的電影,以及未觀看的電影。這是一種隱含回饋的形式,使用者的觀看行為告訴我們他們偏好觀看哪些內容,以及不想觀看哪些內容。
- 也可以視為表示使用者有多喜歡他們觀看的電影。這是一種明確回饋的形式:假設使用者觀看了一部電影,我們可以透過查看他們給予的評分,大致判斷他們有多喜歡這部電影。
在本教學課程中,我們著重於檢索系統:一種模型,可從目錄中預測使用者可能觀看的電影集。通常,隱含資料在這裡更有用,因此我們將把 MovieLens 視為隱含系統。這表示使用者觀看的每部電影都是正面範例,而他們未看過的每部電影都是隱含的負面範例。
匯入
我們先匯入所需的項目。
pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
pip install -q scann
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:14:44.722984: 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:14:44.723084: 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:14:44.723093: 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
準備資料集
我們先來看看資料。
我們使用來自 Tensorflow Datasets 的 MovieLens 資料集。載入 movielens/100k_ratings
會產生包含評分資料的 tf.data.Dataset
物件,而載入 movielens/100k_movies
則會產生僅包含電影資料的 tf.data.Dataset
物件。
請注意,由於 MovieLens 資料集沒有預先定義的分割,因此所有資料都位於 train
分割下。
# Ratings data.
ratings = tfds.load("movielens/100k-ratings", split="train")
# Features of all the available movies.
movies = tfds.load("movielens/100k-movies", split="train")
評分資料集會傳回電影 ID、使用者 ID、指定的評分、時間戳記、電影資訊和使用者資訊的字典
for x in ratings.take(1).as_numpy_iterator():
pprint.pprint(x)
{'bucketized_user_age': 45.0, 'movie_genres': array([7]), 'movie_id': b'357', 'movie_title': b"One Flew Over the Cuckoo's Nest (1975)", 'raw_user_age': 46.0, 'timestamp': 879024327, 'user_gender': True, 'user_id': b'138', 'user_occupation_label': 4, 'user_occupation_text': b'doctor', 'user_rating': 4.0, 'user_zip_code': b'53211'} 2022-12-14 12:14:51.221818: W tensorflow/core/kernels/data/cache_dataset_ops.cc:856] The calling iterator did not fully read the dataset being cached. In order to avoid unexpected truncation of the dataset, the partially cached contents of the dataset will be discarded. This can happen if you have an input pipeline similar to `dataset.cache().take(k).repeat()`. You should use `dataset.take(k).cache().repeat()` instead.
電影資料集包含電影 ID、電影標題,以及電影所屬類型的資料。請注意,類型是使用整數標籤編碼。
for x in movies.take(1).as_numpy_iterator():
pprint.pprint(x)
{'movie_genres': array([4]), 'movie_id': b'1681', 'movie_title': b'You So Crazy (1994)'} 2022-12-14 12:14:51.385630: W tensorflow/core/kernels/data/cache_dataset_ops.cc:856] The calling iterator did not fully read the dataset being cached. In order to avoid unexpected truncation of the dataset, the partially cached contents of the dataset will be discarded. This can happen if you have an input pipeline similar to `dataset.cache().take(k).repeat()`. You should use `dataset.take(k).cache().repeat()` instead.
在此範例中,我們將著重於評分資料。其他教學課程會探討如何也使用電影資訊資料來提升模型品質。
我們只保留資料集中的 user_id
和 movie_title
欄位。
ratings = ratings.map(lambda x: {
"movie_title": x["movie_title"],
"user_id": x["user_id"],
})
movies = movies.map(lambda x: x["movie_title"])
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
為了擬合和評估模型,我們需要將其分割為訓練集和評估集。在工業推薦系統中,這很可能是按時間完成的:時間 \(T\) 之前的資料將用於預測 \(T\) 之後的互動。
然而,在這個簡單的範例中,我們使用隨機分割,將 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 = 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)))
unique_movie_titles[:10]
array([b"'Til There Was You (1997)", b'1-900 (1994)', b'101 Dalmatians (1996)', b'12 Angry Men (1957)', b'187 (1997)', b'2 Days in the Valley (1996)', b'20,000 Leagues Under the Sea (1954)', b'2001: A Space Odyssey (1968)', b'3 Ninjas: High Noon At Mega Mountain (1998)', b'39 Steps, The (1935)'], dtype=object)
實作模型
選擇模型的架構是建模的關鍵部分。
由於我們正在建構雙塔檢索模型,因此我們可以分別建構每個塔,然後在最終模型中組合它們。
查詢塔
我們先從查詢塔開始。
第一步是決定查詢和候選項目表示法的維度
embedding_dimension = 32
較高的值會對應到可能更準確的模型,但擬合速度也會較慢,且更容易過度擬合。
第二步是定義模型本身。在這裡,我們將使用 Keras 預先處理層,先將使用者 ID 轉換為整數,然後透過 Embedding
層將這些整數轉換為使用者嵌入。請注意,我們使用稍早計算的唯一使用者 ID 清單作為詞彙表
user_model = tf.keras.Sequential([
tf.keras.layers.StringLookup(
vocabulary=unique_user_ids, mask_token=None),
# We add an additional embedding to account for unknown tokens.
tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
])
像這樣簡單的模型完全對應到經典的矩陣分解方法。雖然為這個簡單模型定義 tf.keras.Model
的子類別可能有點小題大作,但只要我們在結尾傳回 embedding_dimension
寬度的輸出,我們就可以使用標準 Keras 元件輕鬆將其擴充為任意複雜的模型。
候選項目塔
我們可以對候選項目塔執行相同的操作。
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)
])
指標
在我們的訓練資料中,我們有正面 (使用者、電影) 配對。為了判斷我們的模型有多好,我們需要將模型針對此配對計算的關聯性分數,與所有其他可能候選項目的分數進行比較:如果正面配對的分數高於所有其他候選項目,則我們的模型非常準確。
若要執行此操作,我們可以使用 tfrs.metrics.FactorizedTopK
指標。此指標有一個必要引數:候選項目資料集,用作評估的隱含負面項目。
在我們的案例中,那是 movies
資料集,透過我們的電影模型轉換為嵌入
metrics = tfrs.metrics.FactorizedTopK(
candidates=movies.batch(128).map(movie_model)
)
損失
下一個元件是用於訓練模型的損失。TFRS 有多個損失層和工作,可讓此過程變得容易。
在此範例中,我們將使用 Retrieval
工作物件:一個便利的包裝函式,將損失函數和指標計算捆綁在一起
task = tfrs.tasks.Retrieval(
metrics=metrics
)
工作本身是一個 Keras 層,它會將查詢和候選項目嵌入作為引數,並傳回計算出的損失:我們將使用它來實作模型的訓練迴圈。
完整模型
我們現在可以將所有項目放入模型中。TFRS 公開了一個基本模型類別 (tfrs.models.Model
),可簡化模型的建構:我們只需要在 __init__
方法中設定元件,並實作 compute_loss
方法,以接收原始功能並傳回損失值。
然後,基本模型將負責建立適當的訓練迴圈,以擬合我們的模型。
class MovielensModel(tfrs.Model):
def __init__(self, user_model, movie_model):
super().__init__()
self.movie_model: tf.keras.Model = movie_model
self.user_model: tf.keras.Model = user_model
self.task: tf.keras.layers.Layer = task
def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> 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,
# getting embeddings back.
positive_movie_embeddings = self.movie_model(features["movie_title"])
# The task computes the loss and the metrics.
return self.task(user_embeddings, positive_movie_embeddings)
tfrs.Model
基本類別只是一個便利類別:它可讓我們使用相同的方法計算訓練和測試損失。
在幕後,它仍然是一個普通的 Keras 模型。您可以透過繼承 tf.keras.Model
並覆寫 train_step
和 test_step
函數 (詳情請參閱指南) 來達成相同的功能
class NoBaseClassMovielensModel(tf.keras.Model):
def __init__(self, user_model, movie_model):
super().__init__()
self.movie_model: tf.keras.Model = movie_model
self.user_model: tf.keras.Model = user_model
self.task: tf.keras.layers.Layer = task
def train_step(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
# Set up a gradient tape to record gradients.
with tf.GradientTape() as tape:
# Loss computation.
user_embeddings = self.user_model(features["user_id"])
positive_movie_embeddings = self.movie_model(features["movie_title"])
loss = self.task(user_embeddings, positive_movie_embeddings)
# Handle regularization losses as well.
regularization_loss = sum(self.losses)
total_loss = loss + regularization_loss
gradients = tape.gradient(total_loss, self.trainable_variables)
self.optimizer.apply_gradients(zip(gradients, self.trainable_variables))
metrics = {metric.name: metric.result() for metric in self.metrics}
metrics["loss"] = loss
metrics["regularization_loss"] = regularization_loss
metrics["total_loss"] = total_loss
return metrics
def test_step(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
# Loss computation.
user_embeddings = self.user_model(features["user_id"])
positive_movie_embeddings = self.movie_model(features["movie_title"])
loss = self.task(user_embeddings, positive_movie_embeddings)
# Handle regularization losses as well.
regularization_loss = sum(self.losses)
total_loss = loss + regularization_loss
metrics = {metric.name: metric.result() for metric in self.metrics}
metrics["loss"] = loss
metrics["regularization_loss"] = regularization_loss
metrics["total_loss"] = total_loss
return metrics
然而,在這些教學課程中,我們堅持使用 tfrs.Model
基本類別,以便將重點放在建模上,並抽象化一些樣板程式碼。
擬合與評估
定義模型後,我們可以使用標準 Keras 擬合和評估常式來擬合和評估模型。
我們先例項化模型。
model = MovielensModel(user_model, movie_model)
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 [==============================] - 6s 309ms/step - factorized_top_k/top_1_categorical_accuracy: 7.2500e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0063 - factorized_top_k/top_10_categorical_accuracy: 0.0140 - factorized_top_k/top_50_categorical_accuracy: 0.0753 - factorized_top_k/top_100_categorical_accuracy: 0.1471 - loss: 69820.5881 - regularization_loss: 0.0000e+00 - total_loss: 69820.5881 Epoch 2/3 10/10 [==============================] - 3s 302ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0119 - factorized_top_k/top_10_categorical_accuracy: 0.0260 - factorized_top_k/top_50_categorical_accuracy: 0.1403 - factorized_top_k/top_100_categorical_accuracy: 0.2616 - loss: 67457.6612 - regularization_loss: 0.0000e+00 - total_loss: 67457.6612 Epoch 3/3 10/10 [==============================] - 3s 301ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0014 - factorized_top_k/top_5_categorical_accuracy: 0.0189 - factorized_top_k/top_10_categorical_accuracy: 0.0400 - factorized_top_k/top_50_categorical_accuracy: 0.1782 - factorized_top_k/top_100_categorical_accuracy: 0.3056 - loss: 66284.5682 - regularization_loss: 0.0000e+00 - total_loss: 66284.5682 <keras.callbacks.History at 0x7f6039c48160>
如果您想使用 TensorBoard 監控訓練過程,可以將 TensorBoard 回呼新增至 fit() 函數,然後使用 %tensorboard --logdir logs/fit
啟動 TensorBoard。如需更多詳細資訊,請參閱 TensorBoard 文件。
隨著模型的訓練,損失會下降,並且會更新一組前 k 名檢索指標。這些指標告訴我們,在從整個候選項目集中檢索到的前 k 個項目中,是否包含真正的正面項目。例如,前 5 名類別準確度指標為 0.2 表示平均而言,在 20% 的時間內,真正的正面項目包含在前 5 個檢索到的項目中。
請注意,在此範例中,我們會在訓練和評估期間評估指標。由於使用大型候選項目集時,這可能會非常緩慢,因此在訓練中關閉指標計算,僅在評估中執行可能是明智之舉。
最後,我們可以針對測試集評估我們的模型
model.evaluate(cached_test, return_dict=True)
5/5 [==============================] - 3s 191ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0010 - factorized_top_k/top_5_categorical_accuracy: 0.0087 - factorized_top_k/top_10_categorical_accuracy: 0.0212 - factorized_top_k/top_50_categorical_accuracy: 0.1218 - factorized_top_k/top_100_categorical_accuracy: 0.2334 - loss: 31086.5010 - regularization_loss: 0.0000e+00 - total_loss: 31086.5010 {'factorized_top_k/top_1_categorical_accuracy': 0.0010000000474974513, 'factorized_top_k/top_5_categorical_accuracy': 0.008700000122189522, 'factorized_top_k/top_10_categorical_accuracy': 0.021150000393390656, 'factorized_top_k/top_50_categorical_accuracy': 0.121799997985363, 'factorized_top_k/top_100_categorical_accuracy': 0.23340000212192535, 'loss': 28256.8984375, 'regularization_loss': 0, 'total_loss': 28256.8984375}
測試集效能比訓練效能差很多。這是由於兩個因素:
- 我們的模型在它看過的資料上可能表現更好,僅僅是因為它可以記住這些資料。當模型具有許多參數時,這種過度擬合現象尤其強烈。它可以透過模型正規化以及使用使用者和電影功能來調解,以協助模型更好地概括未見過的資料。
- 模型正在重新推薦使用者已觀看過的一些電影。這些已知的正面觀看行為可能會將測試電影從前 K 名推薦中擠出。
第二個現象可以透過從測試推薦中排除先前看過的電影來解決。這種方法在推薦系統文獻中相對常見,但我們在本教學課程中不遵循這種方法。如果建議不要推薦過去的觀看項目很重要,我們應該期望適當指定的模型從過去的使用者歷史記錄和情境資訊中自動學習此行為。此外,多次推薦相同的項目 (例如,常青電視節目或定期購買的項目) 通常是適當的。
進行預測
現在我們有了模型,我們希望能夠進行預測。我們可以使用 tfrs.layers.factorized_top_k.BruteForce
層來執行此操作。
# Create a model that takes in raw query features, and
index = tfrs.layers.factorized_top_k.BruteForce(model.user_model)
# recommends movies out of the entire movies dataset.
index.index_from_dataset(
tf.data.Dataset.zip((movies.batch(100), movies.batch(100).map(model.movie_model)))
)
# Get recommendations.
_, titles = index(tf.constant(["42"]))
print(f"Recommendations for user 42: {titles[0, :3]}")
Recommendations for user 42: [b'Christmas Carol, A (1938)' b'Rudy (1993)' b'Bridges of Madison County, The (1995)']
當然,BruteForce
層對於服務具有許多可能候選項目的模型來說太慢了。以下章節說明如何使用近似檢索索引來加速此過程。
模型服務
模型訓練完成後,我們需要一種部署模型的方法。
在雙塔檢索模型中,服務包含兩個元件:
- 服務查詢模型:接收查詢的功能,並將其轉換為查詢嵌入,以及
- 服務候選項目模型。這通常採用近似最近鄰 (ANN) 索引的形式,可快速近似查閱候選項目,以回應查詢模型產生的查詢。
在 TFRS 中,這兩個元件都可以封裝到單一可匯出模型中,為我們提供一個模型,該模型會接收原始使用者 ID,並傳回該使用者的熱門電影標題。這是透過將模型匯出為 SavedModel
格式來完成的,這使得可以使用 TensorFlow Serving 進行服務。
若要部署像這樣的模型,我們只需匯出我們在上面建立的 BruteForce
層
# Export the query model.
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "model")
# Save the index.
tf.saved_model.save(index, path)
# Load it back; can also be done in TensorFlow Serving.
loaded = tf.saved_model.load(path)
# Pass a user id in, get top predicted movie titles back.
scores, titles = loaded(["42"])
print(f"Recommendations: {titles[0][:3]}")
WARNING:absl:Found untraced functions such as query_with_exclusions while saving (showing 1 of 1). These functions will not be directly callable after loading. INFO:tensorflow:Assets written to: /tmpfs/tmp/tmptfkkd57q/model/assets INFO:tensorflow:Assets written to: /tmpfs/tmp/tmptfkkd57q/model/assets Recommendations: [b'Christmas Carol, A (1938)' b'Rudy (1993)' b'Bridges of Madison County, The (1995)']
我們也可以匯出近似檢索索引,以加速預測。這將使從數千萬個候選項目集中有效率地呈現推薦成為可能。
若要執行此操作,我們可以使用 scann
套件。這是 TFRS 的選用相依性,我們在本教學課程開始時透過呼叫 !pip install -q scann
單獨安裝了它。
安裝完成後,我們可以使用 TFRS ScaNN
層
scann_index = tfrs.layers.factorized_top_k.ScaNN(model.user_model)
scann_index.index_from_dataset(
tf.data.Dataset.zip((movies.batch(100), movies.batch(100).map(model.movie_model)))
)
<tensorflow_recommenders.layers.factorized_top_k.ScaNN at 0x7f5fa01ff130>
此層將執行近似查閱:這使得檢索的準確性略微降低,但在大型候選項目集上速度快了數個數量級。
# Get recommendations.
_, titles = scann_index(tf.constant(["42"]))
print(f"Recommendations for user 42: {titles[0, :3]}")
Recommendations for user 42: [b'Little Big League (1994)' b'Miracle on 34th Street (1994)' b'Cinderella (1950)']
匯出以進行服務就像匯出 BruteForce
層一樣容易
# Export the query model.
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "model")
# Save the index.
tf.saved_model.save(
scann_index,
path,
options=tf.saved_model.SaveOptions(namespace_whitelist=["Scann"])
)
# Load it back; can also be done in TensorFlow Serving.
loaded = tf.saved_model.load(path)
# Pass a user id in, get top predicted movie titles back.
scores, titles = loaded(["42"])
print(f"Recommendations: {titles[0][:3]}")
WARNING:absl:Found untraced functions such as query_with_exclusions while saving (showing 1 of 1). These functions will not be directly callable after loading. INFO:tensorflow:Assets written to: /tmpfs/tmp/tmpxpt22mi0/model/assets INFO:tensorflow:Assets written to: /tmpfs/tmp/tmpxpt22mi0/model/assets Recommendations: [b'Little Big League (1994)' b'Miracle on 34th Street (1994)' b'Cinderella (1950)']
若要深入瞭解如何使用和調整快速近似檢索模型,請參閱我們的有效率的服務教學課程。
項目對項目推薦
在此模型中,我們建立了一個使用者對電影模型。但是,對於某些應用程式 (例如,產品詳細資訊頁面),執行項目對項目 (例如,電影對電影或產品對產品) 推薦很常見。
訓練像這樣的模型會遵循與本教學課程中所示相同的模式,但使用不同的訓練資料。在這裡,我們有一個使用者塔和一個電影塔,並使用 (使用者、電影) 配對來訓練它們。在項目對項目模型中,我們將有兩個項目塔 (用於查詢和候選項目),並使用 (查詢項目、候選項目) 配對來訓練模型。這些可以從產品詳細資訊頁面的點擊次數建構。
後續步驟
檢索教學課程到此結束。
若要擴充此處呈現的內容,請參閱:
- 學習多工模型:共同最佳化評分和點擊次數。
- 使用電影中繼資料:建構更複雜的電影模型以減輕冷啟動。