RNN(循环神经网络)教程:TensorFlow 示例

⚡ 智能摘要

循环神经网络为普通神经元添加了记忆状态,将每个输出作为下一个输入反馈,从而使模型能够从顺序中学习。本教程将使用 TensorFlow 构建一个循环神经网络,并预测一个时间序列。

  • 🔘 内存状态: 每个循环神经元都会接收前一个输出以及新的输入,这使得网络具有时间感知能力。
  • ☑️ 输出形状: 玩具网络运行一次会返回一个 (3, 2, 6) 张量:批次、时间步长和神经元,顺序如此。
  • 批处理规则: 时间序列输入被重塑为三维,标签数组是同一序列向前移动一个周期。
  • 🧪 模型骨架: 占位符,一个用 dynamic_rnn 包裹的 BasicRNNCell,一个密集投影,然后使用 Adam 优化器计算均方误差。
  • 🛠️ 训练跑: 在 120 个循环神经元上进行 1,500 个周期的训练,可以将损失从 502,893 降低到大约 1,385。
  • 📉 已知极限: 长序列会引发梯度消失问题,而 LSTM 及其门控变体正是为了解决这个问题而设计的。

RNN 循环神经网络

为什么我们需要循环神经网络(RNN)?

循环神经网络 (RNN) 允许您对记忆单元进行建模以保存数据并建模短期依赖关系。它还用于时间序列预测,以识别数据相关性和模式。它还通过提供与人脑类似的行为来帮助产生序列数据的预测结果。

一个结构 人工神经网络 它相对简单,主要涉及矩阵乘法。在第一步中,输入值与初始随机权重和偏置相乘,然后通过激活函数进行变换,最后使用输出值进行预测。这一步可以反映出网络预测结果与实际情况的偏差程度。

所应用的度量是损失。损失函数越高,模型越愚蠢。为了提高网络的知识,需要通过调整网络的权重进行一些优化。随机梯度下降是用于在正确方向上改变权重值的方法。一旦进行调整,网络就可以使用另一批数据来测试其新知识。

幸运的是,误差比之前有所降低,但仍然不够小。优化步骤会迭代进行,直到误差最小化,即无法再引入更多信息为止。trac特德。

这种模型的问题在于,它没有任何记忆。这意味着输入和输出是独立的。换句话说,模型并不关心之前发生了什么。当你需要预测时间序列或句子时,它会产生一些问题,因为网络需要有关历史数据或过去单词的信息。

为了解决这个问题,人们开发了一种新型架构:循环神经网络(以下简称 RNN)

什么是循环神经网络 (RNN)?

循环神经网络(RNN)是一类人工神经网络,其中不同节点之间的连接构成有向图,从而产生时间动态行为。它有助于对源自前馈网络的序列数据进行建模。其工作原理类似于人脑,能够提供预测结果。

循环神经网络与传统神经网络非常相似,只是在神经元中添加了记忆状态。添加记忆的计算很简单。

想象一个简单的模型,它只有一个神经元,接收一批数据作为输入。在传统的神经网络中,模型通过将输入乘以权重和激活函数来产生输出。而在循环神经网络(RNN)中,这个输出会被多次反馈给自身。我们称之为循环。 时间步长 输出作为下一次矩阵乘法输入所花费的时间。

例如,在下图中,你可以看到网络由一个神经元组成。网络计算输入和权重之间的矩阵乘法,并使用激活函数添加非线性。它成为 t-1 时的输出。此输出是第二个矩阵乘法的输入。

单个循环神经元将其在 t-1 时刻的输出作为下一次乘法运算的输入。

递归神经网络(RNN)

下面,我们在 TensorFlow 中编写一个简单的 RNN 来了解步骤和输出的形状。

该网络由以下部分组成:

  • 四个输入
  • 六个神经元
  • 2 时间步

网络将按下图所示进行。

展开的循环神经网络,具有四个输入、六个神经元和两个时间步长

该网络之所以被称为“循环”网络,是因为它在每个激活方块中执行相同的操作。在使用激活函数之前,网络会计算输入和先前输出的权重。

