Costruire modelli di recupero profondo

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

Nel il featurization esercitazione abbiamo incorporato molteplici caratteristiche nei nostri modelli, ma i modelli sono costituiti da uno strato soltanto embedding. Possiamo aggiungere strati più densi ai nostri modelli per aumentare il loro potere espressivo.

In generale, i modelli più profondi sono in grado di apprendere modelli più complessi rispetto ai modelli più superficiali. Ad esempio, il nostro modello di utente incorpora user id e timestamp per le preferenze degli utenti del modello in un punto nel tempo. Un modello superficiale (ad esempio, un singolo livello di incorporamento) potrebbe essere in grado di apprendere solo le relazioni più semplici tra quelle funzionalità e i film: un determinato film è più popolare al momento della sua uscita e un determinato utente generalmente preferisce i film horror alle commedie. Per catturare relazioni più complesse, come le preferenze dell'utente che si evolvono nel tempo, potremmo aver bisogno di un modello più profondo con più strati densi impilati.

Naturalmente, anche i modelli complessi hanno i loro svantaggi. Il primo è il costo computazionale, poiché i modelli più grandi richiedono sia più memoria che più calcolo per adattarsi e funzionare. Il secondo è il requisito di più dati: in generale, sono necessari più dati di addestramento per sfruttare modelli più profondi. Con più parametri, i modelli profondi potrebbero sovraadattare o anche semplicemente memorizzare gli esempi di addestramento invece di apprendere una funzione che può generalizzare. Infine, addestrare modelli più approfonditi può essere più difficile e occorre prestare maggiore attenzione nella scelta di impostazioni come la regolarizzazione e il tasso di apprendimento.

Trovare una buona architettura per un sistema di raccomandazione del mondo reale è un'arte complessa, che richiede buona intuizione e attenta messa a punto iperparametro . Ad esempio, fattori come la profondità e la larghezza del modello, la funzione di attivazione, la velocità di apprendimento e l'ottimizzatore possono modificare radicalmente le prestazioni del modello. Le scelte di modellazione sono ulteriormente complicate dal fatto che buone metriche di valutazione offline potrebbero non corrispondere a buone prestazioni online e che la scelta di cosa ottimizzare è spesso più critica della scelta del modello stesso.

Tuttavia, lo sforzo profuso nella costruzione e nella messa a punto di modelli più grandi spesso ripaga. In questo tutorial, illustreremo come creare modelli di recupero approfonditi utilizzando i suggerimenti TensorFlow. Lo faremo costruendo modelli progressivamente più complessi per vedere come questo influisce sulle prestazioni del modello.

Preliminari

Per prima cosa importiamo i pacchetti necessari.

pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
import os
import tempfile

%matplotlib inline
import matplotlib.pyplot as plt

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

import tensorflow_recommenders as tfrs

plt.style.use('seaborn-whitegrid')

In questo tutorial useremo i modelli dal tutorial featurization per generare incastri. Quindi utilizzeremo solo l'ID utente, il timestamp e le funzionalità del titolo del film.

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

ratings = ratings.map(lambda x: {
    "movie_title": x["movie_title"],
    "user_id": x["user_id"],
    "timestamp": x["timestamp"],
})
movies = movies.map(lambda x: x["movie_title"])
2021-10-02 11:11:47.672650: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

Facciamo anche alcune pulizie per preparare vocabolari di funzionalità.

timestamps = np.concatenate(list(ratings.map(lambda x: x["timestamp"]).batch(100)))

max_timestamp = timestamps.max()
min_timestamp = timestamps.min()

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

unique_movie_titles = np.unique(np.concatenate(list(movies.batch(1000))))
unique_user_ids = np.unique(np.concatenate(list(ratings.batch(1_000).map(
    lambda x: x["user_id"]))))

Definizione del modello

Modello di query

Iniziamo con il modello utente definito tutorial featurization come primo strato del nostro modello, con il compito di convertire esempi di ingresso grezzi in funzione immersioni.

class UserModel(tf.keras.Model):

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

    self.user_embedding = tf.keras.Sequential([
        tf.keras.layers.StringLookup(
            vocabulary=unique_user_ids, mask_token=None),
        tf.keras.layers.Embedding(len(unique_user_ids) + 1, 32),
    ])
    self.timestamp_embedding = tf.keras.Sequential([
        tf.keras.layers.Discretization(timestamp_buckets.tolist()),
        tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32),
    ])
    self.normalized_timestamp = tf.keras.layers.Normalization(
        axis=None
    )

    self.normalized_timestamp.adapt(timestamps)

  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)

