Utilisation des fonctionnalités secondaires : prétraitement des fonctionnalités

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

L'un des grands avantages de l'utilisation d'un framework d'apprentissage en profondeur pour créer des modèles de recommandation est la liberté de créer des représentations de fonctionnalités riches et flexibles.

La première étape consiste à préparer les fonctionnalités, car les fonctionnalités brutes ne seront généralement pas immédiatement utilisables dans un modèle.

Par example:

  • Les identifiants d'utilisateur et d'élément peuvent être des chaînes (titres, noms d'utilisateur) ou de grands entiers non contigus (identifiants de base de données).
  • Les descriptions d'articles peuvent être du texte brut.
  • Les horodatages d'interaction peuvent être des horodatages Unix bruts.

Ceux-ci doivent être transformés de manière appropriée afin d'être utiles dans la construction de modèles :

  • Les identifiants d'utilisateur et d'élément doivent être traduits en vecteurs d'intégration : des représentations numériques de grande dimension qui sont ajustées pendant la formation pour aider le modèle à mieux prédire son objectif.
  • Le texte brut doit être segmenté (divisé en parties plus petites telles que des mots individuels) et traduit en intégrations.
  • Les caractéristiques numériques doivent être normalisées afin que leurs valeurs se situent dans un petit intervalle autour de 0.

Heureusement, en utilisant TensorFlow, nous pouvons faire de ce prétraitement une partie de notre modèle plutôt qu'une étape de prétraitement distincte. Ceci est non seulement pratique, mais garantit également que notre prétraitement est exactement le même pendant l'entraînement et pendant le service. Cela permet de déployer des modèles sûrs et faciles qui incluent même un pré-traitement très sophistiqué.

Dans ce tutoriel, nous allons nous concentrer sur recommandeurs et prétraiter que nous devons faire sur le jeu de données MovieLens . Si vous êtes intéressé par un tutoriel plus sans mise au point du système de recommender, jetez un oeil à la pleine Guide prétraiter Keras .

L'ensemble de données MovieLens

Voyons d'abord les fonctionnalités que nous pouvons utiliser à partir de l'ensemble de données 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.

Il y a quelques fonctionnalités clés ici :

  • Le titre du film est utile comme identifiant de film.
  • L'ID utilisateur est utile comme identifiant d'utilisateur.
  • Les horodatages nous permettront de modéliser l'effet du temps.

Les deux premiers sont des caractéristiques catégorielles ; les horodatages sont une fonction continue.

Transformer des caractéristiques catégorielles en intégrations

Une caractéristique catégorique est une fonction qui ne reflète pas une quantité continue, mais prend plutôt sur l' un d'un ensemble de valeurs fixes.

La plupart des modèles d'apprentissage en profondeur expriment ces caractéristiques en les transformant en vecteurs de grande dimension. Pendant l'apprentissage du modèle, la valeur de ce vecteur est ajustée pour aider le modèle à mieux prédire son objectif.

Par exemple, supposons que notre objectif soit de prédire quel utilisateur va regarder quel film. Pour ce faire, nous représentons chaque utilisateur et chaque film par un vecteur plongeant. Initialement, ces intégrations prendront des valeurs aléatoires - mais au cours de la formation, nous les ajusterons pour que les intégrations des utilisateurs et les films qu'ils regardent se rapprochent les unes des autres.

Prendre des caractéristiques catégorielles brutes et les transformer en intégrations est normalement un processus en deux étapes :

  1. Tout d'abord, nous devons traduire les valeurs brutes en une plage d'entiers contigus, normalement en créant un mappage (appelé "vocabulaire") qui mappe les valeurs brutes ("Star Wars") en entiers (disons, 15).
  2. Deuxièmement, nous devons prendre ces entiers et les transformer en plongements.

Définir le vocabulaire

La première étape consiste à définir un vocabulaire. Nous pouvons le faire facilement en utilisant les couches de prétraitement Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

La couche elle-même n'a pas encore de vocabulaire, mais nous pouvons la construire en utilisant nos données.

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

Une fois que nous avons cela, nous pouvons utiliser la couche pour traduire les jetons bruts en identifiants d'intégration :

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

Notez que le vocabulaire de la couche comprend un (ou plusieurs !) jetons inconnus (ou "hors vocabulaire", OOV). C'est très pratique : cela signifie que la couche peut gérer des valeurs catégorielles qui ne sont pas dans le vocabulaire. Concrètement, cela signifie que le modèle peut continuer à apprendre et à faire des recommandations même en utilisant des fonctionnalités qui n'ont pas été vues lors de la construction du vocabulaire.

Utilisation du hachage de fonctionnalités

En fait, la StringLookup couche permet de configurer plusieurs indices de MHV. Si nous faisons cela, toute valeur brute qui n'est pas dans le vocabulaire sera hachée de manière déterministe à l'un des indices OOV. Plus nous avons de tels indices, moins il est probable que deux valeurs de caractéristiques brutes différentes soient hachées vers le même index OOV. Par conséquent, si nous avons suffisamment de tels indices, le modèle devrait être capable de s'entraîner ainsi qu'un modèle avec un vocabulaire explicite sans l'inconvénient d'avoir à maintenir la liste de jetons.

Nous pouvons pousser cela à son extrême logique et nous fier entièrement au hachage des caractéristiques, sans aucun vocabulaire. Ceci est mis en œuvre dans la tf.keras.layers.Hashing couche.

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

Nous pouvons faire la recherche comme avant sans avoir besoin de construire des vocabulaires :

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

Définir les encastrements

Maintenant que nous avons ids entiers, on peut utiliser la Embedding couche pour transformer ces incorporations en.

Une couche d'incorporation a deux dimensions : la première dimension nous indique combien de catégories distinctes nous pouvons incorporer ; la seconde nous dit quelle peut être la taille du vecteur représentant chacun d'eux.

Lors de la création de la couche d'intégration pour les titres de films, nous allons définir la première valeur sur la taille de notre vocabulaire de titre (ou le nombre de bacs de hachage). La seconde nous appartient : plus il est grand, plus la capacité du modèle est élevée, mais plus il est lent à s'adapter et à 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.

Nous pouvons rassembler les deux en un seul calque qui prend du texte brut et produit des intégrations.

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

Juste comme ça, nous pouvons obtenir directement les embeddings pour nos titres de films :

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

Nous pouvons faire la même chose avec les intégrations utilisateur :

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.

Normalisation des caractéristiques continues

Les fonctionnalités continues doivent également être normalisées. Par exemple, l' timestamp caractéristique est beaucoup trop grand pour être utilisé directement dans un modèle profond:

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

Nous devons le traiter avant de pouvoir l'utiliser. Bien qu'il existe de nombreuses manières de procéder, la discrétisation et la normalisation sont deux méthodes courantes.

Standardisation

Normalisation remet à l' échelle dispose de normaliser leur gamme en soustrayant moyenne de la fonction et en divisant par son écart - type. Il s'agit d'une transformation de prétraitement courante.

Ceci peut être facilement réalisée en utilisant la tf.keras.layers.Normalization couche:

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

Discrétisation

Une autre transformation courante consiste à transformer une caractéristique continue en un certain nombre de caractéristiques catégorielles. Cela a du sens si nous avons des raisons de soupçonner que l'effet d'une caractéristique n'est pas continu.

Pour ce faire, nous devons d'abord établir les limites des buckets que nous utiliserons pour la discrétisation. Le moyen le plus simple est d'identifier la valeur minimale et maximale de l'entité et de diviser l'intervalle résultant de manière égale :

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]

