Redes neuronales recurrentes (RNN) con Keras

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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

Introducción

Las redes neuronales recurrentes (RNN) son una clase de redes neuronales que son poderosas para modelar datos de secuencia, como series temporales o lenguaje natural.

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

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

  • Facilidad de uso: la incorporada en keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU capas le permiten crear 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 células RNN (la parte interna de la for bucle) con un comportamiento personalizado, y la usa en el genérica keras.layers.RNN capa (la for bucle de sí mismo). Esto le permite crear rápidamente prototipos de diferentes ideas de investigación de forma flexible con un código mínimo.

Configuración

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 integradas en Keras:

  1. keras.layers.SimpleRNN , un RNN totalmente conectado donde la salida de paso de tiempo anterior es para ser alimentado a la siguiente paso de tiempo.

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

  3. keras.layers.LSTM , propuso por primera vez en Hochreiter y Schmidhuber, 1997 .

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

Este es un ejemplo sencillo de un Sequential modelo que procesos secuencias de números enteros, incrusta cada número entero en un vector de 64 dimensiones, a continuación, procesa la secuencia de vectores utilizando un LSTM capa.

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 dropout y recurrent_dropout argumentos
  • Capacidad para procesar una secuencia de entrada a la inversa, a través de la go_backwards argumento
  • Desenrollado Loop (que puede conducir a un gran aumento de velocidad al procesar secuencias cortas de CPU), a través de la unroll argumento
  • ...y más.

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

Salidas y estados

De forma predeterminada, la salida de una capa RNN contiene un solo vector por muestra. Este vector es la salida de la 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 units corresponde a la units argumento pasado al constructor de la capa.

La capa A RNN también puede devolver toda la secuencia de salidas para cada muestra (un vector por paso de tiempo por muestra), si se 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 su(s) estado(s) interno(s) final(es). Los estados devueltos se pueden utilizar para reanudar la ejecución RNN más tarde, o para inicializar otra 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 volver a su estado interno, establecer el return_state parámetro a True al crear la capa. Tenga en cuenta que LSTM tiene 2 tensores del estado, pero GRU sólo tiene uno.

Para configurar el estado inicial de la capa, simplemente llame a la capa con el 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 ejemplo a continuación.

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 celdas RNN

Además de las capas de RNN integradas, la API de 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 solo paso de tiempo.

La célula es el interior de la for bucle de una capa RNN. Envolviendo una célula dentro de un keras.layers.RNN capa que da 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, utilizando la incorporada en el GRU y LSTM capas permitir el uso de CuDNN y es posible que vea un mejor rendimiento.

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

La abstracción de células, junto con el genérico keras.layers.RNN clase, hacen que sea muy fácil de implementar arquitecturas personalizados RNN para su investigación.

Estado de lotes cruzados

Al procesar secuencias muy largas (posiblemente infinito), es posible que desee utilizar el patrón de statefulness transversal lote.

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 determinada.

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 forma, la capa puede retener información sobre la totalidad de la secuencia, aunque solo vea una subsecuencia a la vez.

Usted puede hacer esto mediante el establecimiento stateful=True en el constructor.

Si usted tiene una secuencia s = [t0, t1, ... t1546, t1547] , que sería dividirlo 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 se quiere borrar el estado, puede utilizar 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 de RNN no están incluidos en los layer.weights() . Si desea volver a utilizar el estado de una capa de RNN, se puede recuperar el valor de los estados por layer.states y utilizarlo como el estado inicial para una nueva capa a través de la API funcional Keras como new_layer(inputs, initial_state=layer.states) , o subclases de modelos.

Tenga en cuenta también que es posible que no se use el modelo secuencial en este caso, ya que solo admite capas con una sola entrada y salida, la entrada adicional del estado inicial hace que no se pueda 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 (por ejemplo, texto), a menudo ocurre 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 la preceden.

Keras proporciona una API fácil para usted para construir tales RNNs bidireccionales: la keras.layers.Bidirectional envoltura.

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
_________________________________________________________________

Bajo el capó, Bidirectional copiará la capa RNN pasado, y darle la vuelta al go_backwards campo de la capa que acaba de copiar, por lo que va a procesar las entradas en orden inverso.

La salida del Bidirectional RNN será, por defecto, la concatenación 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, la concatenación, cambiar el merge_mode parámetro en el Bidirectional constructor de envoltura. Para más detalles sobre Bidirectional , consulte los documentos de la API .

Optimización del rendimiento y núcleos CuDNN

En TensorFlow 2.0, las capas LSTM y GRU integradas se actualizaron para aprovechar los kernels CuDNN de forma predeterminada cuando hay una GPU disponible. Con este cambio, los anteriores keras.layers.CuDNNLSTM/CuDNNGRU capas han quedado obsoletos, y usted puede construir su modelo sin tener que preocuparse por el hardware que se ejecutará en.

Ya que el núcleo CuDNN está construido con ciertos supuestos, esto significa que la capa no serán capaces de utilizar el núcleo CuDNN si cambia los valores por defecto de las capas LSTM o GRU incorporadas. P.ej:

  • Cambio de la activation la función de tanh a otra cosa.
  • Cambio de la recurrent_activation función del sigmoid a otra cosa.
  • Usando recurrent_dropout > 0.
  • Configuración unroll en True, que las fuerzas LSTM / GRU para descomponer el interior tf.while_loop en un desenrollado for bucle.
  • Configuración use_bias en Falso.
  • Uso de enmascaramiento cuando los datos de entrada no se rellenan estrictamente a la derecha (si la máscara corresponde a datos rellenados estrictamente a la derecha, aún se puede usar CuDNN. Este es el caso más común).

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

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 de 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 del 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 [==============================] - 6s 5ms/step - loss: 0.9510 - accuracy: 0.7029 - val_loss: 0.5633 - val_accuracy: 0.8209
<keras.callbacks.History at 0x7fc9942efad0>

Ahora, comparemos con un modelo que no usa el kernel 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>

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

El mismo modelo habilitado para CuDNN también se puede usar para ejecutar la inferencia en un entorno solo de CPU. El tf.device anotación de abajo es sólo obligando a la colocación del 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 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 sobre la presión. Entonces la representación de los datos podría ser:

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

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

Defina una celda personalizada que admita entrada/salida anidada

Ver Hacer nuevos modelos a través de capas y la subclasificación 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

Vamos a construir un modelo que utiliza un Keras keras.layers.RNN capa y la célula a medida 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"])

Entrena el modelo con datos generados aleatoriamente

Dado que no hay un buen conjunto de datos candidato para este modelo, usamos datos Numpy aleatorios 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 [==============================] - 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>

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

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