ผู้แนะนำแบบมัลติทาสก์

ดูบน TensorFlow.org ทำงานใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

ใน การกวดวิชาพื้นฐานการดึง เราสร้างระบบการเรียกใช้นาฬิกาหนังเป็นสัญญาณการมีปฏิสัมพันธ์ในเชิงบวก

อย่างไรก็ตาม ในหลาย ๆ แอปพลิเคชัน มีแหล่งความคิดเห็นมากมายให้เลือกใช้ ตัวอย่างเช่น ไซต์อีคอมเมิร์ซอาจบันทึกการเข้าชมของผู้ใช้ไปยังหน้าผลิตภัณฑ์ (มีสัญญาณค่อนข้างมาก แต่ค่อนข้างต่ำ) การคลิกรูปภาพ การเพิ่มลงในรถเข็น และสุดท้ายคือการซื้อ อาจบันทึกสัญญาณหลังการซื้อ เช่น บทวิจารณ์และการคืนสินค้า

การผสานรวมความคิดเห็นในรูปแบบต่างๆ เหล่านี้มีความสำคัญต่อการสร้างระบบที่ผู้ใช้ชอบใช้ และไม่ได้ปรับให้เหมาะสมสำหรับเมตริกใด ๆ ทั้งสิ้นโดยบั่นทอนประสิทธิภาพโดยรวม

นอกจากนี้ การสร้างแบบจำลองร่วมสำหรับงานหลายงานอาจให้ผลลัพธ์ที่ดีกว่าการสร้างแบบจำลองเฉพาะงานจำนวนมาก โดยเฉพาะอย่างยิ่งเมื่อมีข้อมูลจำนวนมาก (เช่น การคลิก) และข้อมูลบางส่วนมีน้อย (การซื้อ การคืนสินค้า การตรวจสอบด้วยตนเอง) ในสถานการณ์เหล่านั้นรูปแบบการร่วมทุนอาจจะไม่สามารถที่จะใช้เป็นตัวแทนได้เรียนรู้จากงานมากมายในการปรับปรุงการคาดการณ์ของตนในงานเบาบางผ่านปรากฏการณ์ที่เรียกว่า การเรียนรู้การถ่ายโอน ยกตัวอย่างเช่น การวิจัยนี้ แสดงให้เห็นว่ารูปแบบการทำนายคะแนนของผู้ใช้อย่างชัดเจนจากการสำรวจผู้ใช้เบาบางได้ดีขึ้นอย่างมีนัยสำคัญโดยการเพิ่มงานเสริมที่ใช้ข้อมูลเข้าสู่ระบบคลิกที่อุดมสมบูรณ์

ในบทช่วยสอนนี้ เราจะสร้างผู้แนะนำแบบหลายวัตถุประสงค์สำหรับ Movielens โดยใช้ทั้งสัญญาณโดยนัย (การชมภาพยนตร์) และสัญญาณที่ชัดเจน (การให้คะแนน)

นำเข้า

ขั้นแรกให้นำเข้าของเราออกไปให้พ้นทาง

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
import tensorflow_recommenders as tfrs

กำลังเตรียมชุดข้อมูล

เราจะใช้ชุดข้อมูล Movielens 100K

ratings = tfds.load('movielens/100k-ratings', split="train")
movies = tfds.load('movielens/100k-movies', split="train")

# Select the basic features.
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"])

และทำซ้ำการเตรียมของเราสำหรับการสร้างคำศัพท์และแยกข้อมูลออกเป็นรถไฟและชุดทดสอบ:

# Randomly shuffle data and split between train and test.
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)

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

โมเดลมัลติทาสกิ้ง

มีสองส่วนที่สำคัญสำหรับผู้แนะนำแบบมัลติทาสก์:

  1. พวกมันปรับให้เหมาะสมสำหรับวัตถุประสงค์สองข้อขึ้นไป และดังนั้นจึงมีการสูญเสียสองรายการขึ้นไป
  2. พวกเขาแบ่งปันตัวแปรระหว่างงานเพื่อให้สามารถถ่ายโอนการเรียนรู้

ในบทช่วยสอนนี้ เราจะกำหนดแบบจำลองของเราเหมือนเมื่อก่อน แต่แทนที่จะมีงานเดียว เราจะมีสองงาน: งานหนึ่งที่คาดการณ์เรตติ้ง และอีกงานหนึ่งที่คาดการณ์การดูภาพยนตร์

โมเดลผู้ใช้และภาพยนตร์เหมือนเดิม:

