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.
上图中两个数字要完成加和的操作两个加数被存储在两个变量 a 和 b 当中, 他们的值流入了正方形节点, 即代表他们完成相加操作的位置加和的结果被存储在另一个变量 c 中事实上, a,b 和 c 都可以被视为占位符任何被填入 a,b 的数字将在完成加和操作后存入 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 个批次并按顺序送入网络这时占位符 X 和 Y 参与了这一过程, 它们分别存储输入值和目标值并作为输入和目标送入网络
样本数据 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 已经在做了:)
来源: https://cloud.tencent.com/developer/article/1042820