La definizione di modelli più profondi richiederà di impilare i livelli di modalità sopra questo primo input. Una pila di strati progressivamente più stretta, separata da una funzione di attivazione, è un modello comune:

                            +----------------------+
                            |      128 x 64        |
                            +----------------------+
                                       | relu
                          +--------------------------+
                          |        256 x 128         |
                          +--------------------------+
                                       | relu
                        +------------------------------+
                        |          ... x 256           |
                        +------------------------------+

Poiché la potenza espressiva dei modelli lineari profondi non è maggiore di quella dei modelli lineari superficiali, utilizziamo le attivazioni ReLU per tutti tranne l'ultimo livello nascosto. Il livello nascosto finale non utilizza alcuna funzione di attivazione: l'utilizzo di una funzione di attivazione limiterebbe lo spazio di output degli incorporamenti finali e potrebbe influire negativamente sulle prestazioni del modello. Ad esempio, se le ReLU vengono utilizzate nel livello di proiezione, tutti i componenti nell'incorporamento dell'output sarebbero non negativi.

Proveremo qualcosa di simile qui. Per facilitare la sperimentazione con diverse profondità, definiamo un modello la cui profondità (e larghezza) è definita da un insieme di parametri del costruttore.

class QueryModel(tf.keras.Model):
  """Model for encoding user queries."""

  def __init__(self, layer_sizes):
    """Model for encoding user queries.

    Args:
      layer_sizes:
        A list of integers where the i-th entry represents the number of units
        the i-th layer contains.
    """
    super().__init__()

    # We first use the user model for generating embeddings.
    self.embedding_model = UserModel()

    # Then construct the layers.
    self.dense_layers = tf.keras.Sequential()

    # Use the ReLU activation for all but the last layer.
    for layer_size in layer_sizes[:-1]:
      self.dense_layers.add(tf.keras.layers.Dense(layer_size, activation="relu"))

    # No activation for the last layer.
    for layer_size in layer_sizes[-1:]:
      self.dense_layers.add(tf.keras.layers.Dense(layer_size))

  def call(self, inputs):
    feature_embedding = self.embedding_model(inputs)
    return self.dense_layers(feature_embedding)

Il layer_sizes parametro ci dà la profondità e la larghezza del modello. Possiamo variarlo per sperimentare modelli meno profondi o più profondi.

Modello candidato

Possiamo adottare lo stesso approccio per il modello cinematografico. Anche in questo caso, si parte con la MovieModel dal featurization tutorial:

class MovieModel(tf.keras.Model):

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

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
          vocabulary=unique_movie_titles,mask_token=None),
      tf.keras.layers.Embedding(len(unique_movie_titles) + 1, 32)
    ])

    self.title_vectorizer = tf.keras.layers.TextVectorization(
        max_tokens=max_tokens)

    self.title_text_embedding = tf.keras.Sequential([
      self.title_vectorizer,
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

    self.title_vectorizer.adapt(movies)

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

Ed espandilo con livelli nascosti:

class CandidateModel(tf.keras.Model):
  """Model for encoding movies."""

  def __init__(self, layer_sizes):
    """Model for encoding movies.

    Args:
      layer_sizes:
        A list of integers where the i-th entry represents the number of units
        the i-th layer contains.
    """
    super().__init__()

    self.embedding_model = MovieModel()

    # Then construct the layers.
    self.dense_layers = tf.keras.Sequential()

    # Use the ReLU activation for all but the last layer.
    for layer_size in layer_sizes[:-1]:
      self.dense_layers.add(tf.keras.layers.Dense(layer_size, activation="relu"))

    # No activation for the last layer.
    for layer_size in layer_sizes[-1:]:
      self.dense_layers.add(tf.keras.layers.Dense(layer_size))

  def call(self, inputs):
    feature_embedding = self.embedding_model(inputs)
    return self.dense_layers(feature_embedding)

Modello combinato

Con entrambi QueryModel e CandidateModel definiti, siamo in grado di mettere insieme un modello combinato e mettere in atto la nostra logica perdita e metriche. Per semplificare le cose, imporremo che la struttura del modello sia la stessa tra i modelli di query e candidati.

class MovielensModel(tfrs.models.Model):

  def __init__(self, layer_sizes):
    super().__init__()
    self.query_model = QueryModel(layer_sizes)
    self.candidate_model = CandidateModel(layer_sizes)
    self.task = tfrs.tasks.Retrieval(
        metrics=tfrs.metrics.FactorizedTopK(
            candidates=movies.batch(128).map(self.candidate_model),
        ),
    )

  def compute_loss(self, features, training=False):
    # We only pass the user id and timestamp features into the query model. This
    # is to ensure that the training inputs would have the same keys as the
    # query inputs. Otherwise the discrepancy in input structure would cause an
    # error when loading the query model after saving it.
    query_embeddings = self.query_model({
        "user_id": features["user_id"],
        "timestamp": features["timestamp"],
    })
    movie_embeddings = self.candidate_model(features["movie_title"])

    return self.task(
        query_embeddings, movie_embeddings, compute_metrics=not training)

Addestrare il modello

Preparare i dati

Per prima cosa dividiamo i dati in un set di addestramento e un set di test.

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)

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

Modello poco profondo

Siamo pronti per provare il nostro primo modello superficiale!

num_epochs = 300

model = MovielensModel([32])
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))

