Глубокая и кросс-сеть (DCN)

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

В этом руководстве показано, как использовать Deep & Cross Network (DCN) для эффективного изучения пересечений функций.

Задний план

Что такое кресты функций и почему они важны? Представьте, что мы создаем систему рекомендаций, чтобы продавать блендер клиентам. Затем, предыстория покупки клиента , такие как purchased_bananas и purchased_cooking_books или географические особенности, одиночные функции. Если один купил как бананы и кулинарные книги, то этот клиент будет более вероятно , нажмите на рекомендуемом блендер. Сочетание purchased_bananas и purchased_cooking_books упоминается как особенность креста, которая обеспечивает дополнительную информацию взаимодействия за пределами индивидуальных особенностей.

Какие проблемы возникают при изучении крестиков функций? В веб-приложениях данные в основном категоричны, что приводит к большому и разреженному пространству функций. Выявление эффективных пересечений функций в этой настройке часто требует ручной разработки функций или исчерпывающего поиска. Традиционные модели многослойного персептрона с прямой связью (MLP) представляют собой универсальные аппроксиматоры функций; Однако, они не могут эффективно аппроксимировать даже 2 - го или 3-го порядка функций кресты [ 1 , 2 ].

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

  • Кросс-сеть. Это ядро ​​DCN. Он явно применяет пересечение признаков на каждом слое, и максимальная степень полинома увеличивается с глубиной слоя. На приведенном ниже рисунке показаны \((i+1)\)-го поперечного слоя.
  • Глубокая сеть. Это традиционный многослойный перцептрон с прямой связью (MLP).

Глубокая сеть и сеть кросс затем объединяются , чтобы сформировать DCN [ 1 ]. Обычно мы можем разместить глубокую сеть поверх кросс-сети (многослойная структура); мы также можем разместить их параллельно (параллельная структура).

Далее мы сначала покажем преимущества DCN на игрушечном примере, а затем расскажем о некоторых распространенных способах использования DCN с использованием набора данных MovieLen-1M.

Давайте сначала установим и импортируем необходимые пакеты для этого colab.

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

%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable

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

import tensorflow_recommenders as tfrs

Пример игрушки

Чтобы проиллюстрировать преимущества DCN, давайте рассмотрим простой пример. Предположим, у нас есть набор данных, в котором мы пытаемся смоделировать вероятность того, что клиент нажмет на объявление блендера, с его функциями и меткой, описанными ниже.

Характеристики / этикетка Описание Тип значения / диапазон
\(x_1\) = страна страна, в которой живет этот клиент Int в [0, 199]
\(x_2\) = бананы # банан купил клиент Int в [0, 23]
\(x_3\) = кулинарные книги # кулинарные книги, приобретенные клиентом Int в [0, 5]
\(y\) вероятность нажатия на рекламу блендера -

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

\[y = f(x_1, x_2, x_3) = 0.1x_1 + 0.4x_2+0.7x_3 + 0.1x_1x_2+3.1x_2x_3+0.1x_3^2\]

где вероятность \(y\) линейно зависит как от особенностей \(x_i\)«с, но также и на мультипликативных взаимодействиях между \(x_i\)» ы. В нашем случае, мы можем сказать , что вероятность покупки блендер (\(y\)) зависит не только от покупки бананов (\(x_2\)) или поваренных книг (\(x_3\)), но и на покупку бананов и поваренные книги вместе (\(x_2x_3\)).

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

Генерация синтетических данных

Сначала мы определим \(f(x_1, x_2, x_3)\) , как описано выше.

def get_mixer_data(data_size=100_000, random_seed=42):
  # We need to fix the random seed
  # to make colab runs repeatable.
  rng = np.random.RandomState(random_seed)
  country = rng.randint(200, size=[data_size, 1]) / 200.
  bananas = rng.randint(24, size=[data_size, 1]) / 24.
  coockbooks = rng.randint(6, size=[data_size, 1]) / 6.

  x = np.concatenate([country, bananas, coockbooks], axis=1)

  # # Create 1st-order terms.
  y = 0.1 * country + 0.4 * bananas + 0.7 * coockbooks

  # Create 2nd-order cross terms.
  y += 0.1 * country * bananas + 3.1 * bananas * coockbooks + (
        0.1 * coockbooks * coockbooks)

  return x, y

