Uso de funciones laterales: preprocesamiento de funciones

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Una de las grandes ventajas de utilizar un marco de aprendizaje profundo para crear modelos de recomendación es la libertad de crear representaciones de características ricas y flexibles.

El primer paso para hacerlo es preparar las características, ya que las características sin procesar generalmente no se podrán usar de inmediato en un modelo.

Por ejemplo:

  • Los ID de usuario y de elemento pueden ser cadenas (títulos, nombres de usuario) o enteros grandes no contiguos (ID de base de datos).
  • Las descripciones de los elementos pueden ser texto sin formato.
  • Las marcas de tiempo de interacción pueden ser marcas de tiempo Unix sin procesar.

Estos deben transformarse adecuadamente para que sean útiles en la construcción de modelos:

  • Los identificadores de usuario y de elemento deben traducirse en vectores de incrustación: representaciones numéricas de alta dimensión que se ajustan durante el entrenamiento para ayudar al modelo a predecir mejor su objetivo.
  • El texto sin formato debe ser tokenizado (dividido en partes más pequeñas, como palabras individuales) y traducido a incrustaciones.
  • Las características numéricas deben normalizarse para que sus valores se encuentren en un pequeño intervalo alrededor de 0.

Afortunadamente, al usar TensorFlow podemos hacer que dicho procesamiento previo sea parte de nuestro modelo en lugar de un paso de procesamiento previo separado. Esto no solo es conveniente, sino que también garantiza que nuestro procesamiento previo sea exactamente el mismo durante el entrenamiento y durante el servicio. Esto hace que sea seguro y fácil de implementar modelos que incluyen incluso un preprocesamiento muy sofisticado.

En este tutorial, vamos a centrarnos en recomendadores y el procesamiento previo que tenemos que hacer en el conjunto de datos MovieLens . Si está interesado en un tutorial más grande sin un enfoque sistema de recomendación, echar un vistazo a la completa guía de pre-procesamiento Keras .

El conjunto de datos MovieLens

Primero echemos un vistazo a las características que podemos usar del conjunto de datos 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.

Aquí hay un par de características clave:

  • El título de la película es útil como identificador de película.
  • La identificación de usuario es útil como identificador de usuario.
  • Las marcas de tiempo nos permitirán modelar el efecto del tiempo.

Los dos primeros son características categóricas; Las marcas de tiempo son una característica continua.

Convertir características categóricas en incrustaciones

Una característica categórica es una característica que no expresa una cantidad continua, sino que adquiere una de un conjunto de valores fijos.

La mayoría de los modelos de aprendizaje profundo expresan estas características convirtiéndolas en vectores de alta dimensión. Durante el entrenamiento del modelo, el valor de ese vector se ajusta para ayudar al modelo a predecir mejor su objetivo.

Por ejemplo, supongamos que nuestro objetivo es predecir qué usuario verá qué película. Para hacer eso, representamos a cada usuario y cada película mediante un vector de incrustación. Inicialmente, estas incorporaciones tomarán valores aleatorios, pero durante el entrenamiento, los ajustaremos para que las incorporaciones de los usuarios y las películas que ven terminen más juntas.

Tomar características categóricas sin procesar y convertirlas en incrustaciones es normalmente un proceso de dos pasos:

  1. En primer lugar, necesitamos traducir los valores brutos en un rango de enteros contiguos, normalmente construyendo un mapeo (llamado "vocabulario") que mapea valores brutos ("Star Wars") a enteros (digamos, 15).
  2. En segundo lugar, debemos tomar estos números enteros y convertirlos en incrustaciones.

Definiendo el vocabulario

El primer paso es definir un vocabulario. Podemos hacer esto fácilmente usando capas de preprocesamiento de Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

La capa en sí no tiene vocabulario todavía, pero podemos construirlo usando nuestros datos.

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

Una vez que tengamos esto, podemos usar la capa para traducir tokens sin procesar en identificadores de incrustación:

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

Tenga en cuenta que el vocabulario de la capa incluye una (o más) fichas desconocidas (o "sin vocabulario", OOV). Esto es realmente útil: significa que la capa puede manejar valores categóricos que no están en el vocabulario. En términos prácticos, esto significa que el modelo puede seguir aprendiendo y hacer recomendaciones incluso utilizando características que no se han visto durante la construcción del vocabulario.

Usar hash de características

De hecho, el StringLookup capa nos permite configurar múltiples índices fuera de vocabulario. Si hacemos eso, cualquier valor bruto que no esté en el vocabulario se convertirá de forma determinista en uno de los índices OOV. Cuantos más índices de este tipo tengamos, menos probable es que dos valores de características sin procesar diferentes se registren en el mismo índice OOV. En consecuencia, si tenemos suficientes índices de este tipo, el modelo debería poder entrenarse tan bien como un modelo con un vocabulario explícito sin la desventaja de tener que mantener la lista de fichas.