one_layer_history = model.fit(
    cached_train,
    validation_data=cached_test,
    validation_freq=5,
    epochs=num_epochs,
    verbose=0)

accuracy = one_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"][-1]
print(f"Top-100 accuracy: {accuracy:.2f}.")
Top-100 accuracy: 0.27.

Questo ci dà una precisione top-100 di circa 0,27. Possiamo usare questo come punto di riferimento per valutare modelli più profondi.

Modello più profondo

Che ne dici di un modello più profondo con due strati?

model = MovielensModel([64, 32])
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))

two_layer_history = model.fit(
    cached_train,
    validation_data=cached_test,
    validation_freq=5,
    epochs=num_epochs,
    verbose=0)

accuracy = two_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"][-1]
print(f"Top-100 accuracy: {accuracy:.2f}.")
Top-100 accuracy: 0.29.

La precisione qui è 0,29, un po' migliore rispetto al modello poco profondo.

Possiamo tracciare le curve di accuratezza della convalida per illustrare questo:

num_validation_runs = len(one_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"])
epochs = [(x + 1)* 5 for x in range(num_validation_runs)]

plt.plot(epochs, one_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"], label="1 layer")
plt.plot(epochs, two_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"], label="2 layers")
plt.title("Accuracy vs epoch")
plt.xlabel("epoch")
plt.ylabel("Top-100 accuracy");
plt.legend()
<matplotlib.legend.Legend at 0x7f841c7513d0>

png

Anche all'inizio dell'addestramento, il modello più grande ha un vantaggio chiaro e stabile sul modello superficiale, suggerendo che l'aggiunta di profondità aiuta il modello a catturare relazioni più sfumate nei dati.

Tuttavia, anche i modelli più profondi non sono necessariamente migliori. Il modello seguente estende la profondità a tre livelli:

model = MovielensModel([128, 64, 32])
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))

three_layer_history = model.fit(
    cached_train,
    validation_data=cached_test,
    validation_freq=5,
    epochs=num_epochs,
    verbose=0)

accuracy = three_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"][-1]
print(f"Top-100 accuracy: {accuracy:.2f}.")
Top-100 accuracy: 0.26.

In effetti, non vediamo miglioramenti rispetto al modello superficiale:

plt.plot(epochs, one_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"], label="1 layer")
plt.plot(epochs, two_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"], label="2 layers")
plt.plot(epochs, three_layer_history.history["val_factorized_top_k/top_100_categorical_accuracy"], label="3 layers")
plt.title("Accuracy vs epoch")
plt.xlabel("epoch")
plt.ylabel("Top-100 accuracy");
plt.legend()
<matplotlib.legend.Legend at 0x7f841c6d8590>

png

Questa è una buona illustrazione del fatto che i modelli più profondi e più grandi, sebbene in grado di prestazioni superiori, richiedono spesso una messa a punto molto attenta. Ad esempio, in questo tutorial abbiamo utilizzato un unico tasso di apprendimento fisso. Scelte alternative possono dare risultati molto diversi e vale la pena esplorarle.

Con una messa a punto appropriata e dati sufficienti, lo sforzo profuso per costruire modelli più grandi e più profondi in molti casi ne vale la pena: modelli più grandi possono portare a miglioramenti sostanziali nell'accuratezza della previsione.

Prossimi passi

In questo tutorial abbiamo ampliato il nostro modello di recupero con livelli densi e funzioni di attivazione. Per vedere come creare un modello in grado di eseguire non solo le attività di recupero, ma anche le attività di rating, dare un'occhiata al tutorial multitask .