שימוש בתכונות צד: עיבוד תכונה מראש

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

אחד היתרונות הגדולים של שימוש במסגרת למידה עמוקה לבניית מודלים של ממליצים הוא החופש לבנות ייצוגי תכונות עשירים וגמישים.

השלב הראשון לעשות זאת הוא הכנת התכונות, שכן תכונות גולמיות לרוב לא יהיו ניתנות לשימוש מיידי במודל.

לדוגמה:

  • מזהי משתמש ופריטים עשויים להיות מחרוזות (כותרות, שמות משתמש) או מספרים שלמים גדולים ולא רציפים (מזהי מסד נתונים).
  • תיאורי פריטים יכולים להיות טקסט גולמי.
  • חותמות זמן של אינטראקציה יכולות להיות חותמות זמן גולמיות של Unix.

אלה צריכים לעבור טרנספורמציה כראוי כדי להיות שימושיים בבניית מודלים:

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

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

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

מערך הנתונים של MovieLens

תחילה נסתכל באילו תכונות אנו יכולים להשתמש ממערך הנתונים של MovieLens:

pip install -q --upgrade tensorflow-datasets
import pprint

import tensorflow_datasets as tfds

ratings = tfds.load("movielens/100k-ratings", split="train")

for x in ratings.take(1).as_numpy_iterator():
  pprint.pprint(x)
2021-10-02 11:59:46.956587: E tensorflow/stream_executor/cuda/cuda_driver.cc:271] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected
{'bucketized_user_age': 45.0,
 'movie_genres': array([7]),
 'movie_id': b'357',
 'movie_title': b"One Flew Over the Cuckoo's Nest (1975)",
 'raw_user_age': 46.0,
 'timestamp': 879024327,
 'user_gender': True,
 'user_id': b'138',
 'user_occupation_label': 4,
 'user_occupation_text': b'doctor',
 'user_rating': 4.0,
 'user_zip_code': b'53211'}
2021-10-02 11:59:47.327679: W tensorflow/core/kernels/data/cache_dataset_ops.cc:768] 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.

יש כאן כמה תכונות מפתח:

  • כותרת הסרט שימושית כמזהה סרט.
  • מזהה משתמש שימושי כמזהה משתמש.
  • חותמות זמן יאפשרו לנו להדגים את השפעת הזמן.

שני הראשונים הם מאפיינים קטגוריים; חותמות זמן הן תכונה מתמשכת.

הפיכת מאפיינים קטגוריים להטבעות

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

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

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

נטילת מאפיינים קטגוריים גולמיים והפיכתם להטבעות הוא בדרך כלל תהליך בן שני שלבים:

  1. ראשית, עלינו לתרגם את הערכים הגולמיים לטווח של מספרים שלמים רציפים, בדרך כלל על ידי בניית מיפוי (המכונה "אוצר מילים") הממפה ערכים גולמיים ("מלחמת הכוכבים") למספרים שלמים (נניח, 15).
  2. שנית, עלינו לקחת את המספרים השלמים הללו ולהפוך אותם להטמעות.

הגדרת אוצר המילים

הצעד הראשון הוא הגדרת אוצר מילים. אנחנו יכולים לעשות זאת בקלות באמצעות שכבות עיבוד מקדים של Keras.

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.StringLookup()

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

movie_title_lookup.adapt(ratings.map(lambda x: x["movie_title"]))

print(f"Vocabulary: {movie_title_lookup.get_vocabulary()[:3]}")
Vocabulary: ['[UNK]', 'Star Wars (1977)', 'Contact (1997)']

ברגע שיש לנו את זה נוכל להשתמש בשכבה כדי לתרגם אסימונים גולמיים למזהי הטבעה:

movie_title_lookup(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([ 1, 58])>

שימו לב שאוצר המילים של השכבה כולל אסימון אחד (או יותר!) לא ידוע (או "מחוץ לאוצר המילים", OOV). זה ממש שימושי: זה אומר שהשכבה יכולה להתמודד עם ערכים קטגוריים שאינם באוצר המילים. מבחינה מעשית, זה אומר שהמודל יכול להמשיך ללמוד ולהמליץ ​​גם באמצעות תכונות שלא נראו במהלך בניית אוצר המילים.

שימוש בגיבוב תכונות

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

אנחנו יכולים לקחת את זה לקיצוניות ההגיונית שלו ולהסתמך לחלוטין על hashing תכונות, ללא אוצר מילים כלל. זה מיושם tf.keras.layers.Hashing השכבה.

# We set up a large number of bins to reduce the chance of hash collisions.
num_hashing_bins = 200_000

movie_title_hashing = tf.keras.layers.Hashing(
    num_bins=num_hashing_bins
)

אנחנו יכולים לבצע את החיפוש כמו קודם ללא צורך בבניית אוצר מילים:

movie_title_hashing(["Star Wars (1977)", "One Flew Over the Cuckoo's Nest (1975)"])
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([101016,  96565])>

הגדרת ההטבעות

עכשיו שיש לנו מזהים שלמים, אנו יכולים להשתמש Embedding השכבה להפוך אותם לתוך שיבוצים.

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

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

movie_title_embedding = tf.keras.layers.Embedding(
    # Let's use the explicit vocabulary lookup.
    input_dim=movie_title_lookup.vocab_size(),
    output_dim=32
)
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

אנו יכולים לחבר את השניים לשכבה אחת אשר לוקחת טקסט גולמי ומניבה הטמעות.

movie_title_model = tf.keras.Sequential([movie_title_lookup, movie_title_embedding])

בדיוק ככה, אנחנו יכולים לקבל ישירות את ההטמעות עבור כותרות הסרטים שלנו:

movie_title_model(["Star Wars (1977)"])
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['Star Wars (1977)']
Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor, but we receive a <class 'list'> input: ['Star Wars (1977)']
Consider rewriting this model with the Functional API.
<tf.Tensor: shape=(1, 32), dtype=float32, numpy=
array([[-0.00255408,  0.00941082,  0.02599109, -0.02758816, -0.03652344,
        -0.03852248, -0.03309812, -0.04343383,  0.03444691, -0.02454401,
         0.00619583, -0.01912323, -0.03988413,  0.03595274,  0.00727529,
         0.04844356,  0.04739804,  0.02836904,  0.01647964, -0.02924066,
        -0.00425701,  0.01747661,  0.0114414 ,  0.04916174,  0.02185034,
        -0.00399858,  0.03934855,  0.03666003,  0.01980535, -0.03694187,
        -0.02149243, -0.03765338]], dtype=float32)>

אנחנו יכולים לעשות את אותו הדבר עם הטמעות משתמשים:

user_id_lookup = tf.keras.layers.StringLookup()
user_id_lookup.adapt(ratings.map(lambda x: x["user_id"]))

user_id_embedding = tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32)

user_id_model = tf.keras.Sequential([user_id_lookup, user_id_embedding])
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.

מנרמל תכונות רציפות

גם תכונות מתמשכות זקוקות לנורמליזציה. לדוגמא, timestamp התכונה היא גדולה מדי כדי לשמש ישירות מודל עמוק:

for x in ratings.take(3).as_numpy_iterator():
  print(f"Timestamp: {x['timestamp']}.")
Timestamp: 879024327.
Timestamp: 875654590.
Timestamp: 882075110.

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

תְקִינָה

התקינה rescales תכונות לנרמל טווח שלהם על ידי הפחתת התכונה של ממוצע וחלוקת ידי סטיית התקן שלה. זוהי טרנספורמציה נפוצה של עיבוד מקדים.

ניתן להשיג זאת בקלות באמצעות tf.keras.layers.Normalization השכבה:

timestamp_normalization = tf.keras.layers.Normalization(
    axis=None
)
timestamp_normalization.adapt(ratings.map(lambda x: x["timestamp"]).batch(1024))

for x in ratings.take(3).as_numpy_iterator():
  print(f"Normalized timestamp: {timestamp_normalization(x['timestamp'])}.")
Normalized timestamp: [-0.84293723].
Normalized timestamp: [-1.4735204].
Normalized timestamp: [-0.27203268].

דיסקרטיזציה

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

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

max_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    tf.cast(0, tf.int64), tf.maximum).numpy().max()
min_timestamp = ratings.map(lambda x: x["timestamp"]).reduce(
    np.int64(1e9), tf.minimum).numpy().min()

timestamp_buckets = np.linspace(
    min_timestamp, max_timestamp, num=1000)

print(f"Buckets: {timestamp_buckets[:3]}")
Buckets: [8.74724710e+08 8.74743291e+08 8.74761871e+08]

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

timestamp_embedding_model = tf.keras.Sequential([
  tf.keras.layers.Discretization(timestamp_buckets.tolist()),
  tf.keras.layers.Embedding(len(timestamp_buckets) + 1, 32)
])

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():
  print(f"Timestamp embedding: {timestamp_embedding_model(timestamp)}.")
Timestamp embedding: [[-0.02532113 -0.00415025  0.00458465  0.02080876  0.03103903 -0.03746337
   0.04010465 -0.01709593 -0.00246077 -0.01220842  0.02456966 -0.04816503
   0.04552222  0.03535838  0.00769508  0.04328252  0.00869263  0.01110227
   0.02754457 -0.02659499 -0.01055292 -0.03035731  0.00463334 -0.02848787
  -0.03416766  0.02538678 -0.03446608 -0.0384447  -0.03032914 -0.02391632
   0.02637175 -0.01158618]].

עיבוד תכונות טקסט

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

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

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

Keras tf.keras.layers.TextVectorization השכבה יכולה לעשות את שני השלבים הראשונים בשבילנו:

title_text = tf.keras.layers.TextVectorization()
title_text.adapt(ratings.map(lambda x: x["movie_title"]))

בואו ננסה את זה:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):
  print(title_text(row))
tf.Tensor([[ 32 266 162   2 267 265  53]], shape=(1, 7), dtype=int64)

כל כותר מתורגם לרצף של אסימונים, אחד לכל יצירה שסימנו.

אנו יכולים לבדוק את אוצר המילים הנלמד כדי לוודא שהשכבה משתמשת בטוקניזציה הנכונה:

title_text.get_vocabulary()[40:45]
['first', '1998', '1977', '1971', 'monty']

זה נראה נכון: השכבה היא סמלית כותרות למילים בודדות.

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

מחברים את הכל ביחד

עם רכיבים אלה במקום, אנו יכולים לבנות מודל שעושה את כל העיבוד המקדים ביחד.

מודל משתמש

מודל המשתמש המלא עשוי להיראות כך:

class UserModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    self.user_embedding = tf.keras.Sequential([
        user_id_lookup,
        tf.keras.layers.Embedding(user_id_lookup.vocab_size(), 32),
    ])
    self.timestamp_embedding = tf.keras.Sequential([
      tf.keras.layers.Discretization(timestamp_buckets.tolist()),
      tf.keras.layers.Embedding(len(timestamp_buckets) + 2, 32)
    ])
    self.normalized_timestamp = tf.keras.layers.Normalization(
        axis=None
    )

  def call(self, inputs):

    # Take the input dictionary, pass it through each input layer,
    # and concatenate the result.
    return tf.concat([
        self.user_embedding(inputs["user_id"]),
        self.timestamp_embedding(inputs["timestamp"]),
        tf.reshape(self.normalized_timestamp(inputs["timestamp"]), (-1, 1))
    ], axis=1)

בואו ננסה את זה:

user_model = UserModel()

user_model.normalized_timestamp.adapt(
    ratings.map(lambda x: x["timestamp"]).batch(128))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {user_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.04705765 -0.04739009 -0.04212048]

דגם סרט

אנחנו יכולים לעשות את אותו הדבר עבור מודל הסרט:

class MovieModel(tf.keras.Model):

  def __init__(self):
    super().__init__()

    max_tokens = 10_000

    self.title_embedding = tf.keras.Sequential([
      movie_title_lookup,
      tf.keras.layers.Embedding(movie_title_lookup.vocab_size(), 32)
    ])
    self.title_text_embedding = tf.keras.Sequential([
      tf.keras.layers.TextVectorization(max_tokens=max_tokens),
      tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
      # We average the embedding of individual words to get one embedding vector
      # per title.
      tf.keras.layers.GlobalAveragePooling1D(),
    ])

  def call(self, inputs):
    return tf.concat([
        self.title_embedding(inputs["movie_title"]),
        self.title_text_embedding(inputs["movie_title"]),
    ], axis=1)

בואו ננסה את זה:

movie_model = MovieModel()

movie_model.title_text_embedding.layers[0].adapt(
    ratings.map(lambda x: x["movie_title"]))

for row in ratings.batch(1).take(1):
  print(f"Computed representations: {movie_model(row)[0, :3]}")
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
WARNING:tensorflow:vocab_size is deprecated, please use vocabulary_size.
Computed representations: [-0.01670959  0.02128791  0.04631067]

הצעדים הבאים

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