user_model = tf.keras.Sequential([
  tf.keras.layers.StringLookup(
      vocabulary=unique_user_ids, mask_token=None),
  # We add 1 to account for the unknown token.
  tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
])

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.tasks.Ranking(
    loss=tf.keras.losses.MeanSquaredError(),
    metrics=[tf.keras.metrics.RootMeanSquaredError()],
)

เป้าหมายของมันคือการคาดการณ์การให้คะแนนอย่างแม่นยำที่สุด

ประการที่สองคืองานดึงข้อมูล:

tfrs.tasks.Retrieval(
    metrics=tfrs.metrics.FactorizedTopK(
        candidates=movies.batch(128)
    )
)

ก่อนหน้านี้ เป้าหมายของงานนี้คือการทำนายว่าผู้ใช้จะดูหนังเรื่องไหนหรือไม่ดู

ประกอบเข้าด้วยกัน

เรารวบรวมทุกอย่างไว้ในคลาสโมเดล

องค์ประกอบใหม่ที่นี่คือ - เนื่องจากเรามีงานสองงานและการสูญเสียสองครั้ง - เราจำเป็นต้องตัดสินใจว่าการสูญเสียแต่ละครั้งมีความสำคัญเพียงใด เราสามารถทำได้โดยให้น้ำหนักที่สูญเสียไปแต่ละครั้ง และถือว่าตุ้มน้ำหนักเหล่านี้เป็นพารามิเตอร์ไฮเปอร์ หากเรากำหนดน้ำหนักที่สูญเสียจำนวนมากให้กับงานการให้คะแนน โมเดลของเราจะเน้นไปที่การคาดการณ์การให้คะแนน (แต่ยังคงใช้ข้อมูลบางส่วนจากงานการดึงข้อมูล) หากเรากำหนดน้ำหนักที่สูญเสียไปมากให้กับงานดึงข้อมูล ก็จะเน้นที่การดึงข้อมูลแทน

class MovielensModel(tfrs.models.Model):

  def __init__(self, rating_weight: float, retrieval_weight: float) -> None:
    # We take the loss weights in the constructor: this allows us to instantiate
    # several model objects with different loss weights.

    super().__init__()

    embedding_dimension = 32

    # User and movie models.
    self.movie_model: tf.keras.layers.Layer = 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)
    ])
    self.user_model: tf.keras.layers.Layer = 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)
    ])

    # A small model to take in user and movie embeddings and predict ratings.
    # We can make this as complicated as we want as long as we output a scalar
    # as our prediction.
    self.rating_model = tf.keras.Sequential([
        tf.keras.layers.Dense(256, activation="relu"),
        tf.keras.layers.Dense(128, activation="relu"),
        tf.keras.layers.Dense(1),
    ])

    # The tasks.
    self.rating_task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
        loss=tf.keras.losses.MeanSquaredError(),
        metrics=[tf.keras.metrics.RootMeanSquaredError()],
    )
    self.retrieval_task: tf.keras.layers.Layer = tfrs.tasks.Retrieval(
        metrics=tfrs.metrics.FactorizedTopK(
            candidates=movies.batch(128).map(self.movie_model)
        )
    )

    # The loss weights.
    self.rating_weight = rating_weight
    self.retrieval_weight = retrieval_weight

  def call(self, features: Dict[Text, tf.Tensor]) -> 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.
    movie_embeddings = self.movie_model(features["movie_title"])

    return (
        user_embeddings,
        movie_embeddings,
        # We apply the multi-layered rating model to a concatentation of
        # user and movie embeddings.
        self.rating_model(
            tf.concat([user_embeddings, movie_embeddings], axis=1)
        ),
    )

  def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:

    ratings = features.pop("user_rating")

    user_embeddings, movie_embeddings, rating_predictions = self(features)

    # We compute the loss for each task.
    rating_loss = self.rating_task(
        labels=ratings,
        predictions=rating_predictions,
    )
    retrieval_loss = self.retrieval_task(user_embeddings, movie_embeddings)

    # And combine them using the loss weights.
    return (self.rating_weight * rating_loss
            + self.retrieval_weight * retrieval_loss)

โมเดลเฉพาะเรตติ้ง

โมเดลจะเข้ารหัสยอดคงเหลือที่แตกต่างกันของงาน ทั้งนี้ขึ้นอยู่กับน้ำหนักที่เรากำหนด เริ่มจากโมเดลที่พิจารณาเฉพาะการให้คะแนน

