Рекуррентные нейронные сети (RNN) с Keras

Оптимизируйте свои подборки Сохраняйте и классифицируйте контент в соответствии со своими настройками.

Посмотреть на TensorFlow.org Запустить в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

Введение

Рекуррентные нейронные сети (RNN) — это класс нейронных сетей, которые эффективны для моделирования данных последовательности, таких как временные ряды или естественный язык.

Схематически слой РНН использует for цикла для перебора временных шагов последовательности, сохраняя при этом внутреннее состояние , которое кодирует информацию о временных шагов он видел до сих пор.

Keras RNN API разработан с упором на:

  • Простота использования: встроенный keras.layers.RNN , keras.layers.LSTM , keras.layers.GRU слои позволяют быстро создавать повторяющиеся модели без необходимости делать трудный выбор конфигурации.

  • Простота настройки: Вы также можете определить свой собственный слой РНН клеток (внутреннюю часть из for цикла) с настраиваемыми поведением, и использовать его с общим keras.layers.RNN слоем ( for самого цикла). Это позволяет быстро и гибко создавать прототипы различных исследовательских идей с минимальным кодом.

Настраивать

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

Встроенные слои RNN: простой пример

В Keras есть три встроенных слоя RNN:

  1. keras.layers.SimpleRNN , полностью подключенный-РНН , где выход из предыдущего временного шага должен быть подан в следующем временном шаге.

  2. keras.layers.GRU , впервые предложена в Cho и др., 2014 .

  3. keras.layers.LSTM , первый предложенный в Hochreiter & Шмидхубера, 1997 .

В начале 2015 года у Keras были первые многоразовые реализации Python с открытым исходным кодом LSTM и GRU.

Вот простой пример Sequential модели , которая обрабатывает последовательность целых чисел, каждое целое число вкладывается в 64-мерный вектор, затем обрабатывает последовательность векторов с использованием 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
_________________________________________________________________

Встроенные RNN поддерживают ряд полезных функций:

  • Рецидив отсев, через dropout и recurrent_dropout аргументов
  • Возможность обрабатывать входную последовательность в обратном направлении, через go_backwards аргумент
  • Размотка цикл (который может привести к большому ускорению при обработке коротких последовательностей на CPU), с помощью unroll аргумент
  • ...и больше.

Для получения дополнительной информации см документации API РНН .

Выходы и состояния

По умолчанию выходные данные слоя RNN содержат один вектор на выборку. Этот вектор представляет собой выходные данные ячейки RNN, соответствующие последнему временному шагу и содержащие информацию обо всей входной последовательности. Форма этого выхода (batch_size, units) , где units соответствует units аргументу , передаваемых в конструктор слоя.

