Réseaux de neurones récurrents (RNN) avec Keras

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

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

introduction

Les réseaux de neurones récurrents (RNN) sont une classe de réseaux de neurones puissants pour modéliser des données de séquence telles que les séries chronologiques ou le langage naturel.

Schématiquement, une couche RNN utilise un for boucle pour itérer sur les pas de temps d'une séquence, tout en maintenant un état interne qui code pour des informations sur les pas de temps qu'il a vu jusqu'à présent.

L'API Keras RNN est conçue en mettant l'accent sur :

  • Facilité d'utilisation: le haut- keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU couches vous permettent de construire rapidement des modèles récurrents , sans avoir à faire des choix de configuration difficiles.

  • Facilité de personnalisation: Vous pouvez également définir votre propre couche de cellules RNN (la partie intérieure de la for la boucle) avec un comportement personnalisé, et l' utiliser avec le générique keras.layers.RNN couche (la for boucle elle - même). Cela vous permet de prototyper rapidement différentes idées de recherche de manière flexible avec un minimum de code.

Installer

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

Couches RNN intégrées : un exemple simple

Il existe trois couches RNN intégrées dans Keras :

  1. keras.layers.SimpleRNN , un RNN entièrement connecté , où le signal de sortie timestep précédente doit être alimentée au prochain pas de temps.

  2. keras.layers.GRU , d' abord proposé dans Cho et al., 2014 .

  3. keras.layers.LSTM , d' abord proposé dans Hochreiter & Schmidhuber, 1997 .

Au début de 2015, Keras avait les premières implémentations Python open source réutilisables de LSTM et GRU.

Voici un exemple d'un simple Sequential modèle qui traite les séquences de nombres entiers, chaque entier incorpore dans un vecteur 64 dimensions, puis traite la séquence de vecteurs en utilisant une LSTM couche.

model = keras.Sequential()
# Add an Embedding layer expecting input vocab of size 1000, and
# output embedding dimension of size 64.
model.add(layers.Embedding(input_dim=1000, output_dim=64))

# Add a LSTM layer with 128 internal units.
model.add(layers.LSTM(128))

# Add a Dense layer with 10 units.
model.add(layers.Dense(10))

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 64)          64000     
_________________________________________________________________
lstm (LSTM)                  (None, 128)               98816     
_________________________________________________________________
dense (Dense)                (None, 10)                1290      
=================================================================
Total params: 164,106
Trainable params: 164,106
Non-trainable params: 0
_________________________________________________________________

Les RNN intégrés prennent en charge un certain nombre de fonctionnalités utiles :

  • Abandon récurrent, par les dropout et recurrent_dropout arguments
  • Capacité à traiter une séquence d'entrée dans le sens inverse, par l'intermédiaire du go_backwards thèse
  • Boucle de déroulement (qui peut conduire à une grande accélération lors du traitement des séquences courtes sur CPU), par l'intermédiaire du unroll thèse
  • ...et plus.

Pour plus d' informations, consultez la documentation de l' API RNN .

Sorties et états

Par défaut, la sortie d'une couche RNN contient un seul vecteur par échantillon. Ce vecteur est la sortie de la cellule RNN correspondant au dernier pas de temps, contenant des informations sur l'ensemble de la séquence d'entrée. La forme de cette sortie est (batch_size, units) où des units correspond à l' units argument passé au constructeur de la couche.

Une couche RNN peut également renvoyer toute la séquence de sorties pour chaque échantillon (par un vecteur timestep par échantillon), si vous avez défini return_sequences=True . La forme de cette sortie est (batch_size, timesteps, units) pas de (batch_size, timesteps, units) les (batch_size, timesteps, units) .

model = keras.Sequential()
model.add(layers.Embedding(input_dim=1000, output_dim=64))

# The output of GRU will be a 3D tensor of shape (batch_size, timesteps, 256)
model.add(layers.GRU(256, return_sequences=True))

