¡Confirme su asistencia a su evento local de TensorFlow Everywhere hoy!
Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Enmascaramiento y acolchado con Keras

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Configuración

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

Introducción

El enmascaramiento es una forma de decirle a las capas de procesamiento de secuencias que faltan ciertos pasos de tiempo en una entrada y, por lo tanto, deben omitirse al procesar los datos.

El relleno es una forma especial de enmascaramiento donde los pasos enmascarados están al principio o al final de una secuencia. El relleno proviene de la necesidad de codificar datos de secuencia en lotes contiguos: para que todas las secuencias de un lote se ajusten a una longitud estándar determinada, es necesario rellenar o truncar algunas secuencias.

Echemos un vistazo más de cerca.

Datos de secuencia de relleno

Al procesar datos de secuencia, es muy común que las muestras individuales tengan diferentes longitudes. Considere el siguiente ejemplo (texto tokenizado como palabras):

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

Después de la búsqueda de vocabulario, los datos se pueden vectorizar como números enteros, por ejemplo:

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

Los datos son una lista anidada donde las muestras individuales tienen una longitud de 3, 5 y 6, respectivamente. Dado que los datos de entrada para un modelo de aprendizaje profundo deben ser un solo tensor (de forma, por ejemplo, (batch_size, 6, vocab_size) en este caso), las muestras que son más cortas que el elemento más largo deben rellenarse con algún valor de marcador de posición (alternativamente, uno también puede truncar muestras largas antes de rellenar muestras cortas).

Keras proporciona una función de utilidad para truncar y rellenar listas de Python a una longitud común: 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]]

Enmascaramiento

Ahora que todas las muestras tienen una longitud uniforme, se debe informar al modelo de que una parte de los datos en realidad se está rellenando y se debe ignorar. Ese mecanismo está enmascarando .

Hay tres formas de introducir máscaras de entrada en los modelos de Keras:

  • Agrega una capa de keras.layers.Masking .
  • Configure una capa keras.layers.Embedding con mask_zero=True .
  • Pase un argumento de mask manualmente cuando llame a capas que admitan este argumento (por ejemplo, capas RNN).

Capas generadoras de máscaras: Embedding y Masking

Bajo el capó, estas capas crearán un tensor de máscara (tensor 2D con forma (batch, sequence_length) ) y lo adjuntarán a la salida del tensor devuelta por la capa Masking o 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)

Como puede ver en el resultado impreso, la máscara es un tensor booleano 2D con forma (batch_size, sequence_length) , donde cada entrada False individual indica que el paso de tiempo correspondiente debe ignorarse durante el procesamiento.

Propagación de máscara en la API funcional y la API secuencial

Cuando se utiliza la API funcional o la API secuencial, una máscara generada por una capa de Embedding o Masking se propagará a través de la red para cualquier capa que sea capaz de usarlas (por ejemplo, capas RNN). Keras buscará automáticamente la máscara correspondiente a una entrada y la pasará a cualquier capa que sepa cómo usarla.

Por ejemplo, en el siguiente modelo secuencial, la capa LSTM recibirá automáticamente una máscara, lo que significa que ignorará los valores rellenados:

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

Este también es el caso del siguiente modelo de API funcional:

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)

Pasar tensores de máscara directamente a las capas

Las capas que pueden manejar máscaras (como la capa LSTM ) tienen un argumento de mask en su método __call__ .

Mientras tanto, las capas que producen una máscara (por ejemplo, Embedding ) exponen un compute_mask(input, previous_mask) que puede llamar.

Por lo tanto, puede pasar la salida del método compute_mask() de una capa que produce __call__ método __call__ de una capa que consume máscaras, así:

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([[-0.0071368 ,  0.00202324,  0.00393163, ..., -0.00365972,
        -0.00194294, -0.00275828],
       [ 0.00865301, -0.00411554, -0.00328279, ...,  0.00395685,
         0.01023738, -0.0013066 ],
       [ 0.0115475 , -0.00367757, -0.0049072 , ...,  0.00312295,
         0.00557074,  0.00681297],
       ...,
       [ 0.00537544, -0.00517081,  0.00668133, ...,  0.00428408,
         0.00251086, -0.00211114],
       [ 0.00286667, -0.00301991, -0.0095289 , ...,  0.00381294,
         0.00675705, -0.00599195],
       [-0.0045211 ,  0.0019338 , -0.00031986, ...,  0.00275819,
        -0.00126366, -0.00347176]], dtype=float32)>

