เสิร์ฟอย่างมีประสิทธิภาพ

ดูบน TensorFlow.org ทำงานใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

รุ่นดึง มักจะถูกสร้างขึ้นมาเพื่อผิวกำมือของยอดผู้สมัครออกจากล้านหรือหลายร้อยล้านของผู้สมัคร เพื่อให้สามารถตอบสนองต่อบริบทและพฤติกรรมของผู้ใช้ได้ พวกเขาจำเป็นต้องทำสิ่งนี้ได้ทันทีภายในเวลาไม่กี่วินาที

การค้นหาเพื่อนบ้านที่ใกล้ที่สุดโดยประมาณ (ANN) เป็นเทคโนโลยีที่ทำให้สิ่งนี้เป็นไปได้ ในบทช่วยสอนนี้ เราจะแสดงวิธีใช้ ScanNN ซึ่งเป็นแพ็คเกจการดึงข้อมูลเพื่อนบ้านที่ใกล้ที่สุดที่ทันสมัยที่สุด เพื่อปรับขนาดการดึงข้อมูล TFRS ให้เป็นรายการนับล้านอย่างราบรื่น

ScanNN คืออะไร?

ScanNN เป็นห้องสมุดจาก Google Research ที่ทำการค้นหาความคล้ายคลึงเวกเตอร์หนาแน่นในขนาดใหญ่ จากฐานข้อมูลของการฝังตัวของผู้สมัคร ScanNN จะทำดัชนีการฝังเหล่านี้ในลักษณะที่ช่วยให้ค้นหาได้อย่างรวดเร็วในเวลาอนุมาน ScanNN ใช้เทคนิคการบีบอัดเวคเตอร์ที่ทันสมัยและใช้อัลกอริธึมอย่างระมัดระวังเพื่อให้ได้การประนีประนอมความเที่ยงตรงด้านความเร็วที่ดีที่สุด มันสามารถทำงานได้ดีกว่าการค้นหากำลังเดรัจฉานอย่างมากในขณะที่เสียสละเพียงเล็กน้อยในแง่ของความแม่นยำ

การสร้างแบบจำลองที่ขับเคลื่อนด้วย ScanNN

ลอง ScaNN ในฉบับนี้เราจะสร้าง MovieLens ง่ายรูปแบบการดึงเหมือนกับที่เราทำใน การดึงพื้นฐาน กวดวิชา หากคุณได้ปฏิบัติตามบทช่วยสอนนั้นแล้ว ส่วนนี้จะคุ้นเคยและสามารถข้ามได้อย่างปลอดภัย

ในการเริ่มต้น ติดตั้ง TFRS และชุดข้อมูล TensorFlow:

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

นอกจากนี้เรายังจำเป็นต้องติดตั้ง scann : มันมีการพึ่งพาตัวเลือกของรายงานทางการเงินและความต้องการที่จะติดตั้งแยกต่างหาก

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

ก่อนที่เราจะสามารถสร้างแบบจำลองได้ เราต้องตั้งค่าคำศัพท์ผู้ใช้และภาพยนตร์:

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.

เราจะตั้งค่าการฝึกและการทดสอบด้วย:

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

การติดตั้งและการประเมิน

โมเดล 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 [==============================] - 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>

และประเมินมัน

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}

คำทำนายโดยประมาณ

วิธีที่ง่ายที่สุดในการค้นหาตัวเลือกอันดับต้น ๆ ในการตอบคำถามคือดำเนินการโดยใช้กำลังดุร้าย: คำนวณคะแนนภาพยนตร์ของผู้ใช้สำหรับภาพยนตร์ที่เป็นไปได้ทั้งหมด จัดเรียงและเลือกสองสามรายการแนะนำยอดนิยม

ในฉบับนี้จะประสบความสำเร็จผ่านทาง 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 0x7fbfc1d4fe10>

เมื่อสร้างขึ้นและมีประชากรที่มีผู้สมัคร (ผ่าน index วิธี) เราสามารถเรียกมันว่าจะได้รับการคาดการณ์ออก:

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

ในชุดข้อมูลขนาดเล็กที่มีภาพยนตร์ต่ำกว่า 1,000 เรื่อง การดำเนินการนี้รวดเร็วมาก:

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

แต่จะเกิดอะไรขึ้นถ้าเรามีผู้สมัครเพิ่มขึ้น - หลายล้านแทนที่จะเป็นพันคน?