Давайте сгенерируем данные, которые соответствуют распределению, и разделим данные на 90% для обучения и 10% для тестирования.

x, y = get_mixer_data()
num_train = 90000
train_x = x[:num_train]
train_y = y[:num_train]
eval_x = x[num_train:]
eval_y = y[num_train:]

Конструкция модели

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

  1. Перекрестная сеть только с одним перекрестным слоем;
  2. Глубокая сеть с более широкими и глубокими слоями ReLU.

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

class Model(tfrs.Model):

  def __init__(self, model):
    super().__init__()
    self._model = model
    self._logit_layer = tf.keras.layers.Dense(1)

    self.task = tfrs.tasks.Ranking(
      loss=tf.keras.losses.MeanSquaredError(),
      metrics=[
        tf.keras.metrics.RootMeanSquaredError("RMSE")
      ]
    )

  def call(self, x):
    x = self._model(x)
    return self._logit_layer(x)

  def compute_loss(self, features, training=False):
    x, labels = features
    scores = self(x)

    return self.task(
        labels=labels,
        predictions=scores,
    )

Затем мы указываем кросс-сеть (с 1 кросс-уровнем размера 3) и DNN на основе ReLU (с размерами слоев [512, 256, 128]):

crossnet = Model(tfrs.layers.dcn.Cross())
deepnet = Model(
    tf.keras.Sequential([
      tf.keras.layers.Dense(512, activation="relu"),
      tf.keras.layers.Dense(256, activation="relu"),
      tf.keras.layers.Dense(128, activation="relu")
    ])
)

Модельное обучение

Теперь, когда у нас есть данные и модели, мы собираемся обучить модели. Сначала мы перемешиваем и группируем данные, чтобы подготовиться к обучению модели.

train_data = tf.data.Dataset.from_tensor_slices((train_x, train_y)).batch(1000)
eval_data = tf.data.Dataset.from_tensor_slices((eval_x, eval_y)).batch(1000)

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

epochs = 100
learning_rate = 0.4

Хорошо, теперь все готово, давайте скомпилируем и обучим модели. Вы можете установить verbose=True , если вы хотите увидеть , как модель прогрессирует.

crossnet.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate))
crossnet.fit(train_data, epochs=epochs, verbose=False)
<keras.callbacks.History at 0x7f27d82ef390>
deepnet.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate))
deepnet.fit(train_data, epochs=epochs, verbose=False)
<keras.callbacks.History at 0x7f27d07a3dd0>

Оценка модели

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

crossnet_result = crossnet.evaluate(eval_data, return_dict=True, verbose=False)
print(f"CrossNet(1 layer) RMSE is {crossnet_result['RMSE']:.4f} "
      f"using {crossnet.count_params()} parameters.")

deepnet_result = deepnet.evaluate(eval_data, return_dict=True, verbose=False)
print(f"DeepNet(large) RMSE is {deepnet_result['RMSE']:.4f} "
      f"using {deepnet.count_params()} parameters.")
CrossNet(1 layer) RMSE is 0.0011 using 16 parameters.
DeepNet(large) RMSE is 0.1258 using 166401 parameters.

Мы видим , что крест сети , достигнутые величины снижения RMSE чем РЕЛУ на основе DNN, с величинами меньшим числом параметров. Это свидетельствует об эффективности кросс-сети в изучении кросс-функций.

Модель понимания

Мы уже знаем, какие пересечения признаков важны в наших данных, было бы интересно проверить, действительно ли наша модель изучила важное пересечение признаков. Это можно сделать, визуализировав заученную матрицу весов в DCN. Вес \(W_{ij}\) представляет изученную важность взаимодействия между особенностью \(x_i\) и \(x_j\).

mat = crossnet._model._dense.kernel
features = ["country", "purchased_bananas", "purchased_cookbooks"]

plt.figure(figsize=(9,9))
im = plt.matshow(np.abs(mat.numpy()), cmap=plt.cm.Blues)
ax = plt.gca()
divider = make_axes_locatable(plt.gca())
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im, cax=cax)
cax.tick_params(labelsize=10) 
_ = ax.set_xticklabels([''] + features, rotation=45, fontsize=10)
_ = ax.set_yticklabels([''] + features, fontsize=10)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:11: UserWarning: FixedFormatter should only be used together with FixedLocator
  # This is added back by InteractiveShellApp.init_path()
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:12: UserWarning: FixedFormatter should only be used together with FixedLocator
  if sys.path[0] == '':