# The output of SimpleRNN will be a 2D tensor of shape (batch_size, 128)
model.add(layers.SimpleRNN(128))

model.add(layers.Dense(10))

model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, None, 64)          64000     
_________________________________________________________________
gru (GRU)                    (None, None, 256)         247296    
_________________________________________________________________
simple_rnn (SimpleRNN)       (None, 128)               49280     
_________________________________________________________________
dense_1 (Dense)              (None, 10)                1290      
=================================================================
Total params: 361,866
Trainable params: 361,866
Non-trainable params: 0
_________________________________________________________________

De plus, une couche RNN peut renvoyer son ou ses états internes finaux. Les états retournés peuvent être utilisés pour reprendre l'exécution RNN plus tard, ou pour initialiser un autre RNN . Ce paramètre est couramment utilisé dans le modèle séquence à séquence codeur-décodeur, où l'état final du codeur est utilisé comme état initial du décodeur.

Pour configurer une couche RNN pour retourner son état interne, réglez le return_state paramètre à True lors de la création de la couche. Notez que LSTM a 2 tenseurs de l' État, mais GRU n'a qu'un seul.

Pour configurer l'état initial de la couche, il suffit d' appeler la couche avec l' argument mot - clé supplémentaire initial_state . Notez que la forme de l'état doit correspondre à la taille unitaire du calque, comme dans l'exemple ci-dessous.

encoder_vocab = 1000
decoder_vocab = 2000

encoder_input = layers.Input(shape=(None,))
encoder_embedded = layers.Embedding(input_dim=encoder_vocab, output_dim=64)(
    encoder_input
)

# Return states in addition to output
output, state_h, state_c = layers.LSTM(64, return_state=True, name="encoder")(
    encoder_embedded
)
encoder_state = [state_h, state_c]

decoder_input = layers.Input(shape=(None,))
decoder_embedded = layers.Embedding(input_dim=decoder_vocab, output_dim=64)(
    decoder_input
)

# Pass the 2 states to a new LSTM layer, as initial state
decoder_output = layers.LSTM(64, name="decoder")(
    decoder_embedded, initial_state=encoder_state
)
output = layers.Dense(10)(decoder_output)

