Rekurencyjne sieci neuronowe (RNN) z Keras

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub Pobierz notatnik

Wstęp

Rekurencyjne sieci neuronowe (RNN) to klasa sieci neuronowych, która doskonale nadaje się do modelowania danych sekwencyjnych, takich jak szeregi czasowe lub język naturalny.

Schematycznie warstwa RNN wykorzystuje for pętli do iteracyjnego timesteps sekwencji, przy zachowaniu stanu wewnętrznego, który koduje informacje o timesteps to ma jak do tej pory.

Keras RNN API został zaprojektowany z naciskiem na:

  • Łatwość użytkowania: wbudowany keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU warstw pozwalają szybko budować modele nawracające bez konieczności dokonywania trudnych wyborów konfiguracyjnych.

  • Łatwość dostosowywania: Można również zdefiniować własną warstwę komórek RNN (wewnętrzną częścią for pętli) z niestandardowego zachowania i używać go z ogólnym keras.layers.RNN warstwy (w for pętli samego). Pozwala to na szybkie prototypowanie różnych pomysłów badawczych w elastyczny sposób przy minimalnym kodzie.

Ustawiać

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

Wbudowane warstwy RNN: prosty przykład

Keras ma trzy wbudowane warstwy RNN:

  1. keras.layers.SimpleRNN , w pełni połączone RNN gdzie wyjście z poprzednim kroku to ma być podawany do następnego kroku to.

  2. keras.layers.GRU , po raz pierwszy zaproponowana w Cho i wsp., 2014 r .

  3. keras.layers.LSTM , po raz pierwszy zaproponowana w Hochreiter & Schmidhuber, 1997 .

Na początku 2015 r. Keras miał pierwsze wielokrotnego użytku implementacje open-source Pythona LSTM i GRU.

Oto przykład proste z Sequential modelu procesów sekwencje liczb całkowitych, osadza każda liczba do 64-wymiarowego wektora, a następnie przetwarza ciąg wektorów stosując LSTM warstwy.

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
_________________________________________________________________

Wbudowane sieci RNN obsługują szereg przydatnych funkcji:

  • Nawracające przerywania, za pośrednictwem dropout i recurrent_dropout argumentów
  • Możliwość przetwarzania sekwencji wprowadzania w odwrotnym kierunku, przez go_backwards argumentu
  • Odwijanie pętli (co może prowadzić do dużego SpeedUp podczas obróbki krótkich sekwencji w CPU), za pośrednictwem unroll argumentu
  • ...i więcej.

Aby uzyskać więcej informacji, zobacz dokumentację API RNN .

Wyjścia i stany

Domyślnie wynik warstwy RNN zawiera jeden wektor na próbkę. Ten wektor to wyjście komórki RNN odpowiadające ostatniemu krokowi czasowemu, zawierające informacje o całej sekwencji wejściowej. Kształt tego wyjścia jest (batch_size, units) , gdzie units odpowiada do units argumentu przekazywane do konstruktora warstwy.