<Figure size 648x648 with 0 Axes>

PNG

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

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

Пример киноленты 1М

Рассмотрим теперь эффективность DCN на реальном наборе данных: MovieLens 1M [ 3 ]. Movielens 1M - популярный набор данных для рекомендательных исследований. Он прогнозирует рейтинги фильмов пользователей с учетом пользовательских функций и функций, связанных с фильмами. Мы используем этот набор данных, чтобы продемонстрировать некоторые распространенные способы использования DCN.

Обработка данных

Процедура обработки данных следует аналогичную процедуру в качестве основного ранжирования учебника .

ratings = tfds.load("movie_lens/100k-ratings", split="train")
ratings = ratings.map(lambda x: {
    "movie_id": x["movie_id"],
    "user_id": x["user_id"],
    "user_rating": x["user_rating"],
    "user_gender": int(x["user_gender"]),
    "user_zip_code": x["user_zip_code"],
    "user_occupation_text": x["user_occupation_text"],
    "bucketized_user_age": int(x["bucketized_user_age"]),
})
WARNING:absl:The handle "movie_lens" for the MovieLens dataset is deprecated. Prefer using "movielens" instead.

Затем мы случайным образом разделяем данные на 80% для обучения и 20% для тестирования.

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)

Затем мы создаем словарь для каждой функции.

feature_names = ["movie_id", "user_id", "user_gender", "user_zip_code",
                 "user_occupation_text", "bucketized_user_age"]

vocabularies = {}

for feature_name in feature_names:
  vocab = ratings.batch(1_000_000).map(lambda x: x[feature_name])
  vocabularies[feature_name] = np.unique(np.concatenate(list(vocab)))

Конструкция модели

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

class DCN(tfrs.Model):

  def __init__(self, use_cross_layer, deep_layer_sizes, projection_dim=None):
    super().__init__()

    self.embedding_dimension = 32

    str_features = ["movie_id", "user_id", "user_zip_code",
                    "user_occupation_text"]
    int_features = ["user_gender", "bucketized_user_age"]

    self._all_features = str_features + int_features
    self._embeddings = {}

    # Compute embeddings for string features.
    for feature_name in str_features:
      vocabulary = vocabularies[feature_name]
      self._embeddings[feature_name] = tf.keras.Sequential(
          [tf.keras.layers.StringLookup(
              vocabulary=vocabulary, mask_token=None),
           tf.keras.layers.Embedding(len(vocabulary) + 1,
                                     self.embedding_dimension)
    ])

    # Compute embeddings for int features.
    for feature_name in int_features:
      vocabulary = vocabularies[feature_name]
      self._embeddings[feature_name] = tf.keras.Sequential(
          [tf.keras.layers.IntegerLookup(
              vocabulary=vocabulary, mask_value=None),
           tf.keras.layers.Embedding(len(vocabulary) + 1,
                                     self.embedding_dimension)
    ])

    if use_cross_layer:
      self._cross_layer = tfrs.layers.dcn.Cross(
          projection_dim=projection_dim,
          kernel_initializer="glorot_uniform")
    else:
      self._cross_layer = None

    self._deep_layers = [tf.keras.layers.Dense(layer_size, activation="relu")
      for layer_size in deep_layer_sizes]

    self._logit_layer = tf.keras.layers.Dense(1)

    self.task = tfrs.tasks.Ranking(
      loss=tf.keras.losses.MeanSquaredError(),
      metrics=[tf.keras.metrics.RootMeanSquaredError("RMSE")]
    )

  def call(self, features):
    # Concatenate embeddings
    embeddings = []
    for feature_name in self._all_features:
      embedding_fn = self._embeddings[feature_name]
      embeddings.append(embedding_fn(features[feature_name]))

    x = tf.concat(embeddings, axis=1)

    # Build Cross Network
    if self._cross_layer is not None:
      x = self._cross_layer(x)

    # Build Deep Network
    for deep_layer in self._deep_layers:
      x = deep_layer(x)

    return self._logit_layer(x)

  def compute_loss(self, features, training=False):
    labels = features.pop("user_rating")
    scores = self(features)
    return self.task(
        labels=labels,
        predictions=scores,
    )

