Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Redes neuronales recurrentes (RNN) con Keras

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

Introducción

Las redes neuronales recurrentes (RNN) son una clase de redes neuronales que es poderosa para modelar datos de secuencia como series de tiempo o lenguaje natural.

Esquemáticamente, una capa RNN usa un bucle for para iterar sobre los pasos de tiempo de una secuencia, mientras mantiene un estado interno que codifica información sobre los pasos de tiempo que ha visto hasta ahora.

La API Keras RNN está diseñada con un enfoque en:

  • Facilidad de uso : las capas keras.layers.GRU keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU permiten construir rápidamente modelos recurrentes sin tener que tomar decisiones difíciles de configuración.

  • Facilidad de personalización : también puede definir su propia capa de celda RNN (la parte interna del bucle for ) con un comportamiento personalizado, y usarla con la capa genérica keras.layers.RNN (el bucle for mismo). Esto le permite crear rápidamente prototipos de diferentes ideas de investigación de forma flexible con un código mínimo.

Preparar

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

Capas RNN incorporadas: un ejemplo simple

Hay tres capas RNN incorporadas en Keras:

  1. keras.layers.SimpleRNN , un RNN completamente conectado donde la salida del paso de tiempo anterior se debe alimentar al siguiente paso de tiempo.

  2. keras.layers.GRU , propuesto por primera vez en Cho et al., 2014 .

  3. keras.layers.LSTM , propuesta por primera vez en Hochreiter & Schmidhuber, 1997 .

A principios de 2015, Keras tuvo las primeras implementaciones reutilizables de código abierto de Python de LSTM y GRU.

Aquí hay un ejemplo simple de un modelo Sequential que procesa secuencias de enteros, incrusta cada entero en un vector de 64 dimensiones y luego procesa la secuencia de vectores usando una capa 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
_________________________________________________________________

Los RNN incorporados admiten una serie de características útiles:

  • Deserción recurrente, a través de los argumentos dropout y recurrent_dropout
  • Capacidad para procesar una secuencia de entrada en reversa, a través del argumento go_backwards
  • Desenrollo de bucle (que puede conducir a una gran velocidad al procesar secuencias cortas en la CPU), a través del argumento de unroll
  • ...y más.

Para obtener más información, consulte la documentación de la API RNN .

Salidas y estados

Por defecto, la salida de una capa RNN contiene un solo vector por muestra. Este vector es la salida de celda RNN correspondiente al último paso de tiempo, que contiene información sobre toda la secuencia de entrada. La forma de esta salida es (batch_size, units) donde las units corresponden al argumento de units pasado al constructor de la capa.

