![]() | ![]() | ![]() | ![]() |
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 camadakeras.layers.RNN
genérica (o próprio loopfor
). 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.
Configuração
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:
keras.layers.SimpleRNN
, um RNN totalmente conectado em que a saída do timestep anterior deve ser alimentada para o próximo timestep.keras.layers.GRU
, proposto pela primeira vez em Cho et al., 2014 .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
erecurrent_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, usar as camadas 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.
keras.layers.SimpleRNNCell
corresponde à camadaSimpleRNN
.keras.layers.GRUCell
corresponde à camadaGRU
.keras.layers.LSTMCell
corresponde à camadaLSTM
.
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 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 por 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 entrada e saída única, a entrada extra do estado inicial torna impossível usar 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
detanh
para outra coisa. - Mudar a função
recurrent_activation
desigmoid
para outra coisa. - Usando
recurrent_dropout
> 0. - Definindo
unroll
como True, o que força LSTM / GRU a decompor otf.while_loop
interno em um loopfor
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
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 Criação de 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 .