Apoyar el enmascaramiento en sus capas personalizadas

A veces, es posible que deba escribir capas que generen una máscara (como Embedding ) o capas que necesiten modificar la máscara actual.

Por ejemplo, cualquier capa que produzca un tensor con una dimensión de tiempo diferente a su entrada, como una capa Concatenate que se concatena en la dimensión de tiempo, deberá modificar la máscara actual para que las capas posteriores puedan tomar pasos de tiempo enmascarados correctamente en cuenta.

Para hacer esto, su capa debe implementar el método layer.compute_mask() , que produce una nueva máscara dada la entrada y la máscara actual.

Aquí hay un ejemplo de una capa TemporalSplit que necesita modificar la máscara actual.

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)

Aquí hay otro ejemplo de una capa CustomEmbedding que es capaz de generar una máscara a partir de valores de entrada:

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 False  True]
 [ True  True  True  True  True  True  True  True  True  True]
 [ True  True  True False  True  True  True  True  True  True]], shape=(3, 10), dtype=bool)

Optar por enmascarar la propagación en capas compatibles

La mayoría de las capas no modifican la dimensión de tiempo, por lo que no es necesario modificar la máscara actual. Sin embargo, es posible que aún deseen poder propagar la máscara actual, sin cambios, a la siguiente capa. Este es un comportamiento de suscripción voluntaria. De forma predeterminada, una capa personalizada destruirá la máscara actual (ya que el marco no tiene forma de saber si es seguro propagar la máscara).

Si tiene una capa personalizada que no modifica la dimensión de tiempo y desea que pueda propagar la máscara de entrada actual, debe establecer self.supports_masking = True en el constructor de la capa. En este caso, el comportamiento predeterminado de compute_mask() es simplemente pasar la máscara actual.

A continuación, se muestra un ejemplo de una capa que está incluida en la lista blanca para la propagación de máscaras:

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)

Ahora puede usar esta capa personalizada entre una capa generadora de máscaras (como Embedding ) y una capa que consume máscaras (como LSTM ), y pasará la máscara para que alcance la capa que consume máscaras.

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

Escribir capas que necesitan información de máscara

Algunas capas son consumidores de máscaras: aceptan un argumento de mask en la call y lo usan para determinar si deben omitir ciertos pasos de tiempo.

Para escribir una capa de este tipo, simplemente puede agregar un argumento mask=None en su firma de call . La máscara asociada con las entradas se pasará a su capa siempre que esté disponible.

Aquí hay un ejemplo simple a continuación: una capa que calcula un softmax sobre la dimensión de tiempo (eje 1) de una secuencia de entrada, mientras descarta pasos de tiempo enmascarados.

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

Resumen

Eso es todo lo que necesita saber sobre el acolchado y el enmascaramiento en Keras. Recordar:

  • "Enmascaramiento" es la forma en que las capas pueden saber cuándo omitir / ignorar ciertos pasos de tiempo en las entradas de secuencia.
  • Algunas capas son máscara-generadores: Embedding puede generar una máscara a partir de los valores de entrada (si mask_zero=True ), y así puede el Masking capa.
  • Algunas capas son consumidores de máscaras: exponen un argumento de mask en su método __call__ . Este es el caso de las capas RNN.
  • En la API funcional y la API secuencial, la información de la máscara se propaga automáticamente.
  • Al usar capas de forma independiente, puede pasar los argumentos de la mask a las capas manualmente.
  • Puede escribir fácilmente capas que modifiquen la máscara actual, que generen una nueva máscara o que consuman la máscara asociada con las entradas.