model = keras.Model([encoder_input, decoder_input], output)
model.summary()
Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
input_2 (InputLayer)            [(None, None)]       0                                            
__________________________________________________________________________________________________
embedding_2 (Embedding)         (None, None, 64)     64000       input_1[0][0]                    
__________________________________________________________________________________________________
embedding_3 (Embedding)         (None, None, 64)     128000      input_2[0][0]                    
__________________________________________________________________________________________________
encoder (LSTM)                  [(None, 64), (None,  33024       embedding_2[0][0]                
__________________________________________________________________________________________________
decoder (LSTM)                  (None, 64)           33024       embedding_3[0][0]                
                                                                 encoder[0][1]                    
                                                                 encoder[0][2]                    
__________________________________________________________________________________________________
dense_2 (Dense)                 (None, 10)           650         decoder[0][0]                    
==================================================================================================
Total params: 258,698
Trainable params: 258,698
Non-trainable params: 0
__________________________________________________________________________________________________

Couches RNN et cellules RNN

En plus des couches RNN intégrées, l'API RNN fournit également des API au niveau de la cellule. Contrairement aux couches RNN, qui traitent des lots entiers de séquences d'entrée, la cellule RNN ne traite qu'un seul pas de temps.

La cellule est à l'intérieur de la for boucle d'une couche RNN. Enrouler une cellule à l' intérieur d' une keras.layers.RNN couche donne une couche capable de traiter des lots de séquences, par exemple RNN(LSTMCell(10)) .

Mathématiquement, RNN(LSTMCell(10)) produit le même résultat que LSTM(10) . En fait, l'implémentation de cette couche dans TF v1.x ne faisait que créer la cellule RNN correspondante et l'envelopper dans une couche RNN. Cependant , l' utilisation du haut- GRU et LSTM couches permettre l'utilisation de CuDNN et vous pouvez voir une meilleure performance.

Il existe trois cellules RNN intégrées, chacune correspondant à la couche RNN correspondante.

L'abstraction cellulaire, ainsi que le générique keras.layers.RNN classe, il est très facile à mettre en œuvre des architectures RNN personnalisées pour votre recherche.

État d'état de lots croisés

Lors du traitement des séquences très longues (éventuellement infini), vous pouvez utiliser le modèle de statefulness croisée par lots.

Normalement, l'état interne d'une couche RNN est réinitialisé chaque fois qu'elle voit un nouveau lot (c'est-à-dire que chaque échantillon vu par la couche est supposé être indépendant du passé). La couche ne conservera un état que pendant le traitement d'un échantillon donné.

Cependant, si vous avez des séquences très longues, il est utile de les diviser en séquences plus courtes et d'alimenter séquentiellement ces séquences plus courtes dans une couche RNN sans réinitialiser l'état de la couche. De cette façon, le calque peut conserver des informations sur l'intégralité de la séquence, même s'il ne voit qu'une sous-séquence à la fois.

Vous pouvez le faire en mettant stateful=True dans le constructeur.

Si vous avez une séquence s = [t0, t1, ... t1546, t1547] , vous ne le diviser en exemple

s1 = [t0, t1, ... t100]
s2 = [t101, ... t201]
...
s16 = [t1501, ... t1547]

Ensuite, vous le traiteriez via :

lstm_layer = layers.LSTM(64, stateful=True)
for s in sub_sequences:
  output = lstm_layer(s)

Lorsque vous voulez effacer l'état, vous pouvez utiliser layer.reset_states() .

Voici un exemple complet :

paragraph1 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph2 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph3 = np.random.random((20, 10, 50)).astype(np.float32)

lstm_layer = layers.LSTM(64, stateful=True)
output = lstm_layer(paragraph1)
output = lstm_layer(paragraph2)
output = lstm_layer(paragraph3)

# reset_states() will reset the cached state to the original initial_state.
# If no initial_state was provided, zero-states will be used by default.
lstm_layer.reset_states()

RNN État Réutilisation

Les états enregistrés de la couche RNN ne sont pas inclus dans les layer.weights() . Si vous souhaitez réutiliser l'état d'une couche RNN, vous pouvez récupérer la valeur des états par layer.states et l' utiliser comme l'état initial d'une nouvelle couche via l'API fonctionnelle Keras comme new_layer(inputs, initial_state=layer.states) , ou sous-classement de modèle.

Veuillez également noter que le modèle séquentiel peut ne pas être utilisé dans ce cas car il ne prend en charge que les couches avec une seule entrée et sortie, l'entrée supplémentaire de l'état initial le rend impossible à utiliser ici.

paragraph1 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph2 = np.random.random((20, 10, 50)).astype(np.float32)
paragraph3 = np.random.random((20, 10, 50)).astype(np.float32)

lstm_layer = layers.LSTM(64, stateful=True)
output = lstm_layer(paragraph1)
output = lstm_layer(paragraph2)

existing_state = lstm_layer.states

new_lstm_layer = layers.LSTM(64)
new_output = new_lstm_layer(paragraph3, initial_state=existing_state)

RNN bidirectionnels

Pour les séquences autres que les séries chronologiques (par exemple le texte), il arrive souvent qu'un modèle RNN soit plus performant s'il traite non seulement la séquence du début à la fin, mais aussi en arrière. Par exemple, pour prédire le mot suivant dans une phrase, il est souvent utile d'avoir le contexte autour du mot, pas seulement les mots qui le précèdent.

Keras fournit une API facile pour vous de construire de tels RNNs bidirectionnel: l' keras.layers.Bidirectional emballage.

model = keras.Sequential()

model.add(
    layers.Bidirectional(layers.LSTM(64, return_sequences=True), input_shape=(5, 10))
)
model.add(layers.Bidirectional(layers.LSTM(32)))
model.add(layers.Dense(10))

model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
bidirectional (Bidirectional (None, 5, 128)            38400     
_________________________________________________________________
bidirectional_1 (Bidirection (None, 64)                41216     
_________________________________________________________________
dense_3 (Dense)              (None, 10)                650       
=================================================================
Total params: 80,266
Trainable params: 80,266
Non-trainable params: 0
_________________________________________________________________

Sous le capot, Bidirectional copie la couche RNN passé dans et retourner le go_backwards champ de la couche nouvellement copié, de sorte qu'il traitera les entrées dans l' ordre inverse.

La sortie de la Bidirectional RNN sera, par défaut, la concaténation de la sortie de la couche avant et la couche de sortie vers l' arrière. Si vous avez besoin d' un comportement de fusion différent, par exemple concaténation, modifiez le merge_mode paramètre dans le Bidirectional constructeur d'emballage. Pour plus de détails sur Bidirectional , s'il vous plaît vérifier l'API docs .

Optimisation des performances et noyaux CuDNN

Dans TensorFlow 2.0, les couches LSTM et GRU intégrées ont été mises à jour pour tirer parti des noyaux CuDNN par défaut lorsqu'un GPU est disponible. Avec ce changement, les avant keras.layers.CuDNNLSTM/CuDNNGRU couches ont été désapprouvés, et vous pouvez construire votre modèle sans se soucier du matériel , il fonctionnera sur.

Comme le noyau CuDNN est construit avec certaines hypothèses, cela signifie que la couche ne sera pas en mesure d'utiliser le noyau CuDNN si vous modifiez les paramètres par défaut des couches LSMC ou GRU intégrées. Par exemple:

  • Modification de l' activation fonction de tanh à autre chose.
  • Modification de la recurrent_activation fonction de sigmoid à autre chose.
  • L' utilisation recurrent_dropout > 0.
  • Réglage de unroll True, qui forces LSTM / GRU pour décomposer l'intérieur tf.while_loop dans un déroulé for boucle.
  • Réglage use_bias sur False.
  • Utilisation du masquage lorsque les données d'entrée ne sont pas remplies strictement à droite (si le masque correspond à des données remplies strictement à droite, CuDNN peut toujours être utilisé. C'est le cas le plus courant).

Pour la liste détaillée des contraintes, s'il vous plaît voir la documentation pour les LSMC et GRU couches.

Utilisation des noyaux CuDNN lorsqu'ils sont disponibles

Construisons un modèle LSTM simple pour démontrer la différence de performances.

Nous utiliserons comme séquences d'entrée la séquence de lignes de chiffres MNIST (en traitant chaque ligne de pixels comme un pas de temps), et nous prédirons l'étiquette du chiffre.

batch_size = 64
# Each MNIST image batch is a tensor of shape (batch_size, 28, 28).
# Each input sequence will be of size (28, 28) (height is treated like time).
input_dim = 28

units = 64
output_size = 10  # labels are from 0 to 9

# Build the RNN model
def build_model(allow_cudnn_kernel=True):
    # CuDNN is only available at the layer level, and not at the cell level.
    # This means `LSTM(units)` will use the CuDNN kernel,
    # while RNN(LSTMCell(units)) will run on non-CuDNN kernel.
    if allow_cudnn_kernel:
        # The LSTM layer with default options uses CuDNN.
        lstm_layer = keras.layers.LSTM(units, input_shape=(None, input_dim))
    else:
        # Wrapping a LSTMCell in a RNN layer will not use CuDNN.
        lstm_layer = keras.layers.RNN(
            keras.layers.LSTMCell(units), input_shape=(None, input_dim)
        )
    model = keras.models.Sequential(
        [
            lstm_layer,
            keras.layers.BatchNormalization(),
            keras.layers.Dense(output_size),
        ]
    )
    return model

Chargeons l'ensemble de données MNIST :

mnist = keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
sample, sample_label = x_train[0], y_train[0]

Créons une instance de modèle et formons-la.

Nous choisissons sparse_categorical_crossentropy que la fonction de perte pour le modèle. La sortie du modèle a une forme de [batch_size, 10] . La cible du modèle est un vecteur entier, chacun des entiers est compris entre 0 et 9.

model = build_model(allow_cudnn_kernel=True)

model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer="sgd",
    metrics=["accuracy"],
)


model.fit(
    x_train, y_train, validation_data=(x_test, y_test), batch_size=batch_size, epochs=1
)
938/938 [==============================] - 6s 5ms/step - loss: 0.9510 - accuracy: 0.7029 - val_loss: 0.5633 - val_accuracy: 0.8209
<keras.callbacks.History at 0x7fc9942efad0>

Comparons maintenant à un modèle qui n'utilise pas le noyau CuDNN :

noncudnn_model = build_model(allow_cudnn_kernel=False)
noncudnn_model.set_weights(model.get_weights())
noncudnn_model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer="sgd",
    metrics=["accuracy"],
)
noncudnn_model.fit(
    x_train, y_train, validation_data=(x_test, y_test), batch_size=batch_size, epochs=1
)
938/938 [==============================] - 34s 35ms/step - loss: 0.3894 - accuracy: 0.8846 - val_loss: 0.5677 - val_accuracy: 0.8045
<keras.callbacks.History at 0x7fc945fa2650>