РНН слой также может возвращать всю последовательность выходов для каждого образца (один вектора за временный шаг на один образец), если установить return_sequences=True . Форма этого выхода (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
_________________________________________________________________

Кроме того, уровень RNN может возвращать свое конечное внутреннее состояние. Возвращенные состояния могут быть использованы для продолжения выполнения РНН позже, или для инициализации другого РНН . Этот параметр обычно используется в модели последовательностей кодер-декодер, где конечное состояние кодера используется в качестве начального состояния декодера.

Чтобы настроить слой РНН , чтобы вернуть свое внутреннее состояние, установите return_state параметр в значение True при создании слоя. Обратите внимание , что LSTM имеет 2 государственных тензоры, но GRU имеет только один.

Для того, чтобы настроить начальное состояние слоя, просто вызовите слой с дополнительным аргументом ключевым словом initial_state . Обратите внимание, что форма состояния должна соответствовать размеру единицы слоя, как в примере ниже.

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
__________________________________________________________________________________________________

Слои RNN и ячейки RNN

Помимо встроенных слоев RNN, RNN API также предоставляет API на уровне ячеек. В отличие от слоев RNN, которые обрабатывают целые пакеты входных последовательностей, ячейка RNN обрабатывает только один временной шаг.

Клетка является внутренней частью for петли слоя РНН. Обертывание ячейки внутри keras.layers.RNN слоя дает слой , способному обрабатывать партии последовательностей, например , RNN(LSTMCell(10)) .

Математически, RNN(LSTMCell(10)) дает тот же результат , как LSTM(10) . На самом деле, реализация этого уровня в TF v1.x просто создавала соответствующую ячейку RNN и оборачивала ее в слой RNN. Однако с помощью встроенного в GRU и LSTM слоев позволяют использовать CuDNN и вы можете увидеть более высокую производительность.

Есть три встроенных ячейки RNN, каждая из которых соответствует соответствующему слою RNN.

Клетка абстракция, вместе с родовым keras.layers.RNN класса, сделать это очень легко реализовать пользовательские РНН архитектуры для ваших исследований.

Межпакетная отслеживание состояния

При обработке очень длинных последовательностей (возможно , бесконечное), вы можете использовать шаблон кросс-периодического statefulness.

Обычно внутреннее состояние уровня RNN сбрасывается каждый раз, когда он видит новый пакет (т. е. предполагается, что каждый образец, видимый слоем, не зависит от прошлого). Слой будет поддерживать состояние только при обработке данного семпла.

Однако, если у вас есть очень длинные последовательности, полезно разбить их на более короткие последовательности и последовательно передать эти более короткие последовательности на уровень RNN без сброса состояния слоя. Таким образом, слой может сохранить информацию обо всей последовательности, даже если он видит только одну подпоследовательность за раз.

Вы можете сделать это, установив с stateful=True в конструкторе.

Если есть последовательность s = [t0, t1, ... t1546, t1547] , вы бы разделить его на , например ,

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

Затем вы обработаете его через:

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

Если вы хотите , чтобы очистить состояние, вы можете использовать layer.reset_states() .

Вот полный пример:

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

Записанные состояния слоя РНН не включены в layer.weights() . Если вы хотите повторно использовать состояние из слоя РНН, вы можете получить значение состояний с помощью layer.states и использовать его в качестве исходного состояния для нового слоя с помощью функционального API Keras как new_layer(inputs, initial_state=layer.states) или подкласс модели.

Также обратите внимание, что последовательная модель может не использоваться в этом случае, поскольку она поддерживает только слои с одним входом и выходом, дополнительный вход начального состояния делает невозможным использование здесь.

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

Для последовательностей, отличных от временных рядов (например, текста), модель RNN часто может работать лучше, если она обрабатывает последовательность не только от начала до конца, но и в обратном направлении. Например, чтобы предсказать следующее слово в предложении, часто полезно иметь контекст вокруг слова, а не только слова, которые стоят перед ним.

Keras предоставляет простой API для вас , чтобы построить такую двунаправленную RNNs: в 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
_________________________________________________________________

Под капотом Bidirectional будет копировать слой РНН , переданный в, и флип go_backwards поле вновь скопированного слоя, таким образом , что он будет обрабатывать входные данные в обратном порядке.

Выход Bidirectional RNN будет, по умолчанию, конкатенации выходного сигнала прямого слоя и выходом обратного слоя. Если вам нужно другое поведение слияния, например , конкатенация, изменить merge_mode параметр в Bidirectional обертке конструктора. Для получения более подробной информации о Bidirectional , пожалуйста , проверьте в API документации .

Оптимизация производительности и ядра CuDNN

В TensorFlow 2.0 встроенные уровни LSTM и GRU были обновлены, чтобы использовать ядра CuDNN по умолчанию, когда доступен графический процессор. С этим изменением, предшествующие keras.layers.CuDNNLSTM/CuDNNGRU слои устарели, и вы можете построить модель , не заботясь о начинке он будет работать дальше.

Так как ядро CuDNN построено с некоторыми допущениями, это означает , что слой не будет иметь возможности использовать ядро CuDNN , если вы измените значения по умолчанию из встроенных слоев LSTM или ГРУ. Например:

  • Изменение activation функции от tanh к чему - то еще.
  • Изменение recurrent_activation функции от sigmoid к чему - то еще.
  • Использование recurrent_dropout > 0.
  • Установка unroll к истинному, что силы LSTM / ГРУ , чтобы разложить внутреннюю tf.while_loop в непрокатанную for цикла.
  • Установка use_bias Ложь.
  • Использование маскирования, когда входные данные не заполнены строго справа (если маска соответствует данным, дополненным строго справа, CuDNN все равно можно использовать. Это наиболее распространенный случай).

Подробный список ограничений, обратитесь к документации по LSTM и ГРУ слоев.

Использование ядер CuDNN, когда они доступны

Давайте создадим простую модель LSTM, чтобы продемонстрировать разницу в производительности.

Мы будем использовать в качестве входных последовательностей последовательность строк цифр MNIST (обрабатывая каждую строку пикселей как временной шаг) и предскажем метку цифры.

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

Давайте загрузим набор данных 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]

Давайте создадим экземпляр модели и обучим его.

Мы выбираем sparse_categorical_crossentropy как функции потерь для модели. Выход модели имеет форму [batch_size, 10] . Целью модели является целочисленный вектор, каждое целое число находится в диапазоне от 0 до 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>

Теперь давайте сравним с моделью, которая не использует ядро ​​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>

При работе на машине с графическим процессором NVIDIA и установленным CuDNN модель, построенная с помощью CuDNN, намного быстрее обучается по сравнению с моделью, использующей обычное ядро ​​TensorFlow.

Ту же модель с поддержкой CuDNN также можно использовать для выполнения логических выводов в среде, использующей только ЦП. tf.device аннотации ниже только вынудив размещение устройства. Модель будет работать на процессоре по умолчанию, если нет доступного графического процессора.

Вам просто больше не нужно беспокоиться об оборудовании, на котором вы работаете. Разве это не круто?

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 с входными данными list/dict или вложенными входными данными

Вложенные структуры позволяют разработчикам включать больше информации за один временной шаг. Например, видеокадр может иметь аудио- и видеовход одновременно. Форма данных в этом случае может быть:

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

В другом примере данные рукописного ввода могут иметь как координаты x, так и y для текущего положения пера, а также информацию о нажиме. Таким образом, представление данных может быть:

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

В следующем коде приведен пример создания пользовательской ячейки RNN, которая принимает такие структурированные входные данные.

Определите пользовательскую ячейку, которая поддерживает вложенный ввод/вывод

См Создание новых слоев & Модели с помощью подклассов для получения подробной информации о написании собственных слоев.

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}

Создайте модель RNN с вложенным вводом/выводом

Давайте построим модель Keras , которая использует keras.layers.RNN слой и пользовательские ячейки мы только что определили.

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

Обучите модель с помощью случайно сгенерированных данных

Поскольку для этой модели нет подходящего набора данных-кандидата, для демонстрации мы используем случайные данные 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>

С Keras keras.layers.RNN слоем, Вы только ожидать , чтобы определить математическую логику для отдельного шага в последовательности, и keras.layers.RNN слой будет обрабатывать итерацию последовательности для вас. Это невероятно мощный способ быстрого создания прототипов новых типов RNN (например, варианта LSTM).

Для получения более подробной информации, пожалуйста , посетите API Docs .