Korzystanie z funkcji pobocznych: wstępne przetwarzanie funkcji

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

Jedną z wielkich zalet korzystania z platformy uczenia głębokiego do tworzenia modeli rekomendujących jest swoboda tworzenia bogatych, elastycznych reprezentacji funkcji.

Pierwszym krokiem w tym kierunku jest przygotowanie cech, ponieważ surowe cechy zwykle nie będą od razu dostępne w modelu.

Na przykład:

  • Identyfikatory użytkowników i elementów mogą być ciągami (tytuły, nazwy użytkowników) lub dużymi, nieciągłymi liczbami całkowitymi (identyfikatorami bazy danych).
  • Opisy przedmiotów mogą być nieprzetworzonym tekstem.
  • Znaczniki czasu interakcji mogą być surowymi znacznikami czasu uniksowego.

Muszą one zostać odpowiednio przekształcone, aby były przydatne w budowaniu modeli:

  • Identyfikatory użytkownika i elementu muszą zostać przetłumaczone na wektory osadzania: wielowymiarowe reprezentacje liczbowe, które są dostosowywane podczas uczenia, aby pomóc modelowi lepiej przewidywać jego cel.
  • Surowy tekst musi zostać poddany tokenizacji (podzielony na mniejsze części, takie jak pojedyncze słowa) i przetłumaczony na osadzania.
  • Cechy liczbowe należy znormalizować, aby ich wartości mieściły się w małym przedziale wokół 0.

Na szczęście, korzystając z TensorFlow, możemy uczynić takie przetwarzanie wstępne częścią naszego modelu, a nie oddzielnym etapem przetwarzania wstępnego. Jest to nie tylko wygodne, ale także zapewnia, że ​​nasze wstępne przetwarzanie jest dokładnie takie samo podczas treningu i podczas serwowania. Dzięki temu wdrażanie modeli, które obejmują nawet bardzo wyrafinowane przetwarzanie wstępne, jest bezpieczne i łatwe.

W tym tutorialu, będziemy koncentrować się na rekomendujących i przerób musimy zrobić na zbiorze MovieLens . Jeśli jesteś zainteresowany w większej tutorialu bez naciskiem system rekomendacyjny, przyjrzeć się pełnej Keras przerób przewodnika .

Zbiór danych MovieLens

Przyjrzyjmy się najpierw, jakich funkcji możemy użyć z zestawu danych 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.

Jest tu kilka kluczowych funkcji:

  • Tytuł filmu jest przydatny jako identyfikator filmu.
  • Identyfikator użytkownika jest przydatny jako identyfikator użytkownika.
  • Znaczniki czasu pozwolą nam modelować efekt czasu.

Pierwsze dwie to cechy kategoryczne; znaczniki czasu są funkcją ciągłą.

Przekształcanie cech kategorycznych w osadzania

Kategoryczny funkcja to funkcja, która nie wyraża ciągłe ilość, lecz przybiera jedną z zestawem stałych wartości.

Większość modeli głębokiego uczenia wyraża te cechy, przekształcając je w wektory wielowymiarowe. Podczas uczenia modelu wartość tego wektora jest dostosowywana, aby pomóc modelowi lepiej przewidywać jego cel.

Załóżmy na przykład, że naszym celem jest przewidzenie, który użytkownik obejrzy jaki film. Aby to zrobić, reprezentujemy każdego użytkownika i każdy film za pomocą wektora osadzenia. Początkowo embeddingi te przyjmą losowe wartości - ale podczas treningu dostosujemy je tak, aby embeddingi użytkowników i oglądane przez nich filmy były bliżej siebie.

Przyjmowanie surowych cech kategorycznych i przekształcanie ich w osadzenia jest zwykle procesem dwuetapowym:

  1. Po pierwsze, musimy przetłumaczyć surowe wartości na szereg ciągłych liczb całkowitych, zwykle budując odwzorowanie (nazywane „słownictwo”), które odwzorowuje surowe wartości („Gwiezdne wojny”) na liczby całkowite (powiedzmy 15).
  2. Po drugie, musimy wziąć te liczby całkowite i zamienić je w osadzania.

Definiowanie słownictwa

Pierwszym krokiem jest zdefiniowanie słownictwa. Możemy to łatwo zrobić, korzystając z warstw przetwarzania wstępnego Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

Sama warstwa nie ma jeszcze słownika, ale możemy ją zbudować z naszych danych.

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

Gdy już to mamy, możemy użyć warstwy do przetłumaczenia surowych tokenów na identyfikatory osadzania:

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

Zwróć uwagę, że słownik warstwy zawiera jeden (lub więcej!) nieznany (lub „poza słownikiem”, OOV). Jest to bardzo przydatne: oznacza to, że warstwa może obsługiwać wartości kategoryczne, których nie ma w słowniku. W praktyce oznacza to, że model może nadal uczyć się i wydawać rekomendacje, nawet przy użyciu funkcji, których nie widziano podczas tworzenia słownictwa.

Korzystanie z funkcji hashowania

W rzeczywistości, StringLookup warstwa pozwala na skonfigurowanie wielu indeksów OOV. Jeśli to zrobimy, każda surowa wartość, której nie ma w słowniku, zostanie deterministycznie zahaszowana do jednego z indeksów OOV. Im więcej takich indeksów mamy, tym mniejsze prawdopodobieństwo, że dwie różne surowe wartości cech będą mieszały się z tym samym indeksem OOV. W konsekwencji, jeśli mamy wystarczającą liczbę takich indeksów, model powinien być w stanie trenować równie dobrze, jak model z jawnym słownictwem, bez konieczności utrzymywania listy tokenów.

