Reti neurali ricorrenti (RNN) con Keras

Mantieni tutto organizzato con le raccolte Salva e classifica i contenuti in base alle tue preferenze.

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza l'origine su GitHub Scarica quaderno

introduzione

Le reti neurali ricorrenti (RNN) sono una classe di reti neurali potente per la modellazione di dati di sequenza come serie temporali o linguaggio naturale.

Schematicamente, uno strato RNN utilizza for ciclo per scorrere i Timesteps di una sequenza, mantenendo uno stato interno che codifica le informazioni sui Timesteps ha visto finora.

L'API Keras RNN è progettata con particolare attenzione a:

  • Facilità d'uso: il built-in keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU strati consentono di costruire rapidamente modelli ricorrenti, senza dover fare scelte di configurazione difficili.

  • Facilità di personalizzazione: è anche possibile definire il proprio strato di cellule RNN (la parte interna del for ciclo) con comportamento personalizzato, e utilizzarlo con il generico keras.layers.RNN strato (il for ciclo stesso). Ciò consente di prototipare rapidamente diverse idee di ricerca in modo flessibile con un codice minimo.

Impostare

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

Layer RNN integrati: un semplice esempio

Ci sono tre livelli RNN integrati in Keras:

  1. keras.layers.SimpleRNN , un RNN completamente collegato in cui l'uscita dal passo temporale precedente deve essere alimentata al successivo passo temporale.

  2. keras.layers.GRU , prima proposto in Cho et al. 2014 .

  3. keras.layers.LSTM , prima proposto in Hochreiter & Schmidhuber, 1997 .

All'inizio del 2015, Keras ha avuto le prime implementazioni Python open source riutilizzabili di LSTM e GRU.

Ecco un semplice esempio di un Sequential modello che processi sequenze di numeri interi, incorpora ciascun intero in un vettore 64-dimensionale, quindi elabora la sequenza di vettori usando un LSTM strato.

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
_________________________________________________________________