Модельное обучение

Мы перемешиваем, группируем и кэшируем данные обучения и тестирования.

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

Давайте определим функцию, которая запускает модель несколько раз и возвращает среднее значение RMSE и стандартное отклонение модели из нескольких запусков.

def run_models(use_cross_layer, deep_layer_sizes, projection_dim=None, num_runs=5):
  models = []
  rmses = []

  for i in range(num_runs):
    model = DCN(use_cross_layer=use_cross_layer,
                deep_layer_sizes=deep_layer_sizes,
                projection_dim=projection_dim)
    model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate))
    models.append(model)

    model.fit(cached_train, epochs=epochs, verbose=False)
    metrics = model.evaluate(cached_test, return_dict=True)
    rmses.append(metrics["RMSE"])

  mean, stdv = np.average(rmses), np.std(rmses)

  return {"model": models, "mean": mean, "stdv": stdv}

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

epochs = 8
learning_rate = 0.01

DCN (в стопке). Сначала мы обучаем модель DCN многослойной структуре, то есть входные данные подаются в кросс-сеть, а затем в глубокую сеть.

dcn_result = run_models(use_cross_layer=True,
                        deep_layer_sizes=[192, 192])
WARNING:tensorflow:mask_value is deprecated, use mask_token instead.
WARNING:tensorflow:mask_value is deprecated, use mask_token instead.
5/5 [==============================] - 3s 24ms/step - RMSE: 0.9312 - loss: 0.8674 - regularization_loss: 0.0000e+00 - total_loss: 0.8674
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9339 - loss: 0.8726 - regularization_loss: 0.0000e+00 - total_loss: 0.8726
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9326 - loss: 0.8703 - regularization_loss: 0.0000e+00 - total_loss: 0.8703
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9351 - loss: 0.8752 - regularization_loss: 0.0000e+00 - total_loss: 0.8752
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9339 - loss: 0.8729 - regularization_loss: 0.0000e+00 - total_loss: 0.8729

DCN низкого ранга. Чтобы снизить затраты на обучение и обслуживание, мы используем методы низкого ранга для аппроксимации весовых матриц DCN. Ранг передается через аргумент projection_dim ; Меньшая projection_dim приводит к более низкой стоимости. Обратите внимание , что projection_dim должно быть меньше , чем (размер входного) / 2 , чтобы уменьшить стоимость. На практике мы наблюдали, что использование DCN низкого ранга с рангом (размером ввода) / 4 постоянно сохраняло точность DCN полного ранга.

dcn_lr_result = run_models(use_cross_layer=True,
                           projection_dim=20,
                           deep_layer_sizes=[192, 192])
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9307 - loss: 0.8669 - regularization_loss: 0.0000e+00 - total_loss: 0.8669
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9312 - loss: 0.8668 - regularization_loss: 0.0000e+00 - total_loss: 0.8668
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9303 - loss: 0.8666 - regularization_loss: 0.0000e+00 - total_loss: 0.8666
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9337 - loss: 0.8723 - regularization_loss: 0.0000e+00 - total_loss: 0.8723
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9300 - loss: 0.8657 - regularization_loss: 0.0000e+00 - total_loss: 0.8657

DNN. Мы обучаем модель DNN того же размера в качестве эталона.

dnn_result = run_models(use_cross_layer=False,
                        deep_layer_sizes=[192, 192, 192])
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9462 - loss: 0.8989 - regularization_loss: 0.0000e+00 - total_loss: 0.8989
5/5 [==============================] - 0s 4ms/step - RMSE: 0.9352 - loss: 0.8765 - regularization_loss: 0.0000e+00 - total_loss: 0.8765
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9393 - loss: 0.8840 - regularization_loss: 0.0000e+00 - total_loss: 0.8840
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9362 - loss: 0.8772 - regularization_loss: 0.0000e+00 - total_loss: 0.8772
5/5 [==============================] - 0s 3ms/step - RMSE: 0.9377 - loss: 0.8798 - regularization_loss: 0.0000e+00 - total_loss: 0.8798