Możemy doprowadzić to do logicznego ekstremum i całkowicie polegać na hashowaniu funkcji, bez żadnego słownictwa. Jest to realizowane w tf.keras.layers.Hashing warstwy.

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

Wyszukiwanie możemy wykonać tak jak poprzednio, bez konieczności budowania słowników:

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

Definiowanie osadzeń

Teraz, gdy mamy identyfikatory całkowitą, możemy użyć Embedding warstwy, aby włączyć te do zanurzeń.

Warstwa osadzania ma dwa wymiary: pierwszy wymiar mówi nam, ile różnych kategorii możemy osadzić; druga mówi nam, jak duży może być wektor reprezentujący każdy z nich.

Tworząc warstwę osadzania dla tytułów filmowych, ustawimy pierwszą wartość na rozmiar naszego słownika tytułów (lub liczbę koszy haszujących). Drugie zależy od nas: im jest większy, tym większa pojemność modelu, ale tym wolniej ma się zmieścić i służyć.

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.

Możemy połączyć te dwa elementy w jedną warstwę, która pobiera nieprzetworzony tekst i zapewnia osadzania.

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

Tak po prostu, możemy bezpośrednio uzyskać embeddingi dla naszych tytułów filmowych:

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

To samo możemy zrobić z osadzaniami użytkowników:

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.

Normalizowanie ciągłych cech

Ciągłe funkcje również wymagają normalizacji. Na przykład, timestamp funkcja jest zbyt duża, aby być stosowany bezpośrednio w głębokim modelu:

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

Musimy go przetworzyć, zanim będziemy mogli go użyć. Chociaż istnieje wiele sposobów, na które możemy to zrobić, dyskretyzacja i standaryzacja to dwa powszechne.

Normalizacja

Standaryzacja przeskalowanie wyposażony znormalizować swoją ofertę poprzez odjęcie funkcja jest średnia i podzielenie przez jej odchylenie standardowe. Jest to powszechna transformacja przetwarzania wstępnego.

Można to łatwo zrobić za pomocą tf.keras.layers.Normalization warstwy:

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

Dyskretyzacja

Inną powszechną transformacją jest przekształcenie ciągłej cechy w szereg cech kategorycznych. Ma to sens, jeśli mamy powody, by podejrzewać, że efekt funkcji jest nieciągły.

Aby to zrobić, najpierw musimy ustalić granice wiader, których użyjemy do dyskretyzacji. Najprostszym sposobem jest określenie minimalnej i maksymalnej wartości cechy i równy podział otrzymanego przedziału:

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]

Biorąc pod uwagę granice segmentów, możemy przekształcić znaczniki czasu w osadzania:

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

Funkcje przetwarzania tekstu

Możemy również chcieć dodać funkcje tekstowe do naszego modelu. Zazwyczaj opisy produktów są tekstem w dowolnej formie i możemy mieć nadzieję, że nasz model nauczy się wykorzystywać zawarte w nich informacje do tworzenia lepszych rekomendacji, zwłaszcza w scenariuszu zimnego startu lub długiego ogona.

Chociaż zestaw danych MovieLens nie zapewnia nam bogatych funkcji tekstowych, nadal możemy używać tytułów filmów. Może nam to pomóc w uchwyceniu faktu, że filmy o bardzo podobnych tytułach prawdopodobnie będą należeć do tej samej serii.

Pierwszą transformacją, którą musimy zastosować do tekstu, jest tokenizacja (podział na składowe słowa lub fragmenty), po której następuje nauka słownictwa, a następnie osadzanie.

Keras tf.keras.layers.TextVectorization warstwa może wykonać dwa pierwsze kroki dla nas:

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

Wypróbujmy to:

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)

Każdy tytuł jest tłumaczony na sekwencję żetonów, po jednym na każdy element, który ztokenizowaliśmy.

Możemy sprawdzić wyuczone słownictwo, aby zweryfikować, czy warstwa używa poprawnej tokenizacji:

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

Wygląda to poprawnie: warstwa tokenizuje tytuły na pojedyncze słowa.

Aby zakończyć przetwarzanie, musimy teraz osadzić tekst. Ponieważ każdy tytuł zawiera wiele słów, otrzymamy wiele osadzeń dla każdego tytułu. Do użytku w modelu downstream są one zwykle skompresowane do pojedynczego osadzenia. Modele takie jak RNN ​​lub Transformers są tutaj przydatne, ale uśrednienie wszystkich osadzeń słów razem jest dobrym punktem wyjścia.

Kładąc wszystko razem

Mając te komponenty na swoim miejscu, możemy zbudować model, który wykona wszystkie wstępne przetwarzanie razem.

Model użytkownika

Pełny model użytkownika może wyglądać następująco:

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)

Wypróbujmy to:

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 filmowy

To samo możemy zrobić dla modelu filmowego:

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)

Wypróbujmy to:

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]

Następne kroki

W przypadku dwóch powyższych modeli podjęliśmy pierwsze kroki w celu przedstawienia bogatych funkcji w modelu rekomendacji: aby pójść dalej i dowiedzieć się, jak można je wykorzystać do zbudowania skutecznego modelu głębokiego rekomendacji, zapoznaj się z naszym samouczkiem Deep Recommenders.