מיסוך וריפוד עם קרס

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

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

להכין

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

מבוא

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

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

בואו נסתכל מקרוב.

ריפוד נתוני רצף

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

[
  ["Hello", "world", "!"],
  ["How", "are", "you", "doing", "today"],
  ["The", "weather", "will", "be", "nice", "tomorrow"],
]

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

[
  [71, 1331, 4231]
  [73, 8, 3215, 55, 927],
  [83, 91, 1, 645, 1253, 927],
]

הנתונים הם רשימה מקוננת שבה לדוגמאות בודדות יש אורך 3, 5 ו-6, בהתאמה. מאז נתוני הקלט עבור מודל למידה עמוקה חייב להיות מותח בודד (של למשל הצורה (batch_size, 6, vocab_size) במקרה הזה), דגימות כי הם קצרים יותר מאשר הצורך פריט הארוך כדי להיות מרופד עם כמה ערך מציין מיקום (לחילופין, אחד עשוי גם לקטוע דגימות ארוכות לפני ריפוד דגימות קצרות).

Keras מספק פונקציית תועלת לרשימות Python לקטום ו כרית באורך נפוץ: tf.keras.preprocessing.sequence.pad_sequences .

raw_inputs = [
    [711, 632, 71],
    [73, 8, 3215, 55, 927],
    [83, 91, 1, 645, 1253, 927],
]

# By default, this will pad using 0s; it is configurable via the
# "value" parameter.
# Note that you could "pre" padding (at the beginning) or
# "post" padding (at the end).
# We recommend using "post" padding when working with RNN layers
# (in order to be able to use the
# CuDNN implementation of the layers).
padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(
    raw_inputs, padding="post"
)
print(padded_inputs)
[[ 711  632   71    0    0    0]
 [  73    8 3215   55  927    0]
 [  83   91    1  645 1253  927]]

מִסוּך

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

ישנן שלוש דרכים להציג מסכות קלט בדגמי Keras:

  • הוספת keras.layers.Masking שכבה.
  • הגדרת keras.layers.Embedding שכבה עם mask_zero=True .
  • עבר ערך mask טיעון ידני כאשר קוראים שכבות שתומכות בטיעון הזה (למשל שכבות RNN).

שכבות מסכת מניבה: Embedding ו Masking

מתחת למכסה מנוע, שכבות אלה תיצורנה מותח מסכה (מותח 2D עם צורה (batch, sequence_length) ), וצרפו אותו הפלט המותח שהוחזר על ידי Masking או Embedding השכבה.

embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)
masked_output = embedding(padded_inputs)

print(masked_output._keras_mask)

masking_layer = layers.Masking()
# Simulate the embedding lookup by expanding the 2D input to 3D,
# with embedding dimension of 10.
unmasked_embedding = tf.cast(
    tf.tile(tf.expand_dims(padded_inputs, axis=-1), [1, 1, 10]), tf.float32
)

masked_embedding = masking_layer(unmasked_embedding)
print(masked_embedding._keras_mask)
tf.Tensor(
[[ True  True  True False False False]
 [ True  True  True  True  True False]
 [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)
tf.Tensor(
[[ True  True  True False False False]
 [ True  True  True  True  True False]
 [ True  True  True  True  True  True]], shape=(3, 6), dtype=bool)

כפי שניתן לראות מן התוצאה המודפסת, את המסכה היא מותח בוליאני 2D עם הצורה (batch_size, sequence_length) , שבו כל אדם False ערך מציין כי timestep המתאים צריך להיות מובא בחשבון במהלך עיבוד.

הפצת מסכות ב-Functional API ו-Sequential API

בעת שימוש בממשק ה- Functional או ה- API סדרתית, מסכה שנוצר על ידי Embedding או Masking שכבת יהיה מופצות דרך הרשת עבור כל שכבת כי הוא מסוגל להשתמש בהם (למשל, שכבות RNN). Keras תביא אוטומטית את המסכה המתאימה לקלט ותעביר אותה לכל שכבה שיודעת להשתמש בה.

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

model = keras.Sequential(
    [layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True), layers.LSTM(32),]
)

זה גם המקרה עבור מודל ה-API הפונקציונלי הבא:

inputs = keras.Input(shape=(None,), dtype="int32")
x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)
outputs = layers.LSTM(32)(x)