Мы оцениваем модель на тестовых данных и сообщаем среднее значение и стандартное отклонение из 5 прогонов.

print("DCN            RMSE mean: {:.4f}, stdv: {:.4f}".format(
    dcn_result["mean"], dcn_result["stdv"]))
print("DCN (low-rank) RMSE mean: {:.4f}, stdv: {:.4f}".format(
    dcn_lr_result["mean"], dcn_lr_result["stdv"]))
print("DNN            RMSE mean: {:.4f}, stdv: {:.4f}".format(
    dnn_result["mean"], dnn_result["stdv"]))
DCN            RMSE mean: 0.9333, stdv: 0.0013
DCN (low-rank) RMSE mean: 0.9312, stdv: 0.0013
DNN            RMSE mean: 0.9389, stdv: 0.0039

Мы видим, что DCN обеспечивает лучшую производительность, чем DNN того же размера с уровнями ReLU. Более того, DCN низкого ранга смогла снизить параметры при сохранении точности.

Подробнее о DCN. Кроме того , было показано , Что у выше, есть более творческие еще практически полезные способы использования DCN [ 1 ].

  • DCN с параллельной структурой. Входы подаются параллельно кросс-сети и глубокой сети.

  • Объединение перекрестных слоев. Входные данные подаются параллельно нескольким перекрестным слоям для захвата дополнительных пересечений признаков.

Слева: DCN с параллельной структурой; Справа: Конкатенация поперечных слоев.

Модель понимания

Весовая матрица \(W\) в DCN показывает , что функция пересекает модель научилась быть важным. Напомним , что в предыдущем примере игрушек, важность взаимодействия между \(i\)-м и \(j\)-м показывает захватывается (\(i, j\)) -й элемент \(W\).

Что немного отличается здесь является то , что функция вложения размером 32 вместо размера 1. Таким образом, значение будет характеризоваться \((i, j)\)-м блок\(W_{i,j}\) , который имеет размерность 32 на 32. В дальнейшем мы визуализировать норму Фробениуса [ 4 ] \(||W_{i,j}||_F\) каждого блока, и большую норму хотела бы предложить более высокое значение (при условии вложения особенностей имеет одинаковые шкалы).

Помимо нормы блока, мы также можем визуализировать всю матрицу или среднее / медианное / максимальное значение каждого блока.

model = dcn_result["model"][0]
mat = model._cross_layer._dense.kernel
features = model._all_features

block_norm = np.ones([len(features), len(features)])

dim = model.embedding_dimension

# Compute the norms of the blocks.
for i in range(len(features)):
  for j in range(len(features)):
    block = mat[i * dim:(i + 1) * dim,
                j * dim:(j + 1) * dim]
    block_norm[i,j] = np.linalg.norm(block, ord="fro")

plt.figure(figsize=(9,9))
im = plt.matshow(block_norm, cmap=plt.cm.Blues)
ax = plt.gca()
divider = make_axes_locatable(plt.gca())
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im, cax=cax)
cax.tick_params(labelsize=10) 
_ = ax.set_xticklabels([""] + features, rotation=45, ha="left", fontsize=10)
_ = ax.set_yticklabels([""] + features, fontsize=10)
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:23: UserWarning: FixedFormatter should only be used together with FixedLocator
/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/ipykernel_launcher.py:24: UserWarning: FixedFormatter should only be used together with FixedLocator
<Figure size 648x648 with 0 Axes>

PNG

Вот и все для этого колаба! Мы надеемся, что вам понравилось изучать основы DCN и распространенные способы ее использования. Если вы заинтересованы в получении дополнительной информации , вы можете проверить два соответствующих документ: DCN-v1-бумага , DCN-v2-бумага .


использованная литература

DCN V2: Повышение Deep & Cross Сеть и практические уроки для Web-масштабного обучения ранжировать системы .
Руокси Ван, Ракеш Шиванна, Дерек Чжиюань Ченг, Сагар Джайн, Донг Линь, Личан Хонг, Эд Чи. (2020)

Deep & Cross сеть Ad Click Предсказания .
Жуйси Ван, Бинь Фу, Ганг Фу, Минлян Ван. (AdKDD 2017)