Una capa RNN también puede devolver la secuencia completa de salidas para cada muestra (un vector por paso de tiempo por muestra), si establece return_sequences=True . La forma de esta salida es (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
_________________________________________________________________

Además, una capa RNN puede devolver sus estados internos finales. Los estados devueltos se pueden usar para reanudar la ejecución de RNN más tarde o para inicializar otro RNN . Esta configuración se usa comúnmente en el modelo de secuencia a secuencia codificador-decodificador, donde el estado final del codificador se usa como el estado inicial del decodificador.

Para configurar una capa RNN para que devuelva su estado interno, establezca el parámetro return_state en True cuando cree la capa. Tenga en cuenta que LSTM tiene 2 tensores de estado, pero GRU solo tiene uno.

Para configurar el estado inicial de la capa, simplemente llame a la capa con un argumento de palabra clave adicional initial_state . Tenga en cuenta que la forma del estado debe coincidir con el tamaño de la unidad de la capa, como en el siguiente ejemplo.

 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
__________________________________________________________________________________________________

Capas RNN y células RNN

Además de las capas RNN incorporadas, la API RNN también proporciona API a nivel de celda. A diferencia de las capas RNN, que procesan lotes completos de secuencias de entrada, la celda RNN solo procesa un paso de tiempo único.

La celda es el interior del bucle for de una capa RNN. Al envolver una celda dentro de una capa keras.layers.RNN , se obtiene una capa capaz de procesar lotes de secuencias, por ejemplo, RNN(LSTMCell(10)) .

Matemáticamente, RNN(LSTMCell(10)) produce el mismo resultado que LSTM(10) . De hecho, la implementación de esta capa en TF v1.x fue simplemente crear la celda RNN correspondiente y envolverla en una capa RNN. Sin embargo, el uso de las capas GRU y LSTM incorporadas permite el uso de CuDNN y es posible que vea un mejor rendimiento.

Hay tres celdas RNN incorporadas, cada una de ellas correspondiente a la capa RNN correspondiente.

La abstracción celular, junto con la clase genérica keras.layers.RNN , hace que sea muy fácil implementar arquitecturas RNN personalizadas para su investigación.

Cruce de estado

Al procesar secuencias muy largas (posiblemente infinitas), es posible que desee utilizar el patrón de estado completo entre lotes .

Normalmente, el estado interno de una capa RNN se restablece cada vez que ve un nuevo lote (es decir, se supone que cada muestra vista por la capa es independiente del pasado). La capa solo mantendrá un estado mientras procesa una muestra dada.

Sin embargo, si tiene secuencias muy largas, es útil dividirlas en secuencias más cortas y alimentar estas secuencias más cortas secuencialmente en una capa RNN sin restablecer el estado de la capa. De esa manera, la capa puede retener información sobre la totalidad de la secuencia, aunque solo esté viendo una subsecuencia a la vez.

Puede hacer esto estableciendo stateful=True en el constructor.

Si tiene una secuencia s = [t0, t1, ... t1546, t1547] , la dividiría en, por ejemplo,

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

Entonces lo procesarías a través de:

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

Cuando desee borrar el estado, puede usar layer.reset_states() .

Aquí hay un ejemplo completo:

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

 

Reutilización del estado RNN

Los estados registrados de la capa RNN no están incluidos en la layer.weights() . Si desea reutilizar el estado de una capa RNN, puede recuperar el valor de los estados por layer.states y usarlo como el estado inicial para una nueva capa a través de la API funcional de Keras como new_layer(inputs, initial_state=layer.states) o subclases de modelos.

Tenga en cuenta también que el modelo secuencial podría no usarse en este caso ya que solo admite capas con una sola entrada y salida, la entrada adicional del estado inicial hace que sea imposible de usar aquí.

 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 bidireccionales

Para secuencias que no sean series de tiempo (p. Ej., Texto), a menudo se da el caso de que un modelo RNN puede funcionar mejor si no solo procesa la secuencia de principio a fin, sino también hacia atrás. Por ejemplo, para predecir la siguiente palabra en una oración, a menudo es útil tener el contexto alrededor de la palabra, no solo las palabras que vienen antes.

Keras proporciona una API fácil para que usted pueda construir tales keras.layers.Bidirectional bidireccionales: el contenedor 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
_________________________________________________________________

Debajo del capó, Bidirectional copiará la capa RNN pasada y go_backwards campo go_backwards de la capa recién copiada, para que procese las entradas en orden inverso.

La salida del RNN Bidirectional será, por defecto, la suma de la salida de la capa hacia adelante y la salida de la capa hacia atrás. Si necesita un comportamiento de fusión diferente, por ejemplo, concatenación, cambie el parámetro merge_mode en el constructor de contenedor Bidirectional . Para obtener más detalles sobre Bidirectional , consulte los documentos de API .

Optimización del rendimiento y núcleos CuDNN

En TensorFlow 2.0, las capas LSTM y GRU incorporadas se han actualizado para aprovechar los núcleos CuDNN de forma predeterminada cuando hay una GPU disponible. Con este cambio, las keras.layers.CuDNNLSTM/CuDNNGRU anteriores keras.layers.CuDNNLSTM/CuDNNGRU han quedado en desuso, y puede construir su modelo sin preocuparse por el hardware en el que se ejecutará.

Dado que el núcleo CuDNN está construido con ciertas suposiciones, esto significa que la capa no podrá usar el núcleo CuDNN si cambia los valores predeterminados de las capas LSTM o GRU incorporadas . P.ej:

  • Cambiar la función de activation de tanh a otra cosa.
  • Cambiar la función recurrent_activation de sigmoid a otra cosa.
  • Usando recurrent_dropout > 0.
  • Establecer unroll en True, lo que obliga a LSTM / GRU a descomponer el tf.while_loop interno en un bucle for desenrollado.
  • Establecer use_bias en False.
  • Usar el enmascaramiento cuando los datos de entrada no están estrictamente rellenados a la derecha (si la máscara corresponde a datos estrictamente rellenados a la derecha, CuDNN aún puede usarse. Este es el caso más común).

Para obtener una lista detallada de las restricciones, consulte la documentación de las capas LSTM y GRU .

Usar núcleos CuDNN cuando estén disponibles

Construyamos un modelo LSTM simple para demostrar la diferencia de rendimiento.

Usaremos como secuencias de entrada la secuencia de filas de dígitos MNIST (tratando cada fila de píxeles como un paso de tiempo), y predeciremos la etiqueta del dígito.

 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

 

Carguemos el conjunto de datos 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]
 

