הגשה יעילה

הצג באתר TensorFlow.org הפעל בגוגל קולאב צפה במקור ב-GitHub הורד מחברת

מודלים אחזור קרובות נבנים על פני השטח קומץ המועמדים המובילים מתוך מיליוני או מאות ואף מיליוני מועמדים. כדי להיות מסוגלים להגיב להקשר ולהתנהגות של המשתמש, הם צריכים להיות מסוגלים לעשות זאת תוך כדי תנועה, תוך מספר מילי-שניות.

חיפוש השכנים הקרובים ביותר (ANN) היא הטכנולוגיה שמאפשרת זאת. במדריך זה, נראה כיצד להשתמש ב-ScaNN - חבילת אחזור מהשכן הקרוב ביותר - כדי להגדיל בצורה חלקה את אחזור TFRS למיליוני פריטים.

מה זה ScanNN?

ScaNN היא ספרייה מ-Google Research שמבצעת חיפוש דמיון וקטור צפוף בקנה מידה גדול. בהינתן מסד נתונים של הטמעות מועמדות, ScaNN מצרפת את ההטמעות הללו באופן המאפשר חיפוש מהיר שלהן בזמן מסקנות. ScaNN משתמש בטכניקות דחיסה וקטור מתקדמות ובאלגוריתמים מיושמים בקפידה כדי להשיג את ההחלפה הטובה ביותר בין מהירות לדיוק. זה יכול לעלות בהרבה על חיפוש כוח גס תוך הקרבה מועטה מבחינת דיוק.

בניית מודל מופעל באמצעות ScanNN

כדי לנסות ScaNN ב TFRs, נצטרך לבנות MovieLens פשוט מודל יחזור, בדיוק כפי שעשינו תחזור בסיס ההדרכה. אם עקבת אחר המדריך הזה, החלק הזה יהיה מוכר וניתן לדלג עליו בבטחה.

כדי להתחיל, התקן מערכי נתונים של TFRS ו- TensorFlow:

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

חיזוי משוער

הדרך הפשוטה ביותר לאחזר מועמדים מובילים בתגובה לשאילתה היא לעשות זאת באמצעות כוח גס: חישוב ציוני סרטי משתמש עבור כל הסרטים האפשריים, מיין אותם ובחר כמה המלצות מובילות.

בשנת 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 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)']

במערך נתונים קטן של פחות מ-1000 סרטים, זה מהיר מאוד:

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

אבל הם לוקחים הרבה יותר זמן. עם סט מועמד של מיליון סרטים, חיזוי כוח גס הופך לאיטי למדי:

%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 מושגת באמצעות tfrs.layers.factorized_top_k.ScaNN השכבה. זה עוקב אחר אותו ממשק כמו שאר הק שכבות העליונות:

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 הסרטים המובילים מתוך סט של מיליון בערך תוך 2 אלפיות שניות: פי 15 מהר יותר מאשר על ידי מחשוב המועמדים הטובים ביותר באמצעות כוח גס. היתרון של שיטות משוערות גדל אפילו יותר עבור מערכי נתונים גדולים יותר.

הערכת הקירוב

כאשר משתמשים במנגנוני אחזור K העליון משוערים (כגון ScaNN), מהירות השליפה באה לרוב על חשבון הדיוק. כדי להבין את הפשרה הזו, חשוב למדוד את מדדי ההערכה של המודל בעת שימוש ב-ScaNN, ולהשוות אותם עם קו הבסיס.

למרבה המזל, TFRS עושה זאת בקלות. אנו פשוט מחליפים את המדדים במשימת האחזור עם מדדים באמצעות ScaNN, מרכיבים מחדש את המודל ומפעילים הערכה.

כדי לבצע את ההשוואה, בוא נריץ תחילה את תוצאות הבסיס. אנחנו עדיין צריכים לעקוף את המדדים שלנו כדי לוודא שהם משתמשים בערכת המועמדים המוגדלת ולא בערכת הסרטים המקורית:

# 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 היא הרבה הרבה יותר מהירה: היא מהירה יותר פי עשרה! יתרון זה הולך לגדול עוד יותר עבור מערכי נתונים גדולים יותר, ולכן עבור מערכי נתונים גדולים זה עשוי להיות נבון תמיד להפעיל הערכה מבוססת 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.27

