Wydajna porcja

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

Modele odlewy są często budowane na powierzchnię garstkę najlepszych kandydatów na miliony lub nawet setki milionów kandydatów. Aby móc reagować na kontekst i zachowanie użytkownika, muszą być w stanie zrobić to w locie, w ciągu milisekund.

Przybliżone wyszukiwanie najbliższego sąsiada (ANN) to technologia, która to umożliwia. W tym samouczku pokażemy, jak używać ScanNN — najnowocześniejszego pakietu pobierania najbliższego sąsiada — do bezproblemowego skalowania pobierania TFRS do milionów elementów.

Co to jest ScanNN?

ScanNN to biblioteka Google Research, która przeprowadza wyszukiwanie podobieństw gęstych wektorów na dużą skalę. Mając bazę danych osadzeń kandydujących, ScanNN indeksuje te osadzenia w sposób, który umożliwia ich szybkie przeszukiwanie w czasie wnioskowania. ScanNN wykorzystuje najnowocześniejsze techniki kompresji wektorowej i starannie zaimplementowane algorytmy, aby osiągnąć najlepszy kompromis między szybkością a dokładnością. Może znacznie przewyższyć wyszukiwanie siłowe, poświęcając niewiele pod względem dokładności.

Budowanie modelu opartego na oprogramowaniu ScanNN

Aby wypróbować ScaNN w TFRS będziemy budować proste MovieLens pobierania modelu, tak jak zrobiliśmy to w podstawowej pobierania tutoriala. Jeśli postępowałeś zgodnie z tym samouczkiem, ta sekcja będzie znajoma i można ją bezpiecznie pominąć.

Aby rozpocząć, zainstaluj zestawy danych TFRS i TensorFlow:

pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets

Musimy również zainstalować scann : to opcjonalna zależność TFRS, a więc muszą być instalowane oddzielnie.

pip install -q scann

Skonfiguruj wszystkie niezbędne importy.

from typing import Dict, Text

import os
import pprint
import tempfile

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

I załaduj dane:

# Load the MovieLens 100K data.
ratings = tfds.load(
    "movielens/100k-ratings",
    split="train"
)

# Get the ratings data.
ratings = (ratings
           # Retain only the fields we need.
           .map(lambda x: {"user_id": x["user_id"], "movie_title": x["movie_title"]})
           # Cache for efficiency.
           .cache(tempfile.NamedTemporaryFile().name)
)

# Get the movies data.
movies = tfds.load("movielens/100k-movies", split="train")
movies = (movies
          # Retain only the fields we need.
          .map(lambda x: x["movie_title"])
          # Cache for efficiency.
          .cache(tempfile.NamedTemporaryFile().name))
2021-10-02 11:53:59.413405: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

Zanim będziemy mogli zbudować model, musimy skonfigurować słowniki użytkownika i filmowe:

user_ids = ratings.map(lambda x: x["user_id"])

unique_movie_titles = np.unique(np.concatenate(list(movies.batch(1000))))
unique_user_ids = np.unique(np.concatenate(list(user_ids.batch(1000))))
2021-10-02 11:54:00.296290: W tensorflow/core/kernels/data/cache_dataset_ops.cc:233] 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.
2021-10-02 11:54:04.003150: W tensorflow/core/kernels/data/cache_dataset_ops.cc:233] 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.

Skonfigurujemy również zestawy treningowe i testowe:

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)

Definicja modelu

Podobnie jak w podstawowej pobierania samouczku zbudować prosty model dwuwieżową.

