Использование сторонних функций: предварительная обработка функций

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

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

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

Например:

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

Их необходимо соответствующим образом преобразовать, чтобы их можно было использовать при построении моделей:

  • Идентификаторы пользователей и элементов должны быть преобразованы в векторы внедрения: многомерные числовые представления, которые корректируются во время обучения, чтобы помочь модели лучше предсказать ее цель.
  • Необработанный текст необходимо токенизировать (разбивать на более мелкие части, такие как отдельные слова) и переводить во вложения.
  • Числовые характеристики необходимо нормализовать так, чтобы их значения лежали в небольшом интервале около 0.

К счастью, с помощью TensorFlow мы можем сделать такую ​​предварительную обработку частью нашей модели, а не отдельным этапом предварительной обработки. Это не только удобно, но и гарантирует, что наша предварительная обработка точно такая же во время обучения и во время обслуживания. Это делает безопасным и простым развертывание моделей, которые включают даже очень сложную предварительную обработку.

В этом уроке мы собираемся сосредоточиться на рекомендующих и предварительную обработку мы должны сделать на наборе данных MovieLens . Если вы заинтересованы в большем учебнике без внимания рекомендательной системы, посмотрите на полную Keras предварительной обработки руководства .

Набор данных MovieLens

Давайте сначала посмотрим, какие функции мы можем использовать из набора данных MovieLens:

pip install -q --upgrade tensorflow-datasets
import pprint

import tensorflow_datasets as tfds

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

for x in ratings.take(1).as_numpy_iterator():
  pprint.pprint(x)
2021-10-02 11:59:46.956587: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected
{'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'}
2021-10-02 11:59:47.327679: W tensorflow/core/kernels/data/cache_dataset_ops.cc:768] 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.

Здесь можно выделить несколько ключевых особенностей:

  • Название фильма полезно как идентификатор фильма.
  • Идентификатор пользователя полезен как идентификатор пользователя.
  • Метки времени позволят нам смоделировать влияние времени.

Первые два являются категориальными признаками; отметки времени являются непрерывной функцией.

Превращение категориальных функций во вложения

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

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

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

Преобразование необработанных категориальных функций во вложения обычно представляет собой двухэтапный процесс:

  1. Во-первых, нам нужно преобразовать необработанные значения в ряд смежных целых чисел, обычно путем построения сопоставления (называемого «словарным запасом»), которое преобразует исходные значения («Звездные войны») в целые числа (скажем, 15).
  2. Во-вторых, нам нужно взять эти целые числа и превратить их в вложения.

Определение словарного запаса

Первый шаг - определить словарный запас. Мы можем легко сделать это, используя слои предварительной обработки Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

Сам слой еще не имеет словаря, но мы можем построить его, используя наши данные.

movie_title_lookup.adapt(ratings.map(lambda x: x["movie_title"]))

print(f"Vocabulary: {movie_title_lookup.get_vocabulary()[:3]}")
Vocabulary: ['[UNK]', 'Star Wars (1977)', 'Contact (1997)']

Когда у нас есть это, мы можем использовать слой для преобразования сырых токенов во встраиваемые идентификаторы:

movie_title_lookup(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([ 1, 58])>

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

Использование хеширования функций

На самом деле, StringLookup слой позволяет настроить несколько индексов Oov. Если мы это сделаем, любое необработанное значение, которого нет в словаре, будет детерминированно хешировано до одного из индексов OOV. Чем больше у нас таких индексов, тем меньше вероятность того, что два разных необработанных значения функций будут хешированы в один и тот же индекс OOV. Следовательно, если у нас достаточно таких индексов, модель должна быть способна обучаться примерно так же, как модель с явным словарем, без необходимости поддерживать список токенов.

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

# We set up a large number of bins to reduce the chance of hash collisions.
num_hashing_bins = 200_000

movie_title_hashing = tf.keras.layers.Hashing(
    num_bins=num_hashing_bins
)

Мы можем выполнять поиск, как и раньше, без необходимости создавать словари:

movie_title_hashing(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([101016,  96565])>

Определение вложений

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

Слой внедрения имеет два измерения: первое измерение сообщает нам, сколько различных категорий мы можем встроить; второй говорит нам, насколько большим может быть вектор, представляющий каждый из них.

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

movie_title_embedding = tf.keras.layers.Embedding(
    # Let's use the explicit vocabulary lookup.
    input_dim=movie_title_lookup.vocab_size(),
    output_dim=32
)
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

Мы можем собрать их вместе в один слой, который принимает необработанный текст и дает вложения.

movie_title_model = tf.keras.Sequential([movie_title_lookup, movie_title_embedding])

Точно так же мы можем напрямую получить вложения для названий наших фильмов:

movie_title_model(["Star Wars (1977)"])
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['Star Wars (1977)']
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 'list'> input: ['Star Wars (1977)']
Consider rewriting this model with the Functional API.
<tf.Tensor: shape=(1, 32), dtype=float32, numpy=
array([[-0.00255408,  0.00941082,  0.02599109, -0.02758816, -0.03652344,
        -0.03852248, -0.03309812, -0.04343383,  0.03444691, -0.02454401,
         0.00619583, -0.01912323, -0.03988413,  0.03595274,  0.00727529,
         0.04844356,  0.04739804,  0.02836904,  0.01647964, -0.02924066,
        -0.00425701,  0.01747661,  0.0114414 ,  0.04916174,  0.02185034,
        -0.00399858,  0.03934855,  0.03666003,  0.01980535, -0.03694187,
        -0.02149243, -0.03765338]], dtype=float32)>

Мы можем сделать то же самое с пользовательскими встраиваемыми данными:

user_id_lookup = tf.keras.layers.StringLookup()
user_id_lookup.adapt(ratings.map(lambda x: x["user_id"]))

user_id_embedding = tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32)

user_id_model = tf.keras.Sequential([user_id_lookup, user_id_embedding])
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

Нормализация непрерывных функций

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

for x in ratings.take(3).as_numpy_iterator():
  print(f"Timestamp: {x['timestamp']}.")
Timestamp: 879024327.
Timestamp: 875654590.
Timestamp: 882075110.

Нам нужно обработать его, прежде чем мы сможем его использовать. Хотя есть много способов сделать это, дискретизация и стандартизация - два общих из них.

Стандартизация

Стандартизация перемасштабирует особенность нормализовать их диапазон путем вычитания особенности подло и деления стандартного отклонения. Это обычное преобразование предварительной обработки.

Это может быть легко достигнуто с помощью tf.keras.layers.Normalization слоя:

timestamp_normalization = tf.keras.layers.Normalization(
    axis=None
)
timestamp_normalization.adapt(ratings.map(lambda x: x["timestamp"]).batch(1024))

for x in ratings.take(3).as_numpy_iterator():
  print(f"Normalized timestamp: {timestamp_normalization(x['timestamp'])}.")
Normalized timestamp: [-0.84293723].
Normalized timestamp: [-1.4735204].
Normalized timestamp: [-0.27203268].

Дискретность

Другое распространенное преобразование - превращение непрерывного объекта в ряд категориальных признаков. Это имеет смысл, если у нас есть основания подозревать, что эффект функции не является непрерывным.

Для этого нам сначала нужно установить границы сегментов, которые мы будем использовать для дискретизации. Самый простой способ - определить минимальное и максимальное значение признака и разделить полученный интервал поровну:

max_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    tf.cast(0, tf.int64), tf.maximum).numpy().max()
min_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    np.int64(1e9), tf.minimum).numpy().min()

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

print(f"Buckets: {timestamp_buckets[:3]}")
Buckets: [8.74724710e+08 8.74743291e+08 8.74761871e+08]

Учитывая границы корзины, мы можем преобразовать временные метки во вложения:

timestamp_embedding_model = tf.keras.Sequential([
  tf.keras.layers.Discretization(timestamp_buckets.tolist()),
  tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32)
])

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():
  print(f"Timestamp embedding: {timestamp_embedding_model(timestamp)}.")
Timestamp embedding: [[-0.02532113 -0.00415025  0.00458465  0.02080876  0.03103903 -0.03746337
   0.04010465 -0.01709593 -0.00246077 -0.01220842  0.02456966 -0.04816503
   0.04552222  0.03535838  0.00769508  0.04328252  0.00869263  0.01110227
   0.02754457 -0.02659499 -0.01055292 -0.03035731  0.00463334 -0.02848787
  -0.03416766  0.02538678 -0.03446608 -0.0384447  -0.03032914 -0.02391632
   0.02637175 -0.01158618]].

Обработка текстовых функций

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

Хотя набор данных MovieLens не дает нам богатых текстовых функций, мы все же можем использовать названия фильмов. Это может помочь нам уловить тот факт, что фильмы с очень похожими названиями, скорее всего, принадлежат к одному и тому же сериалу.

Первое преобразование, которое нам нужно применить к тексту, - это токенизация (разделение на составные слова или части слов), за которым следует изучение словаря с последующим встраиванием.

Keras tf.keras.layers.TextVectorization слой может сделать первые два шага для нас:

title_text = tf.keras.layers.TextVectorization()
title_text.adapt(ratings.map(lambda x: x["movie_title"]))

Давайте попробуем:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):
  print(title_text(row))
tf.Tensor([[ 32 266 162   2 267 265  53]], shape=(1, 7), dtype=int64)

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

Мы можем проверить изученную лексику, чтобы убедиться, что уровень использует правильную токенизацию:

title_text.get_vocabulary()[40:45]
['first', '1998', '1977', '1971', 'monty']

Это выглядит правильно: слой разбивает заголовки на отдельные слова.

Чтобы завершить обработку, нам нужно встроить текст. Поскольку каждый заголовок содержит несколько слов, мы получим несколько вложений для каждого заголовка. Для использования в модели donwstream они обычно сжимаются в одно вложение. Здесь полезны такие модели, как RNN или Transformers, но хорошей отправной точкой является усреднение вложений всех слов.

Собираем все вместе

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

Модель пользователя

Полная пользовательская модель может выглядеть следующим образом:

class UserModel(tf.keras.Model):

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

    self.user_embedding = tf.keras.Sequential([
        user_id_lookup,
        tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32),
    ])
    self.timestamp_embedding = tf.keras.Sequential([
      tf.keras.layers.Discretization(timestamp_buckets.tolist()),
      tf.keras.layers.Embedding(len(timestamp_buckets) + 2, 32)
    ])
    self.normalized_timestamp = tf.keras.layers.Normalization(
        axis=None
    )

  def call(self, inputs):

    # Take the input dictionary, pass it through each input layer,
    # and concatenate the result.
    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)

Давайте попробуем:

user_model = UserModel()

user_model.normalized_timestamp.adapt(
    ratings.map(lambda x: x["timestamp"]).batch(128))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {user_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.04705765 -0.04739009 -0.04212048]

Модель фильма

То же самое можно сделать и с моделью фильма:

class MovieModel(tf.keras.Model):

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

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      movie_title_lookup,
      tf.keras.layers.Embedding(movie_title_lookup.vocab_size(), 32)
    ])
    self.title_text_embedding = tf.keras.Sequential([
      tf.keras.layers.TextVectorization(max_tokens=max_tokens),
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      # We average the embedding of individual words to get one embedding vector
      # per title.
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

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

Давайте попробуем:

movie_model = MovieModel()

movie_model.title_text_embedding.layers[0].adapt(
    ratings.map(lambda x: x["movie_title"]))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {movie_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.01670959  0.02128791  0.04631067]

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

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