זה מצביע על כך שבנתונים מלאכותיים זה, יש מעט הפסד מהקירוב. באופן כללי, כל השיטות המשוערות מציגות פשרות מהירות-דיוק. כדי להבין זאת יותר לעומק אתה יכול לבדוק של אריק 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 ו-ScaNN.

זה גם יכול להיות מוגש באמצעות גרסה מותאמת לשרת TensorFlow, זמין כמכל דוקר על דוקר Hub . ניתן גם לבנות את עצמך תמונה מתוך Dockerfile .

כוונון ScanNN

כעת בואו נבחן את כוונון שכבת ה-ScaNN שלנו כדי לקבל התאמה טובה יותר של ביצועים/דיוק. על מנת לעשות זאת ביעילות, ראשית עלינו למדוד את הביצועים והדיוק הבסיסיים שלנו.

מלמעלה, יש לנו כבר מדידה של זמן ההשהיה של המודל שלנו לעיבוד שאילתה בודדת (ללא אצווה) (אם כי שים לב שחלק ניכר מההשהיה הזו היא ממרכיבים שאינם ScanNN של המודל).

כעת עלינו לחקור את הדיוק של ScaNN, אותו אנו מודדים באמצעות ריקול. recall@k של x% פירושו שאם נשתמש בכוח גס כדי לאחזר את ה-k השכנים העליונים האמיתיים, ומשווה את התוצאות הללו לשימוש ב-ScaNN כדי לאחזר גם את ה-k השכנים העליונים, x% מהתוצאות של ScaNN נמצאות בתוצאות ה-rute force האמיתי. בואו לחשב את הריקול עבור מחפש ScaNN הנוכחי.

ראשית, עלינו ליצור את הכוח הגס, האמת הקרקעית 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 חזרו על ידי שליפה-בכוח הזרוע. כעת אנו יכולים לחשב את אותן המלצות בעת שימוש ב-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)
  ])

זה נותן לנו recall@10 בסיסי עם תצורת ה-ScaNN הנוכחית:

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)

בואו נראה אם ​​אנחנו יכולים לעשות יותר טוב!

לשם כך, אנו זקוקים למודל של האופן שבו כפתורי הכוונון של ScaNN משפיעים על הביצועים. המודל הנוכחי שלנו משתמש באלגוריתם העץ-AH של ScaNN. אלגוריתם זה מחלק את מסד הנתונים של ההטמעות ("העץ") ולאחר מכן מקבל את הניקוד המבטיח ביותר מבין המחיצות הללו באמצעות AH, שהיא שגרת חישוב מרחק משוער בעלת אופטימיזציה גבוהה.

פרמטרי ברירת המחדל עבור ערכות שכבת Keras ScaNN TensorFlow הממליץ 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)

באופן כללי, כוונון החיפוש של ScaNN עוסק בבחירת הפשרות הנכונות. כל שינוי פרמטר בודד בדרך כלל לא יהפוך את החיפוש למהיר יותר ומדויק יותר; המטרה שלנו היא לכוון את הפרמטרים כדי לבצע חילוף אופטימלי בין שתי המטרות הסותרות הללו.

במקרה שלנו, 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)

ניתן לכוונן עוד יותר את הכפתורים כדי לבצע אופטימיזציה עבור נקודות שונות לאורך גבול הפארטו של דיוק-ביצועים. האלגוריתמים של ScaNN יכולים להשיג ביצועים עדכניים על פני מגוון רחב של יעדי אחזור.

לקריאה נוספת

ScaNN משתמש בטכניקות קוונטיזציה וקטוריות מתקדמות וביישום אופטימלי ביותר כדי להשיג את התוצאות שלה. לתחום הקוונטיזציה הווקטורית יש היסטוריה עשירה עם מגוון גישות. טכניקת קוונטיזציה הנוכחית של ScaNN מפורטת במאמר זה , שפורסם ב ICML 2020. העיתון גם שוחרר יחד עם הפוסט הזה אשר נותן סקירה ברמה גבוהה של הטכניקה שלנו.

טכניקות קוונטיזציה נלוות רבות מוזכרות האזכור של הנייר שלנו ICML 2020, ומחקר ScaNN אחר הקשורות מופיע ב http://sanjivk.com/