class MovielensModel(tfrs.Model):

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

    embedding_dimension = 32

    # Set up a model for representing movies.
    self.movie_model = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
        vocabulary=unique_movie_titles, mask_token=None),
      # We add an additional embedding to account for unknown tokens.
      tf.keras.layers.Embedding(len(unique_movie_titles) + 1, embedding_dimension)
    ])

    # Set up a model for representing users.
    self.user_model = tf.keras.Sequential([
      tf.keras.layers.StringLookup(
        vocabulary=unique_user_ids, mask_token=None),
        # We add an additional embedding to account for unknown tokens.
      tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension)
    ])

    # Set up a task to optimize the model and compute metrics.
    self.task = tfrs.tasks.Retrieval(
      metrics=tfrs.metrics.FactorizedTopK(
        candidates=movies.batch(128).cache().map(self.movie_model)
      )
    )

  def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
    # We pick out the user features and pass them into the user model.
    user_embeddings = self.user_model(features["user_id"])
    # And pick out the movie features and pass them into the movie model,
    # getting embeddings back.
    positive_movie_embeddings = self.movie_model(features["movie_title"])

    # The task computes the loss and the metrics.

    return self.task(user_embeddings, positive_movie_embeddings, compute_metrics=not training)

Dopasowanie i ocena

Model TFRS to tylko model Keras. Możemy to skompilować:

model = MovielensModel()
model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))

Oszacuj to:

model.fit(train.batch(8192), epochs=3)
Epoch 1/3
10/10 [==============================] - 3s 223ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_5_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_10_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_50_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_100_categorical_accuracy: 0.0000e+00 - loss: 69808.9716 - regularization_loss: 0.0000e+00 - total_loss: 69808.9716
Epoch 2/3
10/10 [==============================] - 3s 222ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_5_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_10_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_50_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_100_categorical_accuracy: 0.0000e+00 - loss: 67485.8842 - regularization_loss: 0.0000e+00 - total_loss: 67485.8842
Epoch 3/3
10/10 [==============================] - 3s 220ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_5_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_10_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_50_categorical_accuracy: 0.0000e+00 - factorized_top_k/top_100_categorical_accuracy: 0.0000e+00 - loss: 66311.9581 - regularization_loss: 0.0000e+00 - total_loss: 66311.9581
<keras.callbacks.History at 0x7fc02423c150>

I oceń to.

model.evaluate(test.batch(8192), return_dict=True)
3/3 [==============================] - 2s 246ms/step - factorized_top_k/top_1_categorical_accuracy: 0.0011 - factorized_top_k/top_5_categorical_accuracy: 0.0095 - factorized_top_k/top_10_categorical_accuracy: 0.0222 - factorized_top_k/top_50_categorical_accuracy: 0.1261 - factorized_top_k/top_100_categorical_accuracy: 0.2363 - loss: 49466.8789 - regularization_loss: 0.0000e+00 - total_loss: 49466.8789
{'factorized_top_k/top_1_categorical_accuracy': 0.0010999999940395355,
 'factorized_top_k/top_5_categorical_accuracy': 0.009549999609589577,
 'factorized_top_k/top_10_categorical_accuracy': 0.022199999541044235,
 'factorized_top_k/top_50_categorical_accuracy': 0.1261499971151352,
 'factorized_top_k/top_100_categorical_accuracy': 0.23634999990463257,
 'loss': 28242.8359375,
 'regularization_loss': 0,
 'total_loss': 28242.8359375}

Przybliżona prognoza

Najprostszym sposobem na znalezienie najlepszych kandydatów w odpowiedzi na zapytanie jest użycie brutalnej siły: obliczenie wyników filmów użytkownika dla wszystkich możliwych filmów, posortowanie ich i wybranie kilku najlepszych rekomendacji.

W TFRS osiąga się to poprzez BruteForce warstwie:

brute_force = tfrs.layers.factorized_top_k.BruteForce(model.user_model)
brute_force.index_from_dataset(
    movies.batch(128).map(lambda title: (title, model.movie_model(title)))
)
<tensorflow_recommenders.layers.factorized_top_k.BruteForce at 0x7fbfc1d4fe10>

Po utworzeniu i wypełniane kandydatów (za pośrednictwem index metody), możemy nazwać to, aby dostać się przepowiedni:

# Get predictions for user 42.
_, titles = brute_force(np.array(["42"]), k=3)

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Homeward Bound: The Incredible Journey (1993)'
 b"Kid in King Arthur's Court, A (1995)" b'Rudy (1993)']

