Phục vụ hiệu quả

Xem trên TensorFlow.org Chạy trong Google Colab Xem nguồn trên GitHub Tải xuống sổ ghi chép

Mô hình hồi thường được xây dựng bề mặt một số ít các ứng cử viên hàng đầu ra của hàng triệu hoặc thậm chí hàng trăm triệu thí sinh. Để có thể phản ứng với ngữ cảnh và hành vi của người dùng, họ cần có thể thực hiện điều này một cách nhanh chóng, chỉ trong vài phần nghìn giây.

Tìm kiếm gần đúng hàng xóm gần nhất (ANN) là công nghệ giúp điều này trở nên khả thi. Trong hướng dẫn này, chúng tôi sẽ chỉ ra cách sử dụng ScaNN - một gói truy xuất hàng xóm gần nhất hiện đại nhất - để mở rộng quy mô truy xuất TFRS một cách liền mạch đến hàng triệu mục.

ScaNN là gì?

ScaNN là một thư viện từ Google Research thực hiện tìm kiếm độ tương tự vectơ dày đặc ở quy mô lớn. Đưa ra một cơ sở dữ liệu về các nhúng ứng viên, ScaNN lập chỉ mục các nhúng này theo cách cho phép chúng được tìm kiếm nhanh chóng tại thời điểm suy luận. ScaNN sử dụng các kỹ thuật nén vectơ hiện đại và các thuật toán được triển khai cẩn thận để đạt được sự cân bằng giữa tốc độ và độ chính xác tốt nhất. Nó có thể vượt trội hơn rất nhiều so với tìm kiếm vũ phu trong khi hy sinh ít về độ chính xác.

Xây dựng mô hình hỗ trợ ScaNN

Để thử ScaNN trong TFR, chúng tôi sẽ xây dựng một MovieLens đơn giản mô hình hồi, giống như chúng ta đã làm trong hồi cơ bản hướng dẫn. Nếu bạn đã làm theo hướng dẫn đó, phần này sẽ quen thuộc và có thể bỏ qua một cách an toàn.

Để bắt đầu, hãy cài đặt Bộ dữ liệu TFRS và TensorFlow:

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

Chúng tôi cũng cần phải cài đặt scann : nó là một sự phụ thuộc tùy chọn của TFR, và do đó cần phải được cài đặt riêng rẽ.

pip install -q scann

Thiết lập tất cả các nhập cần thiết.

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

Và tải dữ liệu:

# 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

Trước khi có thể xây dựng một mô hình, chúng ta cần thiết lập từ vựng cho người dùng và phim:

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.

Chúng tôi cũng sẽ thiết lập các bộ đào tạo và kiểm tra:

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)

Định nghĩa mô hình

Cũng giống như trong hồi cơ bản hướng dẫn, chúng tôi xây dựng một mô hình hai tháp đơn giản.

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)

Phù hợp và đánh giá

Một mô hình TFRS chỉ là một mô hình Keras. Chúng tôi có thể biên dịch nó:

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

Ước tính nó:

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>

Và đánh giá nó.

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}

Dự đoán gần đúng

Cách đơn giản nhất để truy xuất các ứng cử viên hàng đầu để trả lời một truy vấn là thực hiện thông qua brute force: tính điểm phim của người dùng cho tất cả các phim có thể có, sắp xếp chúng và chọn một số đề xuất hàng đầu.

Trong TFR, điều này được thực hiện thông qua các BruteForce lớp:

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>

Sau khi tạo và dân cư với các ứng cử viên (thông qua các index phương pháp), chúng ta có thể gọi nó là để có được dự đoán ra:

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

Trên một tập dữ liệu nhỏ dưới 1000 phim, quá trình này diễn ra rất nhanh:

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

Nhưng điều gì sẽ xảy ra nếu chúng ta có nhiều ứng viên hơn - hàng triệu thay vì hàng nghìn?

Chúng tôi có thể mô phỏng điều này bằng cách lập chỉ mục tất cả các phim của chúng tôi nhiều lần:

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

Chúng ta có thể xây dựng một BruteForce chỉ mục trên tập dữ liệu lớn hơn này:

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>

Các khuyến nghị vẫn như cũ

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

Nhưng chúng mất nhiều thời gian hơn. Với một bộ ứng cử viên gồm 1 triệu phim, việc dự đoán bạo lực trở nên khá chậm chạp:

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

Khi số lượng ứng viên tăng lên, lượng thời gian cần thiết cũng tăng theo tuyến tính: với 10 triệu ứng viên, việc phục vụ các ứng viên hàng đầu sẽ mất 250 mili giây. Điều này rõ ràng là quá chậm đối với một dịch vụ trực tiếp.

Đây là lúc các cơ chế gần đúng xuất hiện.

Sử dụng ScaNN trong TFR được thực hiện thông qua tfrs.layers.factorized_top_k.ScaNN lớp. Nó tuân theo giao diện tương tự như k lớp trên cùng khác:

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>

