Servizio efficiente

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica taccuino

Modelli di recupero sono spesso costruiti alla superficie una manciata di candidati superiore di milioni o addirittura centinaia di milioni di candidati. Per essere in grado di reagire al contesto e al comportamento dell'utente, devono essere in grado di farlo al volo, in pochi millisecondi.

La ricerca del vicino più prossimo (ANN) è la tecnologia che lo rende possibile. In questo tutorial, mostreremo come utilizzare ScanN, un pacchetto all'avanguardia per il recupero del vicino più prossimo, per scalare senza problemi il recupero TFRS a milioni di elementi.

Cos'è ScanNn?

ScanN è una libreria di Google Research che esegue una ricerca densa di similarità vettoriale su larga scala. Dato un database di incorporamenti candidati, ScanN indicizza questi incorporamenti in un modo che consente loro di essere rapidamente cercati al momento dell'inferenza. ScanN utilizza tecniche di compressione vettoriale all'avanguardia e algoritmi implementati con cura per ottenere il miglior compromesso tra velocità e precisione. Può superare notevolmente la ricerca della forza bruta sacrificando poco in termini di precisione.

Costruire un modello basato su ScanN

Per provare Scann in TFRs, costruiremo un semplice MovieLens modello di recupero, proprio come abbiamo fatto nel recupero di base tutorial. Se hai seguito quel tutorial, questa sezione ti sarà familiare e può essere tranquillamente saltata.

Per iniziare, installa TFRS e TensorFlow Dataset:

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

Abbiamo anche bisogno di installare scann : si tratta di una dipendenza opzionale di TFRs, e quindi ha bisogno di essere installato separatamente.

pip install -q scann

Imposta tutte le importazioni necessarie.

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

E carica i dati:

# 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

Prima di poter costruire un modello, dobbiamo impostare l'utente e i vocabolari dei film:

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.

Imposteremo anche i set di allenamento e test:

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)

Definizione del modello

Proprio come nel recupero di base tutorial, abbiamo costruito un semplice modello a due torri.

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)

Montaggio e valutazione

Un modello TFRS è solo un modello Keras. Possiamo compilarlo:

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

Stimalo:

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>

E valutalo.

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}

Previsione approssimativa

Il modo più semplice per recuperare i migliori candidati in risposta a una query è farlo tramite la forza bruta: calcolare i punteggi dei film degli utenti per tutti i film possibili, ordinarli e scegliere un paio di consigli migliori.

In TFRs, ciò è ottenuto attraverso il BruteForce strato:

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>

Una volta creata e popolata con i candidati (attraverso index metodo), si può chiamare per ottenere predizioni out:

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

Su un piccolo set di dati di meno di 1000 film, questo è molto veloce:

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

Ma cosa succede se abbiamo più candidati - milioni invece di migliaia?

Possiamo simularlo indicizzando più volte tutti i nostri film:

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

Siamo in grado di costruire una BruteForce indice su questo set di dati più grandi:

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>

I consigli sono sempre gli stessi

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

Ma impiegano molto più tempo. Con un set candidato di 1 milione di film, la previsione della forza bruta diventa piuttosto lenta:

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

Man mano che il numero di candidati cresce, la quantità di tempo necessaria cresce linearmente: con 10 milioni di candidati, servire i migliori candidati richiederebbe 250 millisecondi. Questo è chiaramente troppo lento per un servizio live.

È qui che entrano in gioco i meccanismi approssimativi.

Utilizzando scann in TFRs avviene tramite il tfrs.layers.factorized_top_k.ScaNN strato. Segue la stessa interfaccia degli altri top k layer:

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>

Le raccomandazioni sono (circa!) le stesse

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

Ma sono molto, molto più veloci da calcolare:

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

In questo caso, possiamo recuperare i primi 3 film da un set di ~1 milione in circa 2 millisecondi: 15 volte più velocemente rispetto al calcolo dei migliori candidati tramite la forza bruta. Il vantaggio dei metodi approssimati diventa ancora più grande per set di dati più grandi.

Valutazione dell'approssimazione

Quando si utilizzano meccanismi di recupero approssimativi del top K (come ScanN), la velocità di recupero spesso va a scapito della precisione. Per comprendere questo compromesso, è importante misurare le metriche di valutazione del modello quando si utilizza ScanN e confrontarle con la linea di base.

Fortunatamente, TFRS lo rende facile. Semplicemente sovrascriviamo le metriche sull'attività di recupero con le metriche utilizzando ScanN, ricompiliamo il modello ed eseguiamo la valutazione.

Per fare il confronto, eseguiamo prima i risultati della linea di base. Dobbiamo ancora sovrascrivere le nostre metriche per assicurarci che utilizzino il set candidato allargato anziché il set originale di film:

# 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

Possiamo fare lo stesso usando ScanN:

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

La valutazione basata su ScanN è molto, molto più veloce: è oltre dieci volte più veloce! Questo vantaggio aumenterà ulteriormente per set di dati più grandi, quindi per set di dati di grandi dimensioni può essere prudente eseguire sempre una valutazione basata su ScanN per migliorare la velocità di sviluppo del modello.

Ma per quanto riguarda i risultati? Fortunatamente, in questo caso i risultati sono quasi gli stessi:

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

Ciò suggerisce che su questo datase artificiale, c'è poca perdita dall'approssimazione. In generale, tutti i metodi approssimativi presentano compromessi tra velocità e precisione. Per capire questo in maniera più approfondita è possibile controllare di Erik Bernhardsson benchmark ANN .

Distribuzione del modello approssimativo

Lo ScaNN modello based è completamente integrato in modelli tensorflow, e servire è facile come servire qualsiasi altro modello tensorflow.

