Zobacz na TensorFlow.org | Uruchom w Google Colab | Wyświetl źródło na GitHub | Pobierz notatnik |
W samouczku dotyczącym funkcji wprowadziliśmy do naszych modeli wiele funkcji wykraczających poza identyfikatory użytkowników i filmów, ale nie badaliśmy, czy te funkcje poprawiają dokładność modelu.
Wiele czynników wpływa na to, czy funkcje poza identyfikatorami są przydatne w modelu rekomendacji:
- Znaczenie kontekstu : jeśli preferencje użytkownika są względnie stabilne w różnych kontekstach i w czasie, funkcje kontekstowe mogą nie przynosić większych korzyści. Jeśli jednak preferencje użytkowników są wysoce kontekstowe, dodanie kontekstu znacznie poprawi model. Na przykład dzień tygodnia może być ważną cechą przy podejmowaniu decyzji, czy polecić krótki klip lub film: użytkownicy mogą mieć czas tylko na obejrzenie krótkich treści w ciągu tygodnia, ale mogą się zrelaksować i cieszyć się pełnometrażowym filmem w weekend . Podobnie sygnatury czasowe zapytań mogą odgrywać ważną rolę w modelowaniu dynamiki popularności: jeden film może być bardzo popularny w momencie premiery, ale potem szybko zanika. I odwrotnie, inne filmy mogą być wiecznie zielone, które są chętnie oglądane od czasu do czasu.
- Rzadkość danych : używanie funkcji innych niż identyfikatory może być krytyczne, jeśli dane są rzadkie. Przy niewielkiej liczbie obserwacji dostępnych dla danego użytkownika lub elementu model może mieć trudności z oszacowaniem dobrej reprezentacji na użytkownika lub na element. Aby zbudować dokładny model, należy użyć innych funkcji, takich jak kategorie elementów, opisy i obrazy, aby pomóc w uogólnieniu modelu poza dane szkoleniowe. Jest to szczególnie istotne w sytuacjach zimnego startu , gdy stosunkowo niewiele danych jest dostępnych na temat niektórych elementów lub użytkowników.
W tym samouczku będziemy eksperymentować z wykorzystaniem funkcji wykraczających poza tytuły filmów i identyfikatory użytkowników w naszym modelu MovieLens.
Czynności wstępne
Najpierw importujemy niezbędne pakiety.
pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
import os
import tempfile
import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
import tensorflow_recommenders as tfrs
Postępujemy zgodnie z samouczkiem funkcji i zachowujemy identyfikator użytkownika, sygnaturę czasową i funkcje tytułu filmu.
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"])
Zajmujemy się również sprzątaniem, aby przygotować słownictwo fabularne.
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"]))))
Definicja modelu
Model zapytania
Zaczynamy od modelu użytkownika zdefiniowanego w samouczku dotyczącym funkcji jako pierwszej warstwy naszego modelu, której zadaniem jest przekształcenie nieprzetworzonych przykładów wejściowych na osadzanie funkcji. Zmieniamy to jednak nieznacznie, aby umożliwić nam włączanie lub wyłączanie funkcji sygnatury czasowej. Umożliwi nam to łatwiejsze zademonstrowanie wpływu funkcji sygnatury czasowej na model. W poniższym kodzie parametr use_timestamps
daje nam kontrolę nad tym, czy używamy funkcji datownika.
class UserModel(tf.keras.Model):
def __init__(self, use_timestamps):
super().__init__()
self._use_timestamps = use_timestamps
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),
])
if use_timestamps:
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):
if not self._use_timestamps:
return self.user_embedding(inputs["user_id"])
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)
Zauważ, że nasze użycie funkcji znaczników czasu w tym samouczku oddziałuje z naszym wyborem podziału treningu-test w niepożądany sposób. Ponieważ podzieliliśmy nasze dane losowo, a nie chronologicznie (aby mieć pewność, że zdarzenia należące do testowego zbioru danych będą miały miejsce później niż te w zbiorze uczącym), nasz model może skutecznie uczyć się z przyszłości. To nierealne: w końcu nie możemy dziś trenować modelu na danych z jutra.
Oznacza to, że dodanie funkcji czasu do modelu pozwala mu poznać przyszłe wzorce interakcji. Robimy to wyłącznie w celach ilustracyjnych: sam zestaw danych MovieLens jest bardzo gęsty i w przeciwieństwie do wielu zestawów danych w świecie rzeczywistym nie korzysta zbytnio z funkcji poza identyfikatorami użytkowników i tytułami filmów.
Pomijając to zastrzeżenie, modele w świecie rzeczywistym mogą czerpać korzyści z innych funkcji opartych na czasie, takich jak pora dnia lub dzień tygodnia, zwłaszcza jeśli dane mają silne wzorce sezonowe.
Model kandydata
Dla uproszczenia utrzymamy model kandydujący na stałe. Ponownie kopiujemy go z samouczka funkcji :
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)
Połączony model
Mając zdefiniowane zarówno UserModel
, jak i MovieModel
, możemy stworzyć połączony model i wdrożyć naszą logikę strat i metryk.
Tutaj budujemy model wyszukiwania. Aby dowiedzieć się, jak to działa, zapoznaj się z samouczkiem dotyczącym podstawowego pobierania .
Należy zauważyć, że musimy również upewnić się, że model zapytania i model kandydujący wyprowadzają osadzania o zgodnym rozmiarze. Ponieważ będziemy zmieniać ich rozmiary, dodając więcej funkcji, najłatwiejszym sposobem na osiągnięcie tego jest użycie gęstej warstwy rzutowania po każdym modelu:
class MovielensModel(tfrs.models.Model):
def __init__(self, use_timestamps):
super().__init__()
self.query_model = tf.keras.Sequential([
UserModel(use_timestamps),
tf.keras.layers.Dense(32)
])
self.candidate_model = tf.keras.Sequential([
MovieModel(),
tf.keras.layers.Dense(32)
])
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)
Eksperymenty
Przygotuj dane
Najpierw podzieliliśmy dane na zestaw uczący i zestaw testowy.
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()
Linia bazowa: brak funkcji sygnatury czasowej
Jesteśmy gotowi do wypróbowania naszego pierwszego modelu: zacznijmy od nieużywania funkcji sygnatury czasowej do ustalenia naszej linii bazowej.
model = MovielensModel(use_timestamps=False)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
train_accuracy = model.evaluate(
cached_train, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
test_accuracy = model.evaluate(
cached_test, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
print(f"Top-100 accuracy (train): {train_accuracy:.2f}.")
print(f"Top-100 accuracy (test): {test_accuracy:.2f}.")
Epoch 1/3 WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 40/40 [==============================] - 10s 169ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0092 - factorized_top_k/top_5_categorical_accuracy: 0.0172 - factorized_top_k/top_10_categorical_accuracy: 0.0256 - factorized_top_k/top_50_categorical_accuracy: 0.0824 - factorized_top_k/top_100_categorical_accuracy: 0.1473 - loss: 14579.4628 - regularization_loss: 0.0000e+00 - total_loss: 14579.4628 Epoch 2/3 40/40 [==============================] - 9s 173ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0020 - factorized_top_k/top_5_categorical_accuracy: 0.0126 - factorized_top_k/top_10_categorical_accuracy: 0.0251 - factorized_top_k/top_50_categorical_accuracy: 0.1129 - factorized_top_k/top_100_categorical_accuracy: 0.2133 - loss: 14136.2137 - regularization_loss: 0.0000e+00 - total_loss: 14136.2137 Epoch 3/3 40/40 [==============================] - 9s 174ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0021 - factorized_top_k/top_5_categorical_accuracy: 0.0155 - factorized_top_k/top_10_categorical_accuracy: 0.0307 - factorized_top_k/top_50_categorical_accuracy: 0.1389 - factorized_top_k/top_100_categorical_accuracy: 0.2535 - loss: 13939.9265 - regularization_loss: 0.0000e+00 - total_loss: 13939.9265 WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 40/40 [==============================] - 10s 189ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0036 - factorized_top_k/top_5_categorical_accuracy: 0.0226 - factorized_top_k/top_10_categorical_accuracy: 0.0427 - factorized_top_k/top_50_categorical_accuracy: 0.1729 - factorized_top_k/top_100_categorical_accuracy: 0.2944 - loss: 13711.3802 - regularization_loss: 0.0000e+00 - total_loss: 13711.3802 5/5 [==============================] - 3s 267ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0010 - factorized_top_k/top_5_categorical_accuracy: 0.0078 - factorized_top_k/top_10_categorical_accuracy: 0.0184 - factorized_top_k/top_50_categorical_accuracy: 0.1051 - factorized_top_k/top_100_categorical_accuracy: 0.2126 - loss: 30995.8988 - regularization_loss: 0.0000e+00 - total_loss: 30995.8988 Top-100 accuracy (train): 0.29. Top-100 accuracy (test): 0.21.
Daje nam to bazową dokładność top-100 wynoszącą około 0,2.
Rejestrowanie dynamiki czasu za pomocą funkcji czasu
Czy wynik zmieni się, jeśli dodamy funkcje czasu?
model = MovielensModel(use_timestamps=True)
model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
model.fit(cached_train, epochs=3)
train_accuracy = model.evaluate(
cached_train, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
test_accuracy = model.evaluate(
cached_test, return_dict=True)["factorized_top_k/top_100_categorical_accuracy"]
print(f"Top-100 accuracy (train): {train_accuracy:.2f}.")
print(f"Top-100 accuracy (test): {test_accuracy:.2f}.")
Epoch 1/3 WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 40/40 [==============================] - 10s 175ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0057 - factorized_top_k/top_5_categorical_accuracy: 0.0148 - factorized_top_k/top_10_categorical_accuracy: 0.0238 - factorized_top_k/top_50_categorical_accuracy: 0.0812 - factorized_top_k/top_100_categorical_accuracy: 0.1487 - loss: 14606.0927 - regularization_loss: 0.0000e+00 - total_loss: 14606.0927 Epoch 2/3 40/40 [==============================] - 9s 176ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0026 - factorized_top_k/top_5_categorical_accuracy: 0.0153 - factorized_top_k/top_10_categorical_accuracy: 0.0304 - factorized_top_k/top_50_categorical_accuracy: 0.1375 - factorized_top_k/top_100_categorical_accuracy: 0.2512 - loss: 13958.5635 - regularization_loss: 0.0000e+00 - total_loss: 13958.5635 Epoch 3/3 40/40 [==============================] - 9s 177ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0026 - factorized_top_k/top_5_categorical_accuracy: 0.0189 - factorized_top_k/top_10_categorical_accuracy: 0.0393 - factorized_top_k/top_50_categorical_accuracy: 0.1713 - factorized_top_k/top_100_categorical_accuracy: 0.3015 - loss: 13696.8511 - regularization_loss: 0.0000e+00 - total_loss: 13696.8511 WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'user_id': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'timestamp': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 40/40 [==============================] - 9s 172ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0050 - factorized_top_k/top_5_categorical_accuracy: 0.0323 - factorized_top_k/top_10_categorical_accuracy: 0.0606 - factorized_top_k/top_50_categorical_accuracy: 0.2254 - factorized_top_k/top_100_categorical_accuracy: 0.3637 - loss: 13382.7869 - regularization_loss: 0.0000e+00 - total_loss: 13382.7869 5/5 [==============================] - 1s 237ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0012 - factorized_top_k/top_5_categorical_accuracy: 0.0097 - factorized_top_k/top_10_categorical_accuracy: 0.0214 - factorized_top_k/top_50_categorical_accuracy: 0.1259 - factorized_top_k/top_100_categorical_accuracy: 0.2468 - loss: 30699.8529 - regularization_loss: 0.0000e+00 - total_loss: 30699.8529 Top-100 accuracy (train): 0.36. Top-100 accuracy (test): 0.25.
To jest trochę lepsze: nie tylko dokładność treningu jest znacznie wyższa, ale również znacznie poprawia się dokładność testu.
Następne kroki
Ten samouczek pokazuje, że nawet proste modele mogą stać się dokładniejsze po włączeniu większej liczby funkcji. Jednak, aby w pełni wykorzystać swoje funkcje, często konieczne jest budowanie większych, głębszych modeli. Zapoznaj się z samouczkiem dotyczącym głębokiego pobierania , aby poznać to bardziej szczegółowo.