Warstwa A RNN może powrócić całą sekwencję wyjściowych dla każdej próbki (jeden wektor według kroku to w próbce), jeśli zestaw return_sequences=True . Kształt tego wyjścia jest (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
_________________________________________________________________

Ponadto warstwa RNN może zwrócić swój końcowy stan wewnętrzny. Zwracany stany mogą być wykorzystane, aby wznowić wykonywanie RNN później, lub zainicjować inną RNN . To ustawienie jest powszechnie używane w modelu sekwencja-sekwencja koder-dekoder, w którym stan końcowy kodera jest używany jako stan początkowy dekodera.

Aby skonfigurować warstwę RNN powrotu swój stan wewnętrzny, ustaw return_state parametru na True podczas tworzenia warstwy. Zauważ, że LSTM ma 2 tensory państwowe, ale GRU ma tylko jeden.

Aby skonfigurować początkowy stan warstwy, po prostu zadzwoń z dodatkową warstwę kluczowego argumentu initial_state . Zwróć uwagę, że kształt stanu musi odpowiadać rozmiarowi jednostki warstwy, jak w poniższym przykładzie.

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
__________________________________________________________________________________________________

Warstwy RNN i komórki RNN

Oprócz wbudowanych warstw RNN interfejs API RNN zapewnia również interfejsy API na poziomie komórki. W przeciwieństwie do warstw RNN, które przetwarzają całe partie sekwencji wejściowych, komórka RNN przetwarza tylko jeden krok czasowy.

Ogniwo jest wnętrze for pętli warstwy RNN. Owijania komórki wewnątrz keras.layers.RNN warstwy daje się warstwę zdolnego do przetwarzania porcji sekwencji, np RNN(LSTMCell(10)) .

Matematycznie RNN(LSTMCell(10)) daje taki sam wynik jak LSTM(10) . W rzeczywistości implementacja tej warstwy w TF v1.x polegała na utworzeniu odpowiedniej komórki RNN i owinięciu jej w warstwę RNN. Jednak za pomocą wbudowanego w GRU i LSTM warstw umożliwiają zastosowanie CuDNN i można zobaczyć lepszą wydajność.

Istnieją trzy wbudowane komórki RNN, z których każda odpowiada pasującej warstwie RNN.

Abstrakcja komórkowej, wraz z ogólnym keras.layers.RNN klasy sprawiają, że bardzo łatwo wdrożyć niestandardowe RNN architektur do badań.

Stanowość między partiami

Podczas przetwarzania bardzo długich sekwencji (prawdopodobnie nieskończone), można użyć wzoru cross-wsadowym statefulness.

Zwykle stan wewnętrzny warstwy RNN jest resetowany za każdym razem, gdy widzi nową partię (tzn. zakłada się, że każda próbka widziana przez warstwę jest niezależna od przeszłości). Warstwa zachowa stan tylko podczas przetwarzania danej próbki.

Jeśli jednak masz bardzo długie sekwencje, warto podzielić je na krótsze sekwencje i podawać sekwencyjnie te krótsze sekwencje do warstwy RNN bez resetowania stanu warstwy. W ten sposób warstwa może zachować informacje o całej sekwencji, nawet jeśli widzi tylko jedną podsekwencję na raz.

Można to zrobić poprzez ustawienie stateful=True w konstruktorze.

Jeśli masz sekwencji s = [t0, t1, ... t1546, t1547] , byś podzielić ją na przykład

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

Następnie przetworzysz to za pomocą:

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

Gdy chcesz, aby usunąć stan, można użyć layer.reset_states() .

Oto pełny przykład:

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

Ponowne wykorzystanie stanu RNN

Nagrane stany warstwy RNN nie są wliczone w layer.weights() . Jeśli chcesz ponownie użyć stan z warstwą RNN można pobrać wartość Zjednoczonych przez layer.states i używać go jako stan początkowy dla nowej warstwy za pośrednictwem interfejsu API funkcjonalnej Keras jak new_layer(inputs, initial_state=layer.states) lub podklasy modelu.

Należy również pamiętać, że w tym przypadku model sekwencyjny może nie być używany, ponieważ obsługuje tylko warstwy z jednym wejściem i wyjściem, dodatkowe wejście stanu początkowego uniemożliwia użycie w tym przypadku.

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)

Dwukierunkowe sieci RNN

W przypadku sekwencji innych niż szeregi czasowe (np. tekst) często zdarza się, że model RNN może działać lepiej, jeśli nie tylko przetwarza sekwencję od początku do końca, ale także wstecz. Na przykład, aby przewidzieć następne słowo w zdaniu, często przydaje się kontekst wokół tego słowa, a nie tylko słowa poprzedzające je.

Keras zapewnia łatwy API do budowania takich dwukierunkowych RNNs: the keras.layers.Bidirectional opakowanie.

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
_________________________________________________________________

Pod maską Bidirectional skopiuje warstwę RNN przekazany w, i przerzucenie go_backwards pole nowo skopiowanej warstwie, tak, że będzie przetwarzać wejść w odwrotnej kolejności.

Wyjście Bidirectional RNN będzie domyślnie, konkatenacji wyjścia warstwy do przodu i do tyłu na wyjściu warstwy. Jeśli potrzebujesz innego zachowania łączenie, np konkatenacji, zmień merge_mode parametr w Bidirectional konstruktora otoki. Więcej informacji na temat Bidirectional , należy sprawdzić w dokumentacji API .

Optymalizacja wydajności i jądra CuDNN

W TensorFlow 2.0 wbudowane warstwy LSTM i GRU zostały zaktualizowane, aby domyślnie wykorzystywać jądra CuDNN, gdy dostępny jest procesor graficzny. Dzięki tej zmianie wcześniejsze keras.layers.CuDNNLSTM/CuDNNGRU warstwy były przestarzałe, a można zbudować swój model, nie martwiąc się o sprzęcie będzie działać dalej.