Na małym zbiorze danych liczącym mniej niż 1000 filmów jest to bardzo szybkie:

%timeit _, titles = brute_force(np.array(["42"]), k=3)
983 µs ± 5.44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Ale co się stanie, jeśli będziemy mieli więcej kandydatów – miliony zamiast tysięcy?

Możemy to zasymulować, wielokrotnie indeksując wszystkie nasze filmy:

# Construct a dataset of movies that's 1,000 times larger. We 
# do this by adding several million dummy movie titles to the dataset.
lots_of_movies = tf.data.Dataset.concatenate(
    movies.batch(4096),
    movies.batch(4096).repeat(1_000).map(lambda x: tf.zeros_like(x))
)

# We also add lots of dummy embeddings by randomly perturbing
# the estimated embeddings for real movies.
lots_of_movies_embeddings = tf.data.Dataset.concatenate(
    movies.batch(4096).map(model.movie_model),
    movies.batch(4096).repeat(1_000)
      .map(lambda x: model.movie_model(x))
      .map(lambda x: x * tf.random.uniform(tf.shape(x)))
)

Możemy zbudować BruteForce indeks na tym większym zbiorze:

brute_force_lots = tfrs.layers.factorized_top_k.BruteForce()
brute_force_lots.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)
<tensorflow_recommenders.layers.factorized_top_k.BruteForce at 0x7fbfc1d80610>

Zalecenia są nadal takie same

_, titles = brute_force_lots(model.user_model(np.array(["42"])), k=3)

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Homeward Bound: The Incredible Journey (1993)'
 b"Kid in King Arthur's Court, A (1995)" b'Rudy (1993)']

Ale trwają znacznie dłużej. Z zestawem kandydatów składającym się z 1 miliona filmów, przewidywanie brutalnej siły staje się dość powolne:

%timeit _, titles = brute_force_lots(model.user_model(np.array(["42"])), k=3)
33 ms ± 245 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Wraz ze wzrostem liczby kandydatów ilość potrzebnego czasu rośnie liniowo: przy 10 milionach kandydatów obsługa najlepszych kandydatów zajęłaby 250 milisekund. Jest to wyraźnie zbyt wolne dla usługi na żywo.

Tutaj wkraczają przybliżone mechanizmy.

Korzystanie ScaNN w TFRS odbywa się poprzez tfrs.layers.factorized_top_k.ScaNN warstwy. Podąża za tym samym interfejsem, co inne górne warstwy k:

scann = tfrs.layers.factorized_top_k.ScaNN(num_reordering_candidates=100)
scann.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)
<tensorflow_recommenders.layers.factorized_top_k.ScaNN at 0x7fbfc2571990>

Zalecenia są (w przybliżeniu!) takie same

_, titles = scann(model.user_model(np.array(["42"])), k=3)

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Homeward Bound: The Incredible Journey (1993)'
 b"Kid in King Arthur's Court, A (1995)" b'Rudy (1993)']

Ale obliczanie ich jest znacznie szybsze:

%timeit _, titles = scann(model.user_model(np.array(["42"])), k=3)
4.35 ms ± 34.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

W tym przypadku możemy pobrać 3 najlepsze filmy z zestawu ~1 miliona w około 2 milisekundy: 15 razy szybciej niż obliczając najlepszych kandydatów metodą brute force. Przewaga metod przybliżonych rośnie jeszcze bardziej w przypadku większych zestawów danych.

Ocena przybliżenia

Podczas korzystania z przybliżonych mechanizmów wyszukiwania szczytu K (takich jak ScaNN), szybkość wyszukiwania często odbywa się kosztem dokładności. Aby zrozumieć ten kompromis, ważne jest, aby zmierzyć metryki oceny modelu podczas korzystania z programu ScanNN i porównać je z linią bazową.

Na szczęście TFRS to ułatwia. Po prostu zastępujemy metryki zadania pobierania metrykami przy użyciu ScanNN, ponownie kompilujemy model i uruchamiamy ocenę.

