Menggunakan fitur samping: fitur preprocessing

Lihat di TensorFlow.org Jalankan di Google Colab Lihat sumber di GitHub Unduh buku catatan

Salah satu keuntungan besar menggunakan kerangka pembelajaran mendalam untuk membangun model pemberi rekomendasi adalah kebebasan untuk membangun representasi fitur yang kaya dan fleksibel.

Langkah pertama dalam melakukannya adalah menyiapkan fitur, karena fitur mentah biasanya tidak akan langsung dapat digunakan dalam sebuah model.

Sebagai contoh:

  • ID pengguna dan item dapat berupa string (judul, nama pengguna) atau bilangan bulat besar yang tidak berdekatan (ID basis data).
  • Deskripsi item bisa berupa teks mentah.
  • Stempel waktu interaksi bisa berupa stempel waktu Unix mentah.

Ini perlu diubah dengan tepat agar berguna dalam membangun model:

  • ID pengguna dan item harus diterjemahkan ke dalam vektor embedding: representasi numerik dimensi tinggi yang disesuaikan selama pelatihan untuk membantu model memprediksi tujuannya dengan lebih baik.
  • Teks mentah perlu diberi token (dibagi menjadi bagian-bagian yang lebih kecil seperti kata-kata individual) dan diterjemahkan ke dalam embeddings.
  • Fitur numerik perlu dinormalisasi sehingga nilainya terletak pada interval kecil sekitar 0.

Untungnya, dengan menggunakan TensorFlow, kita dapat menjadikan prapemrosesan seperti itu sebagai bagian dari model kita, bukan sebagai langkah prapemrosesan yang terpisah. Ini tidak hanya nyaman, tetapi juga memastikan bahwa pra-pemrosesan kami sama persis selama pelatihan dan selama penyajian. Ini membuatnya aman dan mudah untuk menerapkan model yang mencakup pra-pemrosesan yang sangat canggih.

Dalam tutorial ini, kita akan fokus pada Pemberi saran dan preprocessing kita perlu melakukan pada dataset MovieLens . Jika Anda tertarik dalam tutorial yang lebih besar tanpa fokus sistem recommender, kita lihat penuh Keras preprocessing panduan .

Kumpulan data MovieLens

Pertama-tama mari kita lihat fitur apa yang dapat kita gunakan dari dataset 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.

Ada beberapa fitur utama di sini:

  • Judul film berguna sebagai pengenal film.
  • User id berguna sebagai pengenal pengguna.
  • Stempel waktu akan memungkinkan kita untuk memodelkan efek waktu.

Dua yang pertama adalah fitur kategoris; cap waktu adalah fitur berkelanjutan.

Mengubah fitur kategoris menjadi embeddings

Sebuah fitur kategoris adalah fitur yang tidak mengungkapkan kuantitas terus menerus, melainkan mengambil salah satu set nilai tetap.

Sebagian besar model pembelajaran mendalam mengekspresikan fitur ini dengan mengubahnya menjadi vektor berdimensi tinggi. Selama pelatihan model, nilai vektor itu disesuaikan untuk membantu model memprediksi tujuannya dengan lebih baik.

Misalnya, misalkan tujuan kita adalah memprediksi pengguna mana yang akan menonton film mana. Untuk melakukan itu, kami mewakili setiap pengguna dan setiap film dengan vektor embedding. Awalnya, penyematan ini akan mengambil nilai acak - tetapi selama pelatihan, kami akan menyesuaikannya sehingga penyematan pengguna dan film yang mereka tonton menjadi lebih dekat.

Mengambil fitur kategoris mentah dan mengubahnya menjadi embeddings biasanya merupakan proses dua langkah:

  1. Pertama, kita perlu menerjemahkan nilai mentah ke dalam kisaran bilangan bulat yang berdekatan, biasanya dengan membangun pemetaan (disebut "kosa kata") yang memetakan nilai mentah ("Star Wars") ke bilangan bulat (misalnya, 15).
  2. Kedua, kita perlu mengambil bilangan bulat ini dan mengubahnya menjadi embeddings.

Mendefinisikan kosa kata

Langkah pertama adalah mendefinisikan kosakata. Kita dapat melakukan ini dengan mudah menggunakan lapisan preprocessing Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

Lapisan itu sendiri belum memiliki kosakata, tetapi kita dapat membangunnya menggunakan data kita.

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

Setelah kita memiliki ini, kita dapat menggunakan layer untuk menerjemahkan token mentah ke embedding id:

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

Perhatikan bahwa kosakata lapisan mencakup satu (atau lebih!) token yang tidak diketahui (atau "kehabisan kosakata", OOV). Ini sangat berguna: ini berarti bahwa lapisan dapat menangani nilai kategorikal yang tidak ada dalam kosakata. Secara praktis, ini berarti bahwa model dapat terus mempelajari dan membuat rekomendasi bahkan menggunakan fitur yang belum terlihat selama konstruksi kosakata.

Menggunakan fitur hashing

Bahkan, StringLookup lapisan memungkinkan kita untuk mengkonfigurasi beberapa indeks OOV. Jika kita melakukannya, nilai mentah apa pun yang tidak ada dalam kosakata akan di-hash secara deterministik ke salah satu indeks OOV. Semakin banyak indeks yang kita miliki, semakin kecil kemungkinan bahwa dua nilai fitur mentah yang berbeda akan di-hash ke indeks OOV yang sama. Akibatnya, jika kita memiliki cukup indeks seperti itu, model harus dapat dilatih sebaik model dengan kosakata eksplisit tanpa kerugian karena harus mempertahankan daftar token.

