RSVP para seu evento TensorFlow Everywhere hoje mesmo!
Esta página foi traduzida pela API Cloud Translation.
Switch to English

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 poderosa para modelar dados de sequência, como séries temporais ou linguagem natural.

Esquematicamente, uma camada RNN usa um loop for para iterar ao longo dos passos de tempo de uma sequência, enquanto mantém um estado interno que codifica as informações sobre os passos de tempo vistos até agora.

A API Keras RNN foi desenvolvida com foco em:

  • Fácil de usar : as camadas keras.layers.GRU keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU permitem que você crie modelos recorrentes rapidamente sem ter que fazer escolhas de configuração difíceis.

  • Facilidade de personalização : você também pode definir sua própria camada de células RNN (a parte interna do loop for ) com comportamento personalizado e usá-lo com a camada keras.layers.RNN genérica (o próprio loop for ). 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 conectado em que a saída do timestep anterior deve ser alimentada para o próximo timestep.

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

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

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

Aqui está um exemplo simples de um modelo Sequential que processa sequências de inteiros, incorpora cada número inteiro em um vetor de 64 dimensões e, em seguida, processa a sequência de vetores usando uma camada 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
_________________________________________________________________

Os RNNs integrados oferecem suporte a vários recursos úteis:

  • Desistência recorrente, por meio dos argumentos dropout e recurrent_dropout
  • Capacidade de processar uma sequência de entrada ao contrário, por meio do argumento go_backwards
  • Loop unrolling (que pode levar a uma grande aceleração ao processar sequências curtas na CPU), por meio do argumento unroll
  • ...e mais.

Para obter 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 dessa saída é (batch_size, units) onde as units correspondem ao argumento das units passado para o construtor da camada.

Uma camada RNN também pode retornar a sequência inteira de saídas para cada amostra (um vetor por etapa de tempo por amostra), se você definir return_sequences=True . A forma dessa 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 do RNN mais tarde ou para inicializar outro 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 RNN para retornar ao seu estado interno, defina o parâmetro return_state como True ao criar a camada. Observe que LSTM tem 2 tensores de estado, mas GRU tem apenas um.

Para configurar o estado inicial da camada, basta chamar a camada com o argumento de palavra-chave 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 está dentro do loop for de uma camada RNN. Envolver uma célula dentro de uma camada keras.layers.RNN oferece 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, o uso das camadas LSTM GRU e LSTM permite o uso de CuDNN e você pode ver um melhor desempenho.

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

A abstração de célula, junto com a classe keras.layers.RNN genérica, torna muito fácil implementar arquiteturas RNN personalizadas para sua pesquisa.

Statefulness de lote cruzado

Ao processar sequências muito longas (possivelmente infinitas), você pode querer usar o padrão de estado de lote cruzado .

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.

Porém, se você tiver sequências muito longas, é útil dividi-las em sequências mais curtas e alimentar essas sequências mais curtas 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ê deve dividi-la em, por exemplo

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

Em seguida, você o processaria por meio de:

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

Quando você deseja 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()

RNN State Reuse

Os estados registrados da camada RNN não são incluídos em layer.weights() . Se desejar reutilizar o estado de uma camada RNN, você pode recuperar o valor dos 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 uma única entrada e saída, 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, muitas vezes é ú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 RNNs bidirecionais: o 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
_________________________________________________________________

Sob o capô, o Bidirectional irá copiar a camada RNN passada e inverter o campo go_backwards da camada recém-copiada, para que ele processe as entradas na ordem reversa.

A saída do RNN Bidirectional será, por padrão, a soma da saída da camada direta e da saída da camada posterior. Se você precisar de um comportamento de mesclagem diferente, por exemplo, concatenação, altere o parâmetro merge_mode no construtor do wrapper Bidirectional . Para obter mais detalhes sobre Bidirectional , verifique os documentos 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 essa mudança, as camadas keras.layers.CuDNNLSTM/CuDNNGRU anteriores foram descontinuadas e você pode construir seu modelo sem se preocupar com o hardware em que será executado.

Como o kernel CuDNN é construído com certas suposições, isso significa que a camada não poderá usar o kernel CuDNN se você alterar os padrões das camadas LSTM ou GRU integradas . Por exemplo:

  • Mudar a função de activation de tanh para outra coisa.
  • Mudar a função recurrent_activation de sigmoid para outra coisa.
  • Usando recurrent_dropout > 0.
  • Definindo unroll como True, o que força LSTM / GRU a decompor o tf.while_loop interno em um loop for desfeito.
  • Definindo use_bias como False.
  • 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 a lista detalhada de restrições, consulte a documentação das camadas LSTM e GRU .

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.

Escolhemos sparse_categorical_crossentropy como a função de perda para o modelo. A saída do modelo tem o formato [batch_size, 10] . O destino para o 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 [==============================] - 7s 6ms/step - loss: 1.2775 - accuracy: 0.5890 - val_loss: 0.4391 - val_accuracy: 0.8663

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

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

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

Quando executado 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. A anotação tf.device abaixo está apenas forçando o posicionamento 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

Consulte Criando novas camadas e modelos por meio de subclasses para obter 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 uma camada keras.layers.RNN e a célula 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"])

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

Com a camada Keras keras.layers.RNN , espera-se que você defina apenas a lógica matemática para cada etapa da sequência, e a camada keras.layers.RNN cuidará da iteração da sequência para você. É uma forma incrivelmente poderosa de criar protótipos de novos tipos de RNNs (por exemplo, uma variante LSTM).

Para obter mais detalhes, visite os documentos da API .