高效服務

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

擷取模型通常用於從數百萬甚至數億候選項目中找出少數頂尖候選項目。為了能夠回應使用者的情境和行為,這些模型需要能夠即時 (在幾毫秒內) 完成這項作業。

近似最近鄰搜尋 (ANN) 是讓這一切成為可能的技術。在本教學課程中,我們將示範如何使用 ScaNN (最先進的最近鄰擷取套件) 將 TFRS 擷取無縫擴展到數百萬個項目。

何謂 ScaNN?

ScaNN 是 Google Research 提供的一個程式庫,可大規模執行密集向量相似度搜尋。ScaNN 會為候選嵌入資料庫建立索引,以便在推論時快速搜尋這些嵌入。ScaNN 採用最先進的向量壓縮技術和精心實作的演算法,以達到最佳的速度與準確度權衡。在準確度方面幾乎沒有犧牲的情況下,ScaNN 的效能遠遠優於暴力搜尋。

建構以 ScaNN 為基礎的模型

為了在 TFRS 中試用 ScaNN,我們將建構一個簡單的 MovieLens 擷取模型,就像我們在基本擷取教學課程中所做的那樣。如果您已完成該教學課程,則本節內容您應該很熟悉,可以安全地跳過。

首先,安裝 TFRS 和 TensorFlow Datasets

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

我們還需要安裝 scann:這是 TFRS 的選用依附元件,因此需要另外安裝。

pip install -q scann

設定所有必要的匯入。

from typing import Dict, Text

import os
import pprint
import tempfile

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
2022-12-14 12:44:10.744911: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 12:44:10.745003: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 12:44:10.745012: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
import tensorflow_recommenders as tfrs

並載入資料

# 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))
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/tensorflow/python/autograph/pyct/static_analysis/liveness.py:83: Analyzer.lamba_check (from tensorflow.python.autograph.pyct.static_analysis.liveness) is deprecated and will be removed after 2023-09-23.
Instructions for updating:
Lambda fuctions will be no more assumed to be used in the statement where they are used, or at least in the same block. https://github.com/tensorflow/tensorflow/issues/56089
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/tensorflow/python/autograph/pyct/static_analysis/liveness.py:83: Analyzer.lamba_check (from tensorflow.python.autograph.pyct.static_analysis.liveness) is deprecated and will be removed after 2023-09-23.
Instructions for updating:
Lambda fuctions will be no more assumed to be used in the statement where they are used, or at least in the same block. https://github.com/tensorflow/tensorflow/issues/56089

在建構模型之前,我們需要設定使用者和電影詞彙表

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))))
2022-12-14 12:44:17.017232: W tensorflow/core/kernels/data/cache_dataset_ops.cc:296] 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.
2022-12-14 12:44:19.659572: W tensorflow/core/kernels/data/cache_dataset_ops.cc:296] 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.

我們也將設定訓練集和測試集

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)

模型定義

如同在基本擷取教學課程中一樣,我們建構一個簡單的雙塔模型。

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(lambda title: (title, self.movie_model(title)))
        )
      )
    )

  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,
        candidate_ids=features["movie_title"],
        compute_metrics=not training
    )

擬合與評估

TFRS 模型只是一個 Keras 模型。我們可以編譯它

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

估算它

model.fit(train.batch(8192), epochs=3)
Epoch 1/3
10/10 [==============================] - 2s 96ms/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: 69832.4673 - regularization_loss: 0.0000e+00 - total_loss: 69832.4673
Epoch 2/3
10/10 [==============================] - 1s 16ms/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: 67497.9411 - regularization_loss: 0.0000e+00 - total_loss: 67497.9411
Epoch 3/3
10/10 [==============================] - 1s 15ms/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: 66323.0760 - regularization_loss: 0.0000e+00 - total_loss: 66323.0760
<keras.callbacks.History at 0x7f30f00e1280>

並評估它。

model.evaluate(test.batch(8192), return_dict=True)
3/3 [==============================] - 6s 1s/step - factorized_top_k/top_1_categorical_accuracy: 0.0013 - factorized_top_k/top_5_categorical_accuracy: 0.0099 - factorized_top_k/top_10_categorical_accuracy: 0.0219 - factorized_top_k/top_50_categorical_accuracy: 0.1248 - factorized_top_k/top_100_categorical_accuracy: 0.2322 - loss: 49472.8535 - regularization_loss: 0.0000e+00 - total_loss: 49472.8535
{'factorized_top_k/top_1_categorical_accuracy': 0.0013000000035390258,
 'factorized_top_k/top_5_categorical_accuracy': 0.009949999861419201,
 'factorized_top_k/top_10_categorical_accuracy': 0.021900000050663948,
 'factorized_top_k/top_50_categorical_accuracy': 0.12484999746084213,
 'factorized_top_k/top_100_categorical_accuracy': 0.23215000331401825,
 'loss': 28276.328125,
 'regularization_loss': 0,
 'total_loss': 28276.328125}