เราสามารถจำลองสิ่งนี้ได้โดยการจัดทำดัชนีภาพยนตร์ทั้งหมดของเราหลายครั้ง:

# 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 0x7fbfc1d80610>

คำแนะนำยังคงเหมือนเดิม

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

แต่พวกเขาใช้เวลานานกว่ามาก ด้วยชุดภาพยนตร์ 1 ล้านเรื่อง การทำนายกำลังเดรัจฉานจะค่อนข้างช้า:

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

เมื่อจำนวนผู้สมัครเพิ่มขึ้น ระยะเวลาที่ต้องการจะเพิ่มขึ้นเป็นเส้นตรง: ด้วยผู้สมัคร 10 ล้านคน การให้บริการผู้สมัครอันดับต้นๆ จะใช้เวลา 250 มิลลิวินาที เห็นได้ชัดว่าช้าเกินไปสำหรับบริการสด

นี่คือที่มาของกลไกโดยประมาณ

ใช้ ScaNN ในฉบับที่จะประสบความสำเร็จผ่านทาง tfrs.layers.factorized_top_k.ScaNN ชั้น มันทำตามอินเทอร์เฟซเดียวกันกับเลเยอร์ k ด้านบนอื่น ๆ :

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>

คำแนะนำ (โดยประมาณ!) เหมือนกัน

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

แต่คำนวณได้เร็วกว่ามาก:

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

ในกรณีนี้ เราสามารถดึงภาพยนตร์ 3 อันดับแรกจากชุด ~1 ล้านเรื่องในเวลาประมาณ 2 มิลลิวินาที ซึ่งเร็วกว่าการคำนวณตัวเลือกที่ดีที่สุด 15 เท่าโดยใช้กำลังดุร้าย ข้อดีของวิธีการโดยประมาณจะเพิ่มมากขึ้นสำหรับชุดข้อมูลขนาดใหญ่

การประเมินค่าประมาณ

เมื่อใช้กลไกการดึงข้อมูล K ระดับบนสุดโดยประมาณ (เช่น ScaNN) ความเร็วในการดึงข้อมูลมักจะสูญเสียความแม่นยำไป เพื่อให้เข้าใจความแตกต่างนี้ การวัดเมตริกการประเมินของแบบจำลองเป็นสิ่งสำคัญเมื่อใช้ ScanNN และเปรียบเทียบกับค่าพื้นฐาน

โชคดีที่ TFRS ทำให้สิ่งนี้เป็นเรื่องง่าย เราเพียงแค่ลบล้างตัววัดในงานดึงข้อมูลด้วยตัววัดโดยใช้ ScanNN คอมไพล์โมเดลใหม่ และรันการประเมิน

เพื่อทำการเปรียบเทียบ ให้เรียกใช้ผลลัพธ์พื้นฐานก่อน เรายังคงต้องแก้ไขตัวชี้วัดของเราเพื่อให้แน่ใจว่าพวกเขากำลังใช้ชุดตัวเลือกที่ขยายใหญ่ขึ้นแทนชุดดั้งเดิมของภาพยนตร์:

# 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

เราสามารถทำได้เช่นเดียวกันโดยใช้ ScanNN:

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

การประเมินตาม ScanNN นั้นเร็วกว่ามาก เร็วกว่าถึงสิบเท่า! ข้อได้เปรียบนี้จะเพิ่มมากขึ้นสำหรับชุดข้อมูลที่ใหญ่กว่า ดังนั้นสำหรับชุดข้อมูลขนาดใหญ่ ควรใช้การประเมินตาม ScanNN เสมอเพื่อปรับปรุงความเร็วของการพัฒนาแบบจำลอง

แต่ผลลัพธ์เป็นอย่างไร? โชคดีที่ในกรณีนี้ผลลัพธ์เกือบจะเหมือนกัน:

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

นี่แสดงให้เห็นว่าใน datase เทียมนี้ มีการสูญเสียเพียงเล็กน้อยจากการประมาณ โดยทั่วไป วิธีการโดยประมาณทั้งหมดแสดงการประนีประนอมความเร็วและความแม่นยำ เพื่อทำความเข้าใจนี้ในเชิงลึกเพิ่มเติมคุณสามารถตรวจสอบ Erik Bernhardsson ของ มาตรฐาน ANN

กำลังปรับใช้โมเดลโดยประมาณ

ScaNN รุ่นชั่นแบบครบวงจรในรูปแบบ TensorFlow, และการให้บริการมันเป็นเรื่องง่ายที่ให้บริการในรูปแบบ TensorFlow อื่น ๆ