Ponieważ jądro CuDNN zbudowany jest z pewnych założeniach, oznacza to, że warstwa nie będzie w stanie wykorzystać jądro CuDNN po zmianie domyślnych wbudowanych LSTM lub GRU warstwach. Np:

  • Zmiana activation funkcji z tanh do czegoś innego.
  • Zmiana recurrent_activation funkcji z sigmoid do czegoś innego.
  • Korzystanie recurrent_dropout > 0.
  • Ustawianie unroll True, której siły LSTM / GRU rozłożenia wewnętrzną tf.while_loop w produkt rozwinięty for pętli.
  • Ustawianie use_bias False.
  • Korzystanie z maskowania, gdy dane wejściowe nie są wypełnione ściśle prawostronnie (jeśli maska ​​odpowiada danym wypełnionym ściśle prawostronnie, CuDNN nadal może być używany. Jest to najczęstszy przypadek).

Dla szczegółowego wykazu ograniczeń, należy zapoznać się z dokumentacją dla LSTM i GRU warstwach.

Używanie jąder CuDNN, jeśli są dostępne

Zbudujmy prosty model LSTM, aby zademonstrować różnicę w wydajności.

Użyjemy jako sekwencji wejściowych sekwencji rzędów cyfr MNIST (traktując każdy rząd pikseli jako krok czasowy) i przewidzimy etykietę cyfry.

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

Załadujmy zbiór danych 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]

Stwórzmy instancję modelu i wyszkolmy ją.

Wybieramy sparse_categorical_crossentropy jako funkcji straty dla modelu. Wyjście modelu ma kształt [batch_size, 10] . Celem modelu jest wektor całkowity, każda z liczb całkowitych mieści się w zakresie od 0 do 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>

Porównajmy teraz do modelu, który nie korzysta z jądra 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>

Podczas pracy na maszynie z zainstalowanym procesorem graficznym NVIDIA i CuDNN model zbudowany za pomocą CuDNN jest znacznie szybszy do trenowania w porównaniu z modelem, który używa zwykłego jądra TensorFlow.

Ten sam model obsługujący CuDNN może być również używany do uruchamiania wnioskowania w środowisku zawierającym tylko procesor. tf.device adnotacji poniżej jest po prostu zmusza umieszczenie urządzenia. Model będzie domyślnie działał na procesorze, jeśli nie jest dostępny procesor graficzny.

Po prostu nie musisz się już martwić o sprzęt, na którym pracujesz. Czy to nie jest fajne?

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 z wejściami list/dict lub wejściami zagnieżdżonymi

Struktury zagnieżdżone umożliwiają realizatorom zawarcie większej ilości informacji w jednym kroku czasowym. Na przykład ramka wideo może mieć jednocześnie wejście audio i wideo. Kształtem danych w tym przypadku może być:

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

W innym przykładzie dane pisma ręcznego mogą mieć współrzędne x i y dla bieżącej pozycji pióra, a także informacje o nacisku. Tak więc reprezentacja danych może wyglądać następująco:

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

Poniższy kod przedstawia przykład, jak zbudować niestandardową komórkę RNN, która akceptuje takie ustrukturyzowane dane wejściowe.

Zdefiniuj niestandardową komórkę, która obsługuje zagnieżdżone wejście/wyjście

Patrz Nawiązywanie nowych warstw i modele poprzez instacji Szczegółowe informacje na temat pisania własnych warstw.

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}

Zbuduj model RNN z zagnieżdżonymi danymi wejściowymi/wyjściowymi

Zbudować model Keras który wykorzystuje Chodźmy keras.layers.RNN warstwę i komórkę niestandardową właśnie zdefiniowana.

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

Trenuj model z losowo generowanymi danymi

Ponieważ nie ma dobrego kandydującego zestawu danych dla tego modelu, do demonstracji używamy losowych danych Numpy.

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>

Z Keras keras.layers.RNN warstwy, oczekuje się tylko do określenia logiki matematycznej dla pojedynczego kroku w sekwencji, a keras.layers.RNN warstwa będzie obsługiwać sekwencji iteracji dla Ciebie. Jest to niesamowicie potężny sposób na szybkie prototypowanie nowych rodzajów sieci RNN (np. wariantu LSTM).

Aby uzyskać więcej informacji, prosimy odwiedzić docs API .