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

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

Вступление

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

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

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

  • Простота использования: встроенный 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 года у Кераса были первые многоразовые реализации LSTM и GRU на Python с открытым исходным кодом.

Вот простой пример 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 .