此页面由 Cloud Translation API 翻译。
Switch to English

带有Keras的递归神经网络(RNN)

在TensorFlow.org上查看 在Google Colab中运行 在GitHub上查看源代码 下载笔记本

介绍

递归神经网络(RNN)是一类神经网络,对于建模序列数据(例如时间序列或自然语言)非常有力。

在示意图上,RNN层使用for循环在序列的时间步上进行迭代,同时保持内部状态,该状态对迄今为止已看到的时间步的信息进行编码。

Keras RNN API的设计重点是:

  • 易于使用 :内置的keras.layers.RNNkeras.layers.LSTMkeras.layers.GRU图层使您能够快速构建循环模型,而不必进行困难的配置选择。

  • 易于定制 :您还可以使用自定义行为定义自己的RNN单元层( 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 ,一个完全连接的RNN,上一个时间步的输出将馈送到下一个时间步。

  2. keras.layers.GRU ,最早在Cho等人,2014年提出

  3. keras.layers.LSTM ,首先在Hochreiter&Schmidhuber,1997年提出

2015年初,Keras拥有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支持许多有用的功能:

  • 通过dropoutrecurrent_dropout参数进行反复辍学
  • 可以通过go_backwards参数反向处理输入序列
  • 通过unroll参数进行循环展开(在CPU上处理短序列时可能导致较大的加速)
  • ...和更多。

有关更多信息,请参见RNN API文档

输出和状态

默认情况下,RNN层的输出每个样本包含一个矢量。该向量是与最后一个时间步相对应的RNN单元输出,其中包含有关整个输入序列的信息。此输出的形状为(batch_size, units) ,其中units对应于传递给图层构造函数的units参数。

如果您设置return_sequences=True ,则RNN层还可以返回每个样本的整个输出序列(每个样本每个时间步一个向量)。此输出的形状是(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层可以返回其最终内部状态。返回的状态可用于稍后恢复RNN执行或初始化另一个RNN 。此设置通常用于编码器-解码器序列到序列模型,其中编码器的最终状态用作解码器的初始状态。

要将RNN图层配置为返回其内部状态,请在创建图层时将return_state参数设置为True 。请注意, LSTM具有2个状态张量,但GRU仅具有1个。

要配置图层的初始状态,只需使用其他关键字参数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单元仅处理单个时间步。

该单元格是RNN层的for循环的内部。在keras.layers.RNN层中包装一个单元,可以为您提供能够处理一批序列的层,例如RNN(LSTMCell(10))

从数学RNN(LSTMCell(10))RNN(LSTMCell(10))产生与LSTM(10)相同的结果。实际上,在TF v1.x中该层的实现只是创建相应的RNN单元并将其包装在RNN层中。但是,使用内置的GRULSTM层可以使用CuDNN,您可能会看到更好的性能。

内置三个RNN单元,每个单元对应于匹配的RNN层。

单元抽象以及通用的keras.layers.RNN类使为研究实现自定义RNN体系结构变得非常容易。

跨批状态

当处理非常长的序列(可能是无限的)时,您可能需要使用跨批处理有状态性的模式。

通常,每次看到新批次时,都会重置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状态重用

RNN层的记录状态不包含在layer.weights() 。如果您想从RNN层重用状态,则可以通过layer.states功能API(例如new_layer(inputs, initial_state=layer.states)通过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,供您构建此类双向keras.layers.Bidirectionalkeras.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
_________________________________________________________________

go_backwardsBidirectional将复制传入的RNN层,并翻转新复制的层的go_backwards字段,以便它将以相反的顺序处理输入。

默认情况下, Bidirectional RNN的输出将是前向层输出和后向层输出的总和。如果您需要其他合并行为(例如,串联),请在“ Bidirectional包装”构造函数中更改merge_mode参数。有关Bidirectional更多详细信息,请检查API文档

性能优化和CuDNN内核

在TensorFlow 2.0中,内置的LSTM和GRU层已更新为在GPU可用时默认使用CuDNN内核。通过此更改,先前的keras.layers.CuDNNLSTM/CuDNNGRU层已被弃用,您可以构建模型而不必担心它将运行的硬件。

由于CuDNN内核是根据某些假设构建的,因此这意味着如果您更改内置LSTM或GRU层的默认设置,则该层将无法使用CuDNN内核 。例如:

  • activation功能从tanh更改为其他功能。
  • recurrent_activation函数从sigmoid更改为其他。
  • 使用recurrent_dropout > 0。
  • unroll设置为True,这将迫使LSTM / GRU将内部tf.while_loop分解为tf.while_loop for循环。
  • use_bias设置为False。
  • 当输入数据未严格右填充时使用掩蔽(如果掩码对应于严格右填充数据,则仍可以使用CuDNN。这是最常见的情况)。

有关约束的详细列表,请参阅LSTMGRU层的文档。

在可用时使用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 [==============================] - 5s 5ms/step - loss: 0.9479 - accuracy: 0.6979 - val_loss: 0.5026 - val_accuracy: 0.8424

<tensorflow.python.keras.callbacks.History at 0x7fc1900c8128>

现在,让我们与不使用CuDNN内核的模型进行比较:

 noncudnn_model = build_model(allow_cudnn_kernel=False)
noncudnn_model.set_weights(model.get_weights())
noncudnn_model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer="sgd",
    metrics=["accuracy"],
)
noncudnn_model.fit(
    x_train, y_train, validation_data=(x_test, y_test), batch_size=batch_size, epochs=1
)
 
938/938 [==============================] - 36s 38ms/step - loss: 0.3927 - accuracy: 0.8809 - val_loss: 0.2804 - val_accuracy: 0.9132

<tensorflow.python.keras.callbacks.History at 0x7fc144215cc0>

在安装了NVIDIA GPU和CuDNN的计算机上运行时,与使用常规TensorFlow内核的模型相比,使用CuDNN构建的模型的训练速度要快得多。

相同的启用了CuDNN的模型也可以用于在仅CPU的环境中运行推理。下面的tf.device注释只是强制放置设备。如果没有可用的GPU,默认情况下该模型将在CPU上运行。

您完全不必担心正在运行的硬件。那不是很酷吗?

 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

嵌套结构允许实施者在单个时间步之内包括更多信息。例如,一个视频帧可以同时具有音频和视频输入。在这种情况下,数据形状可能是:

[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.layers.RNN层和我们刚刚定义的自定义单元的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 [==============================] - 0s 22ms/step - loss: 0.7499 - rnn_1_loss: 0.2790 - rnn_1_1_loss: 0.4709 - rnn_1_accuracy: 0.0844 - rnn_1_1_accuracy: 0.0336

<tensorflow.python.keras.callbacks.History at 0x7fc21e1d1208>

使用keras.layers.RNN层,只需要为序列中的单个步骤定义数学逻辑,而keras.layers.RNN层将为您处理序列迭代。这是一种快速原型化新型RNN(例如LSTM变体)的强大方法。

有关更多详细信息,请访问API文档