版本说明: 本教程中的代码针对的是 TensorFlow 1.x 图 API。在 TensorFlow 2.x 中,相同的脚本仍然可以运行。 import tensorflow.compat.v1 as tftf.disable_v2_behavior()因为占位符、会话和 rnn_cell 模块现在都位于 tf.compat.v1. 2.x 版本中对应的惯用表达是 tf.keras.layers.SimpleRNN.

import numpy as np
import tensorflow as tf
n_inputs = 4
n_neurons = 6
n_timesteps = 2
The data is a sequence of a number from 0 to 9 and divided into three batches of data.
## Data 
X_batch = np.array([
        [[0, 1, 2, 5], [9, 8, 7, 4]], # Batch 1
        [[3, 4, 5, 2], [0, 0, 0, 0]], # Batch 2
        [[6, 7, 8, 5], [6, 5, 4, 2]], # Batch 3
    ])

我们可以用数据占位符、循环阶段和输出来构建网络。

定义数据的占位符

X = tf.placeholder(tf.float32, [None, n_timesteps, n_inputs])

这里:

  • 无:未知,将占用批次的大小
  • n_timesteps:网络将输出发送回神经元的次数
  • n_inputs:每批输入的数量

定义循环网络

如上图所示,该网络由 6 个神经元组成。网络将计算两个点积:

  • 具有第一组权重的输入数据(即6:等于神经元的数量)
  • 使用第二组权重(即 6:对应于输出数量)的先前输出

请注意,在第一次前馈期间,先前输出的值等于零,因为我们没有任何可用的值。

构建 RNN 的对象是 tf.contrib.rnn.BasicRNNCell,其参数为 num_units,用于定义输入的数量

basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)

现在网络已经定义好了,你可以计算输出和状态

outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)

该对象使用内部循环将矩阵乘以适当的次数。

注意,循环神经元是之前所有时间步输入的函数。这就是网络构建自身记忆的方式。来自前一时间的信息可以传递到未来。这就是循环神经网络的奥妙所在。

## Define the shape of the tensor
X = tf.placeholder(tf.float32, [None, n_timesteps, n_inputs])
## Define the network
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
outputs, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)
init = tf.global_variables_initializer()
init = tf.global_variables_initializer()
with tf.Session() as sess:
    init.run()
    outputs_val = outputs.eval(feed_dict={X: X_batch})
print(states.eval(feed_dict={X: X_batch}))
[[ 0.38941205 -0.9980438   0.99750966  0.7892596   0.9978241   0.9999997 ]
 [ 0.61096436  0.7255889   0.82977575 -0.88226104  0.29261455 -0.15597084]
 [ 0.62091285 -0.87023467  0.99729395 -0.58261937  0.9811445   0.99969864]]

为了便于解释,我们打印前一个状态的值。上面打印的输出显示了最后一个状态的输出。现在打印所有输出,你会发现每个状态都是每个批次的前一个输出。也就是说,前一个输出包含了整个序列的信息。

print(outputs_val)    
print(outputs_val.shape)    
[[[-0.75934666 -0.99537754  0.9735819  -0.9722234  -0.14234993
   -0.9984044 ]
  [ 0.99975264 -0.9983206   0.9999993  -1.         -0.9997506
   -1.        ]]

 [[ 0.97486496 -0.98773265  0.9969686  -0.99950117 -0.7092863
   -0.99998885]
  [ 0.9326837   0.2673438   0.2808514  -0.7535883  -0.43337247
    0.5700631 ]]

 [[ 0.99628735 -0.9998728   0.99999213 -0.99999976 -0.9884324
   -1.        ]
  [ 0.99962527 -0.9467421   0.9997403  -0.99999714 -0.99929446
   -0.9999795 ]]]
(3, 2, 6)

下面带注释的视图将同一个打印张量逐批次分解。

带注释的 RNN 输出张量,标注了三个批次、六个神经元输出以及 (3, 2, 6) 形状

输出的形状为(3,2,6):

  • 3:批次数量
  • 2:时间步数
  • 6:神经元数量