Podemos llevar esto a su extremo lógico y confiar completamente en el hash de características, sin ningún vocabulario. Esto se implementa en el tf.keras.layers.Hashing capa.

# 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
)

Podemos hacer la búsqueda como antes sin la necesidad de construir vocabularios:

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

Definiendo las incrustaciones

Ahora que tenemos los identificadores enteros, podemos utilizar la Embedding capa a su vez, aquellos en los incrustaciones.

Una capa de incrustación tiene dos dimensiones: la primera dimensión nos dice cuántas categorías distintas podemos incrustar; el segundo nos dice qué tan grande puede ser el vector que representa a cada uno de ellos.

Al crear la capa de incrustación para títulos de películas, vamos a establecer el primer valor en el tamaño de nuestro vocabulario del título (o el número de contenedores de hash). El segundo depende de nosotros: cuanto más grande es, mayor es la capacidad del modelo, pero más lento es para encajar y servir.

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.

Podemos poner los dos juntos en una sola capa que toma el texto sin procesar y produce incrustaciones.

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

Así, podemos obtener directamente las incrustaciones para los títulos de nuestras películas:

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

Podemos hacer lo mismo con las incrustaciones de usuarios:

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.

Normalización de características continuas

Las funciones continuas también necesitan normalización. Por ejemplo, la timestamp característica es demasiado grande para ser utilizado directamente en un modelo profundo:

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

Necesitamos procesarlo antes de poder usarlo. Si bien hay muchas formas en las que podemos hacer esto, la discretización y la estandarización son dos comunes.

Estandarización

Normalización cambia la escala características para normalizar su gama restando la función de media y dividiendo por su desviación estándar. Es una transformación de preprocesamiento común.

Esto se puede lograr fácilmente usando el tf.keras.layers.Normalization capa:

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].

Discretización

Otra transformación común es convertir una característica continua en una serie de características categóricas. Esto tiene sentido si tenemos motivos para sospechar que el efecto de una característica no es continuo.

Para hacer esto, primero necesitamos establecer los límites de los depósitos que usaremos para la discretización. La forma más sencilla es identificar el valor mínimo y máximo de la característica y dividir el intervalo resultante en partes iguales:

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]

Dados los límites del depósito, podemos transformar las marcas de tiempo en incrustaciones:

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]].

Procesamiento de funciones de texto

También es posible que deseemos agregar características de texto a nuestro modelo. Por lo general, cosas como las descripciones de productos son texto de forma libre, y podemos esperar que nuestro modelo pueda aprender a usar la información que contienen para hacer mejores recomendaciones, especialmente en un escenario de inicio en frío o de cola larga.

Si bien el conjunto de datos de MovieLens no nos brinda características textuales enriquecidas, aún podemos usar títulos de películas. Esto puede ayudarnos a captar el hecho de que las películas con títulos muy similares probablemente pertenezcan a la misma serie.

La primera transformación que debemos aplicar al texto es la tokenización (dividir en palabras constituyentes o partes de palabras), seguida del aprendizaje de vocabulario, seguido de una incrustación.

El Keras tf.keras.layers.TextVectorization capa puede hacer los dos primeros pasos para nosotros:

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

Probémoslo:

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)

Cada título se traduce en una secuencia de tokens, uno por cada pieza que hemos tokenizado.

Podemos verificar el vocabulario aprendido para verificar que la capa esté usando la tokenización correcta:

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

Esto parece correcto: la capa está tokenizando títulos en palabras individuales.

Para finalizar el procesamiento, ahora necesitamos incrustar el texto. Debido a que cada título contiene varias palabras, obtendremos múltiples incrustaciones para cada título. Para su uso en un modelo donwstream, estos generalmente se comprimen en una sola incrustación. Los modelos como RNN o Transformers son útiles aquí, pero promediar todas las incrustaciones de palabras juntas es un buen punto de partida.

Poniendolo todo junto

Con estos componentes en su lugar, podemos construir un modelo que hace todo el preprocesamiento en conjunto.

Modelo de usuario

El modelo de usuario completo puede tener el siguiente aspecto:

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)

Probémoslo:

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]

Modelo de película

Podemos hacer lo mismo con el modelo de película:

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)

Probémoslo:

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]

Próximos pasos

Con los dos modelos anteriores, hemos dado los primeros pasos para representar características ricas en un modelo de recomendación: para llevar esto más allá y explorar cómo se pueden usar para construir un modelo de recomendación profundo efectivo, eche un vistazo a nuestro tutorial de recomendaciones profundas.