Serviço eficiente

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Modelos de recuperação são muitas vezes construídos para a superfície um punhado de candidatos mais votados fora de milhões ou até centenas de milhões de candidatos. Para ser capaz de reagir ao contexto e ao comportamento do usuário, eles precisam ser capazes de fazer isso instantaneamente, em questão de milissegundos.

A pesquisa de vizinho mais próximo aproximado (ANN) é a tecnologia que torna isso possível. Neste tutorial, mostraremos como usar ScaNN - um pacote de recuperação de vizinho mais próximo de última geração - para dimensionar perfeitamente a recuperação TFRS para milhões de itens.

O que é ScaNN?

ScaNN é uma biblioteca do Google Research que realiza pesquisas por similaridade de vetores densos em grande escala. Dado um banco de dados de embeddings candidatos, o ScaNN indexa esses embeddings de uma maneira que permite que sejam pesquisados ​​rapidamente no momento da inferência. ScaNN usa técnicas de compressão de vetor de última geração e algoritmos cuidadosamente implementados para alcançar a melhor compensação entre velocidade e precisão. Ele pode superar em muito a busca de força bruta enquanto sacrifica pouco em termos de precisão.

Construindo um modelo movido a ScaNN

Para experimentar scann em TFRS, vamos construir um simples MovieLens modelo de recuperação, assim como fizemos na recuperação básico tutorial. Se você seguiu esse tutorial, esta seção lhe parecerá familiar e poderá ser ignorada com segurança.

Para começar, instale os conjuntos de dados TFRS e TensorFlow:

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

Nós também precisa instalar scann : é uma dependência opcional de TFRS, e assim precisa ser instalado separadamente.

pip install -q scann

Configure todas as importações necessárias.

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 carregue os dados:

# 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 podermos construir um modelo, precisamos configurar os vocabulários do usuário e do filme:

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.

Também configuraremos os conjuntos de treinamento e teste:

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)

Definição de modelo

Assim como na recuperação básico tutorial, vamos construir um modelo de dois torre simples.

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)

Adaptação e avaliação

Um modelo TFRS é apenas um modelo Keras. Podemos compilá-lo:

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

Faça uma estimativa:

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

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}

Previsão aproximada

A maneira mais direta de recuperar os principais candidatos em resposta a uma consulta é fazer isso por meio da força bruta: calcule as pontuações dos filmes do usuário para todos os filmes possíveis, classifique-os e escolha algumas das principais recomendações.

Em TFRS, isto é conseguido através da BruteForce camada:

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>

Uma vez criado e preenchido com os candidatos (através do index método), podemos chamá-lo para obter previsões 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)']

Em um pequeno conjunto de dados de menos de 1000 filmes, isso é muito 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)

Mas o que acontece se tivermos mais candidatos - milhões em vez de milhares?

Podemos simular isso indexando todos os nossos filmes várias vezes:

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

Nós podemos construir um BruteForce índice sobre este conjunto de dados maior:

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>

As recomendações ainda são as mesmas

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

Mas eles demoram muito mais. Com um conjunto candidato de 1 milhão de filmes, a previsão de força bruta torna-se 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)

Conforme o número de candidatos aumenta, a quantidade de tempo necessária cresce linearmente: com 10 milhões de candidatos, atender aos principais candidatos levaria 250 milissegundos. Isso é claramente muito lento para um serviço ao vivo.

É aqui que entram os mecanismos aproximados.

Usando scann em TFRS é conseguido através da tfrs.layers.factorized_top_k.ScaNN camada. Ele segue a mesma interface que as outras k camadas principais:

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>

As recomendações são (aproximadamente!) As mesmas

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

Mas eles são muito, muito mais 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)

Nesse caso, podemos recuperar os 3 principais filmes de um conjunto de ~ 1 milhão em cerca de 2 milissegundos: 15 vezes mais rápido do que computando os melhores candidatos por meio de força bruta. A vantagem dos métodos aproximados fica ainda maior para conjuntos de dados maiores.

Avaliando a aproximação

Ao usar mecanismos de recuperação aproximados de K superior (como ScaNN), a velocidade de recuperação geralmente vem em detrimento da precisão. Para entender essa compensação, é importante medir as métricas de avaliação do modelo ao usar o ScaNN e compará-las com a linha de base.

Felizmente, o TFRS torna isso fácil. Simplesmente substituímos as métricas na tarefa de recuperação por métricas usando ScaNN, recompilamos o modelo e executamos a avaliação.

Para fazer a comparação, vamos primeiro executar os resultados da linha de base. Ainda precisamos substituir nossas métricas para ter certeza de que estão usando o conjunto de candidatos ampliado em vez do conjunto original de filmes:

# 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 fazer o mesmo 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

A avaliação baseada no ScaNN é muito, muito mais rápida: é dez vezes mais rápida! Essa vantagem ficará ainda maior para conjuntos de dados maiores e, portanto, para conjuntos de dados grandes, pode ser prudente sempre executar a avaliação baseada em ScaNN para melhorar a velocidade de desenvolvimento do modelo.

Mas e os resultados? Felizmente, neste caso, os resultados são quase os mesmos:

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

Isso sugere que, nessa base de dados artificial, há pouca perda com a aproximação. Em geral, todos os métodos aproximados exibem compensações entre velocidade e precisão. Para entender isso com mais profundidade você pode conferir de Erik Bernhardsson benchmarks ANN .