近似預測

在回應查詢時擷取頂尖候選項目最直接的方法是透過暴力法進行:計算所有可能電影的使用者電影評分,將其排序,然後選取幾個頂尖推薦項目。

在 TFRS 中,這可透過 BruteForce 層來完成

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 0x7f30f015a7c0>

建立並填入候選項目 (透過 index 方法) 後,我們可以呼叫它來取得預測結果

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

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Angels in the Outfield (1994)' b"Kid in King Arthur's Court, A (1995)"
 b'Bedknobs and Broomsticks (1971)']

在不到 1000 部電影的小型資料集中,這非常快速

%timeit _, titles = brute_force(np.array(["42"]), k=3)
1.65 ms ± 6.42 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

但如果我們有更多候選項目 (數百萬而不是數千個),會發生什麼事?

我們可以透過多次為所有電影建立索引來模擬這種情況

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

我們可以在這個較大的資料集上建構 BruteForce 索引

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 0x7f30f0153730>

推薦項目仍然相同

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

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Angels in the Outfield (1994)' b"Kid in King Arthur's Court, A (1995)"
 b'Bedknobs and Broomsticks (1971)']

但它們需要更長的時間。對於包含 100 萬部電影的候選項目集,暴力預測會變得非常慢

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

隨著候選項目數量的增加,所需時間會呈線性成長:對於 1000 萬個候選項目,提供頂尖候選項目將需要 250 毫秒。對於即時服務而言,這顯然太慢了。

這就是近似機制發揮作用的地方。

在 TFRS 中使用 ScaNN 可透過 tfrs.layers.factorized_top_k.ScaNN 層來完成。它遵循與其他頂端 k 層相同的介面

scann = tfrs.layers.factorized_top_k.ScaNN(
    num_reordering_candidates=500,
    num_leaves_to_search=30
)
scann.index_from_dataset(
    tf.data.Dataset.zip((lots_of_movies, lots_of_movies_embeddings))
)
<tensorflow_recommenders.layers.factorized_top_k.ScaNN at 0x7f30f00bebb0>

推薦項目 (大致上!) 相同

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

print(f"Top recommendations: {titles[0]}")
Top recommendations: [b'Angels in the Outfield (1994)' b"Kid in King Arthur's Court, A (1995)"
 b'Bedknobs and Broomsticks (1971)']

但它們的計算速度快得多、快得多

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

在本例中,我們可以在大約 2 毫秒內從約 100 萬個集合中擷取前 3 部電影:比透過暴力法計算最佳候選項目快 15 倍。對於更大的資料集,近似方法的優勢甚至更大。

評估近似值

當使用近似頂端 K 擷取機制 (例如 ScaNN) 時,擷取速度通常會以準確度為代價。為了瞭解這種權衡取捨,務必在使用 ScaNN 時測量模型的評估指標,並將其與基準進行比較。

幸好,TFRS 讓這一切變得容易。我們只需使用 ScaNN 覆寫擷取任務的指標,重新編譯模型,然後執行評估即可。

為了進行比較,我們先執行基準結果。我們仍然需要覆寫我們的指標,以確保它們使用擴大的候選項目集,而不是原始的電影集