循环神经网络的优化与传统神经网络的优化相同。您将在本循环神经网络教程的下一部分中更详细地了解如何编写优化代码。

RNN 的应用

RNN 有多种用途,尤其是在预测未来方面。在金融行业中,RNN 有助于预测股票价格或股市走向(即正向或负向)。

RNN 对于自动驾驶汽车很有用,因为它可以通过预测车辆的轨迹来避免车祸。

RNN 广泛应用于文本分析、图像字幕、情感分析和机器翻译。例如,人们可以利用电影评论来了解观众看完电影后的感受。当电影公司没有足够的时间审查、标记、整合和分析评论时,自动化这项任务非常有用。机器可以以更高的准确度完成这项工作。

RNN 的局限性

理论上,RNN 应该将信息传递到 10 ... 消失梯度问题。如果你还记得的话,神经网络使用梯度下降算法来更新权重。当网络向下层时,梯度会变小。

总之,梯度保持不变意味着没有改进空间。模型通过梯度的变化进行学习;这种变化会影响网络的输出。然而,如果梯度变化太小(即权重变化很小),网络就无法学习任何东西,输出也因此无法改变。因此,面临梯度消失问题的网络无法收敛到理想解。

改进LSTM

为了克服循环神经网络(RNN)可能面临的梯度消失问题,Hochreiter、Schmidhuber 和 Bengio 三位研究人员提出了一种名为长短期记忆网络(LSTM)的架构来改进 RNN。简而言之,LSTM 为网络提供与近期相关的历史信息。机器利用更优的架构来选择和传递信息。

TensorFlow 中提供了 LSTM 架构,具体来说是 tf.contrib.rnn.LSTMCell。LSTM 超出了本教程的范围。您可以参考官方文档。 文件 了解更多信息

在 TensorFlow 2.x 中,该单元格的访问方式如下: tf.compat.v1.nn.rnn_cell.LSTMCell而新代码通常使用 tf.keras.layers.LSTM 或者更轻便的闸门 tf.keras.layers.GRU该方法在参数更少的情况下即可达到类似的精度。

时间序列中的 RNN

在本TensorFlow RNN教程中,您将使用RNN处理时间序列数据。时间序列依赖于过去的时间,这意味着过去的数值包含网络可以从中学习的相关信息。时间序列预测的核心思想是估计序列的未来值,例如股票价格、温度、GDP等等。

Keras RNN 和时间序列的数据准备可能有点棘手。首先,目标是预测序列的下一个值,这意味着您将使用过去的信息来估计 t + 1 时的值。标签等于输入序列并向前移动一个周期。其次,输入的数量设置为 1,即每次观察一次。最后,时间步长等于数值序列。例如,如果将时间步长设置为 10,则输入序列将连续返回十次。

看下面的图表,我们在左边表示时间序列数据,在右边表示虚构的输入序列。你创建一个函数来返回从 2001 年 2016 月到 XNUMX 年 XNUMX 月的每一天的随机值的数据集

# To plot pretty figures
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
def create_ts(start = '2001', n = 201, freq = 'M'):
    rng = pd.date_range(start=start, periods=n, freq=freq)
    ts = pd.Series(np.random.uniform(-18, 18, size=len(rng)), rng).cumsum()
    return ts
ts= create_ts(start = '2001', n = 192, freq = 'M')
ts.tail(5)

输出

2016-08-31    -93.459631
2016-09-30    -95.264791
2016-10-31    -95.551935
2016-11-30   -105.879611
2016-12-31   -123.729319
Freq: M, dtype: float64
ts = create_ts(start = '2001', n = 222)

# Left
plt.figure(figsize=(11,4))
plt.subplot(121)
plt.plot(ts.index, ts)
plt.plot(ts.index[90:100], ts[90:100], "b-", linewidth=3, label="A training instance")
plt.title("A time series (generated)", fontsize=14)

