Usando recursos colaterais: pré-processamento de recursos

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Uma das grandes vantagens de usar uma estrutura de aprendizado profundo para construir modelos de recomendação é a liberdade de construir representações de recursos ricas e flexíveis.

A primeira etapa para fazer isso é preparar os recursos, já que os recursos brutos geralmente não serão imediatamente utilizáveis ​​em um modelo.

Por exemplo:

  • IDs de usuário e item podem ser strings (títulos, nomes de usuário) ou números inteiros não contíguos (IDs de banco de dados).
  • As descrições dos itens podem ser texto bruto.
  • Os carimbos de data / hora de interação podem ser carimbos de data / hora Unix brutos.

Eles precisam ser transformados adequadamente para serem úteis na construção de modelos:

  • IDs de usuário e item devem ser traduzidos em vetores de incorporação: representações numéricas de alta dimensão que são ajustadas durante o treinamento para ajudar o modelo a prever melhor seu objetivo.
  • O texto bruto precisa ser tokenizado (dividido em partes menores, como palavras individuais) e traduzido em embeddings.
  • Os recursos numéricos precisam ser normalizados para que seus valores fiquem em um pequeno intervalo em torno de 0.

Felizmente, usando o TensorFlow, podemos tornar esse pré-processamento parte de nosso modelo, em vez de uma etapa de pré-processamento separada. Isso não é apenas conveniente, mas também garante que nosso pré-processamento seja exatamente o mesmo durante o treinamento e durante o serviço. Isso torna seguro e fácil implantar modelos que incluem até mesmo um pré-processamento muito sofisticado.

Neste tutorial, vamos focar recommenders eo pré-processamento que precisamos fazer sobre o conjunto de dados MovieLens . Se você estiver interessado em um tutorial maior sem um foco sistema de recomendação, ter um olhar para o completo guia de pré-processamento Keras .

O conjunto de dados MovieLens

Vamos primeiro dar uma olhada em quais recursos podemos usar do conjunto de dados 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.

Existem alguns recursos principais aqui:

  • O título do filme é útil como um identificador de filme.
  • O ID do usuário é útil como um identificador de usuário.
  • Os carimbos de data / hora nos permitirão modelar o efeito do tempo.

Os dois primeiros são características categóricas; carimbos de data / hora são um recurso contínuo.

Transformando recursos categóricos em embeddings

Uma característica categórica é um recurso que não expressa uma quantidade contínua, mas sim assume um de um conjunto de valores fixos.

A maioria dos modelos de aprendizado profundo expressa esses recursos, transformando-os em vetores de alta dimensão. Durante o treinamento do modelo, o valor desse vetor é ajustado para ajudar o modelo a prever melhor seu objetivo.

Por exemplo, suponha que nosso objetivo seja prever qual usuário irá assistir a qual filme. Para fazer isso, representamos cada usuário e cada filme por um vetor de incorporação. Inicialmente, esses embeddings assumirão valores aleatórios - mas durante o treinamento, vamos ajustá-los para que os embeddings dos usuários e os filmes que eles assistem fiquem mais próximos.

Pegar recursos categóricos brutos e transformá-los em incorporações é normalmente um processo de duas etapas:

  1. Em primeiro lugar, precisamos traduzir os valores brutos em um intervalo de inteiros contíguos, normalmente construindo um mapeamento (chamado de "vocabulário") que mapeia valores brutos ("Star Wars") para inteiros (digamos, 15).
  2. Em segundo lugar, precisamos pegar esses inteiros e transformá-los em embeddings.

Definindo o vocabulário

O primeiro passo é definir um vocabulário. Podemos fazer isso facilmente usando as camadas de pré-processamento Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

A camada em si ainda não possui um vocabulário, mas podemos construí-la usando nossos dados.

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

Assim que tivermos isso, podemos usar a camada para traduzir tokens brutos em ids de incorporação:

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

Observe que o vocabulário da camada inclui um (ou mais!) Tokens desconhecidos (ou "fora do vocabulário", OOV). Isso é muito útil: significa que a camada pode lidar com valores categóricos que não estão no vocabulário. Em termos práticos, isso significa que o modelo pode continuar a aprender e fazer recomendações, mesmo usando recursos que não foram vistos durante a construção do vocabulário.

Usando hash de recurso

Na verdade, o StringLookup camada nos permite configurar vários índices OOV. Se fizermos isso, qualquer valor bruto que não estiver no vocabulário será deterministicamente hash para um dos índices OOV. Quanto mais índices tivermos, menos provável será que dois valores brutos de recurso diferentes tenham o mesmo índice OOV. Conseqüentemente, se tivermos índices suficientes, o modelo deve ser capaz de treinar tão bem quanto um modelo com um vocabulário explícito, sem a desvantagem de ter que manter a lista de tokens.