Kami dapat membawa ini ke ekstrem logisnya dan sepenuhnya mengandalkan hashing fitur, tanpa kosakata sama sekali. Hal ini diimplementasikan dalam tf.keras.layers.Hashing lapisan.

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

Kita bisa melakukan pencarian seperti sebelumnya tanpa perlu membangun kosakata:

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

Mendefinisikan embeddings

Sekarang bahwa kita memiliki id integer, kita dapat menggunakan Embedding lapisan untuk mengubah mereka menjadi embeddings.

Lapisan penyematan memiliki dua dimensi: dimensi pertama memberi tahu kita berapa banyak kategori berbeda yang dapat kita sematkan; yang kedua memberitahu kita seberapa besar vektor yang mewakili masing-masing vektor tersebut.

Saat membuat lapisan penyematan untuk judul film, kita akan menetapkan nilai pertama ke ukuran kosakata judul kita (atau jumlah bin hashing). Yang kedua terserah kita: semakin besar, semakin tinggi kapasitas model, tetapi semakin lambat untuk menyesuaikan dan melayani.

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.

Kita dapat menggabungkan keduanya menjadi satu lapisan yang mengambil teks mentah dan menghasilkan embeddings.

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

Begitu saja, kita bisa langsung mendapatkan embeddings untuk judul film kita:

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

Kami dapat melakukan hal yang sama dengan penyematan pengguna:

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.

Menormalkan fitur berkelanjutan

Fitur berkelanjutan juga membutuhkan normalisasi. Sebagai contoh, timestamp fitur terlalu besar untuk digunakan secara langsung dalam model yang mendalam:

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

Kita perlu memprosesnya sebelum kita dapat menggunakannya. Meskipun ada banyak cara di mana kita dapat melakukan ini, diskritisasi dan standardisasi adalah dua yang umum.

Standardisasi

Standardisasi rescales fitur untuk menormalkan jangkauan mereka dengan mengurangi fitur ini mean dan membaginya dengan deviasi standar. Ini adalah transformasi preprocessing yang umum.

Hal ini dapat dengan mudah dicapai menggunakan tf.keras.layers.Normalization lapisan:

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

Diskritisasi

Transformasi umum lainnya adalah mengubah fitur kontinu menjadi sejumlah fitur kategoris. Ini masuk akal jika kami memiliki alasan untuk mencurigai bahwa efek fitur tidak berkelanjutan.

Untuk melakukan ini, pertama-tama kita perlu menetapkan batas-batas ember yang akan kita gunakan untuk diskritisasi. Cara termudah adalah mengidentifikasi nilai minimum dan maksimum fitur, dan membagi interval yang dihasilkan secara merata:

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]

Mengingat batasan bucket, kami dapat mengubah stempel waktu menjadi 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]].

Fitur pemrosesan teks

Kami mungkin juga ingin menambahkan fitur teks ke model kami. Biasanya, hal-hal seperti deskripsi produk adalah teks bentuk bebas, dan kami dapat berharap bahwa model kami dapat belajar menggunakan informasi yang dikandungnya untuk membuat rekomendasi yang lebih baik, terutama dalam skenario cold-start atau long tail.

Meskipun kumpulan data MovieLens tidak memberi kami fitur tekstual yang kaya, kami masih dapat menggunakan judul film. Ini dapat membantu kami menangkap fakta bahwa film dengan judul yang sangat mirip kemungkinan besar berasal dari seri yang sama.

Transformasi pertama yang perlu kita terapkan pada teks adalah tokenization (pemisahan menjadi kata-kata atau potongan-potongan kata), diikuti dengan pembelajaran kosa kata, diikuti dengan embedding.

The Keras tf.keras.layers.TextVectorization lapisan dapat melakukan dua langkah pertama bagi kami:

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

Mari kita coba:

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)

Setiap judul diterjemahkan ke dalam urutan token, satu untuk setiap bagian yang telah kami tandai.

Kami dapat memeriksa kosakata yang dipelajari untuk memverifikasi bahwa lapisan menggunakan tokenisasi yang benar:

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

Ini terlihat benar: layer ini menandai judul menjadi kata-kata individual.

Untuk menyelesaikan pemrosesan, sekarang kita perlu menyematkan teks. Karena setiap judul berisi beberapa kata, kita akan mendapatkan beberapa embeddings untuk setiap judul. Untuk digunakan dalam model donwstream ini biasanya dikompresi menjadi satu embedding. Model seperti RNNs atau Transformers berguna di sini, tetapi merata-ratakan semua penyematan kata bersama-sama adalah titik awal yang baik.

Menyatukan semuanya

Dengan komponen-komponen ini di tempatnya, kita dapat membangun model yang melakukan semua pra-pemrosesan bersama-sama.

Model pengguna

Model pengguna lengkap mungkin terlihat seperti berikut:

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)

Mari kita coba:

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]

Model film

Kita dapat melakukan hal yang sama untuk model 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)

Mari kita coba:

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]

Langkah selanjutnya

Dengan dua model di atas, kami telah mengambil langkah pertama untuk mewakili fitur yang kaya dalam model pemberi rekomendasi: untuk melangkah lebih jauh dan menjelajahi bagaimana ini dapat digunakan untuk membangun model pemberi rekomendasi mendalam yang efektif, lihat tutorial Rekomendasi Jauh kami.