使用TensorFlow实现股票价格预测深度学习模型

时间:2022-04-28
本文章向大家介绍使用TensorFlow实现股票价格预测深度学习模型,主要内容包括导入数据集、准备训练集和测试集数据、数据缩放、TensorFlow简介、占位符、变量、设计网络架构、损失函数、优化器、初始化器、拟合神经网络、总结与展望、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

Sebastian Heinz. A simple deep learning model for stock price prediction using TensorFlow

在最近的黑客马拉松中,我们在STATWORX上进行协作,团队的一些成员利用Google Finance API抓取了每分钟的标准普尔500指数。除了标准普尔500指数以外,我们还收集了其对应的500家公司的股价。在得到了这些数据之后,我立刻想到了一点子:基于标准普尔指数观察的500家公司的股价,用深度学习模型来预测标准普尔500指数。

把玩这些数据并用TensorFlow在其上建立深度学习模型是很有趣的,所以我决定写下这篇文章:预测标准普尔500指数的简易TensorFlow教程。你将看到的不是一个深入的教程,更多的是从高层次来讲解TensorFlow模型的重要构成组件和概念。我编写的Python代码并没有做专门的性能优化但是可读性还可以。下载我使用的数据集

注意:本文只是基于TensorFlow的一个实战教程。真正预测股价是非常具有挑战性的,尤其在分钟级这样频率较高的预测中,要考虑的因素的量是庞大的。

导入数据集

我们的团队将抓取到的股票数据从爬虫服务器上导出为CSV格式的文件。该数据集包含了从2017年四月到八月共计n=41266分钟的标准普尔500指数以及500家公司的股价。

# 导入数据
data = pd.read_csv('data_stocks.csv')
# 移除日期列
data = data.drop(['DATE'], 1)
# 数据集的维度
n = data.shape[0]
p = data.shape[1]
# 将数据集转化为numpy数组
data = data.values

数据是经过清洗准备好的,这意味着指数数据和股票数据是遵循LOCF(Last Observation Carried Forward)方法的,所以文件中不包含任何的缺失值。

可以通过pyplot.plot('SP500')来快速查看标准普尔500指数的时间序列。

Time series plot of the S&P 500 index.

注意:这里展示的是标普500指数的领先(lead),也就是说其值是原始值在时间轴上后移一分钟得到的。因为我们要预测的是下一分钟的指数而不是当前的指数,所以这一操作是必不可少的。

准备训练集和测试集数据

原始数据集被划分为训练集和测试集。训练数据集包含了整个数据集的80%。注意这里的数据集划分不是随机划分得到的,而是顺序切片得到的。训练数据集是从2017年的4月到大约7月底,测试数据集则为到17年8月底的剩余数据。

# 划分训练集和测试集
train_start = 0
train_end = int(np.floor(0.8*n))
test_start = train_end + 1
test_end = n
data_train = data[np.arange(train_start, train_end), :]
data_test = data[np.arange(test_start, test_end), :]

时间序列的交叉验证方法有很多,像有无refitting或其他像time series bootstrap resampling的精细概念的滚动预测(rolling forecasts)。后者(time series bootstrap resampling)中的重复样本是考虑时间序列的周期性分解的结果,这是为了使模拟采样同样具有周期性的特征而不是单单复制采样值。

数据缩放

大多数的神经网络都受益于输入值的缩放(有时也有输出值)。为什么呢?因为大多数神经网络的激励函数都是定义在0, 1区间或-1, 1区间,像sigmoid函数和tanh函数一样。虽然如今线性整流单元已经被广泛引用于无界的激活值问题中,但是我们还是选择将输入输出值做统一的缩放。缩放操作可以通过sklearn中的MinMaxScaler轻松实现。

# 数据缩放
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(data_train)
data_train = scaler.transform(data_train)
data_test = scaler.transform(data_test)
# 构建 X and y
X_train = data_train[:, 1:]
y_train = data_train[:, 0]
X_test = data_test[:, 1:]
y_test = data_test[:, 0]

备注:应当仔细考虑好什么数据要在什么时候被缩放。一个常见的错误是在训练集和测试集划分前进行特征缩放。为什么这样做是错误的呢?因为缩放的计算需要调用数据的统计值(像数据的最大最小值)。当你在真实生活中进行预测时你并没有来自未来的观测信息,所以相应地,训练数据特征缩放所用的统计值应当来源于训练集,测试集也一样。否则,在预测时使用了包含未来信息往往会导致性能指标向好的方向偏移。

TensorFlow简介

TensorFlow是一个非常棒的软件,是深度学习和神经网络计算框架中的领头羊。它的底层后端是用C++编写的,通常通过Python来进行控制(还有R语言版的TensorFlow,由RStudio维护)。TensorFlow用图来描述底层的计算任务,这种方法使得用户可以通过表征数据,变量和操作的元素组合得到的计算图来指定相应的数学操作。由于神经网络实际上就是数据和数学操作的图,TensorFlow可以完美地应用于神经网络和深度学习,可以看下面给出的一个简单例子(取自作者的博文:Deep learning introduction

A very simple graph that adds two numbers together.

上图中两个数字要完成加和的操作。两个加数被存储在两个变量ab当中,他们的值流入了正方形节点,即代表他们完成相加操作的位置。加和的结果被存储在另一个变量c中。事实上,abc都可以被视为占位符。任何被填入ab的数字将在完成加和操作后存入c中。这就是TensorFlow的工作原理,用户通过变量和占位符来定义模型(神经网络)的抽象表示。随后,占位符被实际的数字填充并开始进行实际的运算。下面的代码实现了上面简单的计算图。

# 引入 TensorFlow
import tensorflow as tf

# 定义 a 和 b 为占位符
a = tf.placeholder(dtype=tf.int8)
b = tf.placeholder(dtype=tf.int8)

# 定义加法运算
c = tf.add(a, b)

# 初始化图
graph = tf.Session()

# 运行图
graph.run(c, feed_dict={a: 5, b: 4})

在引入TensorFlow的库之后,两个占位符可以以tf.placeholder()的方式定义,对应上面图示中左侧两个蓝色的图形。随后通过tf.add()来定义数学加法操作,运算的结果为c = 9。当建立占位符之后,可以用任意的整数值a,b来执行计算图。当然,以上的问题不过是一个简单的示例而已,真正神经网络中的图和运算要复杂得多。

占位符

正如上面所说,所有的过程都从占位符开始。为了拟合模型,我们需要定义两个占位符:X包含模型输入(在T = t时刻500个成员公司的股价),Y为模型输出(T = t + 1时刻的标普指数)。

占位符的shape分别为[None, n_stocks][None],意味着输入为二维矩阵,输出为一维向量。设计出恰当的神经网络的必要条件之一就是清楚神经网络需要的输入和输出维度。

# 占位符
X = tf.placeholder(dtype=tf.float32, shape=[None, n_stocks])
Y = tf.placeholder(dtype=tf.float32, shape=[None])

None值代表着我们当前不知道每个批次中流经神经网络的观测值数量,所以为了保持该量的弹性,我们用None来填充。稍后我们将定义控制每个批次中观测样本数量的变量batch_size

变量

除了占位符,TensorFlow中的另一个基本概念是变量。占位符在图中用来存储输入数据和输出数据,变量在图的执行过程中可以变化,是一个弹性的容器。为了在训练中调整权重和偏置,它们被定义为变量。变量需要在训练开始前进行初始化。变量的初始化稍后我们会单独讲解。

我们的模型包含四个层。第一层有1024个神经元,比输入变量的两倍还要多一点。紧接在后面的隐藏层是前面一层的一半,即后面层的神经元个数分别为512,256和128。每层中神经元数量的减少也意味着信息量的压缩。当然还有其他的神经网络结构,但是不在本文的讨论范围当中。

# 模型结构参数
n_stocks = 500
n_neurons_1 = 1024
n_neurons_2 = 512
n_neurons_3 = 256
n_neurons_4 = 128
n_target = 1
# 第一层 : 隐藏层权重和偏置变量
W_hidden_1 = tf.Variable(weight_initializer([n_stocks, n_neurons_1]))
bias_hidden_1 = tf.Variable(bias_initializer([n_neurons_1]))
# 第二层 : 隐藏层权重和偏置变量
W_hidden_2 = tf.Variable(weight_initializer([n_neurons_1, n_neurons_2]))
bias_hidden_2 = tf.Variable(bias_initializer([n_neurons_2]))
# 第三层: 隐藏层权重和偏置变量
W_hidden_3 = tf.Variable(weight_initializer([n_neurons_2, n_neurons_3]))
bias_hidden_3 = tf.Variable(bias_initializer([n_neurons_3]))
# 第四层: 隐藏层权重和偏置变量
W_hidden_4 = tf.Variable(weight_initializer([n_neurons_3, n_neurons_4]))
bias_hidden_4 = tf.Variable(bias_initializer([n_neurons_4]))

# 输出层: 输出权重和偏置变量
W_out = tf.Variable(weight_initializer([n_neurons_4, n_target]))
bias_out = tf.Variable(bias_initializer([n_target]))

清楚输入层,隐藏层和输出层的变量对应的维度是非常重要的。在多层感知机的经验法则中(MLPs,本文就是按照该准则设计的网络),前一层权重的维度数组中的第二个元素与当前层中权重维度数组的第一个元素数值相等。听起来可能有些复杂,但是为了使当前层的输入作为输入传入下一层,这样的法则是必要的。偏置的维度等于当前层权重维度数组中的第二个元素,对应当前层中神经元的数量。

设计网络架构

在定义了所需的权重和偏置变量之后,网络的拓扑结构即网络的架构需要被确定下来。在TensorFlow中,即需要将占位符(数据)和变量(权重和偏置)整合入矩阵乘法的序列当中。

除此之外,神经网络中是经过了激活函数的转换的。激活函数是神经网络架构中非常的元素之一,在非线性系统中尤其如此。目前已经有很多中可供使用的激活函数,本文中的模型选用了最常用的整流线性单元(ReLU)。

# 隐藏层
hidden_1 = tf.nn.relu(tf.add(tf.matmul(X, W_hidden_1), bias_hidden_1))
hidden_2 = tf.nn.relu(tf.add(tf.matmul(hidden_1, W_hidden_2), bias_hidden_2))
hidden_3 = tf.nn.relu(tf.add(tf.matmul(hidden_2, W_hidden_3), bias_hidden_3))
hidden_4 = tf.nn.relu(tf.add(tf.matmul(hidden_3, W_hidden_4), bias_hidden_4))

# 输出层 (必须经过转置)
out = tf.transpose(tf.add(tf.matmul(hidden_4, W_out), bias_out))

下面的图形说明了网络架构。模型一共包含了三个主要的组件:输入层,隐藏层和输出层。图示的结构被称为前馈网络,前馈意味着从左侧输入的数据将径自向右传播。与之相对的网络结构如recurrent neural networks(RNN)允许数据流在网络结构中反向传播。

我们使用的前馈网络架构图展示

损失函数

网络的损失函数可以根据网络的预测值和训练集中的实际观测值来生成度量偏差程度的指标。在回归问题当中,最常用的损失函数为均方误差(MSE)。均方误差计算的就是预测值和目标值的误差平方值的平均值。基本上任何可微函数都可以用于计算预测值和目标值之间的偏差程度。

# 损失函数
mse = tf.reduce_mean(tf.squared_difference(out, Y))

但是,在我们的问题中,MSE展示出了一些更有利与解决我们问题的特性。

优化器

优化器负责训练过程中调整网络的权重和偏置的关键操作。这些操作中包含着梯度运算,梯度方向对应的就是训练过程中最小化网络损失函数的方向。稳定而又高效的优化器是神经网络中深入研究的课题之一。

# 优化器
opt = tf.train.AdamOptimizer().minimize(mse)

这里我们使用Adam优化器,目前它是深度学习中默认的优化器。Adam的全称为Adaptive Moment Estimation,可以视为其他两个优化器AdaGrad和RMSProp的结合。

初始化器

初始化器用于在训练前初始化网络的权重。由于神经网络是利用数值方法进行训练,所以优化问题的起始点是能否找到问题的最优解(或次优解)的关键因素之一。TensorFlow中内置了多种优化器,每个优化器使用了不同的初始化方法。这里我使用的是默认的初始化器之一——tf.variance_scaling_initializer()

# 初始化器
sigma = 1
weight_initializer = tf.variance_scaling_initializer(mode="fan_avg", distribution="uniform", scale=sigma)
bias_initializer = tf.zeros_initializer()

注意:在TensorFlow的计算图中,不同的变量可以定义不同的初始化函数。不过在大多数情况下统一的初始化函数就可以满足要求了。

拟合神经网络

在定义了网络的占位符,变量,初始化器,损失函数和优化器之后,模型需要进入正式的训练过程。通常我们使用minibatch的方式进行训练(小的batch size)。在这种训练方式中,我们从训练集中随机抽取n = sample_size的数据样本送入网络进行训练。训练集被划分为n / batch_size个批次并按顺序送入网络。这时占位符XY参与了这一过程,它们分别存储输入值和目标值并作为输入和目标送入网络。

样本数据X将在网络中传播直至输出层。到达输出层后,TensorFlow将把模型的当前预测值与当前批次的实际观测值Y进行比较。随后,TensorFlow将根据选择的学习方案对网络参数进行优化更新。权重和偏置更新完毕后,下一批采样数据将再次送入网络并重复这一过程。这一过程将一直持续至所有批次的数据都已经送入网络。所有的批次构成的一个完整训练过程被称为一个epoch。

当达到训练批次数或者用户指定的标准之后,网络的训练停止。

# 定义会话
net = tf.Session()
# 运行初始化器
net.run(tf.global_variables_initializer())

# 设定用于展示交互的图表
plt.ion()
fig = plt.figure()
ax1 = fig.add_subplot(111)
line1, = ax1.plot(y_test)
line2, = ax1.plot(y_test*0.5)
plt.show()

# 设定 epochs 数和每批次的数据量
epochs = 10
batch_size = 256

for e in range(epochs):

    # 打乱训练集
    shuffle_indices = np.random.permutation(np.arange(len(y_train)))
    X_train = X_train[shuffle_indices]
    y_train = y_train[shuffle_indices]

    # Minibatch 训练
    for i in range(0, len(y_train) // batch_size):
        start = i * batch_size
        batch_x = X_train[start:start + batch_size]
        batch_y = y_train[start:start + batch_size]
        # 在当前batch上运行优化器
        net.run(opt, feed_dict={X: batch_x, Y: batch_y})

        # 展示进度
        if np.mod(i, 5) == 0:
            # Prediction
            pred = net.run(out, feed_dict={X: X_test})
            line2.set_ydata(pred)
            plt.title('Epoch ' + str(e) + ', Batch ' + str(i))
            file_name = 'img/epoch_' + str(e) + '_batch_' + str(i) + '.jpg'
            plt.savefig(file_name)
            plt.pause(0.01)
# 展示训练结束时最终的MSE
mse_final = net.run(mse, feed_dict={X: X_test, Y: y_test})
print(mse_final)

每隔5个批次的训练,我们用测试集(网络没有在这些数据上进行训练)来评估一次模型的预测性能并进行可视化。我们特意将每个节点的图像到处至磁盘制作了一个视频来展示训练的过程。可以看到模型很快习得了原始时间序列的形状和位置并且在一定的epochs后可以达到比较准确的预测值。这真是太好了!

可以观察到模型先是迅速习得了时间序列的大致形状,随后继续学习数据中精细结构。这与Adam学习方案为了避免越过最小优化值而不断降低学习率是相互照应的。在10个epochs后,我们完美地拟合了训练数据!最终的MSE只有0.00078(注意到我们的数据是缩放过的,所以这个值其实已经很小了)。在测试集绝对误差的占比等于5.31%,表现不错。注意:这只是测试集上的效果,并不能代表实际场景中的性能。

标普指数预测值和实际值的散点图(缩放后)

这里再给出一些可以进一步提升结果的方法:规划网络层数和神经元个数,选择不同的初始化和激活方案,引入神经元的dropout层,early stopping等等。除此之外,换用其他类型的深度学习模型,比方说RNN也许可以在任务上达到更优的性能。在此我们不做讨论,读者可以自行尝试。

总结与展望

TensorFlow的发布是深度学习研究的一个里程碑。它的灵活性和良好的性能使研究者可以借助它完成一系列复杂的网络结构以及其他机器学习算法。不过,与Keras或Mxnet的高层级API相比,TensorFlow高度的灵活性是以增加模型建立的时间周期为代价的。尽管如此,我仍然认为TensorFlow会在神经网络和深度学习的理论研究和实际应用中走向标准化。我们的很多顾客已经开始使用TensorFlow并用它来开发项目,我们在STATWORX上的数据科学顾问也越来越频繁地使用TensorFlow进行研究和开发。看过了Google对TensorFlow的未来规划后,我觉得有一件事被遗忘了(从我的观点来看),就是利用TensorFlow作为后端去设计和开发神经网络的标准用户界面。当然,可能Google已经在做了:)

本文的代码可以从Github上下载