เราสามารถบันทึกเป็น SavedModel วัตถุ

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

แล้วโหลดและให้บริการ ได้ผลลัพธ์เหมือนเดิมทุกประการ:

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

โมเดลผลลัพธ์สามารถให้บริการในบริการ Python ใดๆ ที่ติดตั้ง TensorFlow และ ScanNN

นอกจากนี้ยังสามารถทำหน้าที่ใช้รุ่นที่กำหนดเองของ TensorFlow บริการสามารถใช้ได้เป็นภาชนะเทียบท่าบน หาง Hub นอกจากนี้คุณยังสามารถสร้างภาพตัวเองจาก Dockerfile

การปรับ ScanNN

ตอนนี้ มาดูการปรับชั้น ScanNN ของเราเพื่อให้ได้ประสิทธิภาพ/ความเที่ยงตรงที่ดีขึ้น เพื่อที่จะทำสิ่งนี้ได้อย่างมีประสิทธิภาพ อันดับแรก เราต้องวัดประสิทธิภาพและความแม่นยำพื้นฐานของเรา

จากด้านบน เรามีการวัดเวลาแฝงของแบบจำลองของเราแล้วสำหรับการประมวลผลการสืบค้นข้อมูลเดียว (ไม่ใช่เป็นชุด) (แม้ว่าโปรดทราบว่าเวลาในการตอบสนองนี้ในปริมาณที่เหมาะสมนั้นมาจากส่วนประกอบที่ไม่ใช่ของ ScanN ของโมเดล)

ตอนนี้ เราต้องตรวจสอบความถูกต้องของ ScanNN ซึ่งเราวัดผ่านการเรียกคืน Recall@k ของ x% หมายความว่าถ้าเราใช้กำลังเดรัจฉานเพื่อดึงเพื่อนบ้าน k อันดับสูงสุด และเปรียบเทียบผลลัพธ์เหล่านั้นกับการใช้ ScaNN เพื่อดึงเพื่อนบ้าน k อันดับสูงสุด x% ของผลลัพธ์ของ ScaNN จะอยู่ในผลลัพธ์ของแรงเดรัจฉานที่แท้จริง มาคำนวณการเรียกคืนสำหรับผู้ค้นหาปัจจุบันของ ScanNN

อันดับแรก เราต้องสร้างกำลังเดรัจฉาน ความจริงพื้น 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)

ตัวแปรของเรา titles_ground_truth ตอนนี้มีคำแนะนำ 10 อันดับภาพยนตร์ที่ส่งกลับโดยการดึงแรงเดรัจฉาน ตอนนี้ เราสามารถคำนวณคำแนะนำเดียวกันเมื่อใช้ ScanNN:

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

ต่อไป เรากำหนดฟังก์ชันของเราที่คำนวณการเรียกคืน สำหรับแต่ละแบบสอบถาม จะนับจำนวนผลลัพธ์ที่อยู่ในจุดตัดของแรงเดรัจฉานและผลลัพธ์ของ ScanNN แล้วหารด้วยจำนวนผลลัพธ์ของแรงเดรัจฉาน ค่าเฉลี่ยของปริมาณนี้จากคำถามทั้งหมดคือการเรียกคืนของเรา

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

สิ่งนี้ทำให้เรามีพื้นฐานการเรียกคืน@10ด้วยการกำหนดค่า ScanNN ปัจจุบัน:

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

นอกจากนี้เรายังสามารถวัดเวลาแฝงพื้นฐาน:

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

มาดูกันว่าเราสามารถทำได้ดีกว่านี้หรือไม่!

ในการทำเช่นนี้ เราจำเป็นต้องมีแบบจำลองว่าปุ่มปรับเสียงของ ScanNN ส่งผลต่อประสิทธิภาพอย่างไร โมเดลปัจจุบันของเราใช้อัลกอริธึม tree-AH ของ ScanNN อัลกอริธึมนี้แบ่งฐานข้อมูลของการฝัง ("ต้นไม้") แล้วให้คะแนนพาร์ติชั่นที่มีแนวโน้มมากที่สุดโดยใช้ AH ซึ่งเป็นรูทีนการคำนวณระยะทางโดยประมาณที่ได้รับการปรับให้เหมาะสมที่สุด