Implantar o modelo aproximado

O ScaNN modelo baseado é totalmente integrado em modelos TensorFlow, e servi-lo é tão fácil como servir qualquer outro modelo TensorFlow.

Podemos salvá-lo como um 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

e, em seguida, carregá-lo e veiculá-lo, obtendo exatamente os mesmos 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)']

O modelo resultante pode ser servido em qualquer serviço Python que tenha TensorFlow e ScaNN instalados.

Ele também pode ser servido com uma versão personalizada do TensorFlow Servir, disponível como um recipiente Docker em Docker Hub . Você também pode construir a si mesmo imagem do Dockerfile .

Tuning ScaNN

Agora, vamos dar uma olhada em como ajustar nossa camada ScaNN para obter uma melhor relação desempenho / precisão. Para fazer isso de forma eficaz, primeiro precisamos medir nosso desempenho e precisão de linha de base.

Acima, já temos uma medição da latência do nosso modelo para processar uma única consulta (não em lote) (embora observe que uma boa parte dessa latência é proveniente de componentes não ScaNN do modelo).

Agora precisamos investigar a precisão do ScaNN, que medimos por meio de recall. Um recall @ k de x% significa que se usarmos força bruta para recuperar os k vizinhos verdadeiros e compararmos esses resultados com o uso de ScaNN para também recuperar os k vizinhos principais, x% dos resultados de ScaNN estão nos resultados de força bruta verdadeiros. Vamos calcular a rechamada para o pesquisador ScaNN atual.

Primeiro, precisamos gerar a força bruta, a verdade 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)

Nossa variável titles_ground_truth agora contém os top-10 recomendações de filmes devolvidos pela recuperação de força bruta. Agora podemos calcular as mesmas recomendações ao usar 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 seguir, definimos nossa função que calcula a recuperação. Para cada consulta, ele conta quantos resultados estão na interseção da força bruta e os resultados ScaNN e divide isso pelo número de resultados de força bruta. A média dessa quantidade em todas as consultas é nosso recall.

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

Isso nos dá um recall de linha de base @ 10 com a configuração ScaNN atual:

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

Também podemos medir a latência da linha 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)

Vamos ver se podemos fazer melhor!

Para fazer isso, precisamos de um modelo de como os botões de ajuste do ScaNN afetam o desempenho. Nosso modelo atual usa o algoritmo de árvore-AH do ScaNN. Esse algoritmo particiona o banco de dados de embeddings (a "árvore") e, em seguida, pontua a mais promissora dessas partições usando AH, que é uma rotina de cálculo de distância aproximada altamente otimizada.

Os parâmetros padrão para TensorFlow recommenders' scann Keras conjuntos de camadas num_leaves=100 e num_leaves_to_search=10 . Isso significa que nosso banco de dados é particionado em 100 subconjuntos separados e as 10 mais promissoras dessas partições são pontuadas com AH. Isso significa que 10/100 = 10% do conjunto de dados está sendo pesquisado com AH.

Se tivermos, digamos, num_leaves=1000 e num_leaves_to_search=100 , que também seria à procura de 10% do banco de dados com AH. No entanto, em comparação com a definição anterior, os 10% que iria procurar conterá os candidatos de maior qualidade, porque uma mais elevados num_leaves nos permite tomar decisões de grão mais finos sobre o que partes do conjunto de dados valem a pena busca.

Não é nenhuma surpresa, então, que com num_leaves=1000 e num_leaves_to_search=100 chegarmos significativamente maior recordação:

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

No entanto, como compensação, nossa latência também aumentou. Isso ocorre porque a etapa de particionamento ficou mais cara; scann escolhe o topo 10 de divisórias enquanto 100 scann2 pega do topo 100 de 1000 partições. O último pode ser mais caro porque envolve examinar 10 vezes mais partições.

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

Em geral, ajustar a pesquisa ScaNN é escolher as compensações certas. Cada alteração de parâmetro individual geralmente não tornará a pesquisa mais rápida e precisa; nosso objetivo é ajustar os parâmetros para otimizar o equilíbrio entre esses dois objetivos conflitantes.

No nosso caso, scann2 melhorou significativamente recordação sobre scann com algum custo na latência. Podemos diminuir alguns outros botões para reduzir a latência, preservando a maior parte de nossa vantagem de recall?

Vamos tentar pesquisar 70/1000 = 7% do conjunto de dados com AH e reajustar apenas os 400 candidatos finais:

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 proporciona um ganho recordação absoluta de 3% sobre scann oferecendo também menor latência:

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

Esses botões podem ser ajustados para otimizar diferentes pontos ao longo da fronteira de pareto de precisão e desempenho. Os algoritmos do ScaNN podem atingir desempenho de última geração em uma ampla gama de alvos de recall.

Leitura adicional

ScaNN usa técnicas avançadas de quantização vetorial e implementação altamente otimizada para atingir seus resultados. O campo da quantização vetorial tem uma história rica com uma variedade de abordagens. Técnica de quantização atual da scann é detalhado no presente trabalho , publicado na ICML 2020. O documento também foi lançado junto com este artigo do blog que dá uma visão geral de alto nível da nossa técnica.

Muitas técnicas de quantização relacionados são mencionados nas referências do nosso papel ICML 2020 e outra investigação relacionada com a scann está listado na http://sanjivk.com/