Utilizzo delle funzionalità secondarie: pre-elaborazione delle funzionalità

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica taccuino

Uno dei grandi vantaggi dell'utilizzo di un framework di deep learning per creare modelli di raccomandazione è la libertà di creare rappresentazioni di funzionalità ricche e flessibili.

Il primo passo per farlo è preparare le feature, poiché le feature grezze di solito non saranno immediatamente utilizzabili in un modello.

Per esempio:

  • Gli ID utente e elemento possono essere stringhe (titoli, nomi utente) o interi grandi non contigui (ID database).
  • Le descrizioni degli articoli potrebbero essere testo non elaborato.
  • I timestamp di interazione potrebbero essere timestamp Unix grezzi.

Questi devono essere opportunamente trasformati per essere utili nella costruzione di modelli:

  • Gli ID utente e oggetto devono essere tradotti in vettori di incorporamento: rappresentazioni numeriche ad alta dimensione che vengono adattate durante l'addestramento per aiutare il modello a prevedere meglio il suo obiettivo.
  • Il testo non elaborato deve essere tokenizzato (diviso in parti più piccole come singole parole) e tradotto in incorporamenti.
  • Le caratteristiche numeriche devono essere normalizzate in modo che i loro valori si trovino in un piccolo intervallo intorno a 0.

Fortunatamente, utilizzando TensorFlow possiamo rendere tale pre-elaborazione parte del nostro modello piuttosto che una fase di pre-elaborazione separata. Questo non è solo conveniente, ma garantisce anche che la nostra pre-elaborazione sia esattamente la stessa durante l'allenamento e durante il servizio. Ciò rende sicuro e facile l'implementazione di modelli che includono anche una pre-elaborazione molto sofisticata.

In questo tutorial, ci accingiamo a concentrarsi sul recommenders e la pre-elaborazione che dobbiamo fare sul set di dati MovieLens . Se siete interessati a un tutorial più grande senza un focus sistema di raccomandazione, hanno uno sguardo alla completa guida pre-elaborazione Keras .

Il set di dati di MovieLens

Diamo prima un'occhiata a quali funzionalità possiamo utilizzare dal set di dati 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.

Ci sono un paio di caratteristiche chiave qui:

  • Il titolo del film è utile come identificatore del film.
  • L'ID utente è utile come identificatore utente.
  • I timestamp ci permetteranno di modellare l'effetto del tempo.

I primi due sono caratteristiche categoriali; i timestamp sono una caratteristica continua.

Trasformare le funzionalità categoriche in incorporamenti

Una caratteristica categorica è una caratteristica che non esprime una quantità continua, ma assume uno di un insieme di valori fissi.

La maggior parte dei modelli di deep learning esprime queste caratteristiche trasformandole in vettori ad alta dimensionalità. Durante l'addestramento del modello, il valore di quel vettore viene regolato per aiutare il modello a prevedere meglio il suo obiettivo.

Ad esempio, supponiamo che il nostro obiettivo sia prevedere quale utente guarderà quale film. Per fare ciò, rappresentiamo ogni utente e ogni film con un vettore di incorporamento. Inizialmente, questi incorporamenti assumeranno valori casuali, ma durante l'addestramento li regoleremo in modo che gli incorporamenti degli utenti e i film che guardano si avvicinino l'uno all'altro.

Prendere caratteristiche categoriche grezze e trasformarle in incorporamenti è normalmente un processo in due fasi:

  1. In primo luogo, dobbiamo tradurre i valori grezzi in un intervallo di interi contigui, normalmente costruendo una mappatura (chiamata "vocabolario") che mappi i valori grezzi ("Star Wars") in numeri interi (diciamo 15).
  2. In secondo luogo, dobbiamo prendere questi numeri interi e trasformarli in incorporamenti.

Definire il vocabolario

Il primo passo è definire un vocabolario. Possiamo farlo facilmente usando i livelli di pre-elaborazione di Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

Il livello stesso non ha ancora un vocabolario, ma possiamo costruirlo usando i nostri dati.

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 volta ottenuto questo, possiamo usare il livello per tradurre i token non elaborati in id di incorporamento:

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

Nota che il vocabolario del livello include uno (o più!) token sconosciuti (o "fuori dal vocabolario", OOV). Questo è davvero utile: significa che il livello può gestire valori categoriali che non sono nel vocabolario. In termini pratici, ciò significa che il modello può continuare a conoscere e formulare raccomandazioni anche utilizzando caratteristiche che non sono state viste durante la costruzione del vocabolario.

Utilizzo dell'hashing delle funzioni

Infatti, lo StringLookup strato permette di configurare più indici OOV. Se lo facciamo, qualsiasi valore grezzo che non è nel vocabolario verrà hash in modo deterministico a uno degli indici OOV. Più indici di questo tipo abbiamo, meno è probabile che due diversi valori di funzionalità grezzi vengano hash nello stesso indice OOV. Di conseguenza, se disponiamo di un numero sufficiente di tali indici, il modello dovrebbe essere in grado di allenarsi così come un modello con un vocabolario esplicito senza lo svantaggio di dover mantenere l'elenco dei token.