Étant donné les limites du bucket, nous pouvons transformer les horodatages en intégrations :

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

Traitement des fonctionnalités de texte

Nous pouvons également souhaiter ajouter des fonctionnalités de texte à notre modèle. Habituellement, des éléments tels que les descriptions de produits sont sous forme de texte libre, et nous pouvons espérer que notre modèle pourra apprendre à utiliser les informations qu'ils contiennent pour faire de meilleures recommandations, en particulier dans un scénario de démarrage à froid ou de longue traîne.

Bien que l'ensemble de données MovieLens ne nous offre pas de fonctionnalités textuelles riches, nous pouvons toujours utiliser des titres de films. Cela peut nous aider à saisir le fait que les films avec des titres très similaires sont susceptibles d'appartenir à la même série.

La première transformation que nous devons appliquer au texte est la tokenisation (séparation en mots constitutifs ou en morceaux de mots), suivie par l'apprentissage du vocabulaire, suivi d'une intégration.

La Keras tf.keras.layers.TextVectorization couche peut faire les deux premières étapes pour nous:

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

Essayons :

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)

Chaque titre est traduit en une séquence de jetons, un pour chaque pièce que nous avons symbolisée.

Nous pouvons vérifier le vocabulaire appris pour vérifier que la couche utilise la tokenisation correcte :

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

Cela semble correct : la couche segmente les titres en mots individuels.

Pour terminer le traitement, nous devons maintenant incorporer le texte. Parce que chaque titre contient plusieurs mots, nous obtiendrons plusieurs intégrations pour chaque titre. Pour une utilisation dans un modèle aval, ceux-ci sont généralement compressés en une seule intégration. Des modèles comme les RNN ou les Transformers sont utiles ici, mais faire la moyenne de tous les plongements de mots ensemble est un bon point de départ.

Mettre tous ensemble

Une fois ces composants en place, nous pouvons créer un modèle qui effectue tout le prétraitement ensemble.

Modèle utilisateur

Le modèle d'utilisateur complet peut ressembler à ce qui suit :

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)

Essayons :

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]

Modèle de film

Nous pouvons faire la même chose pour le modèle de film :

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)

Essayons :

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]

Prochaines étapes

Avec les deux modèles ci-dessus, nous avons pris les premières mesures pour représenter des fonctionnalités riches dans un modèle de recommandation : pour aller plus loin et explorer comment celles-ci peuvent être utilisées pour créer un modèle de recommandation profond efficace, jetez un œil à notre didacticiel sur les recommandations profondes.