Aby dokonać porównania, najpierw przeanalizujmy wyniki z planu bazowego. Nadal musimy nadpisać nasze metryki, aby upewnić się, że używają powiększonego zestawu kandydatów, a nie oryginalnego zestawu filmów:

# Override the existing streaming candidate source.
model.task.factorized_metrics = tfrs.metrics.FactorizedTopK(
    candidates=lots_of_movies_embeddings
)
# Need to recompile the model for the changes to take effect.
model.compile()

%time baseline_result = model.evaluate(test.batch(8192), return_dict=True, verbose=False)
CPU times: user 22min 5s, sys: 2min 7s, total: 24min 12s
Wall time: 51.9 s

Możemy zrobić to samo za pomocą ScanNN:

model.task.factorized_metrics = tfrs.metrics.FactorizedTopK(
    candidates=scann
)
model.compile()

# We can use a much bigger batch size here because ScaNN evaluation
# is more memory efficient.
%time scann_result = model.evaluate(test.batch(8192), return_dict=True, verbose=False)
CPU times: user 10.5 s, sys: 3.26 s, total: 13.7 s
Wall time: 1.85 s

Ocena oparta na ScanNN jest znacznie, znacznie szybsza: jest ponad dziesięć razy szybsza! Ta zaleta będzie jeszcze większa w przypadku większych zestawów danych, dlatego w przypadku dużych zestawów danych rozsądne może być zawsze uruchamianie oceny opartej na oprogramowaniu ScanNN, aby poprawić szybkość opracowywania modelu.

Ale co z wynikami? Na szczęście w tym przypadku wyniki są prawie takie same:

print(f"Brute force top-100 accuracy: {baseline_result['factorized_top_k/top_100_categorical_accuracy']:.2f}")
print(f"ScaNN top-100 accuracy:       {scann_result['factorized_top_k/top_100_categorical_accuracy']:.2f}")
Brute force top-100 accuracy: 0.15
ScaNN top-100 accuracy:       0.27

Sugeruje to, że w tej sztucznej bazie danych istnieje niewielka strata z przybliżenia. Ogólnie rzecz biorąc, wszystkie metody przybliżone wykazują kompromisy między szybkością a dokładnością. Aby zrozumieć głębiej można sprawdzić Erik Bernhardsson za ANN odniesienia .

Wdrażanie przybliżonego modelu

ScaNN -na model jest w pełni zintegrowany z modeli TensorFlow i obsługujących to jest tak proste jak jakiemukolwiek inny model TensorFlow.

Możemy zapisać jako SavedModel obiektu

lots_of_movies_embeddings
<ConcatenateDataset shapes: (None, 32), types: tf.float32>
# We re-index the ScaNN layer to include the user embeddings in the same model.
# This way we can give the saved model raw features and get valid predictions
# back.
scann = tfrs.layers.factorized_top_k.ScaNN(model.user_model, num_reordering_candidates=1000)
scann.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)

# Need to call it to set the shapes.
_ = scann(np.array(["42"]))

with tempfile.TemporaryDirectory() as tmp:
  path = os.path.join(tmp, "model")
  tf.saved_model.save(
      scann,
      path,
      options=tf.saved_model.SaveOptions(namespace_whitelist=["Scann"])
  )

  loaded = tf.saved_model.load(path)
2021-10-02 11:55:53.875291: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
WARNING:absl:Found untraced functions such as query_with_exclusions while saving (showing 1 of 1). These functions will not be directly callable after loading.
INFO:tensorflow:Assets written to: /tmp/tmpm0piq8hx/model/assets
INFO:tensorflow:Assets written to: /tmp/tmpm0piq8hx/model/assets

a następnie załaduj i podawaj, uzyskując dokładnie te same wyniki:

_, titles = loaded(tf.constant(["42"]))

print(f"Top recommendations: {titles[0][:3]}")
Top recommendations: [b'Homeward Bound: The Incredible Journey (1993)'
 b"Kid in King Arthur's Court, A (1995)" b'Rudy (1993)']