# Right
plt.subplot(122)
plt.title("A training instance", fontsize=14)
plt.plot(ts.index[90:100], ts[90:100], "b-", markersize=8, label="instance")
plt.plot(ts.index[91:101], ts[91:101], "bo", markersize=10, label="target", markerfacecolor='red')
plt.legend(loc="upper left")
plt.xlabel("Time")

plt.show()

左侧为生成的时间序列,右侧为目标值偏移的十步训练实例。

图表的右侧部分显示了所有序列,从 2001 年开始,到 2019 年结束。将所有数据都输入网络是没有意义的,因此需要创建一个长度等于时间步长的数据批次。这个批次将作为 X 变量。Y 变量与 X 相同,但向前移动了一个周期(即,您要预测 t+1 的值)。

两个向量的长度相同。您可以在上图的右侧看到这一点。线表示 X 输入的十个值,而红点表示标签 Y 的十个值。请注意,标签比 X 早一个周期开始,比 X 晚一个周期结束。

在 TensorFlow 中构建 RNN 来预测时间序列

现在在这个 RNN 训练中,是时候构建你的第一个 RNN 来预测上面的序列了。你需要为模型指定一些超参数(模型的参数,即神经元的数量等):

  • 输入数量:1
  • 时间步长(时间序列中的窗口):10
  • 神经元数量:120
  • 输出数量:1

你的神经网络将学习10天的序列数据,包含120个循环神经元。你只需向模型输入一个数据,即一天的数据。你可以随意更改这些值,看看模型是否有所改进。

在构建模型之前,需要将数据集拆分为训练集和测试集。完整数据集包含 222 个数据点;您将使用前 201 个数据点来训练模型,使用后 21 个数据点来测试模型。

定义好训练集和测试集后,需要创建一个包含批次的对象。这些批次包含 X 值和 Y 值。请记住,X 值滞后一个周期。因此,您需要使用前 200 个观测值,时间步长为 10。X_batches 对象应包含 20 个大小为 10*1 的批次。y_batches 对象与 X_batches 对象结构相同,但时间步长超前一个周期。

步骤1) 创建训练并测试

首先,将系列转换成 NumPy的 数组;然后定义窗口(即网络将学习的时间数)、输入、输出的数量以及训练集的大小,如下面的 TensorFlow RNN 示例所示。

series = np.array(ts)
n_windows = 20   
n_input =  1
n_output = 1
size_train = 201

之后,只需将数组分成两个数据集。

## Split data
train = series[:size_train]
test = series[size_train:]
print(train.shape, test.shape)
(201,) (21,)

步骤2) 创建函数以返回 X_batches 和 y_batches

为了使其更简单,您可以创建一个返回两个不同数组的函数,一个用于 X_batches,一个用于 y_batches。

让我们编写一个 RNN TensorFlow 函数来构建批次。

请注意,X 个批次滞后一个周期(我们取值为 t-1)。函数的输出应该有三个维度。第一个维度等于批次数量,第二个维度等于窗口大小,最后一个维度等于输入数量。

棘手的部分是正确选择数据点。对于 X 数据点,您选择从 t = 1 到 t =200 的观测值,而对于 Y 数据点,您返回从 t​​ = 2 到 201 的观测值。一旦您有了正确的数据点,就可以轻松重塑序列。

要使用批次构建对象,您需要将数据集拆分为十个长度相等的批次(即 20)。您可以使用 reshape 方法并传递 -1,以便系列与批次大小相似。值 20 是每批次的观察值数量,1 是输入数量。

您需要对标签执行相同的步骤。

请注意,您需要将数据偏移您想要预测的时间倍数。例如,如果您想要预测未来一天,则将序列偏移 1。如果您想要预测两天,则将数据偏移 2。

x_data = train[:size_train-1]: Select all the training instance minus one day
X_batches = x_data.reshape(-1, windows, input): create the right shape for the batch e.g (10, 20, 1)
def create_batches(df, windows, input, output):
    ## Create X         
        x_data = train[:size_train-1] # Select the data
        X_batches = x_data.reshape(-1, windows, input)  # Reshape the data 
    ## Create y
        y_data = train[n_output:size_train]
        y_batches = y_data.reshape(-1, windows, output)
        return X_batches, y_batches

