استخدام الميزات الجانبية: ميزة المعالجة المسبقة

عرض على TensorFlow.org تشغيل في Google Colab عرض المصدر على جيثب تحميل دفتر

واحدة من المزايا العظيمة لاستخدام إطار عمل التعلم العميق لبناء نماذج التوصية هي حرية بناء تمثيلات غنية ومرنة للميزات.

تتمثل الخطوة الأولى في القيام بذلك في إعداد الميزات ، حيث لن تكون الميزات الأولية عادةً قابلة للاستخدام على الفور في النموذج.

فمثلا:

  • قد تكون معرّفات المستخدمين والعناصر سلاسل (عناوين ، أسماء مستخدمين) أو أعداد صحيحة كبيرة غير متجاورة (معرّفات قاعدة البيانات).
  • يمكن أن تكون أوصاف العنصر نصًا خامًا.
  • يمكن أن تكون الطوابع الزمنية للتفاعل طوابع زمنية أولية لـ Unix.

يجب أن يتم تحويلها بشكل مناسب حتى تكون مفيدة في نماذج البناء:

  • يجب ترجمة معرّفات المستخدمين والعناصر إلى متجهات تضمين: تمثيلات رقمية عالية الأبعاد يتم تعديلها أثناء التدريب لمساعدة النموذج على التنبؤ بهدفه بشكل أفضل.
  • يجب ترميز النص الخام (تقسيمه إلى أجزاء أصغر مثل الكلمات الفردية) وترجمته إلى حفلات الزفاف.
  • تحتاج الميزات العددية إلى التطبيع بحيث تقع قيمها في فاصل زمني صغير حول 0.

لحسن الحظ ، باستخدام TensorFlow ، يمكننا أن نجعل مثل هذه المعالجة المسبقة جزءًا من نموذجنا بدلاً من خطوة معالجة مسبقة منفصلة. هذا ليس مريحًا فحسب ، ولكنه يضمن أيضًا أن المعالجة المسبقة لدينا هي نفسها تمامًا أثناء التدريب وأثناء الخدمة. وهذا يجعل نشر النماذج التي تتضمن معالجة مسبقة معقدة للغاية أمرًا آمنًا وسهلاً.

في هذا البرنامج التعليمي، ونحن نذهب إلى التركيز على recommenders وتجهيزها يتعين علينا القيام به على بيانات 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. وبالتالي ، إذا كان لدينا عدد كافٍ من هذه المؤشرات ، فيجب أن يكون النموذج قادرًا على التدريب بالإضافة إلى نموذج بمفردات واضحة دون ازدراء الاضطرار إلى الاحتفاظ بقائمة الرموز.

يمكننا أن نأخذ هذا إلى أقصى الحدود المنطقية والاعتماد كليًا على تجزئة الميزة ، بدون أي مفردات على الإطلاق. ويتم تنفيذ ذلك في 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']

يبدو هذا صحيحًا: تقوم الطبقة بترميز العناوين إلى كلمات فردية.

لإنهاء المعالجة ، نحتاج الآن إلى تضمين النص. لأن كل عنوان يحتوي على كلمات متعددة ، فسنحصل على عدة حفلات زفاف لكل عنوان. لاستخدامها في نموذج دونوستري ، يتم ضغطها عادةً في دمج واحد. نماذج مثل RNNs أو Transformers مفيدة هنا ، ولكن حساب متوسط ​​كل كلمات الزخارف معًا يعد نقطة انطلاق جيدة.

ضع كل شيء معا

مع وجود هذه المكونات في مكانها الصحيح ، يمكننا بناء نموذج يقوم بكل عمليات المعالجة المسبقة معًا.

نموذج المستخدم

قد يبدو نموذج المستخدم الكامل كما يلي:

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]

الخطوات التالية

من خلال النموذجين أعلاه ، اتخذنا الخطوات الأولى لتمثيل الميزات الغنية في نموذج التوصية: للمضي قدمًا في ذلك واستكشاف كيف يمكن استخدامهما لبناء نموذج Recomender عميق فعال ، ألق نظرة على البرنامج التعليمي Deep التوصية.