พารามิเตอร์เริ่มต้นสำหรับ TensorFlow Recommenders' ScaNN Keras ชุดชั้น num_leaves=100 และ num_leaves_to_search=10 ซึ่งหมายความว่าฐานข้อมูลของเราถูกแบ่งออกเป็น 100 ชุดย่อยที่ไม่เกี่ยวข้องกัน และ 10 พาร์ติชั่นที่มีแนวโน้มมากที่สุดจะถูกให้คะแนนด้วย AH ซึ่งหมายความว่า 10/100=10% ของชุดข้อมูลกำลังถูกค้นหาด้วย AH

ถ้าเรามีการพูด, num_leaves=1000 และ num_leaves_to_search=100 เราจะยังจะมีการค้นหา 10% ของฐานข้อมูลด้วย AH อย่างไรก็ตามในการเปรียบเทียบกับการตั้งค่าก่อนหน้านี้ 10% เราจะค้นหาจะมีผู้สมัครที่มีคุณภาพสูงขึ้นเพราะสูง num_leaves ช่วยให้เราในการตัดสินใจปลีกย่อยเม็ดเล็กเกี่ยวกับสิ่งที่เป็นส่วนหนึ่งของชุดข้อมูลที่มีมูลค่าการค้นหา

มันไม่แปลกใจแล้วว่ามีการ num_leaves=1000 และ num_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.966

อย่างไรก็ตาม เวลาในการตอบสนองของเราก็เพิ่มขึ้นเช่นกัน เนื่องจากขั้นตอนการแบ่งพาร์ติชั่นมีราคาแพงกว่า scann หยิบด้านบน 10 จาก 100 ในขณะที่พาร์ทิชัน scann2 หยิบด้านบน 100 1000 พาร์ทิชัน หลังอาจมีราคาแพงกว่าเพราะต้องดูพาร์ติชั่นมากกว่า 10 เท่า

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

โดยทั่วไป การปรับการค้นหา ScanNN เป็นการเลือกจุดประนีประนอมที่เหมาะสม โดยทั่วไปการเปลี่ยนแปลงพารามิเตอร์แต่ละรายการจะไม่ทำให้การค้นหารวดเร็วและแม่นยำยิ่งขึ้น เป้าหมายของเราคือการปรับพารามิเตอร์เพื่อแลกเปลี่ยนระหว่างเป้าหมายที่ขัดแย้งกันทั้งสองนี้อย่างเหมาะสมที่สุด

ในกรณีของเรา scann2 ดีขึ้นอย่างมีนัยสำคัญมากกว่าการเรียกคืน scann ที่ค่าใช้จ่ายบางอย่างในความล่าช้า เราสามารถหมุนปุ่มอื่นกลับเพื่อลดเวลาแฝงในขณะที่รักษาความได้เปรียบในการเรียกคืนส่วนใหญ่ไว้ได้หรือไม่

มาลองค้นหา 70/1000=7% ของชุดข้อมูลด้วย AH และให้คะแนนผู้สมัคร 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.957

scann3 มอบเกี่ยวกับการเรียกคืนกำไร 3% แน่นอนกว่า scann ในขณะที่ยังส่งมอบแฝงล่าง:

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

ลูกบิดเหล่านี้สามารถปรับเพิ่มเติมเพื่อปรับให้เหมาะสมสำหรับจุดต่างๆ ตามแนวชายแดน pareto ประสิทธิภาพความแม่นยำ อัลกอริธึมของ ScanNN สามารถบรรลุประสิทธิภาพที่ล้ำสมัยเหนือเป้าหมายการเรียกคืนที่หลากหลาย

อ่านเพิ่มเติม

ScanNN ใช้เทคนิคการควอนไทซ์เวกเตอร์ขั้นสูงและการใช้งานที่ปรับให้เหมาะสมที่สุดเพื่อให้ได้ผลลัพธ์ เขตข้อมูลของการหาปริมาณเวกเตอร์มีประวัติอันยาวนานด้วยแนวทางที่หลากหลาย เทคนิค quantization ScaNN ปัจจุบันเป็นรายละเอียดใน บทความนี้ ตีพิมพ์ในปี 2020 ICML กระดาษยังถูกปล่อยออกมาพร้อมกับ บทความบล็อกนี้ ซึ่งจะช่วยให้ภาพรวมระดับสูงของเทคนิคของเรา

หลายเทคนิคที่เกี่ยวข้องควอนกล่าวถึงในการอ้างอิงของเรากระดาษ ICML ปี 2020 และการวิจัย ScaNN อื่น ๆ ที่เกี่ยวข้องไว้ที่ http://sanjivk.com/