RSVP pour votre événement TensorFlow Everywhere local dès aujourd'hui!
Cette page a été traduite par l'API Cloud Translation.
Switch to English

Masquage et rembourrage avec Keras

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher la source sur GitHub Télécharger le cahier

Installer

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

introduction

Le masquage est un moyen d'indiquer aux couches de traitement de séquence que certains pas de temps dans une entrée sont manquants et doivent donc être ignorés lors du traitement des données.

Le remplissage est une forme spéciale de masquage où les étapes masquées sont au début ou à la fin d'une séquence. Le remplissage provient de la nécessité de coder les données de séquence en lots contigus: pour que toutes les séquences d'un lot correspondent à une longueur standard donnée, il est nécessaire de compléter ou de tronquer certaines séquences.

Regardons de plus près.

Données de séquence de remplissage

Lors du traitement des données de séquence, il est très courant que des échantillons individuels aient des longueurs différentes. Prenons l'exemple suivant (texte symbolisé par des mots):

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

Après la recherche de vocabulaire, les données peuvent être vectorisées sous forme d'entiers, par exemple:

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

Les données sont une liste imbriquée dans laquelle les échantillons individuels ont respectivement des longueurs de 3, 5 et 6. Étant donné que les données d'entrée pour un modèle d'apprentissage en profondeur doivent être un seul tenseur (de forme par exemple (batch_size, 6, vocab_size) dans ce cas), les échantillons qui sont plus courts que l'élément le plus long doivent être remplis avec une valeur d'espace réservé (alternativement, un peut également tronquer les échantillons longs avant de compléter les échantillons courts).

Keras fournit une fonction utilitaire pour tronquer et tf.keras.preprocessing.sequence.pad_sequences listes Python à une longueur commune: 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]]

Masquage

Maintenant que tous les échantillons ont une longueur uniforme, le modèle doit être informé qu'une partie des données est en fait un remplissage et doit être ignorée. Ce mécanisme masque .

Il existe trois façons d'introduire des masques de saisie dans les modèles Keras:

  • Ajoutez un calque keras.layers.Masking .
  • Configurez un calque keras.layers.Embedding avec mask_zero=True .
  • Passez un argument de mask manuellement lors de l'appel des couches qui prennent en charge cet argument (par exemple, les couches RNN).

Calques générant des masques: Embedding et Masking

Sous le capot, ces couches créeront un tenseur de masque (tenseur 2D avec forme (batch, sequence_length) ) et l'attacheront à la sortie du tenseur renvoyée par le calque Masking ou 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)

Comme vous pouvez le voir sur le résultat imprimé, le masque est un tenseur booléen 2D avec forme (batch_size, sequence_length) , où chaque entrée individuelle False indique que le pas de temps correspondant doit être ignoré pendant le traitement.

Propagation de masque dans l'API fonctionnelle et l'API séquentielle

Lors de l'utilisation de l'API fonctionnelle ou de l'API séquentielle, un masque généré par une couche d' Embedding ou de Masking sera propagé à travers le réseau pour toute couche capable de les utiliser (par exemple, les couches RNN). Keras récupérera automatiquement le masque correspondant à une entrée et le transmettra à tout calque sachant l'utiliser.

Par exemple, dans le modèle séquentiel suivant, le calque LSTM recevra automatiquement un masque, ce qui signifie qu'il ignorera les valeurs remplies:

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

C'est également le cas pour le modèle d'API fonctionnelle suivant:

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)

Passer des tenseurs de masque directement aux calques

Les couches qui peuvent gérer les masques (comme la couche LSTM ) ont un argument de mask dans leur méthode __call__ .

Pendant ce temps, les couches qui produisent un masque (par exemple Embedding ) exposent une compute_mask(input, previous_mask) que vous pouvez appeler.

Ainsi, vous pouvez passer la sortie de la méthode compute_mask() d'un calque produisant un masque à la méthode __call__ d'un calque consommateur de masque, comme ceci:

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

Prise en charge du masquage dans vos calques personnalisés

Parfois, vous devrez peut-être écrire des calques qui génèrent un masque (comme l' Embedding ) ou des calques qui doivent modifier le masque actuel.

Par exemple, tout calque qui produit un tenseur avec une dimension temporelle différente de celle de son entrée, comme un calque Concatenate qui concatène sur la dimension temporelle, devra modifier le masque actuel afin que les calques en aval soient en mesure de prendre correctement les pas de temps masqués dans Compte.

Pour ce faire, votre calque doit implémenter la méthode layer.compute_mask() , qui produit un nouveau masque étant donné l'entrée et le masque courant.

Voici un exemple de calque TemporalSplit qui doit modifier le masque actuel.

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)

Voici un autre exemple de calque CustomEmbedding capable de générer un masque à partir de valeurs d'entrée:

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)

Activation pour masquer la propagation sur des couches compatibles

La plupart des calques ne modifient pas la dimension temporelle, il n'est donc pas nécessaire de modifier le masque actuel. Cependant, ils peuvent toujours souhaiter pouvoir propager le masque actuel, inchangé, au calque suivant. Il s'agit d'un comportement opt-in. Par défaut, un calque personnalisé détruira le masque actuel (puisque le framework n'a aucun moyen de dire si la propagation du masque est sûre).

Si vous disposez d'un calque personnalisé qui ne modifie pas la dimension temporelle et si vous souhaitez qu'il puisse propager le masque de saisie actuel, vous devez définir self.supports_masking = True dans le constructeur de calque. Dans ce cas, le comportement par défaut de compute_mask() est de simplement passer le masque actuel.

Voici un exemple de couche qui est sur la liste blanche pour la propagation de masque:

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)

Vous pouvez maintenant utiliser ce calque personnalisé entre un calque générateur de masque (comme Embedding ) et un calque consommateur de masque (comme LSTM ), et il transmettra le masque pour qu'il atteigne le calque consommateur de masque.

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

Écriture de calques nécessitant des informations de masque

Certains calques sont des consommateurs de masque: ils acceptent un argument de mask dans l' call et l'utilisent pour déterminer s'il faut sauter certains pas de temps.

Pour écrire un tel calque, vous pouvez simplement ajouter un argument mask=None dans votre signature d' call . Le masque associé aux entrées sera transmis à votre calque dès qu'il sera disponible.

Voici un exemple simple ci-dessous: un calque qui calcule un softmax sur la dimension temporelle (axe 1) d'une séquence d'entrée, tout en ignorant les pas temporels masqués.

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

Résumé

C'est tout ce que vous devez savoir sur le remplissage et le masquage dans Keras. Récapituler:

  • Le «masquage» permet aux calques de savoir quand ignorer / ignorer certains pas de temps dans les entrées de séquence.
  • Certains calques sont des générateurs de masque: l' Embedding peut générer un masque à partir des valeurs d'entrée (si mask_zero=True ), de même que le calque de Masking .
  • Certains calques sont des consommateurs de masque: ils exposent un argument de mask dans leur méthode __call__ . C'est le cas des couches RNN.
  • Dans l'API fonctionnelle et l'API séquentielle, les informations de masque sont propagées automatiquement.
  • Lorsque vous utilisez des calques de manière autonome, vous pouvez transmettre manuellement les arguments de mask aux calques.
  • Vous pouvez facilement écrire des calques qui modifient le masque actuel, qui génèrent un nouveau masque ou qui consomment le masque associé aux entrées.