Lors de l'exécution sur une machine avec un GPU NVIDIA et CuDNN installés, le modèle construit avec CuDNN est beaucoup plus rapide à former par rapport au modèle qui utilise le noyau TensorFlow standard.

Le même modèle compatible CuDNN peut également être utilisé pour exécuter l'inférence dans un environnement CPU uniquement. La tf.device annotation ci - dessous est juste le placement oblige l' appareil. Le modèle fonctionnera sur le CPU par défaut si aucun GPU n'est disponible.

Vous n'avez tout simplement plus à vous soucier du matériel sur lequel vous utilisez. N'est-ce pas plutôt cool ?

import matplotlib.pyplot as plt

with tf.device("CPU:0"):
    cpu_model = build_model(allow_cudnn_kernel=True)
    cpu_model.set_weights(model.get_weights())
    result = tf.argmax(cpu_model.predict_on_batch(tf.expand_dims(sample, 0)), axis=1)
    print(
        "Predicted result is: %s, target result is: %s" % (result.numpy(), sample_label)
    )
    plt.imshow(sample, cmap=plt.get_cmap("gray"))
Predicted result is: [3], target result is: 5

png

RNN avec entrées list/dict ou entrées imbriquées

Les structures imbriquées permettent aux implémenteurs d'inclure plus d'informations dans un seul pas de temps. Par exemple, une image vidéo peut avoir une entrée audio et vidéo en même temps. La forme des données dans ce cas pourrait être :

[batch, timestep, {"video": [height, width, channel], "audio": [frequency]}]

Dans un autre exemple, les données d'écriture manuscrite pourraient avoir à la fois les coordonnées x et y pour la position actuelle du stylet, ainsi que des informations sur la pression. Ainsi, la représentation des données pourrait être :

[batch, timestep, {"location": [x, y], "pressure": [force]}]

Le code suivant fournit un exemple de création d'une cellule RNN personnalisée qui accepte de telles entrées structurées.

Définir une cellule personnalisée qui prend en charge les entrées/sorties imbriquées

Voir Faire de nouvelles couches et modèles via le sous - classement pour plus de détails sur l' écriture de vos propres couches.

class NestedCell(keras.layers.Layer):
    def __init__(self, unit_1, unit_2, unit_3, **kwargs):
        self.unit_1 = unit_1
        self.unit_2 = unit_2
        self.unit_3 = unit_3
        self.state_size = [tf.TensorShape([unit_1]), tf.TensorShape([unit_2, unit_3])]
        self.output_size = [tf.TensorShape([unit_1]), tf.TensorShape([unit_2, unit_3])]
        super(NestedCell, self).__init__(**kwargs)

    def build(self, input_shapes):
        # expect input_shape to contain 2 items, [(batch, i1), (batch, i2, i3)]
        i1 = input_shapes[0][1]
        i2 = input_shapes[1][1]
        i3 = input_shapes[1][2]

        self.kernel_1 = self.add_weight(
            shape=(i1, self.unit_1), initializer="uniform", name="kernel_1"
        )
        self.kernel_2_3 = self.add_weight(
            shape=(i2, i3, self.unit_2, self.unit_3),
            initializer="uniform",
            name="kernel_2_3",
        )

    def call(self, inputs, states):
        # inputs should be in [(batch, input_1), (batch, input_2, input_3)]
        # state should be in shape [(batch, unit_1), (batch, unit_2, unit_3)]
        input_1, input_2 = tf.nest.flatten(inputs)
        s1, s2 = states

        output_1 = tf.matmul(input_1, self.kernel_1)
        output_2_3 = tf.einsum("bij,ijkl->bkl", input_2, self.kernel_2_3)
        state_1 = s1 + output_1
        state_2_3 = s2 + output_2_3

        output = (output_1, output_2_3)
        new_states = (state_1, state_2_3)

        return output, new_states

    def get_config(self):
        return {"unit_1": self.unit_1, "unit_2": unit_2, "unit_3": self.unit_3}

Construire un modèle RNN avec entrée/sortie imbriquée

La construction de laisser un modèle Keras qui utilise une keras.layers.RNN couche et la cellule personnalisée que nous venons de définir.

unit_1 = 10
unit_2 = 20
unit_3 = 30

i1 = 32
i2 = 64
i3 = 32
batch_size = 64
num_batches = 10
timestep = 50

cell = NestedCell(unit_1, unit_2, unit_3)
rnn = keras.layers.RNN(cell)

input_1 = keras.Input((None, i1))
input_2 = keras.Input((None, i2, i3))

outputs = rnn((input_1, input_2))

model = keras.models.Model([input_1, input_2], outputs)

model.compile(optimizer="adam", loss="mse", metrics=["accuracy"])

Entraîner le modèle avec des données générées aléatoirement

Puisqu'il n'y a pas de bon ensemble de données candidat pour ce modèle, nous utilisons des données Numpy aléatoires pour la démonstration.

input_1_data = np.random.random((batch_size * num_batches, timestep, i1))
input_2_data = np.random.random((batch_size * num_batches, timestep, i2, i3))
target_1_data = np.random.random((batch_size * num_batches, unit_1))
target_2_data = np.random.random((batch_size * num_batches, unit_2, unit_3))
input_data = [input_1_data, input_2_data]
target_data = [target_1_data, target_2_data]

model.fit(input_data, target_data, batch_size=batch_size)
10/10 [==============================] - 1s 26ms/step - loss: 0.7316 - rnn_1_loss: 0.2590 - rnn_1_1_loss: 0.4725 - rnn_1_accuracy: 0.1016 - rnn_1_1_accuracy: 0.0328
<keras.callbacks.History at 0x7fc5686e6f50>

Avec la Keras keras.layers.RNN couche, vous ne prévu de définir la logique mathématique pour l' étape individuelle dans la séquence et la keras.layers.RNN couche gérerez l'itération de la séquence pour vous. C'est un moyen incroyablement puissant de prototyper rapidement de nouveaux types de RNN (par exemple une variante LSTM).

Pour plus de détails, s'il vous plaît visitez les API docs .