לשמור את התאריך! קלט / פלט של Google חוזר 18-20 במאי הירשם עכשיו
דף זה תורגם על ידי Cloud Translation API.
Switch to English

שימוש בתכונות צד: עיבוד מקדים של תכונות

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

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

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

לדוגמה:

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

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

  • יש לתרגם מזהים של משתמשים ופריטים להטמעה של וקטורים: ייצוגים מספריים בעלי ממדים גבוהים המותאמים במהלך האימון על מנת לסייע למודל לחזות טוב יותר את מטרתו.
  • יש לסמל טקסט גולמי (לפצל אותו לחלקים קטנים יותר כגון מילים בודדות) ולתרגמו להטמעות.
  • יש לנרמל את התכונות המספריות כך שהערכים שלהם יהיו במרווח קטן סביב 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)
{'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'}

יש כאן כמה תכונות עיקריות:

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

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

הפיכת תכונות קטגוריות לטבילות

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

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

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

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

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

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

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

import numpy as np
import tensorflow as tf

movie_title_lookup = tf.keras.layers.experimental.preprocessing.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)']

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

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

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

באמצעות גיבוב תכונות

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

אנחנו יכולים לקחת את זה לקיצוניות ההגיונית ולהסתמך לחלוטין על גיבוב תכונות, ללא אוצר מילים בכלל. זה מיושם בשכבת tf.keras.layers.experimental.preprocessing.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.experimental.preprocessing.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
)

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

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.00969323,  0.00489185, -0.04925991, -0.01755667, -0.03242903,
         0.048209  , -0.00058778,  0.03010542,  0.04593058,  0.00638406,
        -0.01864717, -0.02462925,  0.0124921 , -0.02785023,  0.00886985,
         0.02368834, -0.00750258,  0.03731059, -0.0210851 ,  0.02141182,
         0.04444833, -0.04175395,  0.03945151, -0.04986783, -0.04204438,
        -0.00832187, -0.02516021, -0.03831302, -0.04065866, -0.02181952,
        -0.01168469,  0.00944215]], dtype=float32)>

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

user_id_lookup = tf.keras.layers.experimental.preprocessing.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])

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

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

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

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

תְקִינָה

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

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

timestamp_normalization = tf.keras.layers.experimental.preprocessing.Normalization()
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.84293705]].
Normalized timestamp: [[-1.47352]].
Normalized timestamp: [[-0.27203262]].

דיסקרטיזציה

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

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

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.experimental.preprocessing.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)}.")
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f79859db950> and will run it as-is.
Cause: could not parse the source code:

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f79859db950> and will run it as-is.
Cause: could not parse the source code:

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING: AutoGraph could not transform <function <lambda> at 0x7f79859db950> and will run it as-is.
Cause: could not parse the source code:

for timestamp in ratings.take(1).map(lambda x: x["timestamp"]).batch(1).as_numpy_iterator():

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
Timestamp embedding: [[-0.02747613 -0.00615388 -0.00518906  0.03686668 -0.03123426 -0.03459657
  -0.01413486  0.02942265 -0.02695948 -0.03725258  0.01880633  0.00245643
  -0.03751732 -0.04196785 -0.03264894 -0.022715   -0.00240433 -0.04320173
  -0.03503735 -0.04935455 -0.00380387 -0.02850516  0.04652354  0.03587868
   0.04568979 -0.04698962  0.00158113  0.02843685 -0.03367974  0.02686216
   0.03084537 -0.03416919]].

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

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

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

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

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

title_text = tf.keras.layers.experimental.preprocessing.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))
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f792c070158> and will run it as-is.
Cause: could not parse the source code:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x7f792c070158> and will run it as-is.
Cause: could not parse the source code:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
WARNING: AutoGraph could not transform <function <lambda> at 0x7f792c070158> and will run it as-is.
Cause: could not parse the source code:

for row in ratings.batch(1).map(lambda x: x["movie_title"]).take(1):

This error may be avoided by creating the lambda in a standalone statement.

To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
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.experimental.preprocessing.Discretization(timestamp_buckets.tolist()),
      tf.keras.layers.Embedding(len(timestamp_buckets) + 2, 32)
    ])
    self.normalized_timestamp = tf.keras.layers.experimental.preprocessing.Normalization()

  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"]),
        self.normalized_timestamp(inputs["timestamp"])
    ], 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]}")
Computed representations: [-0.02975558  0.03509596 -0.02994236]

דוגמנית קולנוע

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

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.experimental.preprocessing.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]}")
Computed representations: [-0.02882576 -0.00666581 -0.00883247]

הצעדים הבאים

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