model = keras.Model(inputs, outputs)

העברת חותכי מסכות ישירות לשכבות

שכבות שיכולות לטפל מסכות (כגון LSTM שכבה) יש mask טיעון שלהם __call__ השיטה.

בינתיים, שכבות המייצרים מסכה (למשל Embedding ) לחשוף compute_mask(input, previous_mask) שיטה שבה אתה יכול לקרוא.

לפיכך, אתה יכול להעביר את הפלט של compute_mask() השיטה של שכבת מייצרי מסכה אל __call__ שיטת שכבה רב-מסכה, כמו זה:

class MyLayer(layers.Layer):
    def __init__(self, **kwargs):
        super(MyLayer, self).__init__(**kwargs)
        self.embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)
        self.lstm = layers.LSTM(32)

    def call(self, inputs):
        x = self.embedding(inputs)
        # Note that you could also prepare a `mask` tensor manually.
        # It only needs to be a boolean tensor
        # with the right shape, i.e. (batch_size, timesteps).
        mask = self.embedding.compute_mask(inputs)
        output = self.lstm(x, mask=mask)  # The layer will ignore the masked values
        return output


layer = MyLayer()
x = np.random.random((32, 10)) * 100
x = x.astype("int32")
layer(x)
<tf.Tensor: shape=(32, 32), dtype=float32, numpy=
array([[-3.6287602e-04,  8.8942451e-03, -4.5623952e-03, ...,
         3.6509466e-04, -4.3871473e-03, -1.7532009e-03],
       [ 2.6261162e-03, -2.5420082e-03,  7.6517118e-03, ...,
         5.8210879e-03, -1.5617531e-03, -1.7562184e-03],
       [ 6.8687932e-03,  1.2330032e-03, -1.2028826e-02, ...,
         2.0486799e-03,  5.7172528e-03,  2.6641595e-03],
       ...,
       [-3.4327951e-04,  1.3967649e-03, -1.2102776e-02, ...,
         3.8406218e-03, -2.3374180e-03, -4.9669710e-03],
       [-2.3023323e-03,  1.8474255e-03,  2.7329330e-05, ...,
         6.1798934e-03,  4.2709545e-04,  3.9026213e-03],
       [ 7.4090287e-03,  1.9879336e-03, -2.0261200e-03, ...,
         8.2100276e-03,  8.7051848e-03,  9.9167246e-03]], dtype=float32)>

תמיכה במיסוך בשכבות המותאמות אישית שלך

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

למשל, כול שכבה שמייצרת מותח עם ממד זמן שונה הקלט שלה, כגון Concatenate שכבה שמשרשר על ממד הזמן, יצטרך לשנות את המסכה הנוכחית, כך שכבות במורד זרם תוכלנה לקחת timesteps רעול פנים כראוי לתוך חֶשְׁבּוֹן.

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

הנה דוגמא של TemporalSplit שכבה שצריכה לשנות את המסכה הנוכחית.

class TemporalSplit(keras.layers.Layer):
    """Split the input tensor into 2 tensors along the time dimension."""

    def call(self, inputs):
        # Expect the input to be 3D and mask to be 2D, split the input tensor into 2
        # subtensors along the time axis (axis 1).
        return tf.split(inputs, 2, axis=1)

    def compute_mask(self, inputs, mask=None):
        # Also split the mask into 2 if it presents.
        if mask is None:
            return None
        return tf.split(mask, 2, axis=1)


first_half, second_half = TemporalSplit()(masked_embedding)
print(first_half._keras_mask)
print(second_half._keras_mask)
tf.Tensor(
[[ True  True  True]
 [ True  True  True]
 [ True  True  True]], shape=(3, 3), dtype=bool)
tf.Tensor(
[[False False False]
 [ True  True False]
 [ True  True  True]], shape=(3, 3), dtype=bool)

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

class CustomEmbedding(keras.layers.Layer):
    def __init__(self, input_dim, output_dim, mask_zero=False, **kwargs):
        super(CustomEmbedding, self).__init__(**kwargs)
        self.input_dim = input_dim
        self.output_dim = output_dim
        self.mask_zero = mask_zero

    def build(self, input_shape):
        self.embeddings = self.add_weight(
            shape=(self.input_dim, self.output_dim),
            initializer="random_normal",
            dtype="float32",
        )

    def call(self, inputs):
        return tf.nn.embedding_lookup(self.embeddings, inputs)

    def compute_mask(self, inputs, mask=None):
        if not self.mask_zero:
            return None
        return tf.not_equal(inputs, 0)