model = MovielensModel(rating_weight=1.0, retrieval_weight=0.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
cached_train = train.shuffle(100_000).batch(8192).cache()
cached_test = test.batch(4096).cache()
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 7s 331ms/step - root_mean_squared_error: 2.0903 - factorized_top_k/top_1_categorical_accuracy: 2.7500e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0024 - factorized_top_k/top_10_categorical_accuracy: 0.0054 - factorized_top_k/top_50_categorical_accuracy: 0.0294 - factorized_top_k/top_100_categorical_accuracy: 0.0589 - loss: 4.0315 - regularization_loss: 0.0000e+00 - total_loss: 4.0315
Epoch 2/3
10/10 [==============================] - 3s 321ms/step - root_mean_squared_error: 1.1531 - factorized_top_k/top_1_categorical_accuracy: 1.8750e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0024 - factorized_top_k/top_10_categorical_accuracy: 0.0054 - factorized_top_k/top_50_categorical_accuracy: 0.0297 - factorized_top_k/top_100_categorical_accuracy: 0.0591 - loss: 1.3189 - regularization_loss: 0.0000e+00 - total_loss: 1.3189
Epoch 3/3
10/10 [==============================] - 3s 316ms/step - root_mean_squared_error: 1.1198 - factorized_top_k/top_1_categorical_accuracy: 1.6250e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0025 - factorized_top_k/top_10_categorical_accuracy: 0.0055 - factorized_top_k/top_50_categorical_accuracy: 0.0300 - factorized_top_k/top_100_categorical_accuracy: 0.0597 - loss: 1.2479 - regularization_loss: 0.0000e+00 - total_loss: 1.2479
5/5 [==============================] - 3s 194ms/step - root_mean_squared_error: 1.1130 - factorized_top_k/top_1_categorical_accuracy: 4.5000e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0028 - factorized_top_k/top_10_categorical_accuracy: 0.0052 - factorized_top_k/top_50_categorical_accuracy: 0.0295 - factorized_top_k/top_100_categorical_accuracy: 0.0597 - loss: 1.2336 - regularization_loss: 0.0000e+00 - total_loss: 1.2336
Retrieval top-100 accuracy: 0.060.
Ranking RMSE: 1.113.

โมเดลนี้ใช้ได้ดีในการคาดการณ์การจัดเรต (โดยมีค่า RMSE ประมาณ 1.11) แต่คาดการณ์ได้ไม่ดีว่าจะดูภาพยนตร์เรื่องใด: ความแม่นยำที่ 100 นั้นแย่กว่ารุ่นที่ได้รับการฝึกฝนมาเพียงเพื่อทำนายนาฬิกาเกือบ 4 เท่า

โมเดลเฉพาะสำหรับการเรียกค้นข้อมูล

คราวนี้มาลองใช้งานโมเดลที่เน้นการดึงเท่านั้น

model = MovielensModel(rating_weight=0.0, retrieval_weight=1.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 4s 313ms/step - root_mean_squared_error: 3.7238 - factorized_top_k/top_1_categorical_accuracy: 7.5000e-05 - factorized_top_k/top_5_categorical_accuracy: 0.0014 - factorized_top_k/top_10_categorical_accuracy: 0.0041 - factorized_top_k/top_50_categorical_accuracy: 0.0473 - factorized_top_k/top_100_categorical_accuracy: 0.1135 - loss: 69818.0298 - regularization_loss: 0.0000e+00 - total_loss: 69818.0298
Epoch 2/3
10/10 [==============================] - 3s 326ms/step - root_mean_squared_error: 3.7495 - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0116 - factorized_top_k/top_10_categorical_accuracy: 0.0268 - factorized_top_k/top_50_categorical_accuracy: 0.1425 - factorized_top_k/top_100_categorical_accuracy: 0.2658 - loss: 67473.2884 - regularization_loss: 0.0000e+00 - total_loss: 67473.2884
Epoch 3/3
10/10 [==============================] - 3s 314ms/step - root_mean_squared_error: 3.7648 - factorized_top_k/top_1_categorical_accuracy: 0.0014 - factorized_top_k/top_5_categorical_accuracy: 0.0180 - factorized_top_k/top_10_categorical_accuracy: 0.0388 - factorized_top_k/top_50_categorical_accuracy: 0.1773 - factorized_top_k/top_100_categorical_accuracy: 0.3050 - loss: 66329.2543 - regularization_loss: 0.0000e+00 - total_loss: 66329.2543
5/5 [==============================] - 1s 193ms/step - root_mean_squared_error: 3.7730 - factorized_top_k/top_1_categorical_accuracy: 0.0012 - factorized_top_k/top_5_categorical_accuracy: 0.0097 - factorized_top_k/top_10_categorical_accuracy: 0.0218 - factorized_top_k/top_50_categorical_accuracy: 0.1253 - factorized_top_k/top_100_categorical_accuracy: 0.2352 - loss: 31085.0697 - regularization_loss: 0.0000e+00 - total_loss: 31085.0697
Retrieval top-100 accuracy: 0.235.
Ranking RMSE: 3.773.

เราได้รับผลลัพธ์ที่ตรงกันข้าม: แบบจำลองที่ทำงานได้ดีในการดึงข้อมูล แต่คาดการณ์คะแนนได้ไม่ดี

รุ่นร่วม

ตอนนี้ เรามาฝึกแบบจำลองที่กำหนดน้ำหนักบวกให้กับงานทั้งสองกัน

model = MovielensModel(rating_weight=1.0, retrieval_weight=1.0)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
metrics = model.evaluate(cached_test, return_dict=True)

print(f"Retrieval top-100 accuracy: {metrics['factorized_top_k/top_100_categorical_accuracy']:.3f}.")
print(f"Ranking RMSE: {metrics['root_mean_squared_error']:.3f}.")
Epoch 1/3
10/10 [==============================] - 4s 299ms/step - root_mean_squared_error: 2.5007 - factorized_top_k/top_1_categorical_accuracy: 3.7500e-05 - factorized_top_k/top_5_categorical_accuracy: 0.0014 - factorized_top_k/top_10_categorical_accuracy: 0.0043 - factorized_top_k/top_50_categorical_accuracy: 0.0450 - factorized_top_k/top_100_categorical_accuracy: 0.1102 - loss: 69811.8274 - regularization_loss: 0.0000e+00 - total_loss: 69811.8274
Epoch 2/3
10/10 [==============================] - 3s 312ms/step - root_mean_squared_error: 1.2097 - factorized_top_k/top_1_categorical_accuracy: 9.8750e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0110 - factorized_top_k/top_10_categorical_accuracy: 0.0255 - factorized_top_k/top_50_categorical_accuracy: 0.1385 - factorized_top_k/top_100_categorical_accuracy: 0.2605 - loss: 67481.2713 - regularization_loss: 0.0000e+00 - total_loss: 67481.2713
Epoch 3/3
10/10 [==============================] - 3s 305ms/step - root_mean_squared_error: 1.1200 - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0175 - factorized_top_k/top_10_categorical_accuracy: 0.0380 - factorized_top_k/top_50_categorical_accuracy: 0.1758 - factorized_top_k/top_100_categorical_accuracy: 0.3040 - loss: 66297.9318 - regularization_loss: 0.0000e+00 - total_loss: 66297.9318
5/5 [==============================] - 1s 187ms/step - root_mean_squared_error: 1.1312 - factorized_top_k/top_1_categorical_accuracy: 9.5000e-04 - factorized_top_k/top_5_categorical_accuracy: 0.0083 - factorized_top_k/top_10_categorical_accuracy: 0.0220 - factorized_top_k/top_50_categorical_accuracy: 0.1248 - factorized_top_k/top_100_categorical_accuracy: 0.2347 - loss: 31062.8206 - regularization_loss: 0.0000e+00 - total_loss: 31062.8206
Retrieval top-100 accuracy: 0.235.
Ranking RMSE: 1.131.

ผลลัพธ์ที่ได้คือโมเดลที่ทำงานได้ดีพอๆ กับงานทั้งสองอย่างเหมือนกับโมเดลเฉพาะทางแต่ละแบบ

การทำนาย

เราสามารถใช้โมเดลมัลติทาสก์ที่ได้รับการฝึกอบรมเพื่อรับการฝึกอบรมการฝังผู้ใช้และภาพยนตร์ ตลอดจนการให้คะแนนที่คาดการณ์ไว้:

trained_movie_embeddings, trained_user_embeddings, predicted_rating = model({
      "user_id": np.array(["42"]),
      "movie_title": np.array(["Dances with Wolves (1990)"])
  })
print("Predicted rating:")
print(predicted_rating)
Predicted rating:
tf.Tensor([[3.4021819]], shape=(1, 1), dtype=float32)

แม้ว่าผลลัพธ์ในที่นี้จะไม่ได้แสดงให้เห็นถึงประโยชน์ที่แม่นยำอย่างชัดเจนจากแบบจำลองร่วมในกรณีนี้ แต่การเรียนรู้แบบมัลติทาสก์โดยทั่วไปเป็นเครื่องมือที่มีประโยชน์อย่างยิ่ง เราสามารถคาดหวังผลลัพธ์ที่ดีขึ้นได้เมื่อเราสามารถถ่ายโอนความรู้จากงานที่มีข้อมูลมากมาย (เช่น การคลิก) ไปยังงานแยกข้อมูลที่เกี่ยวข้องอย่างใกล้ชิด (เช่น การซื้อ)