Otrzymany model może być obsługiwany w dowolnej usłudze Pythona, która ma zainstalowane TensorFlow i ScanNN.

Może być również podawane za pomocą dostosowaną wersję TensorFlow porcji, dostępne jako pojemnik na Docker Docker Hub . Można także zbudować sobie obraz z Dockerfile .

Skanowanie strojeniaNN

Teraz przyjrzyjmy się dostrojeniu naszej warstwy ScanNN, aby uzyskać lepszy kompromis między wydajnością a dokładnością. Aby zrobić to skutecznie, najpierw musimy zmierzyć naszą wyjściową wydajność i dokładność.

Z góry mamy już pomiar opóźnienia naszego modelu w przetwarzaniu pojedynczego (niezbiorowego) zapytania (choć należy zauważyć, że spora część tego opóźnienia pochodzi z komponentów modelu innych niż ScanNN).

Teraz musimy zbadać dokładność ScanNN, którą mierzymy poprzez przywołanie. Odwołanie@k równe x% oznacza, że ​​jeśli użyjemy metody brute force do pobrania prawdziwych k sąsiadów z prawdziwego zdarzenia i porównamy te wyniki z użyciem ScanNN do pobrania również pierwszych k sąsiadów, x% wyników ScanNN będzie w wynikach True brute force. Obliczmy przywołanie dla bieżącej wyszukiwarki ScanNN.

Najpierw musimy wygenerować brutalną siłę, ugruntowaną prawdę top-k:

# Process queries in groups of 1000; processing them all at once with brute force
# may lead to out-of-memory errors, because processing a batch of q queries against
# a size-n dataset takes O(nq) space with brute force.
titles_ground_truth = tf.concat([
  brute_force_lots(queries, k=10)[1] for queries in
  test.batch(1000).map(lambda x: model.user_model(x["user_id"]))
], axis=0)

Nasza zmienna titles_ground_truth zawiera teraz top-10 zaleceń filmowe zwracane przez pobieranie brute-force. Teraz możemy obliczyć te same zalecenia podczas korzystania ze ScanNN:

# Get all user_id's as a 1d tensor of strings
test_flat = np.concatenate(list(test.map(lambda x: x["user_id"]).batch(1000).as_numpy_iterator()), axis=0)

# ScaNN is much more memory efficient and has no problem processing the whole
# batch of 20000 queries at once.
_, titles = scann(test_flat, k=10)

Następnie definiujemy naszą funkcję, która oblicza przywołanie. Dla każdego zapytania zlicza, ile wyników znajduje się na przecięciu wyników brute force i ScanNN, i dzieli to przez liczbę wyników brute force. Średnia z tej ilości dla wszystkich zapytań to nasza wycofanie.

def compute_recall(ground_truth, approx_results):
  return np.mean([
      len(np.intersect1d(truth, approx)) / len(truth)
      for truth, approx in zip(ground_truth, approx_results)
  ])

Daje nam to bazowe przywołanie@10 z obecną konfiguracją ScanNN:

print(f"Recall: {compute_recall(titles_ground_truth, titles):.3f}")
Recall: 0.931

Możemy również zmierzyć bazowe opóźnienie:

%timeit -n 1000 scann(np.array(["42"]), k=10)
4.67 ms ± 25 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Zobaczmy, czy możemy zrobić lepiej!

Aby to zrobić, potrzebujemy modelu, w jaki sposób pokrętła strojenia ScanNN wpływają na wydajność. Nasz obecny model wykorzystuje algorytm drzewa-AH firmy ScanNN. Algorytm ten dzieli bazę danych osadzeń („drzewo”), a następnie ocenia najbardziej obiecujące z tych partycji przy użyciu AH, która jest wysoce zoptymalizowaną procedurą obliczania przybliżonej odległości.