Gli RNN integrati supportano una serie di utili funzioni:

  • Dropout ricorrente, tramite i dropout e recurrent_dropout argomenti
  • Capacità di elaborare una sequenza di immissione in retromarcia, con il go_backwards argomento
  • Srotolamento Loop (che può portare ad un grande aumento di velocità durante l'elaborazione di brevi sequenze di CPU), tramite il unroll argomento
  • ...e altro ancora.

Per ulteriori informazioni, consultare la documentazione API RNN .

Uscite e stati

Per impostazione predefinita, l'output di un livello RNN contiene un singolo vettore per campione. Questo vettore è l'output della cella RNN corrispondente all'ultimo timestep, contenente informazioni sull'intera sequenza di input. La forma di questa uscita è (batch_size, units) dove units corrisponde alla units argomento passato al costruttore del livello.

Strato A RNN può anche restituire l'intera sequenza di uscite per ciascun campione (un vettore per ogni passo temporale per campione), se si imposta return_sequences=True . La forma di questa uscita è (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
_________________________________________________________________

Inoltre, uno strato RNN può restituire i suoi stati interni finali. Gli stati restituiti possono essere utilizzati per riprendere l'esecuzione RNN seguito, o per inizializzare un altro RNN . Questa impostazione è comunemente utilizzata nel modello da sequenza a sequenza encoder-decodificatore, in cui lo stato finale dell'encoder viene utilizzato come stato iniziale del decoder.

Per configurare un livello RNN per tornare il suo stato interno, impostare il return_state parametro su True durante la creazione dello strato. Si noti che LSTM ha 2 tensori statali, ma GRU ha una sola.

Per configurare lo stato iniziale dello strato, basta chiamare il livello con ulteriore argomento chiave initial_state . Nota che la forma dello stato deve corrispondere alla dimensione dell'unità del livello, come nell'esempio seguente.

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
__________________________________________________________________________________________________

Strati RNN e celle RNN

Oltre ai livelli RNN incorporati, l'API RNN fornisce anche API a livello di cella. A differenza dei livelli RNN, che elaborano interi batch di sequenze di input, la cella RNN elabora solo un singolo timestep.

La cella è l'interno del for ciclo di uno strato RNN. Avvolgendo una cella all'interno di una keras.layers.RNN strato fornisce uno strato in grado di elaborare lotti di sequenze, ad esempio RNN(LSTMCell(10)) .

Matematicamente, RNN(LSTMCell(10)) produce lo stesso risultato di LSTM(10) . In effetti, l'implementazione di questo livello in TF v1.x stava semplicemente creando la cella RNN corrispondente e avvolgendola in un livello RNN. Tuttavia utilizzando il built-in GRU e LSTM strati consentire l'utilizzo di CuDNN e si può vedere la migliore prestazione.

Ci sono tre celle RNN integrate, ognuna delle quali corrisponde al livello RNN corrispondente.

L'astrazione delle cellule, insieme con il generico keras.layers.RNN di classe, lo rendono molto facile da implementare architetture RNN personalizzati per la vostra ricerca.

Stateness cross-batch

Durante l'elaborazione di sequenze molto lunghe (possibilmente infinita), si consiglia di utilizzare il modello di statefulness cross-batch.

Normalmente, lo stato interno di un layer RNN viene ripristinato ogni volta che vede un nuovo batch (cioè ogni campione visto dal layer è considerato indipendente dal passato). Il livello manterrà uno stato solo durante l'elaborazione di un determinato campione.

Tuttavia, se si dispone di sequenze molto lunghe, è utile suddividerle in sequenze più brevi e alimentare queste sequenze più brevi in ​​sequenza in un livello RNN senza ripristinare lo stato del livello. In questo modo, il livello può conservare le informazioni sull'intera sequenza, anche se vede solo una sottosequenza alla volta.

È possibile farlo impostando stateful=True nel costruttore.

Se si dispone di una sequenza s = [t0, t1, ... t1546, t1547] , si sarebbe dividerlo in pe

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

Quindi lo elaborerai tramite:

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

Quando si desidera cancellare lo stato, è possibile utilizzare layer.reset_states() .

Ecco un esempio 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()

Riutilizzo dello stato RNN

Gli stati registrati dello strato RNN non sono inclusi nelle layer.weights() . Se si desidera riutilizzare lo stato da uno strato RNN, è possibile recuperare il valore Uniti da layer.states e usarlo come lo stato iniziale per un nuovo livello tramite l'API funzionale Keras come new_layer(inputs, initial_state=layer.states) , o la sottoclasse del modello.

Si noti inoltre che il modello sequenziale potrebbe non essere utilizzato in questo caso poiché supporta solo livelli con input e output singoli, l'input aggiuntivo dello stato iniziale ne rende impossibile l'uso qui.

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 bidirezionali

Per sequenze diverse dalle serie temporali (ad es. testo), accade spesso che un modello RNN possa funzionare meglio se non solo elabora la sequenza dall'inizio alla fine, ma anche all'indietro. Ad esempio, per prevedere la parola successiva in una frase, è spesso utile avere il contesto intorno alla parola, non solo le parole che la precedono.

Keras fornisce un'API facile per voi per costruire tali RNR bidirezionali: il keras.layers.Bidirectional involucro.

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
_________________________________________________________________

Sotto il cofano, Bidirectional copierà strato RNN passata, e capovolgere il go_backwards campo dello strato appena copiato, in modo che possa elaborare gli ingressi in ordine inverso.

L'uscita del Bidirectional RNN sarà, per default, la concatenazione dell'uscita strato in avanti e l'uscita strato all'indietro. Se avete bisogno di un comportamento fusione diverso, ad esempio la concatenazione, modificare il merge_mode parametro nella Bidirectional costruttore di involucro. Per maggiori dettagli su Bidirectional , si prega di consultare la documentazione API .

Ottimizzazione delle prestazioni e kernel CuDNN

In TensorFlow 2.0, i livelli LSTM e GRU integrati sono stati aggiornati per sfruttare i kernel CuDNN per impostazione predefinita quando è disponibile una GPU. Con questa modifica, i precedenti keras.layers.CuDNNLSTM/CuDNNGRU strati sono stati deprecati, e si può costruire il modello senza preoccuparsi dell'hardware che verrà eseguito su.

Dal momento che il kernel CuDNN è costruito con determinati presupposti, questo significa che il livello non sarà in grado di utilizzare il kernel CuDNN se si modificano le impostazioni predefinite degli strati LSTM o GRU incorporati. Per esempio:

  • Modifica della activation funzione dal tanh a qualcos'altro.
  • Modifica della recurrent_activation funzione dal sigmoid a qualcos'altro.
  • Utilizzando recurrent_dropout > 0.
  • Impostazione unroll a True, che le forze LSTM / GRU per decomporre l'interno tf.while_loop in un srotolato for ciclo.
  • Impostazione use_bias False.
  • Utilizzo della mascheratura quando i dati di input non sono rigorosamente riempiti a destra (se la maschera corrisponde a dati rigorosamente riempiti a destra, CuDNN può ancora essere utilizzato. Questo è il caso più comune).

Per l'elenco dettagliato dei vincoli, si prega di consultare la documentazione per le LSTM e GRU strati.

Utilizzo dei kernel CuDNN quando disponibili

Costruiamo un semplice modello LSTM per dimostrare la differenza di prestazioni.

Utilizzeremo come sequenze di input la sequenza di righe di cifre MNIST (trattando ogni riga di pixel come un timestep) e prevederemo l'etichetta della cifra.

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

Carichiamo il set di dati 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]