# Override the existing streaming candidate source.
model.task.factorized_metrics = tfrs.metrics.FactorizedTopK(
    candidates=tf.data.Dataset.zip((lots_of_movies, 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 24min 23s, sys: 2min, total: 26min 23s
Wall time: 3min 35s

我們可以使用 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 15.6 s, sys: 633 ms, total: 16.3 s
Wall time: 1.95 s

以 ScaNN 為基礎的評估速度快得多、快得多。對於更大的資料集,這種優勢將會變得更大,因此對於大型資料集,始終執行以 ScaNN 為基礎的評估可能是明智之舉,以提高模型開發速度。

但結果如何?幸好,在本例中,結果幾乎相同

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

這表示在這個人工資料集中,近似值幾乎沒有損失。一般而言,所有近似方法都展現出速度與準確度之間的權衡取捨。若要更深入瞭解這方面,您可以查看 Erik Bernhardsson 的 ANN 基準

部署近似模型

ScaNN 為基礎的模型已完全整合到 TensorFlow 模型中,而且如同服務任何其他 TensorFlow 模型一樣容易。

我們可以將其儲存為 SavedModel 物件

lots_of_movies_embeddings
<ConcatenateDataset element_spec=TensorSpec(shape=(None, 32), dtype=tf.float32, name=None)>
# 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)
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: /tmpfs/tmp/tmpqad59_39/model/assets
INFO:tensorflow:Assets written to: /tmpfs/tmp/tmpqad59_39/model/assets

然後載入並服務它,取回完全相同的結果

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

print(f"Top recommendations: {titles[0][:3]}")
Top recommendations: [b'Angels in the Outfield (1994)' b"Kid in King Arthur's Court, A (1995)"
 b'Rudy (1993)']

產生的模型可以在任何已安裝 TensorFlow 和 ScaNN 的 Python 服務中提供服務。

它也可以使用自訂版本的 TensorFlow Serving 來提供服務,此版本以 Docker 容器的形式在 Docker Hub 上提供。您也可以從 Dockerfile 自行建構映像檔。

調整 ScaNN

現在讓我們來看看如何調整 ScaNN 層,以獲得更好的效能/準確度權衡。為了有效地執行此操作,我們首先需要測量我們的基準效能和準確度。

從上方可知,我們已經測量了模型處理單一 (非批次) 查詢的延遲時間 (但請注意,此延遲時間的很大一部分來自模型的非 ScaNN 元件)。

現在我們需要調查 ScaNN 的準確度,我們透過召回率來測量。召回率@k 為 x% 表示,如果我們使用暴力法擷取真正的頂端 k 個鄰居,並將這些結果與使用 ScaNN 也擷取頂端 k 個鄰居的結果進行比較,則 ScaNN 結果中有 x% 位於真正的暴力法結果中。讓我們計算目前 ScaNN 搜尋器的召回率。

首先,我們需要產生暴力法、基本事實頂端 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)

我們的變數 titles_ground_truth 現在包含暴力擷取傳回的頂端 10 部電影推薦項目。現在我們可以使用 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)

接下來,我們定義我們的函式來計算召回率。對於每個查詢,它會計算暴力法和 ScaNN 結果交集中有多少結果,並將此數字除以暴力法結果的數量。所有查詢中此數量的平均值就是我們的召回率。

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

這為我們提供了目前 ScaNN 設定的基準召回率@10

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

我們也可以測量基準延遲時間

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

讓我們看看我們是否可以做得更好!

若要執行此操作,我們需要瞭解 ScaNN 的調整旋鈕如何影響效能的模型。我們目前的模型使用 ScaNN 的樹狀 AH 演算法。此演算法會分割嵌入資料庫 (「樹狀結構」),然後使用 AH (高度最佳化的近似距離計算常式) 對這些分割中最有希望的部分進行評分。

TensorFlow Recommenders 的 ScaNN Keras 層的預設參數設定 num_leaves=100num_leaves_to_search=10。這表示我們的資料庫已分割成 100 個不相交的子集,而這些分割中最有希望的 10 個將使用 AH 進行評分。這表示使用 AH 搜尋的資料集為 10/100=10%。

如果我們有 num_leaves=1000num_leaves_to_search=100,我們也將使用 AH 搜尋 10% 的資料庫。但是,與先前的設定相比,我們將搜尋的 10% 將包含更高品質的候選項目,因為更高的 num_leaves 讓我們能夠對資料集的哪些部分值得搜尋做出更精細的決策。

因此,對於 num_leaves=1000num_leaves_to_search=100,我們獲得顯著更高的召回率,這並不令人意外

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

但是,作為權衡取捨,我們的延遲時間也增加了。這是因為分割步驟變得更加昂貴;scann 選取 100 個分割中的前 10 個,而 scann2 選取 1000 個分割中的前 100 個。後者可能會更昂貴,因為它涉及查看 10 倍數量的分割。

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

一般而言,調整 ScaNN 搜尋是為了選取正確的權衡取捨。每個個別的參數變更通常不會使搜尋既更快又更準確;我們的目標是調整參數,以便在這兩個衝突的目標之間進行最佳權衡。

在我們的案例中,scann2 在延遲時間方面付出了一些代價,但與 scann 相比,召回率顯著提高。我們是否可以調回其他一些旋鈕來減少延遲時間,同時保留我們大部分的召回率優勢?

讓我們嘗試使用 AH 搜尋 70/1000=7% 的資料集,並且僅重新評分最後的 400 個候選項目

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

scann3 相較於 scann,提供了約 3% 的絕對召回率增益,同時也提供了更低的延遲時間

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

可以進一步調整這些旋鈕,以針對準確度與效能帕雷托前沿上的不同點進行最佳化。ScaNN 的演算法可以在各種召回率目標上實現最先進的效能。

延伸閱讀

ScaNN 使用進階向量量化技術和高度最佳化的實作來實現其結果。向量量化領域歷史悠久,方法多樣。ScaNN 目前的量化技術詳述於 本文,該論文在 ICML 2020 上發表。該論文也與 這篇部落格文章 一起發佈,該文章概述了我們的技術。

我們的 ICML 2020 論文的參考文獻中提到了許多相關的量化技術,其他與 ScaNN 相關的研究列在 http://sanjivk.com/