什么是循环神经网络(RNN)
循环神经网络(RNN)是一种人工神经网络(ANN),被应用于苹果的 Siri 和谷歌的语音搜索。RNN 由于具有内部记忆,能够记住过去的输入,这对于预测股价、生成文本、转录以及机器翻译都很有用。
在传统神经网络中,输入和输出彼此独立;而在 RNN 中,输出依赖于序列中之前的元素。循环网络还会在网络的各层之间共享参数。在前馈网络中,各个节点拥有不同的权重;而 RNN 在网络的每一层中共享相同的权重,并且在梯度下降过程中,会分别调整权重和偏置以降低损失。

上图是循环神经网络的一个简单示意。如果我们使用简单数据 [45,56,45,49,50,…] 来预测股价,从X0 到 Xt 的每个输入都包含一个过去的数值。例如,X0 为 45,X1 为 56,而这些值用于预测序列中的下一个数字。
循环神经网络如何工作
在 RNN 中,信息在回路中循环,因此输出由当前输入以及先前接收到的输入共同决定。

输入层 X 处理初始输入并将其传递给中间层 A。中间层由多个隐藏层组成,每个隐藏层都有自己的激活函数、权重和偏置。这些参数在隐藏层中是标准化/共享的,因此并非创建多个独立的隐藏层,而是创建一个并对其进行循环展开。
循环神经网络并非使用传统的反向传播,而是使用时间反向传播(BPTT)算法来计算梯度。在反向传播中,模型通过从输出层回传到输入层计算误差来调整参数。由于 RNN 在各层间共享参数,BPTT 会对每个时间步的误差求和。想进一步了解 RNN 及其工作原理,请参阅What are Recurrent Neural Networks?。
循环神经网络的类型
前馈网络通常是单输入单输出,而循环神经网络更灵活,因为输入和输出的长度可以变化。这种灵活性使 RNN 能够用于音乐生成、情感分类以及机器翻译。
根据输入和输出长度的不同,RNN 主要有四种类型。
- 一对一是最简单的神经网络。常用于单一输入与单一输出的机器学习问题。
- 一对多是单一输入对应多个输出。常用于图像字幕生成。
- 多对一接收一段序列输入并预测一个单一输出。在情感分类中很常见,输入是文本,输出是一个类别。
- 多对多接收多个输入并生成多个输出。最常见的应用是机器翻译。

热门机器学习课程
Python 深度学习入门
CNN 与 RNN
卷积神经网络(CNN)是一种能够处理空间数据的前馈神经网络。它常用于计算机视觉场景,例如图像分类。简单的神经网络擅长处理简单的二分类,但无法处理具有像素依赖性的图像。CNN 的模型架构由卷积层、ReLU 层、池化层以及全连接输出层组成。您可以通过实践项目来学习 CNN,例如用 Python 实现卷积神经网络。

CNN 与 RNN 的关键差异
- CNN 适用于稀疏数据,如图像;RNN 适用于时间序列和序列数据。
- 在训练模型时,CNN 使用简单的反向传播,而 RNN 使用时间反向传播来计算损失。
- RNN 的输入和输出长度不受限制;而 CNN 的输入和输出是有限的。
- CNN 是前馈网络;RNN 通过循环来处理序列数据。
- CNN 也可用于视频和图像处理;RNN 主要用于语音和文本分析。
RNN 的局限性
简单的 RNN 模型通常会遇到两个主要问题。这些问题与梯度有关,梯度是损失函数(以及误差函数)的斜率。
- 梯度消失问题:当梯度变得很小,以至于参数更新微不足道;最终算法停止学习。
- 梯度爆炸问题:当梯度过大,模型变得不稳定。在这种情况下,较大的误差梯度会累积,模型权重会变得过大。这会导致训练时间延长和模型性能不佳。
解决这些问题的简单方法是减少神经网络中的隐藏层数量,从而降低 RNN 的复杂度。也可以通过使用更高级的 RNN 架构(如 LSTM 和 GRU)来解决这些问题。
RNN 的高级架构
简单 RNN 的重复模块通常只有一个 tanh 层的基本结构。简单结构存在短期记忆问题,即在较长序列数据中难以保留先前时间步的信息。长短期记忆网络(LSTM)和门控循环单元(GRU)能够记住较长时间的信息,可以轻松解决这些问题。

长短期记忆网络(LSTM)
长短期记忆网络(LSTM)是 RNN 的高级类型,旨在同时防止梯度消失和梯度爆炸。与 RNN 一样,LSTM 具有重复模块,但结构不同。LSTM 并非只有一个 tanh 层,而是由四个相互作用的层彼此通信。这种四层结构帮助 LSTM 保留长期记忆,可用于多种序列问题,包括机器翻译、语音合成、语音识别和手写识别。您可以通过以下指南获得 LSTM 的上手经验:用 Python 实现 LSTM 股价预测。

门控循环单元(GRU)
门控循环单元(GRU)是 LSTM 的一种变体,两者在设计上有相似之处,并且在某些情况下能产生类似的结果。GRU 使用更新门和重置门来解决梯度消失问题。这些门会决定哪些信息重要并将其传递到输出中。通过训练,这些门可以长期存储关键信息而不随时间消失,同时去除无关信息。
与 LSTM 不同,GRU 没有细胞状态 Ct,只有隐藏状态 ht。由于结构更简单,GRU 的训练时间通常比 LSTM 更短。GRU 的架构也更易理解:它接收输入 xt 和上一个时间步的隐藏状态 ht-1,输出新的隐藏状态 ht。您可以在Understanding GRU Networks中深入了解 GRU。

使用 LSTM 与 GRU 预测 MasterCard 股价
在本项目中,我们将使用 Kaggle 的MasterCard 股票数据集(时间范围为 2006-05-25 至 2021-10-11),并训练 LSTM 和 GRU 模型来预测股价。这是一个基于项目的简单教程,我们将分析数据、对数据进行预处理以用于高级 RNN 模型训练,最后评估结果。
本项目需要使用 Pandas 和 Numpy 进行数据处理,使用 Matplotlib.pyplot 进行数据可视化,使用 scikit-learn 进行缩放与评估,并使用 TensorFlow 进行建模。我们还将设置随机种子以保证可复现性。
# Importing the libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Dropout, GRU, Bidirectional
from tensorflow.keras.optimizers import SGD
from tensorflow.random import set_seed
set_seed(455)
np.random.seed(455)
数据分析
本部分我们将导入 MasterCard 数据集,将 Date 列设为索引并转换为 DateTime 格式。由于我们只关注股价、成交量和日期,因此会从数据集中删除无关列。
该数据集以 Date 为索引,列包括 Open、High、Low、Close 和 Volume。看起来我们已成功导入了一个清洗过的数据集。
dataset = pd.read_csv(
"data/Mastercard_stock_history.csv", index_col="Date", parse_dates=["Date"]
).drop(["Dividends", "Stock Splits"], axis=1)
print(dataset.head())
Open High Low Close Volume
Date
2006-05-25 3.748967 4.283869 3.739664 4.279217 395343000
2006-05-26 4.307126 4.348058 4.103398 4.179680 103044000
2006-05-30 4.183400 4.184330 3.986184 4.093164 49898000
2006-05-31 4.125723 4.219679 4.125723 4.180608 30002000
2006-06-01 4.179678 4.474572 4.176887 4.419686 62344000
.describe() 函数有助于我们更深入地分析数据。我们将重点关注 High 列,因为我们将用它来训练模型。我们也可以选择 Close 或 Open 作为特征,但使用 High 更合理,因为它能提供某日股价达到的最高水平。
最低股价为 4.10 美元,最高为 400.5 美元。均值为 105.9 美元,标准差为 107.3,说明该股票的波动性较高。
print(dataset.describe())
Open High Low Close Volume
count 3872.000000 3872.000000 3872.000000 3872.000000 3.872000e+03
mean 104.896814 105.956054 103.769349 104.882714 1.232250e+07
std 106.245511 107.303589 105.050064 106.168693 1.759665e+07
min 3.748967 4.102467 3.739664 4.083861 6.411000e+05
25% 22.347203 22.637997 22.034458 22.300391 3.529475e+06
50% 70.810079 71.375896 70.224002 70.856083 5.891750e+06
75% 147.688448 148.645373 146.822013 147.688438 1.319775e+07
max 392.653890 400.521479 389.747812 394.685730 3.953430e+08
通过使用 .isna().sum() 我们可以确定数据集中的缺失值。看起来该数据集没有缺失值。
dataset.isna().sum()
Open 0
High 0
Low 0
Close 0
Volume 0
dtype: int64
train_test_plot 函数接收三个参数:dataset、tstart 和 tend,并绘制一个简单的折线图。tstart 和 tend 是以年份为单位的时间范围。我们可以修改这些参数以分析特定时期。折线图分为两部分:训练集与测试集。这将帮助我们决定测试数据集的划分。
自 2016 年以来,MasterCard 的股价总体呈上升趋势。它在 2020 年第一季度出现下跌,但在当年下半年恢复到较为稳定的状态。我们的测试数据集为一年,即从 2021 年到 2022 年,其余数据用于训练。
tstart = 2016
tend = 2020
def train_test_plot(dataset, tstart, tend):
dataset.loc[f"{tstart}":f"{tend}", "High"].plot(figsize=(16, 4), legend=True)
dataset.loc[f"{tend+1}":, "High"].plot(figsize=(16, 4), legend=True)
plt.legend([f"Train (Before {tend+1})", f"Test ({tend+1} and beyond)"])
plt.title("MasterCard stock price")
plt.show()
train_test_plot(dataset,tstart,tend)

数据预处理
train_test_split 函数将数据集分为两个子集:training_set 和 test_set。
def train_test_split(dataset, tstart, tend):
train = dataset.loc[f"{tstart}":f"{tend}", "High"].values
test = dataset.loc[f"{tend+1}":, "High"].values
return train, test
training_set, test_set = train_test_split(dataset, tstart, tend)
我们将使用 MinMaxScaler 函数对训练集进行标准化,这有助于避免离群点或异常值的影响。您也可以尝试使用 StandardScaler 或其他缩放方法来规范化数据并提升模型表现。
sc = MinMaxScaler(feature_range=(0, 1))
training_set = training_set.reshape(-1, 1)
training_set_scaled = sc.fit_transform(training_set)
split_sequence 函数使用训练数据集并将其转换为输入(X_train)和输出(y_train)。
例如,如果序列为 [1,2,3,4,5,6,7,8,9,10,11,12],n_step 为 3,那么它会将序列转换为三个输入时间步和一个输出,如下所示:
| X | y |
|---|---|
| 1,2,3 | 4 |
| 2,3,4 | 5 |
| 3,4,5 | 6 |
| 4,5,6 | 7 |
| … | … |
在本项目中,我们使用 60 个 n_steps。我们也可以根据需要减少或增加步数以优化模型表现。
def split_sequence(sequence, n_steps):
X, y = list(), list()
for i in range(len(sequence)):
end_ix = i + n_steps
if end_ix > len(sequence) - 1:
break
seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
X.append(seq_x)
y.append(seq_y)
return np.array(X), np.array(y)
n_steps = 60
features = 1
# split into samples
X_train, y_train = split_sequence(training_set_scaled, n_steps)
我们处理的是单变量序列,因此特征数为 1,需要将 X_train 重塑以适配 LSTM 模型。X_train 原为 [samples, timesteps],我们将其重塑为 [samples, timesteps, features]。
# Reshaping X_train for model
X_train = X_train.reshape(X_train.shape[0],X_train.shape[1],features)
LSTM 模型
该模型包含一个 LSTM 隐藏层和一个输出层。您可以尝试调整单元数量,通常更多的单元会带来更好的结果。本实验中,我们将 LSTM 单元设为 125,激活函数为 tanh,并设置输入尺寸。
作者注:TensorFlow 库对用户十分友好,因此我们无需从零搭建 LSTM 或 GRU 模型。只需直接使用相应的 LSTM 或 GRU 模块构建模型即可。
最后,我们将使用 RMSprop 优化器和均方误差损失函数来编译模型。
# The LSTM architecture
model_lstm = Sequential()
model_lstm.add(LSTM(units=125, activation="tanh", input_shape=(n_steps, features)))
model_lstm.add(Dense(units=1))
# Compiling the model
model_lstm.compile(optimizer="RMSprop", loss="mse")
model_lstm.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
lstm (LSTM) (None, 125) 63500
_________________________________________________________________
dense (Dense) (None, 1) 126
=================================================================
Total params: 63,626
Trainable params: 63,626
Non-trainable params: 0
_________________________________________________________________
模型将以 32 的批量大小训练 50 个 epoch。您可以调整超参数以缩短训练时间或提高效果。模型训练顺利完成,并取得了尽可能低的损失。
model_lstm.fit(X_train, y_train, epochs=50, batch_size=32)
Epoch 50/50
38/38 [==============================] - 1s 30ms/step - loss: 3.1642e-04
结果
我们将重复预处理并对测试集进行归一化。首先,我们会对数据进行变换,然后将数据拆分为样本、重塑形状、预测,并将预测值逆变换回原始尺度。
dataset_total = dataset.loc[:,"High"]
inputs = dataset_total[len(dataset_total) - len(test_set) - n_steps :].values
inputs = inputs.reshape(-1, 1)
#scaling
inputs = sc.transform(inputs)
# Split into samples
X_test, y_test = split_sequence(inputs, n_steps)
# reshape
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], features)
#prediction
predicted_stock_price = model_lstm.predict(X_test)
#inverse transform the values
predicted_stock_price = sc.inverse_transform(predicted_stock_price)
plot_predictions 函数将绘制实际值与预测值的折线图。这有助于我们直观比较真实值与预测值之间的差异。
return_rmse 函数接收测试集与预测值作为参数,并输出均方根误差(rmse)指标。
def plot_predictions(test, predicted):
plt.plot(test, color="gray", label="Real")
plt.plot(predicted, color="red", label="Predicted")
plt.title("MasterCard Stock Price Prediction")
plt.xlabel("Time")
plt.ylabel("MasterCard Stock Price")
plt.legend()
plt.show()
def return_rmse(test, predicted):
rmse = np.sqrt(mean_squared_error(test, predicted))
print("The root mean squared error is {:.2f}.".format(rmse))
从下方折线图可以看出,单层 LSTM 模型表现良好。
plot_predictions(test_set,predicted_stock_price)