layer = CustomEmbedding(10, 32, mask_zero=True)
x = np.random.random((3, 10)) * 9
x = x.astype("int32")

y = layer(x)
mask = layer.compute_mask(x)

print(mask)
tf.Tensor(
[[ True  True  True  True  True  True  True  True  True  True]
 [ True  True  True  True  True False  True  True  True  True]
 [ True  True  True  True  True  True  True  True False  True]], shape=(3, 10), dtype=bool)

הסכמה להפצת מסיכה בשכבות תואמות

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

אם יש לך שכבת מנהג שאינו לשנות את ממד הזמן, ואם אתה רוצה את זה כדי להיות מסוגל להפיץ את מסכת ההזנה הנוכחית, אתה צריך להגדיר self.supports_masking = True בנאי השכבה. במקרה זה, את התנהגות ברירת המחדל של compute_mask() היא פשוט להעביר את המסכה הנוכחית דרך.

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

class MyActivation(keras.layers.Layer):
    def __init__(self, **kwargs):
        super(MyActivation, self).__init__(**kwargs)
        # Signal that the layer is safe for mask propagation
        self.supports_masking = True

    def call(self, inputs):
        return tf.nn.relu(inputs)

כעת תוכל להשתמש בשכבת המנהג הזה-בין שכבה מניבות מסכה (כמו Embedding ) ושכבה רב-מסכה (כמו LSTM ), וזה יעבור את המסכה יחד, כך שהוא מגיע לשכבה רב-מסכה.

inputs = keras.Input(shape=(None,), dtype="int32")
x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)
x = MyActivation()(x)  # Will pass the mask along
print("Mask found:", x._keras_mask)
outputs = layers.LSTM(32)(x)  # Will receive the mask

model = keras.Model(inputs, outputs)
Mask found: KerasTensor(type_spec=TensorSpec(shape=(None, None), dtype=tf.bool, name=None), name='Placeholder_1:0')

כתיבת שכבות שצריכות מידע על מסיכה

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

כדי לכתוב שכבה כזה, אתה יכול פשוט להוסיף mask=None טיעון שלך call החתימה. המסכה המשויכת לכניסות תועבר לשכבה שלך בכל פעם שהיא זמינה.

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

class TemporalSoftmax(keras.layers.Layer):
    def call(self, inputs, mask=None):
        broadcast_float_mask = tf.expand_dims(tf.cast(mask, "float32"), -1)
        inputs_exp = tf.exp(inputs) * broadcast_float_mask
        inputs_sum = tf.reduce_sum(
            inputs_exp * broadcast_float_mask, axis=-1, keepdims=True
        )
        return inputs_exp / inputs_sum


inputs = keras.Input(shape=(None,), dtype="int32")
x = layers.Embedding(input_dim=10, output_dim=32, mask_zero=True)(inputs)
x = layers.Dense(1)(x)
outputs = TemporalSoftmax()(x)

model = keras.Model(inputs, outputs)
y = model(np.random.randint(0, 10, size=(32, 100)), np.random.random((32, 100, 1)))

סיכום

זה כל מה שאתה צריך לדעת על ריפוד ומיסוך ב-Keras. לסיכום:

  • "מיסוך" הוא האופן שבו שכבות מסוגלות לדעת מתי לדלג / להתעלם משלבי זמן מסוימים בקלט רצף.
  • שכבות חלקם-גנרטורים מסכה: Embedding יכולה ליצור מסכה מן ערכי קלט (אם mask_zero=True ), ועל כך יכולה Masking השכבה.
  • שכבות חלקם-צרכני מסכה: הם חושפים mask טיעון שלהם __call__ השיטה. זה המקרה עבור שכבות RNN.
  • ב-Functional API וב-Sequential API, מידע המסיכה מופץ באופן אוטומטי.
  • בעת שימוש בשכבות באופן עצמאי, אתה יכול לעבור את mask טיעונים לשכבות ידנית.
  • אתה יכול בקלות לכתוב שכבות שמשנות את המסכה הנוכחית, שמייצרות מסיכה חדשה, או שצורכות את המסכה המשויכת לכניסות.