Siamo in grado di salvarla come SavedModel oggetto

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

e quindi caricalo e servi, ottenendo esattamente gli stessi risultati:

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

Il modello risultante può essere servito in qualsiasi servizio Python su cui sono installati TensorFlow e ScanN.

Può anche essere servita con una versione personalizzata di tensorflow Servire, disponibili come un contenitore Docker su Docker Hub . È anche possibile costruire il te immagine dal Dockerfile .

Sintonizzazione ScanN

Ora esaminiamo la messa a punto del nostro livello ScanN per ottenere un migliore compromesso prestazioni/precisione. Per farlo in modo efficace, dobbiamo prima misurare le prestazioni e l'accuratezza della nostra linea di base.

Da sopra, abbiamo già una misurazione della latenza del nostro modello per l'elaborazione di una singola query (non in batch) (sebbene si noti che una discreta quantità di questa latenza proviene da componenti non ScanN del modello).

Ora dobbiamo indagare sull'accuratezza di ScanN, che misuriamo attraverso il richiamo. Un richiamo@k di x% significa che se usiamo la forza bruta per recuperare i veri primi k vicini e confrontiamo questi risultati con l'uso di ScanN per recuperare anche i primi k vicini, x% dei risultati di ScanN sono nei veri risultati della forza bruta. Calcoliamo il richiamo per l'attuale ricercatore ScanN.

Per prima cosa, dobbiamo generare la forza bruta, ground truth 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)

La nostra variabile titles_ground_truth ora contiene le raccomandazioni di un film Top-10 restituiti da brute-force recupero. Ora possiamo calcolare gli stessi consigli quando si usa ScanN:

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

Successivamente, definiamo la nostra funzione che calcola il richiamo. Per ogni query, conta quanti risultati ci sono nell'intersezione tra la forza bruta e i risultati di ScanN e divide questo per il numero di risultati della forza bruta. La media di questa quantità su tutte le query è il nostro richiamo.

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

Questo ci dà il richiamo della linea di base@10 con l'attuale configurazione di ScanN:

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

Possiamo anche misurare la latenza di base:

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

Vediamo se possiamo fare di meglio!

Per fare ciò, abbiamo bisogno di un modello di come le manopole di sintonia di ScanN influenzino le prestazioni. Il nostro modello attuale utilizza l'algoritmo tree-AH di ScanN. Questo algoritmo partiziona il database degli incorporamenti (l'"albero") e quindi valuta la più promettente di queste partizioni utilizzando AH, che è una routine di calcolo della distanza approssimativa altamente ottimizzata.

I parametri di default per tensorflow recommenders' Scann KERAS insiemi strato num_leaves=100 e num_leaves_to_search=10 . Ciò significa che il nostro database è partizionato in 100 sottoinsiemi disgiunti e le 10 più promettenti di queste partizioni sono valutate con AH. Ciò significa che il 10/100=10% del set di dati viene cercato con AH.

Se abbiamo, per esempio, num_leaves=1000 e num_leaves_to_search=100 , ci sarebbe anche la ricerca il 10% della base di dati con AH. Tuttavia, rispetto all'impostazione precedente, il 10% avremmo cercare conterrà i candidati di qualità superiore, in quanto un più alto num_leaves ci permette di prendere decisioni più dettagliato su quali parti del set di dati vale la pena di ricerca.

Non è una sorpresa quindi che, con num_leaves=1000 e num_leaves_to_search=100 otteniamo significativamente più alto richiamo:

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

Tuttavia, come compromesso, anche la nostra latenza è aumentata. Questo perché la fase di partizionamento è diventata più costosa; scann raccoglie top 10 su 100 partizioni mentre scann2 raccoglie nella parte superiore 100 del 1000 partizioni. Quest'ultimo può essere più costoso perché comporta la visualizzazione di 10 volte il numero di partizioni.

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

In generale, l'ottimizzazione della ricerca ScanN significa scegliere i giusti compromessi. Ogni singolo cambiamento di parametro generalmente non renderà la ricerca più veloce e più accurata; il nostro obiettivo è mettere a punto i parametri per bilanciare in modo ottimale questi due obiettivi in ​​conflitto.

Nel nostro caso, scann2 significativamente migliorata richiamo sopra scann ad un certo costo in latenza. Possiamo ridurre alcune altre manopole per ridurre la latenza, preservando la maggior parte del nostro vantaggio di richiamo?

Proviamo a cercare 70/1000 = 7% del set di dati con AH e solo a rivalutare i 400 candidati finali:

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 fornisce circa un guadagno richiamo assoluto 3% rispetto scann fornendo inoltre una latenza inferiore:

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

Queste manopole possono essere ulteriormente regolate per ottimizzare per diversi punti lungo la frontiera di Pareto precisione-prestazioni. Gli algoritmi di ScanN possono raggiungere prestazioni all'avanguardia su un'ampia gamma di obiettivi di richiamo.

Ulteriori letture

ScanN utilizza tecniche avanzate di quantizzazione vettoriale e un'implementazione altamente ottimizzata per ottenere i suoi risultati. Il campo della quantizzazione vettoriale ha una ricca storia con una varietà di approcci. La tecnica di quantizzazione corrente di Scann è dettagliata in questo documento , pubblicato all'indirizzo ICML 2020. Il documento è stato anche rilasciato insieme a questo articolo del blog che dà una panoramica di alto livello della nostra tecnica.

Molte tecniche di quantizzazione correlati sono menzionati nei riferimenti del nostro giornale ICML 2020, e altre ricerche Scann relativo è elencato in http://sanjivk.com/