Các đề xuất (gần đúng!) Giống nhau

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

Nhưng chúng nhanh hơn rất nhiều để tính toán:

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

Trong trường hợp này, chúng tôi có thể truy xuất 3 bộ phim hàng đầu trong số ~ 1 triệu bộ phim trong khoảng 2 mili giây: nhanh hơn 15 lần so với việc tính toán các ứng cử viên xuất sắc nhất thông qua bạo lực. Lợi thế của các phương pháp gần đúng thậm chí còn lớn hơn đối với các bộ dữ liệu lớn hơn.

Đánh giá sự gần đúng

Khi sử dụng các cơ chế truy xuất K hàng đầu gần đúng (chẳng hạn như ScaNN), tốc độ truy xuất thường đi kèm với độ chính xác. Để hiểu được sự đánh đổi này, điều quan trọng là phải đo lường các chỉ số đánh giá của mô hình khi sử dụng ScaNN và so sánh chúng với đường cơ sở.

May mắn thay, TFRS làm cho điều này dễ dàng. Chúng tôi chỉ cần ghi đè các chỉ số trên nhiệm vụ truy xuất bằng các chỉ số sử dụng ScaNN, biên dịch lại mô hình và chạy đánh giá.

Để so sánh, trước tiên chúng ta hãy chạy các kết quả cơ bản. Chúng tôi vẫn cần ghi đè các chỉ số của mình để đảm bảo rằng chúng đang sử dụng tập hợp ứng cử viên được phóng to hơn là tập hợp phim ban đầu:

# 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

Chúng ta có thể làm tương tự bằng cách sử dụng 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

Đánh giá dựa trên ScaNN nhanh hơn rất nhiều: nhanh hơn mười lần! Lợi thế này sẽ còn phát triển lớn hơn đối với các bộ dữ liệu lớn hơn và vì vậy đối với các bộ dữ liệu lớn, có thể cần thận trọng khi luôn chạy đánh giá dựa trên ScaNN để cải thiện tốc độ phát triển mô hình.

Nhưng kết quả như thế nào? May mắn thay, trong trường hợp này, kết quả gần như giống nhau:

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

Điều này cho thấy rằng trên cơ sở dữ liệu nhân tạo này, có rất ít tổn thất so với ước tính. Nói chung, tất cả các phương pháp gần đúng đều thể hiện sự cân bằng giữa tốc độ và độ chính xác. Để hiểu điều này sâu hơn bạn có thể kiểm tra Erik Bernhardsson của tiêu chuẩn ANN .

Triển khai mô hình gần đúng

Các ScaNN mô hình dựa trên được tích hợp đầy đủ vào các mô hình TensorFlow, và phục vụ nó là dễ dàng như việc phục vụ bất kỳ mô hình TensorFlow khác.

Chúng ta có thể lưu nó như một SavedModel đối tượng

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

rồi tải nó và phân phát, nhận lại kết quả chính xác như sau:

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

Mô hình kết quả có thể được phục vụ trong bất kỳ dịch vụ Python nào đã cài đặt TensorFlow và ScaNN.

Nó cũng có thể được phân phối bằng một phiên bản tùy biến của TensorFlow Serving, có sẵn như là một container Docker trên Docker Hub . Bạn cũng có thể xây dựng cho mình hình ảnh từ Dockerfile .

Điều chỉnh ScaNN

Bây giờ chúng ta hãy xem xét điều chỉnh lớp ScaNN của chúng tôi để có được sự cân bằng về hiệu suất / độ chính xác tốt hơn. Để làm điều này một cách hiệu quả, trước tiên chúng ta cần đo lường hiệu suất và độ chính xác cơ bản của chúng ta.

Từ trên, chúng tôi đã có một phép đo độ trễ của mô hình để xử lý một truy vấn (không theo đợt) (mặc dù lưu ý rằng phần lớn độ trễ này là từ các thành phần không thuộc ScaNN của mô hình).

Bây giờ chúng tôi cần điều tra độ chính xác của ScaNN, chúng tôi đo lường thông qua thu hồi. Gọi lại @ k của x% có nghĩa là nếu chúng ta sử dụng brute force để truy xuất k lân cận hàng đầu thực sự và so sánh các kết quả đó với việc sử dụng ScaNN để truy xuất k lân cận hàng đầu, x% kết quả của ScaNN là kết quả brute force thực sự. Hãy tính toán truy xuất cho trình tìm kiếm ScaNN hiện tại.

Đầu tiên, chúng ta cần tạo ra lực lượng vũ phu, sự thật cơ bản 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)

Biến của chúng tôi titles_ground_truth bây giờ chứa top-10 đề xuất phim trả về bởi brute-force hồi. Bây giờ chúng ta có thể tính toán các đề xuất tương tự khi sử dụng 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)