现在函数已经定义,您可以调用它来创建批次,如下面的 RNN 示例所示。

X_batches, y_batches = create_batches(df = train,
                                      windows = n_windows,
                                      input = n_input,
                                      output = n_output)

您可以打印该形状以确保尺寸正确。

print(X_batches.shape, y_batches.shape)
(10, 20, 1) (10, 20, 1)

您需要创建仅包含一批数据和 20 个观测值的测试集。

请注意,您连续几天进行预测,这意味着第二个预测值将基于测试数据集第一天(t+1)的真实值。事实上,真实值是已知的。

如果要预测 t+2(即提前两天),则需要使用 t+1 的预测值;如果要预测 t+3(即提前三天),则需要使用 t+1 和 t+2 的预测值。这很正常,因为 t+n 天的预测很难准确。

X_test, y_test = create_batches(df = test, windows = 20,input = 1, output = 1)
print(X_test.shape, y_test.shape)
(10, 20, 1) (10, 20, 1)

好了,您的批处理大小已准备好,您可以构建 RNN 架构。请记住,您有 120 个循环神经元。

步骤3) 建立模型

要创建模型,您需要定义三个部分:

  1. 具有张量的变量
  2. RNN
  3. 损失与优化

步骤3.1) 变量

您需要指定具有适当形状的 X 和 y 变量。此步骤很简单。张量具有与对象 X_batches 和 y_batches 相同的维度。

例如,张量 X 是一个占位符(请参阅教程)。 TensorFlow简介 (刷新你对变量声明的认识)有三个维度:

  • 注意:批次大小
  • n_windows:窗口长度,即模型向后观察的次数。
  • n_input:输入的数量

结果是:

tf.placeholder(tf.float32, [None, n_windows, n_input])
## 1. Construct the tensors
X = tf.placeholder(tf.float32, [None, n_windows, n_input])   
y = tf.placeholder(tf.float32, [None, n_windows, n_output])

步骤3.2) 创建 RNN

在这个 RNN TensorFlow 示例的第二部分中,您需要定义网络的架构。与之前一样,您使用来自 TensorFlow 估算器的对象 BasicRNNCell 和 dynamic_rnn。

## 2. create the model
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=r_neuron, activation=tf.nn.relu)   
rnn_output, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)   

下一部分有点棘手,但可以加快计算速度。您需要将运行输出转换为密集层,然后再次将其转换为与输入相同的维度。

stacked_rnn_output = tf.reshape(rnn_output, [-1, r_neuron])          
stacked_outputs = tf.layers.dense(stacked_rnn_output, n_output)       
outputs = tf.reshape(stacked_outputs, [-1, n_windows, n_output])  

步骤3.3) 创建损失和优化

模型优化取决于您正在执行的任务。在之前的教程中,我们讨论了…… CNN之前你的目标是对图像进行分类,而在这个RNN教程中,目标略有不同。你需要根据一个连续变量,将其与一个类别进行比较,从而做出预测。

这种差异很重要,因为它会改变优化问题。连续变量的优化问题是最小化均方误差。要在 TF 中构建这些指标,您可以使用:

tf.reduce_sum(tf.square(输出 - y))

RNN 代码的其余部分与之前相同;使用 Adam 优化器来减少损失(即 MSE):

tf.train.AdamOptimizer(学习率=学习率)

优化器.最小化(损失)

就这样,你可以把所有东西打包在一起,然后你的模型就可以进行训练了。

tf.reset_default_graph()
r_neuron = 120    

## 1. Construct the tensors
X = tf.placeholder(tf.float32, [None, n_windows, n_input])   
y = tf.placeholder(tf.float32, [None, n_windows, n_output])

## 2. create the model
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=r_neuron, activation=tf.nn.relu)   
rnn_output, states = tf.nn.dynamic_rnn(basic_cell, X, dtype=tf.float32)              

stacked_rnn_output = tf.reshape(rnn_output, [-1, r_neuron])          
stacked_outputs = tf.layers.dense(stacked_rnn_output, n_output)       
outputs = tf.reshape(stacked_outputs, [-1, n_windows, n_output])   

