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

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

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

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 des séries chronologiques ou un langage naturel.

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

L'API Keras RNN est conçue avec un accent sur:

  • Facilité d'utilisation : les couches keras.layers.GRU keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU vous permettent de créer 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 interne de la boucle for ) avec un comportement personnalisé et l'utiliser avec la couche générique keras.layers.RNN (la boucle for elle-même). Cela vous permet de prototyper rapidement différentes idées de recherche de manière flexible avec un code minimal.

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ù la sortie du pas de temps précédent doit être transmise au prochain pas de temps.

  2. keras.layers.GRU , proposé pour la première fois dans Cho et al., 2014 .

  3. keras.layers.LSTM , proposé pour la première fois dans Hochreiter & Schmidhuber, 1997 .

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

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

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:

  • Décrochage récurrent, via les arguments dropout et recurrent_dropout
  • Possibilité de traiter une séquence d'entrée à l'envers, via l'argument go_backwards
  • 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 cellule RNN correspondant au dernier pas de temps, contenant des informations sur la séquence d'entrée entière. La forme de cette sortie est (batch_size, units) où les units correspondent à l'argument units passé au constructeur de la couche.

Une couche RNN peut également renvoyer la séquence entière de sorties pour chaque échantillon (un vecteur par pas de temps par échantillon), si vous définissez return_sequences=True . La forme de cette sortie est (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 (ses) état (s) interne (s) final (s). Les états renvoyés peuvent être utilisés pour reprendre l'exécution du RNN ultérieurement, 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 afin qu'elle renvoie son état interne, définissez le paramètre return_state sur True lors de la création de la couche. Notez que LSTM a 2 tenseurs d'état, mais GRU n'en a qu'un.

Pour configurer l'état initial de la couche, appelez simplement la couche avec l'argument de 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 boucle for d'une couche RNN. Enveloppant une cellule dans une couche keras.layers.RNN vous 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, la mise en œuvre de cette couche dans TF v1.x créait simplement la cellule RNN correspondante et l'enveloppait dans une couche RNN. Cependant, l'utilisation des couches GRU et LSTM permet l'utilisation de CuDNN et vous pouvez voir de meilleures performances.

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

L'abstraction de cellule, associée à la classe générique keras.layers.RNN , facilite la mise en œuvre d'architectures RNN personnalisées pour vos recherches.

État inter-lots

Lors du traitement de très longues séquences (éventuellement infinies), vous souhaiterez peut-être utiliser le modèle d'état inter-batch .

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 de nourrir ces séquences plus courtes séquentiellement dans une couche RNN sans réinitialiser l'état de la couche. De cette façon, la couche peut conserver des informations sur l'intégralité de la séquence, même si elle ne voit qu'une sous-séquence à la fois.

Vous pouvez le faire en définissant stateful=True dans le constructeur.

Si vous avez une séquence s = [t0, t1, ... t1546, t1547] , vous la s = [t0, t1, ... t1546, t1547] par 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 souhaitez 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()

Réutilisation de l'état RNN

Les états enregistrés de la couche RNN ne sont pas inclus dans le 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 état initial pour une nouvelle couche via l'API fonctionnelle Keras comme new_layer(inputs, initial_state=layer.states) , ou sous-classification 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 rend son utilisation impossible 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 temporelles (par exemple le texte), il arrive souvent qu'un modèle RNN puisse mieux fonctionner 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 simple pour vous permettre de créer de tels RNN bidirectionnels: le wrapper keras.layers.Bidirectional .

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 copiera la couche RNN transmise et retournera le champ go_backwards de la couche nouvellement copiée, afin de traiter les entrées dans l'ordre inverse.

La sortie du RNN Bidirectional sera, par défaut, la somme de la sortie de la couche avant et de la sortie de la couche arrière. Si vous avez besoin d'un comportement de fusion différent, par exemple la concaténation, modifiez le paramètre merge_mode dans le constructeur d'encapsuleur Bidirectional . Pour plus de détails sur Bidirectional , veuillez consulter la documentation de l'API .

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 keras.layers.CuDNNLSTM/CuDNNGRU précédentes keras.layers.CuDNNLSTM/CuDNNGRU sont keras.layers.CuDNNLSTM/CuDNNGRU obsolètes et vous pouvez créer votre modèle sans vous soucier du matériel sur lequel il fonctionnera.

Puisque le noyau CuDNN est construit avec certaines hypothèses, cela signifie que la couche ne pourra pas utiliser le noyau CuDNN si vous modifiez les valeurs par défaut des couches LSTM ou GRU intégrées . Par exemple:

  • Changer la fonction d' activation de tanh à autre chose.
  • Changer la fonction recurrent_activation de sigmoid à autre chose.
  • Utilisation de recurrent_dropout > 0.
  • Définir unroll sur True, ce qui force LSTM / GRU à décomposer le tf.while_loop interne en une boucle for déroulée.
  • Définition de use_bias sur False.
  • Utilisation du masquage lorsque les données d'entrée ne sont pas strictement remplies à droite (si le masque correspond à des données strictement remplies à droite, CuDNN peut toujours être utilisé. C'est le cas le plus courant).

Pour la liste détaillée des contraintes, veuillez consulter la documentation des couches LSTM et GRU .

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 (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 le jeu 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 entraînons-la.

Nous choisissons sparse_categorical_crossentropy comme fonction de perte pour le modèle. La sortie du modèle a la forme de [batch_size, 10] . La cible du modèle est un vecteur entier, chacun des nombres entiers étant 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 [==============================] - 7s 6ms/step - loss: 1.2775 - accuracy: 0.5890 - val_loss: 0.4391 - val_accuracy: 0.8663

<tensorflow.python.keras.callbacks.History at 0x7f769430c630>

Maintenant, comparons à 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 [==============================] - 36s 37ms/step - loss: 0.3995 - accuracy: 0.8823 - val_loss: 0.3322 - val_accuracy: 0.8953

<tensorflow.python.keras.callbacks.History at 0x7f76941cde10>

Lorsqu'il est exécuté sur une machine avec un GPU NVIDIA et CuDNN installés, le modèle construit avec CuDNN est beaucoup plus rapide à entraîner 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. L'annotation tf.device ci-dessous ne fait que forcer le placement de l'appareil. Le modèle fonctionnera par défaut sur le processeur si aucun GPU n'est disponible.

Vous n'avez 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 liste / 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 les coordonnées x et y pour la position actuelle du stylet, ainsi que des informations de 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 ces entrées structurées.

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

Voir Créer de nouveaux calques et modèles via la sous-classification pour plus de détails sur l'écriture de vos propres calques.

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}

Créer un modèle RNN avec des entrées / sorties imbriquées

Construisons un modèle Keras qui utilise une couche keras.layers.RNN 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înez le modèle avec des données générées aléatoirement

Puisqu'il n'y a pas un 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 27ms/step - loss: 0.8448 - rnn_1_loss: 0.2678 - rnn_1_1_loss: 0.5770 - rnn_1_accuracy: 0.1061 - rnn_1_1_accuracy: 0.0344

<tensorflow.python.keras.callbacks.History at 0x7f7795504208>

Avec la couche Keras keras.layers.RNN , vous devez uniquement définir la logique mathématique pour chaque étape de la séquence, et la couche keras.layers.RNN gérera 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, veuillez consulter la documentation sur l' API .