الاستفادة من ميزات السياق

عرض على TensorFlow.org تشغيل في Google Colab عرض المصدر على جيثب تحميل دفتر

في البرنامج التعليمي الخاص بالتمييز ، قمنا بدمج ميزات متعددة تتجاوز مجرد معرفات المستخدم والأفلام في نماذجنا ، لكننا لم نستكشف ما إذا كانت هذه الميزات تعمل على تحسين دقة النموذج.

تؤثر العديد من العوامل على ما إذا كانت الميزات التي تتجاوز المعرفات مفيدة في نموذج التوصية:

  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 نفسها كثيفة للغاية ، وعلى عكس العديد من مجموعات البيانات الواقعية لا تستفيد بشكل كبير من الميزات التي تتجاوز معرفات المستخدمين وعناوين الأفلام.

بغض النظر عن هذا التحذير ، قد تستفيد نماذج العالم الحقيقي من الميزات الأخرى المستندة إلى الوقت مثل الوقت من اليوم أو اليوم من الأسبوع ، خاصةً إذا كانت البيانات تحتوي على أنماط موسمية قوية.

نموذج المرشح

للتبسيط ، سنبقي النموذج المرشح ثابتًا. مرة أخرى ، نقوم بنسخه من البرنامج التعليمي الخاص بالسمات :

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.

هذا أفضل قليلاً: ليس فقط دقة التدريب أعلى بكثير ، ولكن تم تحسين دقة الاختبار أيضًا بشكل كبير.

الخطوات التالية

يوضح هذا البرنامج التعليمي أنه حتى النماذج البسيطة يمكن أن تصبح أكثر دقة عند دمج المزيد من الميزات. ومع ذلك ، لتحقيق أقصى استفادة من ميزاتك ، غالبًا ما يكون من الضروري إنشاء نماذج أكبر وأعمق. ألق نظرة على البرنامج التعليمي للاسترجاع العميق لاستكشاف هذا بمزيد من التفصيل.