Tiếp theo, chúng tôi xác định chức năng của chúng tôi để tính toán việc thu hồi. Đối với mỗi truy vấn, nó đếm có bao nhiêu kết quả nằm trong giao điểm của bạo lực và kết quả ScaNN và chia kết quả này cho số kết quả bạo lực. Mức trung bình của số lượng này trên tất cả các truy vấn là mức thu hồi của chúng tôi.

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

Điều này cung cấp cho chúng tôi nhớ lại đường cơ sở @ 10 với cấu hình ScaNN hiện tại:

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

Chúng tôi cũng có thể đo độ trễ đường cơ sở:

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

Hãy xem nếu chúng ta có thể làm tốt hơn!

Để làm điều này, chúng tôi cần một mô hình về cách các nút điều chỉnh của ScaNN ảnh hưởng đến hiệu suất. Mô hình hiện tại của chúng tôi sử dụng thuật toán cây-AH của ScaNN. Thuật toán này phân vùng cơ sở dữ liệu của các lần nhúng ("cây") và sau đó cho điểm hứa hẹn nhất của các phân vùng này bằng cách sử dụng AH, đây là một quy trình tính toán khoảng cách gần đúng được tối ưu hóa cao.

Các thông số mặc định cho người giới thiệu TensorFlow ScaNN Keras bộ lớp num_leaves=100num_leaves_to_search=10 . Điều này có nghĩa là cơ sở dữ liệu của chúng tôi được phân chia thành 100 tập con rời rạc và 10 phân vùng hứa hẹn nhất trong số các phân vùng này được chấm điểm bằng AH. Điều này có nghĩa là 10/100 = 10% tập dữ liệu đang được tìm kiếm bằng AH.

Nếu chúng ta có, nói, num_leaves=1000num_leaves_to_search=100 , chúng tôi sẽ còn phải tìm kiếm 10% số cơ sở dữ liệu với AH. Tuy nhiên, so với các cài đặt trước đó, 10% chúng tôi sẽ tìm kiếm sẽ chứa các ứng cử viên có chất lượng cao hơn, bởi vì một cao num_leaves cho phép chúng ta đưa ra quyết định tốt hơn hạt viên về những gì các bộ phận của tập dữ liệu có giá trị tìm kiếm.

Đó là Không ngạc nhiên rằng với num_leaves=1000num_leaves_to_search=100 chúng tôi nhận thu hồi cao hơn đáng kể:

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

Tuy nhiên, như một sự cân bằng, độ trễ của chúng tôi cũng tăng lên. Điều này là do bước phân vùng đã trở nên đắt hơn; scann chọn top 10 của 100 phân vùng trong khi scann2 chọn top 100 1000 phân vùng. Cái thứ hai có thể đắt hơn vì nó liên quan đến việc xem xét nhiều phân vùng gấp 10 lần.

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

Nói chung, điều chỉnh tìm kiếm ScaNN chính là chọn các điểm cân bằng phù hợp. Mỗi thay đổi thông số riêng lẻ thường không giúp tìm kiếm nhanh hơn và chính xác hơn; mục tiêu của chúng tôi là điều chỉnh các thông số để cân bằng tối ưu giữa hai mục tiêu mâu thuẫn này.

Trong trường hợp của chúng tôi, scann2 cải thiện đáng kể thu hồi trên scann tại một số chi phí trong thời gian trễ. Chúng ta có thể quay lại một số nút bấm khác để giảm độ trễ, trong khi vẫn duy trì hầu hết lợi thế thu hồi của chúng ta không?

Hãy thử tìm kiếm 70/1000 = 7% tập dữ liệu với AH và chỉ thu thập lại 400 ứng cử viên cuối cùng:

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 mang về mức tăng thu hồi tuyệt đối 3% so với scann trong khi cũng cung cấp độ trễ thấp hơn:

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

Các nút này có thể được điều chỉnh thêm để tối ưu hóa cho các điểm khác nhau dọc theo biên giới pareto hiệu suất chính xác. Các thuật toán của ScaNN có thể đạt được hiệu suất hiện đại trên một loạt các mục tiêu thu hồi.

đọc thêm

ScaNN sử dụng các kỹ thuật lượng tử hóa vectơ tiên tiến và triển khai được tối ưu hóa cao để đạt được kết quả của nó. Lĩnh vực lượng tử hóa vectơ có một lịch sử phong phú với nhiều cách tiếp cận. Kỹ thuật lượng tử hiện ScaNN được nêu chi tiết trong bài viết này , được công bố tại ICML năm 2020. Tài liệu này cũng được phát hành cùng với bài viết trên blog này mà đưa ra một cái nhìn tổng quan mức độ cao về kỹ thuật của chúng tôi.

Nhiều kỹ thuật lượng tử có liên quan được đề cập trong các tài liệu tham khảo giấy ICML 2020 của chúng tôi, và nghiên cứu ScaNN liên quan khác được liệt kê ở http://sanjivk.com/