Possiamo portare questo al suo estremo logico e fare affidamento interamente sull'hashing delle funzionalità, senza alcun vocabolario. Questo è implementato nel tf.keras.layers.Hashing strato.

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

Possiamo eseguire la ricerca come prima senza la necessità di creare vocabolari:

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

Definire gli incastri

Ora che abbiamo ID interi, possiamo usare l' Embedding strato di trasformare quelli in incastri.

Un livello di incorporamento ha due dimensioni: la prima dimensione ci dice quante categorie distinte possiamo incorporare; il secondo ci dice quanto può essere grande il vettore che rappresenta ciascuno di essi.

Quando creiamo il livello di incorporamento per i titoli dei film, imposteremo il primo valore alla dimensione del nostro vocabolario dei titoli (o al numero di contenitori di hashing). Il secondo sta a noi: più è grande, maggiore è la capacità del modello, ma più lento è l'adattamento e il servizio.

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.

Possiamo mettere insieme i due in un unico livello che prende il testo grezzo e produce incorporamenti.

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

Proprio così, possiamo ottenere direttamente gli incorporamenti per i titoli dei nostri film:

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

Possiamo fare lo stesso con gli incorporamenti utente:

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.

Normalizzazione delle funzioni continue

Anche le funzionalità continue necessitano di normalizzazione. Ad esempio, il timestamp caratteristica è troppo grande per essere utilizzata direttamente in un modello profondo:

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

Dobbiamo elaborarlo prima di poterlo utilizzare. Sebbene ci siano molti modi in cui possiamo farlo, la discretizzazione e la standardizzazione sono due comuni.

Standardizzazione

Normalizzazione ridimensiona caratteristiche di normalizzare la loro gamma sottraendo la funzionalità del media e dividendo per la sua deviazione standard. È una comune trasformazione di pre-elaborazione.

Questo può essere facilmente realizzato utilizzando il tf.keras.layers.Normalization strato:

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

discretizzazione

Un'altra trasformazione comune consiste nel trasformare una caratteristica continua in una serie di caratteristiche categoriali. Questo ha senso se abbiamo motivo di sospettare che l'effetto di una funzione non sia continuo.

Per fare ciò, dobbiamo prima stabilire i confini dei bucket che utilizzeremo per la discretizzazione. Il modo più semplice è identificare il valore minimo e massimo della caratteristica e dividere equamente l'intervallo risultante:

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]

Dati i limiti del bucket, possiamo trasformare i timestamp in incorporamenti:

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

Funzionalità di elaborazione del testo

Potremmo anche voler aggiungere funzionalità di testo al nostro modello. Di solito, cose come le descrizioni dei prodotti sono testo in formato libero e possiamo sperare che il nostro modello possa imparare a utilizzare le informazioni che contengono per fornire consigli migliori, specialmente in uno scenario di partenza a freddo o coda lunga.

Sebbene il set di dati MovieLens non ci fornisca funzionalità testuali avanzate, possiamo comunque utilizzare i titoli dei film. Questo può aiutarci a catturare il fatto che è probabile che i film con titoli molto simili appartengano alla stessa serie.

La prima trasformazione che dobbiamo applicare al testo è la tokenizzazione (suddivisione in parole costitutive o frammenti di parole), seguita dall'apprendimento del vocabolario, seguito da un incorporamento.

Il Keras tf.keras.layers.TextVectorization strato può fare i primi due passi per noi:

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

Proviamolo:

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)

Ogni titolo viene tradotto in una sequenza di token, uno per ogni pezzo che abbiamo tokenizzato.

Possiamo controllare il vocabolario appreso per verificare che il livello stia utilizzando la tokenizzazione corretta:

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

Questo sembra corretto: il livello sta tokenizzando i titoli in singole parole.

Per completare l'elaborazione, ora dobbiamo incorporare il testo. Poiché ogni titolo contiene più parole, otterremo più incorporamenti per ogni titolo. Per l'uso in un modello donwstream, questi vengono solitamente compressi in un'unica incorporazione. Modelli come RNN o Transformers sono utili qui, ma fare la media di tutte le parole incorporate è un buon punto di partenza.

Mettere tutto insieme

Con questi componenti in posizione, possiamo costruire un modello che esegua tutta la preelaborazione insieme.

Modello utente

Il modello utente completo può essere simile al seguente:

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)

Proviamolo:

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]

Modello di film

Possiamo fare lo stesso per il modello del 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)

Proviamolo:

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]

Prossimi passi

Con i due modelli sopra abbiamo fatto i primi passi per rappresentare funzionalità avanzate in un modello di raccomandazione: per approfondire ed esplorare come questi possono essere utilizzati per costruire un modello efficace di raccomandazione profonda, dai un'occhiata al nostro tutorial Deep Recommenders.