Ver en TensorFlow.org | Ejecutar en Google Colab | Ver fuente en GitHub | Descargar cuaderno |
Modelos de recuperación se construyen a menudo a la superficie un puñado de los mejores candidatos de millones o incluso cientos de millones de candidatos. Para poder reaccionar al contexto y el comportamiento del usuario, debe poder hacerlo sobre la marcha, en cuestión de milisegundos.
La búsqueda aproximada del vecino más cercano (ANN) es la tecnología que lo hace posible. En este tutorial, mostraremos cómo usar ScaNN, un paquete de recuperación de vecino más cercano de última generación, para escalar sin problemas la recuperación de TFRS a millones de elementos.
¿Qué es ScaNN?
ScaNN es una biblioteca de Google Research que realiza búsquedas de similitudes vectoriales densas a gran escala. Dada una base de datos de incrustaciones candidatas, ScaNN indexa estas incrustaciones de una manera que permite buscarlas rápidamente en el momento de la inferencia. ScaNN utiliza técnicas de compresión de vectores de vanguardia y algoritmos cuidadosamente implementados para lograr el mejor equilibrio entre velocidad y precisión. Puede superar con creces la búsqueda de fuerza bruta y sacrificar poco en términos de precisión.
Construyendo un modelo basado en ScaNN
Para probar ScanN en TfRs, vamos a construir un simple MovieLens modelo de recuperación, tal como lo hicimos en la recuperación básica tutorial. Si ha seguido ese tutorial, esta sección le resultará familiar y puede omitirla con seguridad.
Para comenzar, instale los conjuntos de datos TFRS y TensorFlow:
pip install -q tensorflow-recommenders
pip install -q --upgrade tensorflow-datasets
También tenemos que instalar scann
: es una dependencia opcional del TGF, y así tiene que ser instalado por separado.
pip install -q scann
Configure todas las importaciones necesarias.
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
Y cargue los datos:
# 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
Antes de que podamos construir un modelo, necesitamos configurar los vocabularios de usuario y película:
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.
También configuraremos los conjuntos de entrenamiento y prueba:
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)
Definición de modelo
Al igual que en la recuperación básica tutorial, construimos un modelo simple de dos torres.
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)
Adecuación y evaluación
Un modelo TFRS es solo un modelo de Keras. Podemos compilarlo:
model = MovielensModel()
model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))
Estimarlo:
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>
Y evalúelo.
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}
Predicción aproximada
La forma más sencilla de recuperar los mejores candidatos en respuesta a una consulta es hacerlo mediante la fuerza bruta: calcule las puntuaciones de las películas de usuario para todas las películas posibles, ordénelas y elija un par de recomendaciones principales.
En TfRs, esto se logra a través de la BruteForce
capa:
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 vez creada y poblada de candidatos (a través del index
método), podemos decir que es para obtener predicciones a cabo:
# 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)']
En un pequeño conjunto de datos de menos de 1000 películas, esto es muy rápido:
%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)
Pero, ¿qué sucede si tenemos más candidatos, millones en lugar de miles?
Podemos simular esto indexando todas nuestras películas varias veces:
# 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)))
)
Podemos construir un BruteForce
índice en este conjunto de datos más grande:
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>
Las recomendaciones siguen siendo las mismas
_, 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)']
Pero tardan mucho más. Con un conjunto de candidatos de 1 millón de películas, la predicción de fuerza bruta se vuelve bastante 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)
A medida que crece el número de candidatos, la cantidad de tiempo necesario crece de forma lineal: con 10 millones de candidatos, atender a los mejores candidatos tomaría 250 milisegundos. Esto es claramente demasiado lento para un servicio en vivo.
Aquí es donde entran en juego los mecanismos aproximados.
Usando SCANN en TfRs se logra a través de la tfrs.layers.factorized_top_k.ScaNN
capa. Sigue la misma interfaz que las otras k capas superiores:
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>
Las recomendaciones son (¡aproximadamente!) Las mismas
_, 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)']
Pero son mucho, mucho más rápidos de calcular:
%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)
En este caso, podemos recuperar las 3 mejores películas de un conjunto de ~ 1 millón en aproximadamente 2 milisegundos: 15 veces más rápido que calculando los mejores candidatos a través de la fuerza bruta. La ventaja de los métodos aproximados aumenta aún más para conjuntos de datos más grandes.
Evaluar la aproximación
Cuando se utilizan mecanismos de recuperación de K superior aproximados (como ScaNN), la velocidad de recuperación a menudo se obtiene a expensas de la precisión. Para comprender esta compensación, es importante medir las métricas de evaluación del modelo cuando se usa ScaNN y compararlas con la línea de base.
Afortunadamente, TFRS lo hace fácil. Simplemente anulamos las métricas en la tarea de recuperación con métricas usando ScaNN, recompilamos el modelo y ejecutamos la evaluación.
Para hacer la comparación, primero ejecutemos los resultados de la línea de base. Aún necesitamos anular nuestras métricas para asegurarnos de que estén usando el conjunto de candidatos ampliado en lugar del conjunto original de películas:
# 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
Podemos hacer lo mismo 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 evaluación basada en ScaNN es mucho, mucho más rápida: ¡es diez veces más rápida! Esta ventaja aumentará aún más para conjuntos de datos más grandes, por lo que, para conjuntos de datos grandes, puede ser prudente ejecutar siempre una evaluación basada en ScaNN para mejorar la velocidad de desarrollo del modelo.
Pero, ¿qué hay de los resultados? Afortunadamente, en este caso los resultados son casi los mismos:
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
Esto sugiere que en esta datase artificial, hay poca pérdida de aproximación. En general, todos los métodos aproximados exhiben compensaciones entre velocidad y precisión. Para entender esto con mayor profundidad se puede extraer de Erik Bernhardsson puntos de referencia ANN .
Implementar el modelo aproximado
El ScaNN
modelo basado está totalmente integrado en los modelos TensorFlow, y sirviendo es tan fácil como cualquier otro modelo que sirve TensorFlow.
Podemos guardarlo como un SavedModel
objeto
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
y luego cárguelo y sirva, obteniendo exactamente los mismos resultados:
_, 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)']
El modelo resultante se puede entregar en cualquier servicio de Python que tenga TensorFlow y ScaNN instalados.
También se puede servir utilizando una versión personalizada de TensorFlow Servir, disponible como un contenedor estibador en acoplable Hub . También es posible construir la imagen desde el mismo Dockerfile .
Tuning ScaNN
Ahora veamos cómo ajustar nuestra capa ScaNN para obtener una mejor compensación de rendimiento / precisión. Para hacer esto de manera efectiva, primero debemos medir nuestro rendimiento y precisión de referencia.
Desde arriba, ya tenemos una medida de la latencia de nuestro modelo para procesar una consulta única (no por lotes) (aunque tenga en cuenta que una buena parte de esta latencia proviene de componentes del modelo que no son ScaNN).
Ahora tenemos que investigar la precisión de ScaNN, que medimos mediante la recuperación. Una recuperación @ k de x% significa que si usamos la fuerza bruta para recuperar los verdaderos k vecinos principales, y comparamos esos resultados con el uso de ScaNN para recuperar también los k principales vecinos, el x% de los resultados de ScaNN están en los verdaderos resultados de la fuerza bruta. Calculemos la recuperación del buscador actual de ScaNN.
Primero, necesitamos generar la fuerza bruta, verdad fundamental 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)
Nuestra variable titles_ground_truth
ahora contiene las recomendaciones de la película entre los 10 devueltos por la recuperación de la fuerza bruta. Ahora podemos calcular las mismas recomendaciones cuando usamos 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)
A continuación, definimos nuestra función que calcula la recuperación. Para cada consulta, cuenta cuántos resultados hay en la intersección de la fuerza bruta y los resultados de ScaNN y los divide por el número de resultados de fuerza bruta. El promedio de esta cantidad sobre todas las consultas es nuestro recordatorio.
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)
])
Esto nos da una recuperación de referencia @ 10 con la configuración actual de ScaNN:
print(f"Recall: {compute_recall(titles_ground_truth, titles):.3f}")
Recall: 0.931
También podemos medir la latencia de la línea de 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)
¡Veamos si podemos hacerlo mejor!
Para hacer esto, necesitamos un modelo de cómo las perillas de afinación de ScaNN afectan el rendimiento. Nuestro modelo actual utiliza el algoritmo tree-AH de ScaNN. Este algoritmo divide la base de datos de incrustaciones (el "árbol") y luego califica la más prometedora de estas particiones usando AH, que es una rutina de cálculo de distancia aproximada altamente optimizada.
Los parámetros por defecto para TensorFlow Recommenders' SCANN Keras conjuntos de capa num_leaves=100
y num_leaves_to_search=10
. Esto significa que nuestra base de datos está dividida en 100 subconjuntos disjuntos, y las 10 más prometedoras de estas particiones se califican con AH. Esto significa que 10/100 = 10% del conjunto de datos se está buscando con AH.
Si tenemos, por ejemplo, num_leaves=1000
y num_leaves_to_search=100
, que también sería buscar el 10% de la base de datos con AH. Sin embargo, en comparación con la configuración anterior, el 10%, buscaríamos contendrá los candidatos de mayor calidad, debido a mayores num_leaves
nos permite tomar decisiones más fino sobre qué partes del conjunto de datos son vale la pena buscar.
No es de extrañar entonces que con num_leaves=1000
y num_leaves_to_search=100
que reciben significativamente mayor recuperación:
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
Sin embargo, como compensación, nuestra latencia también ha aumentado. Esto se debe a que el paso de particionamiento se ha vuelto más caro; scann
recoge la parte superior 10 de 100 particiones mientras scann2
recoge la parte superior 100 de 1000 particiones. Este último puede ser más caro porque implica mirar 10 veces más particiones.
%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)
En general, ajustar la búsqueda de ScaNN se trata de elegir las compensaciones correctas. Cada cambio de parámetro individual generalmente no hará que la búsqueda sea más rápida y precisa; Nuestro objetivo es ajustar los parámetros para lograr un equilibrio óptimo entre estos dos objetivos en conflicto.
En nuestro caso, scann2
mejoró significativamente la memoria sobre scann
con algún coste en latencia. ¿Podemos volver a marcar algunas otras perillas para reducir la latencia y, al mismo tiempo, conservar la mayor parte de nuestra ventaja de recuperación?
Intentemos buscar el 70/1000 = 7% del conjunto de datos con AH, y solo vuelva a calificar los 400 candidatos finales:
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
suministra aproximadamente una ganancia de recordatorio absoluta 3% durante scann
tiempo que entrega una menor latencia:
%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)
Estas perillas se pueden ajustar aún más para optimizar diferentes puntos a lo largo de la frontera de Pareto de precisión-rendimiento. Los algoritmos de ScaNN pueden lograr un rendimiento de vanguardia en una amplia gama de objetivos de recuperación.
Otras lecturas
ScaNN utiliza técnicas avanzadas de cuantificación de vectores y una implementación altamente optimizada para lograr sus resultados. El campo de la cuantificación vectorial tiene una rica historia con una variedad de enfoques. Técnica de cuantificación actual de ScanN se detalla en este documento , publicado en el ICML 2020. El documento también fue lanzado junto con este artículo de blog que da una visión general de alto nivel de nuestra técnica.
Muchas de las técnicas de cuantificación relacionados se mencionan en las referencias de nuestro documento ICML 2020, y otras investigaciones relacionadas ScanN-aparezca con http://sanjivk.com/