Redes Neurais Recorrentes (RNN) com Keras

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Introdução

Redes neurais recorrentes (RNN) são uma classe de redes neurais poderosas para modelar dados de sequência, como séries temporais ou linguagem natural.

Esquematicamente, uma camada RNN utiliza um for loop para repetir os Timesteps de uma sequência, mantendo ao mesmo tempo um estado interno que codifica a informação sobre os Timesteps tem visto até agora.

A API Keras RNN foi projetada com foco em:

  • Facilidade de uso: o built-in keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU camadas permitem-lhe criar rapidamente modelos recorrentes sem ter que fazer escolhas de configuração difíceis.

  • Facilidade de personalização: Você também pode definir a sua própria camada de células RNN (a parte interna do for circular) com comportamento personalizado, e usá-lo com o genérico keras.layers.RNN camada (o for si loop). Isso permite que você crie rapidamente um protótipo de diferentes ideias de pesquisa de maneira flexível com o mínimo de código.

Configurar

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

Camadas RNN integradas: um exemplo simples

Existem três camadas RNN integradas no Keras:

  1. keras.layers.SimpleRNN , um RNN totalmente ligado, onde a saída do passo de tempo anterior é para ser alimentado para o próximo passo de tempo.

  2. keras.layers.GRU , proposta pela primeira vez em Cho et al., 2014 .

  3. keras.layers.LSTM , proposto pela primeira vez em Hochreiter & Schmidhuber de 1997 .

No início de 2015, Keras teve as primeiras implementações Python de código aberto reutilizáveis ​​de LSTM e GRU.

Aqui é um exemplo simples de uma Sequential modelo que processa as sequências de números inteiros, cada número inteiro incorpora num vector 64-dimensional, em seguida, processa a sequência de vectores utilizando um LSTM camada.

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
_________________________________________________________________

Os RNNs integrados oferecem suporte a uma série de recursos úteis:

  • Dropout recorrente, através dos dropout e recurrent_dropout argumentos
  • Capacidade de processar uma sequência de entrada no sentido inverso, através do go_backwards argumento
  • Loop desenrolar (o que pode levar a um grande aumento de velocidade no processamento de sequências curtas de CPU), através do unroll argumento
  • ...e mais.

Para mais informações, consulte a documentação da API RNN .

Saídas e estados

Por padrão, a saída de uma camada RNN contém um único vetor por amostra. Este vetor é a saída da célula RNN correspondente ao último passo de tempo, contendo informações sobre toda a seqüência de entrada. A forma desta saída é (batch_size, units) onde units corresponde ao units argumento passados para o construtor da camada.

Uma camada RNN pode também devolver toda a sequência de saídas para cada amostra (um vector por iteração por amostra), se definir return_sequences=True . A forma desta saída é (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
_________________________________________________________________

Além disso, uma camada RNN pode retornar seu (s) estado (s) interno (s) final (is). Os estados retornados podem ser usados para retomar a execução RNN mais tarde, ou para inicializar outra RNN . Essa configuração é comumente usada no modelo de sequência a sequência de codificador-decodificador, onde o estado final do codificador é usado como o estado inicial do decodificador.

Para configurar uma camada de RNN para retornar seu estado interno, definir o return_state parâmetro para True ao criar a camada. Note-se que LSTM tem 2 tensores estaduais, mas GRU tem apenas um.

Para configurar o estado inicial da camada, basta ligar para a camada com chave argumento adicional initial_state . Observe que a forma do estado precisa corresponder ao tamanho da unidade da camada, como no exemplo abaixo.

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
__________________________________________________________________________________________________

Camadas RNN e células RNN

Além das camadas RNN integradas, a API RNN também fornece APIs em nível de célula. Ao contrário das camadas RNN, que processam lotes inteiros de sequências de entrada, a célula RNN processa apenas um único passo de tempo.

A célula é o interior da for lacete de uma camada RNN. Envolvendo uma célula dentro de um keras.layers.RNN camada dá-lhe uma camada capaz de processar lotes de sequências, por exemplo, RNN(LSTMCell(10)) .

Matematicamente, RNN(LSTMCell(10)) produz o mesmo resultado que LSTM(10) . Na verdade, a implementação dessa camada no TF v1.x foi apenas criar a célula RNN correspondente e envolvê-la em uma camada RNN. No entanto usando o built-in GRU e LSTM camadas permitem o uso de CuDNN e você pode ver um melhor desempenho.

Existem três células RNN integradas, cada uma delas correspondendo à camada RNN correspondente.

A abstração celular, juntamente com o genérico keras.layers.RNN classe, torná-lo muito fácil de implementar personalizados arquiteturas RNN para sua pesquisa.

Statefulness de lote cruzado

Ao processar as sequências muito longas (possivelmente infinita), você pode querer usar o padrão de statefulness cross-batch.

Normalmente, o estado interno de uma camada RNN é redefinido toda vez que ela vê um novo lote (ou seja, cada amostra vista pela camada é considerada independente do passado). A camada manterá apenas um estado durante o processamento de uma determinada amostra.

No entanto, se você tiver sequências muito longas, é útil dividi-las em sequências mais curtas e alimentar essas sequências mais curtas sequencialmente em uma camada RNN sem redefinir o estado da camada. Dessa forma, a camada pode reter informações sobre a totalidade da sequência, embora esteja vendo apenas uma subseqüência de cada vez.

Você pode fazer isso definindo stateful=True no construtor.

Se você tem uma sequência s = [t0, t1, ... t1546, t1547] , você dividi-lo em eg

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

Em seguida, você deve processá-lo por meio de:

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

Quando você quiser limpar o estado, você pode usar layer.reset_states() .

Aqui está um exemplo 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()

Reutilização do Estado RNN

Os estados gravadas da camada RNN não estão incluídos nas layer.weights() . Se você gostaria de reutilizar o estado de uma camada RNN, você pode recuperar o valor estados por layer.states e usá-lo como o estado inicial para uma nova camada por meio da API funcional Keras como new_layer(inputs, initial_state=layer.states) , ou subclasse de modelo.

Observe também que o modelo sequencial pode não ser usado neste caso, uma vez que suporta apenas camadas com entrada e saída única, a entrada extra do estado inicial torna impossível o uso aqui.

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)

