การใช้ประโยชน์จากคุณสมบัติบริบท

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

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

มีหลายปัจจัยที่ส่งผลต่อว่าฟีเจอร์ที่นอกเหนือจาก id นั้นมีประโยชน์ในโมเดลผู้แนะนำหรือไม่:

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

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

เบื้องต้น

ก่อนอื่นเรานำเข้าแพ็คเกจที่จำเป็น

pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
import os
import tempfile

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds

import tensorflow_recommenders as tfrs

เราปฏิบัติตาม บทช่วยสอนคุณสมบัติ และเก็บรหัสผู้ใช้ ประทับเวลา และคุณสมบัติชื่อภาพยนตร์

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"],
    "timestamp": x["timestamp"],
})
movies = movies.map(lambda x: x["movie_title"])

เรายังดูแลทำความสะอาดเพื่อเตรียมคำศัพท์เกี่ยวกับคุณลักษณะอีกด้วย

timestamps = np.concatenate(list(ratings.map(lambda x: x["timestamp"]).batch(100)))

max_timestamp = timestamps.max()
min_timestamp = timestamps.min()

timestamp_buckets = np.linspace(
    min_timestamp, max_timestamp, num=1000,
)

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

คำจำกัดความของโมเดล

แบบสอบถามรุ่น

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

class UserModel(tf.keras.Model):

  def __init__(self, use_timestamps):
    super().__init__()

    self._use_timestamps = use_timestamps

    self.user_embedding = tf.keras.Sequential([
        tf.keras.layers.StringLookup(
            vocabulary=unique_user_ids, mask_token=None),
        tf.keras.layers.Embedding(len(unique_user_ids) + 1, 32),
    ])

    if use_timestamps:
      self.timestamp_embedding = tf.keras.Sequential([
          tf.keras.layers.Discretization(timestamp_buckets.tolist()),
          tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32),
      ])
      self.normalized_timestamp = tf.keras.layers.Normalization(
          axis=None
      )

      self.normalized_timestamp.adapt(timestamps)

  def call(self, inputs):
    if not self._use_timestamps:
      return self.user_embedding(inputs["user_id"])

    return tf.concat([
        self.user_embedding(inputs["user_id"]),
        self.timestamp_embedding(inputs["timestamp"]),
        tf.reshape(self.normalized_timestamp(inputs["timestamp"]), (-1, 1)),
    ], axis=1)

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

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

นอกเหนือจากข้อแม้นี้ โมเดลในโลกแห่งความเป็นจริงอาจได้รับประโยชน์จากคุณลักษณะตามเวลาอื่นๆ เช่น ช่วงเวลาของวันหรือวันในสัปดาห์ โดยเฉพาะอย่างยิ่งหากข้อมูลมีรูปแบบตามฤดูกาลที่ชัดเจน

รูปแบบผู้สมัคร

เพื่อความง่าย เราจะคงรูปแบบผู้สมัครให้คงที่ อีกครั้ง เราคัดลอกจากบท ช่วย สอนคุณสมบัติ:

class MovieModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
          vocabulary=unique_movie_titles, mask_token=None),
      tf.keras.layers.Embedding(len(unique_movie_titles) + 1, 32)
    ])

    self.title_vectorizer = tf.keras.layers.TextVectorization(
        max_tokens=max_tokens)

    self.title_text_embedding = tf.keras.Sequential([
      self.title_vectorizer,
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

    self.title_vectorizer.adapt(movies)

  def call(self, titles):
    return tf.concat([
        self.title_embedding(titles),
        self.title_text_embedding(titles),
    ], axis=1)

รุ่นรวม

ด้วยทั้ง UserModel และ MovieModel ที่กำหนดไว้ เราสามารถรวบรวมโมเดลที่รวมกัน และใช้ลอจิกการสูญเสียและเมตริกของเรา

ที่นี่เรากำลังสร้างแบบจำลองการดึงข้อมูล สำหรับการทบทวนเกี่ยวกับวิธีการทำงาน ดูบทแนะนำ การดึงข้อมูลพื้นฐาน

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

class MovielensModel(tfrs.models.Model):

  def __init__(self, use_timestamps):
    super().__init__()
    self.query_model = tf.keras.Sequential([
      UserModel(use_timestamps),
      tf.keras.layers.Dense(32)
    ])
    self.candidate_model = tf.keras.Sequential([
      MovieModel(),
      tf.keras.layers.Dense(32)
    ])
    self.task = tfrs.tasks.Retrieval(
        metrics=tfrs.metrics.FactorizedTopK(
            candidates=movies.batch(128).map(self.candidate_model),
        ),
    )

  def compute_loss(self, features, training=False):
    # We only pass the user id and timestamp features into the query model. This
    # is to ensure that the training inputs would have the same keys as the
    # query inputs. Otherwise the discrepancy in input structure would cause an
    # error when loading the query model after saving it.
    query_embeddings = self.query_model({
        "user_id": features["user_id"],
        "timestamp": features["timestamp"],
    })
    movie_embeddings = self.candidate_model(features["movie_title"])

    return self.task(query_embeddings, movie_embeddings)

การทดลอง

เตรียมข้อมูล

ก่อนอื่นเราแบ่งข้อมูลออกเป็นชุดฝึกอบรมและชุดทดสอบ

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)

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

พื้นฐาน: ไม่มีคุณสมบัติการประทับเวลา

เราพร้อมที่จะลองใช้โมเดลแรกของเราแล้ว: เริ่มจากไม่ใช้คุณสมบัติการประทับเวลาเพื่อสร้างพื้นฐานของเรา

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

model.fit(cached_train, epochs=3)

train_accuracy = model.evaluate(
    cached_train, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
test_accuracy = model.evaluate(
    cached_test, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]

print(f"Top-100 accuracy (train): {train_accuracy:.2f}.")
print(f"Top-100 accuracy (test): {test_accuracy:.2f}.")
Epoch 1/3
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
40/40 [==============================] - 10s 169ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0092 - factorized_top_k/top_5_categorical_accuracy: 0.0172 - factorized_top_k/top_10_categorical_accuracy: 0.0256 - factorized_top_k/top_50_categorical_accuracy: 0.0824 - factorized_top_k/top_100_categorical_accuracy: 0.1473 - loss: 14579.4628 - regularization_loss: 0.0000e+00 - total_loss: 14579.4628
Epoch 2/3
40/40 [==============================] - 9s 173ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0020 - factorized_top_k/top_5_categorical_accuracy: 0.0126 - factorized_top_k/top_10_categorical_accuracy: 0.0251 - factorized_top_k/top_50_categorical_accuracy: 0.1129 - factorized_top_k/top_100_categorical_accuracy: 0.2133 - loss: 14136.2137 - regularization_loss: 0.0000e+00 - total_loss: 14136.2137
Epoch 3/3
40/40 [==============================] - 9s 174ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0021 - factorized_top_k/top_5_categorical_accuracy: 0.0155 - factorized_top_k/top_10_categorical_accuracy: 0.0307 - factorized_top_k/top_50_categorical_accuracy: 0.1389 - factorized_top_k/top_100_categorical_accuracy: 0.2535 - loss: 13939.9265 - regularization_loss: 0.0000e+00 - total_loss: 13939.9265
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
40/40 [==============================] - 10s 189ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0036 - factorized_top_k/top_5_categorical_accuracy: 0.0226 - factorized_top_k/top_10_categorical_accuracy: 0.0427 - factorized_top_k/top_50_categorical_accuracy: 0.1729 - factorized_top_k/top_100_categorical_accuracy: 0.2944 - loss: 13711.3802 - regularization_loss: 0.0000e+00 - total_loss: 13711.3802
5/5 [==============================] - 3s 267ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0010 - factorized_top_k/top_5_categorical_accuracy: 0.0078 - factorized_top_k/top_10_categorical_accuracy: 0.0184 - factorized_top_k/top_50_categorical_accuracy: 0.1051 - factorized_top_k/top_100_categorical_accuracy: 0.2126 - loss: 30995.8988 - regularization_loss: 0.0000e+00 - total_loss: 30995.8988
Top-100 accuracy (train): 0.29.
Top-100 accuracy (test): 0.21.

สิ่งนี้ทำให้เรามีความแม่นยำสูงสุด 100 อันดับแรกที่ประมาณ 0.2

จับภาพไดนามิกของเวลาด้วยคุณสมบัติด้านเวลา

ผลลัพธ์จะเปลี่ยนไปหรือไม่หากเราเพิ่มคุณสมบัติเวลา

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

model.fit(cached_train, epochs=3)

train_accuracy = model.evaluate(
    cached_train, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
test_accuracy = model.evaluate(
    cached_test, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]

print(f"Top-100 accuracy (train): {train_accuracy:.2f}.")
print(f"Top-100 accuracy (test): {test_accuracy:.2f}.")
Epoch 1/3
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
40/40 [==============================] - 10s 175ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0057 - factorized_top_k/top_5_categorical_accuracy: 0.0148 - factorized_top_k/top_10_categorical_accuracy: 0.0238 - factorized_top_k/top_50_categorical_accuracy: 0.0812 - factorized_top_k/top_100_categorical_accuracy: 0.1487 - loss: 14606.0927 - regularization_loss: 0.0000e+00 - total_loss: 14606.0927
Epoch 2/3
40/40 [==============================] - 9s 176ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0026 - factorized_top_k/top_5_categorical_accuracy: 0.0153 - factorized_top_k/top_10_categorical_accuracy: 0.0304 - factorized_top_k/top_50_categorical_accuracy: 0.1375 - factorized_top_k/top_100_categorical_accuracy: 0.2512 - loss: 13958.5635 - regularization_loss: 0.0000e+00 - total_loss: 13958.5635
Epoch 3/3
40/40 [==============================] - 9s 177ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0026 - factorized_top_k/top_5_categorical_accuracy: 0.0189 - factorized_top_k/top_10_categorical_accuracy: 0.0393 - factorized_top_k/top_50_categorical_accuracy: 0.1713 - factorized_top_k/top_100_categorical_accuracy: 0.3015 - loss: 13696.8511 - regularization_loss: 0.0000e+00 - total_loss: 13696.8511
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
40/40 [==============================] - 9s 172ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0050 - factorized_top_k/top_5_categorical_accuracy: 0.0323 - factorized_top_k/top_10_categorical_accuracy: 0.0606 - factorized_top_k/top_50_categorical_accuracy: 0.2254 - factorized_top_k/top_100_categorical_accuracy: 0.3637 - loss: 13382.7869 - regularization_loss: 0.0000e+00 - total_loss: 13382.7869
5/5 [==============================] - 1s 237ms/step - 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.0214 - factorized_top_k/top_50_categorical_accuracy: 0.1259 - factorized_top_k/top_100_categorical_accuracy: 0.2468 - loss: 30699.8529 - regularization_loss: 0.0000e+00 - total_loss: 30699.8529
Top-100 accuracy (train): 0.36.
Top-100 accuracy (test): 0.25.

สิ่งนี้ค่อนข้างดีขึ้น: ไม่เพียงแต่ความแม่นยำในการฝึกจะสูงขึ้นมากเท่านั้น แต่ความแม่นยำในการทดสอบยังได้รับการปรับปรุงอย่างมากอีกด้วย

ขั้นตอนถัดไป

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