Podemos levar isso ao extremo lógico e confiar inteiramente no hashing de recursos, sem nenhum vocabulário. Isso é implementado no tf.keras.layers.Hashing camada.

# 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 fazer a pesquisa como antes, sem a necessidade de construir vocabulários:

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

Definindo os embeddings

Agora que temos ids inteiros, podemos usar a Embedding camada para transformá-las em mergulhos.

Uma camada de incorporação tem duas dimensões: a primeira dimensão nos diz quantas categorias distintas podemos incorporar; a segunda nos diz quão grande pode ser o vetor que representa cada um deles.

Ao criar a camada de incorporação para títulos de filmes, vamos definir o primeiro valor para o tamanho do nosso vocabulário de títulos (ou o número de caixas de hash). A segunda é nossa: quanto maior for, maior será a capacidade do modelo, mas mais lento será para se ajustar e 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 colocar os dois juntos em uma única camada que recebe texto bruto e produz embeddings.

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

Assim, podemos obter diretamente os embeddings para os títulos de nossos filmes:

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 fazer o mesmo com os embeddings do usuário:

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.

Normalizando recursos contínuos

Os recursos contínuos também precisam de normalização. Por exemplo, o timestamp recurso é demasiado grande para ser usado diretamente em um modelo de profundidade:

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

Precisamos processá-lo antes de podermos usá-lo. Embora haja muitas maneiras de fazer isso, discretização e padronização são duas formas comuns.

estandardização

Normalização rescales apresenta para normalizar sua gama subtraindo o recurso de média e dividindo pelo seu desvio padrão. É uma transformação de pré-processamento comum.

Isto pode ser facilmente conseguido usando o tf.keras.layers.Normalization camada:

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

Discretização

Outra transformação comum é transformar um recurso contínuo em vários recursos categóricos. Isso faz sentido se tivermos motivos para suspeitar que o efeito de um recurso não é contínuo.

Para fazer isso, primeiro precisamos estabelecer os limites dos intervalos que usaremos para discretização. A maneira mais fácil é identificar o valor mínimo e máximo do recurso e dividir o intervalo resultante igualmente:

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 os limites do intervalo, podemos transformar carimbos de data / hora em embeddings:

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

Processando recursos de texto

Também podemos querer adicionar recursos de texto ao nosso modelo. Normalmente, coisas como descrições de produtos são textos em formato livre, e podemos esperar que nosso modelo possa aprender a usar as informações nelas contidas para fazer recomendações melhores, especialmente em um cenário de inicialização a frio ou cauda longa.

Embora o conjunto de dados MovieLens não forneça recursos textuais ricos, ainda podemos usar títulos de filmes. Isso pode nos ajudar a capturar o fato de que filmes com títulos muito semelhantes provavelmente pertencem à mesma série.

A primeira transformação que precisamos aplicar ao texto é a tokenização (divisão em palavras constituintes ou pedaços de palavras), seguida pela aprendizagem de vocabulário, seguida por uma incorporação.

O Keras tf.keras.layers.TextVectorization camada pode fazer as duas primeiras etapas para nós:

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

Vamos experimentar:

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 é traduzido em uma sequência de tokens, um para cada peça que tokenizamos.

Podemos verificar o vocabulário aprendido para verificar se a camada está usando a tokenização correta:

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

Parece correto: a camada está tokenizando títulos em palavras individuais.

Para terminar o processamento, agora precisamos incorporar o texto. Como cada título contém várias palavras, obteremos vários embeddings para cada título. Para uso em um modelo donwstream, eles geralmente são compactados em uma única incorporação. Modelos como RNNs ou Transformers são úteis aqui, mas calcular a média de todas as palavras embeddings juntas é um bom ponto de partida.

Juntando tudo

Com esses componentes no lugar, podemos construir um modelo que faz todo o pré-processamento juntos.

Modelo de usuário

O modelo de usuário completo pode ser parecido com o seguinte:

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)

Vamos experimentar:

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 filme

Podemos fazer o mesmo para o modelo do filme:

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)

Vamos experimentar:

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 passos

Com os dois modelos acima, demos as primeiras etapas para representar recursos ricos em um modelo de recomendador: para levar isso adiante e explorar como eles podem ser usados ​​para construir um modelo de recomender profundo eficaz, dê uma olhada em nosso tutorial de Recomendadores Profundos.