结果相当可观:该模型在测试集上取得了 6.70 的 rmse。
return_rmse(test_set,predicted_stock_price)
>>> The root mean squared error is 6.70.
GRU 模型
我们将保持其余部分不变,仅将 LSTM 层替换为 GRU 层,以便更好地比较结果。该模型结构包含一个 125 单元的 GRU 层和一个输出层。
model_gru = Sequential()
model_gru.add(GRU(units=125, activation="tanh", input_shape=(n_steps, features)))
model_gru.add(Dense(units=1))
# Compiling the RNN
model_gru.compile(optimizer="RMSprop", loss="mse")
model_gru.summary()
Model: "sequential_5"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
gru_4 (GRU) (None, 125) 48000
_________________________________________________________________
dense_5 (Dense) (None, 1) 126
=================================================================
Total params: 48,126
Trainable params: 48,126
Non-trainable params: 0
_________________________________________________________________
模型以 32 的批量大小成功训练了 50 个 epoch。
model_gru.fit(X_train, y_train, epochs=50, batch_size=32)
Epoch 50/50
38/38 [==============================] - 1s 29ms/step - loss: 2.6691e-04
结果
可以看到,真实值与预测值相当接近。预测折线几乎贴合实际值。
GRU_predicted_stock_price = model_gru.predict(X_test)
GRU_predicted_stock_price = sc.inverse_transform(GRU_predicted_stock_price)
plot_predictions(test_set, GRU_predicted_stock_price)

GRU 模型在测试集上的 rmse 为 5.50,相比 LSTM 有所提升。
return_rmse(test_set,GRU_predicted_stock_price)
>>> The root mean squared error is 5.50.
结论
当今世界正迈向混合式解决方案,数据科学家在图像字幕生成、情绪识别、视频字幕以及 DNA 测序等领域使用 CNN-RNN 混合网络。混合网络为模型同时提供视觉与时序特征。通过参加课程进一步学习 RNN:Recurrent Neural Networks for Language Modeling in Python。
本教程前半部分介绍了循环神经网络的基础、其局限性以及以更高级架构形式给出的解决方案。后半部分展示了如何使用 LSTM 与 GRU 模型开发 MasterCard 股价预测。结果清晰表明,在结构与超参数相近的情况下,GRU 模型优于 LSTM。
该项目可在DataCamp 工作空间获取。