RNNs bidirecionais

Para sequências diferentes de séries temporais (por exemplo, texto), é comum que um modelo RNN tenha um desempenho melhor se não processar apenas a sequência do início ao fim, mas também para trás. Por exemplo, para prever a próxima palavra em uma frase, geralmente é útil ter o contexto em torno da palavra, não apenas as palavras que vêm antes dela.

Keras fornece uma API fácil para você construir tais RNNs bidirecional: o keras.layers.Bidirectional wrapper.

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
_________________________________________________________________

Sob o capô, Bidirectional copiará a camada RNN transmitido, e inverter o go_backwards campo da camada recentemente copiados, de modo que ele irá processar as entradas em ordem inversa.

A saída do Bidirectional RNN será, por defeito, a concatenação da saída camada para a frente e a saída camada para trás. Se você precisa de um comportamento fusão diferente, por exemplo, concatenação, altere o merge_mode parâmetro no Bidirectional construtor wrapper. Para mais detalhes sobre Bidirectional , por favor, verifique a documentação da API .

Otimização de desempenho e kernels CuDNN

No TensorFlow 2.0, as camadas LSTM e GRU integradas foram atualizadas para aproveitar os kernels CuDNN por padrão quando uma GPU está disponível. Com esta mudança, os anteriores keras.layers.CuDNNLSTM/CuDNNGRU camadas foram reprovados, e você pode construir o seu modelo sem se preocupar com o hardware que será executado.

Desde o kernel CuDNN é construído com certos pressupostos, isso significa que a camada não será capaz de usar o kernel CuDNN se você alterar os padrões das camadas LSTM ou GRU embutidos. Por exemplo:

  • Alterar a activation da função de tanh para outra coisa.
  • Alterar o recurrent_activation função de sigmoid para outra coisa.
  • Usando recurrent_dropout > 0.
  • Definir unroll como True, que forças LSTM / GRU para decompor o interior tf.while_loop em um desenrolado for loop.
  • Definir use_bias para Falso.
  • Usar o mascaramento quando os dados de entrada não são preenchidos estritamente à direita (se a máscara corresponder aos dados preenchidos à direita, CuDNN ainda pode ser usado. Este é o caso mais comum).

Para obter a lista detalhada de restrições, consulte a documentação para os LSTM e GRU camadas.

Usando kernels CuDNN quando disponíveis

Vamos construir um modelo LSTM simples para demonstrar a diferença de desempenho.

Usaremos como sequências de entrada a sequência de linhas de dígitos MNIST (tratando cada linha de pixels como um intervalo de tempo) e preveremos o rótulo do 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

Vamos carregar o conjunto de dados 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 criar uma instância de modelo e treiná-la.

Nós escolhemos sparse_categorical_crossentropy como a função de perda para o modelo. O resultado do modelo tem a forma do [batch_size, 10] . O destino do modelo é um vetor de inteiros, cada um dos inteiros está no intervalo 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>

Agora, vamos comparar com um modelo que não usa o 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>

Ao executar em uma máquina com uma GPU NVIDIA e CuDNN instalados, o modelo construído com CuDNN é muito mais rápido de treinar em comparação com o modelo que usa o kernel TensorFlow normal.

O mesmo modelo habilitado para CuDNN também pode ser usado para executar inferência em um ambiente apenas de CPU. O tf.device anotação abaixo é apenas forçando a colocação do dispositivo. O modelo será executado na CPU por padrão se nenhuma GPU estiver disponível.

Você simplesmente não precisa mais se preocupar com o hardware em que está executando. Não é muito legal?

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

RNNs com entradas list / dict ou entradas aninhadas

As estruturas aninhadas permitem que os implementadores incluam mais informações em um único intervalo de tempo. Por exemplo, um quadro de vídeo pode ter entrada de áudio e vídeo ao mesmo tempo. A forma dos dados neste caso poderia ser:

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

Em outro exemplo, os dados de escrita à mão podem ter coordenadas xey para a posição atual da caneta, bem como informações de pressão. Portanto, a representação dos dados pode ser:

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

O código a seguir fornece um exemplo de como construir uma célula RNN personalizada que aceita essas entradas estruturadas.

Defina uma célula personalizada que suporte entrada / saída aninhada

Veja Fazer novos Layers & Models via subclasse para obter mais detalhes sobre como escrever suas próprias camadas.

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}

Construir um modelo RNN com entrada / saída aninhada

Vamos construir um modelo Keras que usa um keras.layers.RNN camada e a célula personalizado 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"])

Treine o modelo com dados gerados aleatoriamente

Como não há um bom conjunto de dados candidato para este modelo, usamos dados Numpy aleatórios para demonstração.

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>

Com a Keras keras.layers.RNN camada, você é esperado apenas para definir a lógica matemática para a etapa individual dentro da seqüência, eo keras.layers.RNN camada irá lidar com a iteração seqüência para você. É uma maneira incrivelmente poderosa de criar rapidamente protótipos de novos tipos de RNNs (por exemplo, uma variante LSTM).

Para mais detalhes, visite os docs API .