Creiamo un'istanza del modello e la addestriamo.

Abbiamo scelto sparse_categorical_crossentropy come la funzione di perdita per il modello. L'uscita del modello ha forma di [batch_size, 10] . L'obiettivo per il modello è un vettore intero, ciascuno dei numeri interi è compreso tra 0 e 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>

Ora, confrontiamo con un modello che non utilizza il 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>

Quando è in esecuzione su una macchina con una GPU NVIDIA e CuDNN installati, il modello creato con CuDNN è molto più veloce da addestrare rispetto al modello che utilizza il normale kernel TensorFlow.

Lo stesso modello abilitato per CuDNN può essere utilizzato anche per eseguire l'inferenza in un ambiente solo CPU. Il tf.device annotazione di seguito è solo forzando il posizionamento del dispositivo. Il modello verrà eseguito su CPU per impostazione predefinita se non è disponibile alcuna GPU.

Semplicemente non devi più preoccuparti dell'hardware su cui stai utilizzando. Non è abbastanza bello?

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 input list/dict o input nidificati

Le strutture nidificate consentono agli implementatori di includere più informazioni in un unico passaggio temporale. Ad esempio, un fotogramma video potrebbe avere input audio e video contemporaneamente. La forma dei dati in questo caso potrebbe essere:

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

In un altro esempio, i dati sulla scrittura a mano potrebbero avere sia le coordinate xey per la posizione corrente della penna, sia le informazioni sulla pressione. Quindi la rappresentazione dei dati potrebbe essere:

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

Il codice seguente fornisce un esempio di come creare una cella RNN personalizzata che accetti tali input strutturati.

Definire una cella personalizzata che supporti l'input/output nidificato

Vedere Conoscere nuovi Livelli e Costruzioni via sottoclassi per i dettagli su come scrivere i propri livelli.

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}

Crea un modello RNN con input/output nidificato

Costruzione di un modello Let Keras che utilizza un keras.layers.RNN livello e la cella personalizzato che abbiamo appena definito.

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

Addestrare il modello con dati generati casualmente

Poiché non esiste un buon set di dati candidato per questo modello, utilizziamo dati Numpy casuali per la dimostrazione.

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 il Keras keras.layers.RNN livello, ci si aspetta solo di definire la logica matematica per singola fase all'interno della sequenza, e la keras.layers.RNN strato gestirà l'iterazione sequenza per voi. È un modo incredibilmente potente per prototipare rapidamente nuovi tipi di RNN (ad esempio una variante LSTM).

Per ulteriori informazioni, si prega di visitare i documentazione API .