## 3. Loss + optimization
learning_rate = 0.001  
 
loss = tf.reduce_sum(tf.square(outputs - y))    
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)         
training_op = optimizer.minimize(loss)                                          

init = tf.global_variables_initializer() 

您将使用 1500 个时期训练模型,并每 150 次迭代打印一次损失。模型训练完成后,您将在测试集上评估模型并创建一个包含预测的对象,如下面的循环神经网络示例所示。

iteration = 1500 

with tf.Session() as sess:
    init.run()
    for iters in range(iteration):
        sess.run(training_op, feed_dict={X: X_batches, y: y_batches})
        if iters % 150 == 0:
            mse = loss.eval(feed_dict={X: X_batches, y: y_batches})
            print(iters, "\tMSE:", mse)
    
    y_pred = sess.run(outputs, feed_dict={X: X_test})
0 	MSE: 502893.34
150 	MSE: 13839.129
300 	MSE: 3964.835
450 	MSE: 2619.885
600 	MSE: 2418.772
750 	MSE: 2110.5923
900 	MSE: 1887.9644
1050 	MSE: 1747.1377
1200 	MSE: 1556.3398
1350 	MSE: 1384.6113

最后,在这个 RNN 深度学习教程中,你可以绘制序列的实际值和预测值。如果你的模型被修正了,预测值应该放在实际值的上面。

如您所见,该模型还有改进空间。您可以根据需要更改超参数,例如窗口大小、批次大小或循环神经元的数量。

plt.title("Forecast vs Actual", fontsize=14)
plt.plot(pd.Series(np.ravel(y_test)), "bo", markersize=8, label="Actual", color='green')
plt.plot(pd.Series(np.ravel(y_pred)), "r.", markersize=8, label="Forecast", color='red')
plt.legend(loc="lower left")
plt.xlabel("Time")

plt.show()

预测值与实际值对比图,绿色为实际值点,红色为预测值点。

预测与实际

常见问题

首先使用一个能够覆盖完整周期模式的窗口,然后进行调整。更长的窗口可以捕捉更多上下文信息,但会减慢训练速度并加剧梯度衰减。对于日序列而言,十步即可;而周序列通常需要更多步长。

是的,需要使用一个 shim。将导入语句替换为 tensorflow.compat.v1,并调用 disable_v2_behavior(),因为占位符、会话和 rnn_cell 已移至兼容命名空间下。全新 TensorFlow 项目应该改用 tf.keras.layers.SimpleRNN。

AI助手可以建议窗口大小,在运行前检测形状不匹配,并根据模式编写数据加载代码。它们可以减少循环模型开发中常见的反复试验,但最终的超参数选择仍需在预留数据上进行验证。

Copilot 能很好地处理重复性的脚手架操作:会话块、数据字典和周期性损失打印。张量形状和标签偏移仍然是风险所在,因此在信任生成的循环之前,务必自行验证批次维度。

循环层需要 (批次、时间步长、特征) 参数。将参数重塑为 (-1、窗口、输入) 会告诉框架存在多少个序列、每个序列回溯的时间范围以及每步接收到的值的数量。缺少某个维度会导致秩错误。

LSTM 使用三个门和一个独立的单元状态;GRU 合并了遗忘门和输入门,并丢弃了单元状态。GRU 训练速度更快,参数更少,而 LSTM 在处理非常长的依赖关系时通常略胜一筹。

连续预测可以最小化均方误差,就像本教程使用 `reduce_sum of squaredresidents` 函数来减少残差平方和一样。序列分类,例如情感标签或图像任务,在……中也有介绍。 CNN 教程中改用交叉熵。

Transformer模型在语言处理领域占据主导地位,因为注意力机制能够并行处理序列数据。循环模型在流式信号、短时单变量预测和低内存设备等领域仍然具有竞争力,因为在这些应用中,一次处理一个步骤反而是一种优势,而不是瓶颈。

总结一下这篇文章: