跳至内容

循环神经网络(RNN)教程

了解最流行的深度学习模型 RNN,并通过构建一个 MasterCard 股价预测器获得上手实践。
已更新 2026年10月6日  · 14分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

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

循环神经网络(RNN)是一种人工神经网络(ANN),被应用于苹果的 Siri 和谷歌的语音搜索。RNN 由于具有内部记忆,能够记住过去的输入,这对于预测股价、生成文本、转录以及机器翻译都很有用。

在传统神经网络中,输入和输出彼此独立;而在 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 主要有四种类型。

  • 一对一是最简单的神经网络。常用于单一输入与单一输出的机器学习问题。
  • 一对多是单一输入对应多个输出。常用于图像字幕生成。
  • 多对一接收一段序列输入并预测一个单一输出。在情感分类中很常见,输入是文本,输出是一个类别。
  • 多对多接收多个输入并生成多个输出。最常见的应用是机器翻译。
RNN 的类型

热门机器学习课程

使用 Keras 构建语言建模的循环神经网络(RNN)

BasicSkill Level
4 小时
16.4K learners
学习如何使用 RNN 对文本情感进行分类、生成句子,并在不同语言之间翻译文本。
See DetailsRight Arrow
Start Course

CNN 与 RNN

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

CNN 模型架构

CNN 与 RNN 的关键差异

  • CNN 适用于稀疏数据,如图像;RNN 适用于时间序列和序列数据。
  • 在训练模型时,CNN 使用简单的反向传播,而 RNN 使用时间反向传播来计算损失。
  • RNN 的输入和输出长度不受限制;而 CNN 的输入和输出是有限的。
  • CNN 是前馈网络;RNN 通过循环来处理序列数据。
  • CNN 也可用于视频和图像处理;RNN 主要用于语音和文本分析。

RNN 的局限性

简单的 RNN 模型通常会遇到两个主要问题。这些问题与梯度有关,梯度是损失函数(以及误差函数)的斜率。

  1. 梯度消失问题:当梯度变得很小,以至于参数更新微不足道;最终算法停止学习。
  2. 梯度爆炸问题:当梯度过大,模型变得不稳定。在这种情况下,较大的误差梯度会累积,模型权重会变得过大。这会导致训练时间延长和模型性能不佳。

解决这些问题的简单方法是减少神经网络中的隐藏层数量,从而降低 RNN 的复杂度。也可以通过使用更高级的 RNN 架构(如 LSTM 和 GRU)来解决这些问题。

RNN 的高级架构

简单 RNN 的重复模块通常只有一个 tanh 层的基本结构。简单结构存在短期记忆问题,即在较长序列数据中难以保留先前时间步的信息。长短期记忆网络(LSTM)和门控循环单元(GRU)能够记住较长时间的信息,可以轻松解决这些问题。

简单 RNN 单元

长短期记忆网络(LSTM)

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

LSTM 单元

门控循环单元(GRU)

门控循环单元(GRU)是 LSTM 的一种变体,两者在设计上有相似之处,并且在某些情况下能产生类似的结果。GRU 使用更新门和重置门来解决梯度消失问题。这些门会决定哪些信息重要并将其传递到输出中。通过训练,这些门可以长期存储关键信息而不随时间消失,同时去除无关信息。

与 LSTM 不同,GRU 没有细胞状态 Ct,只有隐藏状态 ht。由于结构更简单,GRU 的训练时间通常比 LSTM 更短。GRU 的架构也更易理解:它接收输入 xt 和上一个时间步的隐藏状态 ht-1,输出新的隐藏状态 ht。您可以在Understanding GRU Networks中深入了解 GRU。

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 工作空间获取。

主题
人工智能
机器学习