Parametry domyślne TensorFlow Recommenders' ScaNN Keras zestawów warstw num_leaves=100 i num_leaves_to_search=10 . Oznacza to, że nasza baza danych jest podzielona na 100 rozłącznych podzbiorów, a 10 najbardziej obiecujących z tych partycji ma ocenę AH. Oznacza to, że 10/100=10% zbioru danych jest przeszukiwane za pomocą AH.

Jeśli mamy, powiedzmy, num_leaves=1000 i num_leaves_to_search=100 , to będzie również poszukiwanie 10% danych z AH. Jednak w porównaniu do poprzedniego ustawienia, 10% będziemy szukać będzie zawierać wyższej jakości kandydatów, gdyż wyższy num_leaves pozwala nam podejmować decyzje drobniejsze granulowane o jakie części zbioru danych poszukujesz warto.

Nic dziwnego więc, że z num_leaves=1000 i num_leaves_to_search=100 możemy uzyskać znacznie wyższą przywołanie:

scann2 = tfrs.layers.factorized_top_k.ScaNN(
    model.user_model, 
    num_leaves=1000,
    num_leaves_to_search=100,
    num_reordering_candidates=1000)
scann2.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)

_, titles2 = scann2(test_flat, k=10)

print(f"Recall: {compute_recall(titles_ground_truth, titles2):.3f}")
Recall: 0.966

Jednak jako kompromis, nasze opóźnienie również się zwiększyło. Dzieje się tak, ponieważ etap partycjonowania stał się droższy; scann podnosi górną 10 z 100 partycji podczas scann2 podnosi górną 100 1000 partycji. Ta ostatnia może być droższa, ponieważ wymaga oglądania 10 razy większej liczby partycji.

%timeit -n 1000 scann2(np.array(["42"]), k=10)
4.86 ms ± 21.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Ogólnie rzecz biorąc, dostrajanie wyszukiwania ScanNN polega na wyborze właściwych kompromisów. Każda pojedyncza zmiana parametru generalnie nie sprawi, że wyszukiwanie będzie szybsze i dokładniejsze; naszym celem jest dostrojenie parametrów w celu optymalnego kompromisu między tymi dwoma sprzecznymi celami.

W naszym przypadku, scann2 znacząco poprawiła przywołanie nad scann w pewnym kosztem w latencji. Czy możemy cofnąć kilka innych pokręteł, aby zmniejszyć opóźnienia, jednocześnie zachowując większość naszej przewagi przywołania?

Spróbujmy przeszukać 70/1000=7% zbioru danych za pomocą AH i odtworzyć tylko ostatnich 400 kandydatów:

scann3 = tfrs.layers.factorized_top_k.ScaNN(
    model.user_model,
    num_leaves=1000,
    num_leaves_to_search=70,
    num_reordering_candidates=400)
scann3.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)

_, titles3 = scann3(test_flat, k=10)
print(f"Recall: {compute_recall(titles_ground_truth, titles3):.3f}")
Recall: 0.957

scann3 dostarcza około 3% absolutnej wycofania pozyskać scann jednocześnie zapewniają niskie latencję

%timeit -n 1000 scann3(np.array(["42"]), k=10)
4.58 ms ± 37.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Pokrętła te można dalej regulować w celu optymalizacji dla różnych punktów na granicy dokładności i wydajności pareto. Algorytmy ScanNN mogą osiągnąć najnowocześniejszą wydajność w szerokim zakresie celów wycofania.

Dalsze czytanie

Aby osiągnąć swoje wyniki, ScanNN wykorzystuje zaawansowane techniki kwantyzacji wektorowej i wysoce zoptymalizowaną implementację. Dziedzina kwantyzacji wektorowej ma bogatą historię z różnymi podejściami. Obecna technika kwantyzacji ScaNN jest opisane w tym artykule , opublikowanym w ICML 2020. Dokument został wydany wraz z tym blogu artykuł , który daje przegląd wysoki poziom naszej techniki.

Wiele związanych techniki kwantowania są wymienione w odnośnikach naszego papieru ICML 2020 oraz inne badania związane ScaNN-jest notowana na http://sanjivk.com/