Vamos a crear una instancia modelo y entrenarla.

Elegimos sparse_categorical_crossentropy como la función de pérdida para el modelo. La salida del modelo tiene forma de [batch_size, 10] . El objetivo para el modelo es un vector entero, cada uno de los enteros está en el rango de 0 a 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 [==============================] - 5s 5ms/step - loss: 0.9479 - accuracy: 0.6979 - val_loss: 0.5026 - val_accuracy: 0.8424

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

Ahora, comparemos con un modelo que no usa el núcleo 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 38ms/step - loss: 0.3927 - accuracy: 0.8809 - val_loss: 0.2804 - val_accuracy: 0.9132

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

Cuando se ejecuta en una máquina con una GPU NVIDIA y CuDNN instalados, el modelo construido con CuDNN es mucho más rápido de entrenar en comparación con el modelo que usa el núcleo normal TensorFlow.

El mismo modelo habilitado para CuDNN también se puede usar para ejecutar inferencias en un entorno solo de CPU. La tf.device anotación de tf.device solo obliga a colocar el dispositivo. El modelo se ejecutará en la CPU de forma predeterminada si no hay una GPU disponible.

Simplemente ya no tiene que preocuparse por el hardware en el que se está ejecutando. ¿No es genial?

 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 con entradas de lista / dict o entradas anidadas

Las estructuras anidadas permiten a los implementadores incluir más información en un solo paso de tiempo. Por ejemplo, un cuadro de video podría tener entrada de audio y video al mismo tiempo. La forma de los datos en este caso podría ser:

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

En otro ejemplo, los datos de escritura a mano podrían tener las coordenadas x e y para la posición actual del lápiz, así como información de presión. Entonces la representación de datos podría ser:

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

El siguiente código proporciona un ejemplo de cómo construir una celda RNN personalizada que acepte dichas entradas estructuradas.

Defina una celda personalizada que admita entrada / salida anidada

Consulte Crear nuevas capas y modelos mediante subclases para obtener detalles sobre cómo escribir sus propias capas.

 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}

 

Cree un modelo RNN con entrada / salida anidada

Construyamos un modelo Keras que use una capa keras.layers.RNN y la celda personalizada que acabamos de definir.

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

Entrene el modelo con datos generados aleatoriamente

Como no hay un buen conjunto de datos candidatos para este modelo, utilizamos datos aleatorios de Numpy para la demostración.

 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 [==============================] - 0s 22ms/step - loss: 0.7499 - rnn_1_loss: 0.2790 - rnn_1_1_loss: 0.4709 - rnn_1_accuracy: 0.0844 - rnn_1_1_accuracy: 0.0336

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

Con la capa Keras keras.layers.RNN , solo se espera que defina la lógica matemática para cada paso individual dentro de la secuencia, y la capa keras.layers.RNN manejará la iteración de la secuencia por usted. Es una forma increíblemente poderosa de crear rápidamente prototipos de nuevos tipos de RNN (por ejemplo, una variante LSTM).

Para más detalles, visite los documentos de la API .