Использование контекстных функций

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

В руководстве по изменению характеристик мы включили в наши модели несколько функций, помимо идентификаторов пользователей и фильмов, но мы не исследовали, улучшают ли эти функции точность модели.

Многие факторы влияют на то, полезны ли функции, выходящие за рамки идентификаторов, в рекомендательной модели:

  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 параметр use_timestamps дает нам контроль над тем, используем ли мы функции 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.experimental.preprocessing.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.experimental.preprocessing.Discretization(timestamp_buckets.tolist()),
          tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32),
      ])
      self.normalized_timestamp = tf.keras.layers.experimental.preprocessing.Normalization()

      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"]),
        self.normalized_timestamp(inputs["timestamp"]),
    ], axis=1)

Обратите внимание, что использование нами функций отметки времени в этом руководстве нежелательным образом взаимодействует с нашим выбором разделения на обучение и тест. Поскольку мы разделили наши данные случайным образом, а не в хронологическом порядке (чтобы гарантировать, что события, которые принадлежат набору тестовых данных, происходят позже, чем события в обучающем наборе), наша модель может эффективно учиться у будущего. Это нереально: в конце концов, мы не можем обучить модель сегодня на данных завтрашнего дня.

Это означает, что добавление временных характеристик к модели позволяет ей изучить будущие шаблоны взаимодействия. Мы делаем это только для иллюстративных целей: сам набор данных MovieLens очень плотный и, в отличие от многих реальных наборов данных, не получает больших преимуществ от функций, помимо идентификаторов пользователей и названий фильмов.

Если оставить в стороне это предостережение, в реальных моделях могут быть полезны другие особенности, основанные на времени, такие как время суток или день недели, особенно если данные имеют сильные сезонные закономерности.

Модель кандидата

Для простоты мы оставим модель кандидата неизменной. Опять же , мы копируем его из featurization урока:

class MovieModel(tf.keras.Model):

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

    max_tokens = 10_000

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

    self.title_vectorizer = tf.keras.layers.experimental.preprocessing.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)

Комбинированная модель

MovieModel как 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, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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 [==============================] - 7s 141ms/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.1474 - loss: 14579.4614 - regularization_loss: 0.0000e+00 - total_loss: 14579.4614
Epoch 2/3
40/40 [==============================] - 6s 142ms/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.2130 - regularization_loss: 0.0000e+00 - total_loss: 14136.2130
Epoch 3/3
40/40 [==============================] - 6s 141ms/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.9252 - regularization_loss: 0.0000e+00 - total_loss: 13939.9252
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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 [==============================] - 6s 139ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0039 - factorized_top_k/top_5_categorical_accuracy: 0.0229 - factorized_top_k/top_10_categorical_accuracy: 0.0429 - factorized_top_k/top_50_categorical_accuracy: 0.1732 - factorized_top_k/top_100_categorical_accuracy: 0.2945 - loss: 13711.3804 - regularization_loss: 0.0000e+00 - total_loss: 13711.3804
5/5 [==============================] - 1s 180ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0010 - factorized_top_k/top_5_categorical_accuracy: 0.0074 - factorized_top_k/top_10_categorical_accuracy: 0.0187 - factorized_top_k/top_50_categorical_accuracy: 0.1049 - factorized_top_k/top_100_categorical_accuracy: 0.2128 - loss: 30995.9036 - regularization_loss: 0.0000e+00 - total_loss: 30995.9036
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, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Using a while_loop for converting BoostedTreesBucketize
40/40 [==============================] - 8s 166ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0059 - factorized_top_k/top_5_categorical_accuracy: 0.0150 - factorized_top_k/top_10_categorical_accuracy: 0.0238 - factorized_top_k/top_50_categorical_accuracy: 0.0808 - factorized_top_k/top_100_categorical_accuracy: 0.1482 - loss: 14607.4785 - regularization_loss: 0.0000e+00 - total_loss: 14607.4785
Epoch 2/3
40/40 [==============================] - 7s 167ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0024 - factorized_top_k/top_5_categorical_accuracy: 0.0153 - factorized_top_k/top_10_categorical_accuracy: 0.0303 - factorized_top_k/top_50_categorical_accuracy: 0.1366 - factorized_top_k/top_100_categorical_accuracy: 0.2505 - loss: 13959.0584 - regularization_loss: 0.0000e+00 - total_loss: 13959.0584
Epoch 3/3
40/40 [==============================] - 7s 166ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0024 - factorized_top_k/top_5_categorical_accuracy: 0.0190 - factorized_top_k/top_10_categorical_accuracy: 0.0392 - factorized_top_k/top_50_categorical_accuracy: 0.1710 - factorized_top_k/top_100_categorical_accuracy: 0.3013 - loss: 13695.6521 - regularization_loss: 0.0000e+00 - total_loss: 13695.6521
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'dict'> input: {'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, but we receive a <class 'dict'> input: {'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:Using a while_loop for converting BoostedTreesBucketize
WARNING:tensorflow:Using a while_loop for converting BoostedTreesBucketize
40/40 [==============================] - 7s 166ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0055 - factorized_top_k/top_5_categorical_accuracy: 0.0325 - factorized_top_k/top_10_categorical_accuracy: 0.0606 - factorized_top_k/top_50_categorical_accuracy: 0.2249 - factorized_top_k/top_100_categorical_accuracy: 0.3637 - loss: 13381.9970 - regularization_loss: 0.0000e+00 - total_loss: 13381.9970
5/5 [==============================] - 1s 232ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0094 - factorized_top_k/top_10_categorical_accuracy: 0.0214 - factorized_top_k/top_50_categorical_accuracy: 0.1268 - factorized_top_k/top_100_categorical_accuracy: 0.2467 - loss: 30696.5029 - regularization_loss: 0.0000e+00 - total_loss: 30696.5029
Top-100 accuracy (train): 0.36.
Top-100 accuracy (test): 0.25.

Это намного лучше: не только точность обучения намного выше, но и точность теста также значительно улучшена.

Следующие шаги

В этом руководстве показано, что даже простые модели могут стать более точными при включении большего количества функций. Однако, чтобы максимально использовать ваши функции, часто необходимо создавать более крупные и глубокие модели. Взгляните на руководство по глубокому извлечению, чтобы изучить это более подробно.