前言
STATWORX 团队曾从 Google Finance API 中选取了 S&P 500 数据,该数据集包含了指数和成分股的股价信息。他们的目标是利用深度学习模型,通过 500 支成分股的股价来预测 S&P 500 指数。虽然原团队仅使用了包含四个隐藏层的全连接网络,但该数据集为初学者理解如何使用 TensorFlow 构建神经网络提供了一个很好的起点。本文旨在全面展示构建 TensorFlow 模型所涉及的核心概念与模块。
需要注意的是,本文基于较早的 TensorFlow 1.x 版本编写,部分 API 与当前主流的 TensorFlow 2.x 有所不同。读者在实践时应注意版本差异。数据集仍可供下载,有经验的读者可以尝试使用循环神经网络(RNN)或长短期记忆网络(LSTM)等更擅长处理时序数据的模型。
数据集地址:http://files.statworx.com/sp500.zip
数据导入与预处理
数据集以 CSV 格式提供,包含了从 2017 年 4 月到 8 月共 41266 分钟的 500 支股票及 S&P 500 指数记录。数据已经过清理,缺失值采用了“末次观测值结转法”(LOCF)进行处理,因此没有缺损值。
首先,我们导入数据并查看其维度:
# Import data
data = pd.read_csv('data_stocks.csv')
# Dimensions of dataset
n = data.shape[0]
p = data.shape[1]
我们可以使用 Matplotlib 绘制 S&P 500 指数的时间序列图以直观了解数据趋势。
划分训练集与测试集
我们将数据集按时间顺序划分为训练集和测试集,其中前 80% 的数据用于训练,后 20% 用于测试。这种按时间顺序的划分对于时间序列预测至关重要,可以避免使用未来信息预测过去。
# Training and test data
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), :]
对于时间序列数据,更严谨的方法可能包括滚动窗口预测或时间序列交叉验证,但此处为简化,我们采用一次性的静态划分。
数据标准化
神经网络的输入通常需要进行标准化,以帮助梯度下降算法更高效地收敛。即使使用 ReLU 激活函数,缩放数据也是有益的。我们使用 sklearn 的 MinMaxScaler,但必须注意:拟合(fit)操作只能在训练数据上进行,然后用同样的缩放器转换(transform)测试数据,以防止数据泄露。
# Scale data
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(data_train)
data_train_scaled = scaler.transform(data_train)
data_test_scaled = scaler.transform(data_test)
# Build X and y
X_train = data_train_scaled[:, 1:]
y_train = data_train_scaled[:, 0]
X_test = data_test_scaled[:, 1:]
y_test = data_test_scaled[:, 0]
这里,X 代表所有股票价格特征(第1列及之后),y 代表目标值 S&P 500 指数(第0列)。
TensorFlow 基础概念
TensorFlow 是一个使用数据流图进行数值计算的开源库。“Tensor”意为张量(多维数组),“Flow”代表计算图中数据的流动。计算图由节点(操作)和边(张量)组成。
一个简单的计算图示例是实现两个标量的加法:
# Import TensorFlow
import tensorflow as tf
# Define a and b as placeholders
a = tf.placeholder(dtype=tf.float32)
b = tf.placeholder(dtype=tf.float32)
# Define the addition
c = tf.add(a, b)
# Initialize and run the graph
with tf.Session() as sess:
result = sess.run(c, feed_dict={a: 5.0, b: 4.0})
print(result) # Output: 9.0
在 TensorFlow 1.x 中,我们首先定义占位符和操作构建计算图,然后在会话(Session)中运行它,并通过 feed_dict 传入实际数据。
占位符
占位符用于在构建图时定义数据的输入点。对于我们的模型,需要两个占位符:一个用于输入特征 X,一个用于目标值 Y。
# Placeholder
X = tf.placeholder(dtype=tf.float32, shape=[None, n_stocks])
Y = tf.placeholder(dtype=tf.float32, shape=[None])
shape=[None, n_stocks] 表示输入是一个二维张量,行数(批量大小)可变,列数为股票数量。shape=[None] 表示目标值是一维张量。
变量与网络参数
变量用于存储模型在训练过程中需要更新的参数,如权重和偏置。我们构建一个具有四个隐藏层的全连接网络,神经元数量依次为 1024、512、256、128。
# Model architecture parameters
n_stocks = 500
n_neurons_1 = 1024
n_neurons_2 = 512
n_neurons_3 = 256
n_neurons_4 = 128
n_target = 1
# Layer 1: Variables for hidden weights and biases
W_hidden_1 = tf.Variable(weight_initializer([n_stocks, n_neurons_1]))
bias_hidden_1 = tf.Variable(bias_initializer([n_neurons_1]))
# Layer 2
W_hidden_2 = tf.Variable(weight_initializer([n_neurons_1, n_neurons_2]))
bias_hidden_2 = tf.Variable(bias_initializer([n_neurons_2]))
# Layer 3
W_hidden_3 = tf.Variable(weight_initializer([n_neurons_2, n_neurons_3]))
bias_hidden_3 = tf.Variable(bias_initializer([n_neurons_3]))
# Layer 4
W_hidden_4 = tf.Variable(weight_initializer([n_neurons_3, n_neurons_4]))
bias_hidden_4 = tf.Variable(bias_initializer([n_neurons_4]))
# Output layer
W_out = tf.Variable(weight_initializer([n_neurons_4, n_target]))
bias_out = tf.Variable(bias_initializer([n_target]))
权重矩阵的维度遵循规则:当前层的权重矩阵的列数等于下一层的神经元数。
构建网络架构
将占位符和变量通过矩阵乘法和激活函数连接起来,形成前向传播路径。这里使用 ReLU 作为隐藏层的激活函数。
# Hidden layer with ReLU activation
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))
# Output layer (transposed to match target shape)
out = tf.transpose(tf.add(tf.matmul(hidden_4, W_out), bias_out))
损失函数与优化器
对于回归任务,我们使用均方误差(MSE)作为损失函数。优化器则选择 Adam,它是一种自适应学习率的优化算法。
# Cost function (MSE)
mse = tf.reduce_mean(tf.squared_difference(out, Y))
# Optimizer
opt = tf.train.AdamOptimizer().minimize(mse)
参数初始化
在训练开始前,需要初始化所有变量。这里使用方差缩放初始化器来初始化权重,偏置则初始化为零。
# Initializers
sigma = 1.0
weight_initializer = tf.variance_scaling_initializer(mode="fan_avg", distribution="uniform", scale=sigma)
bias_initializer = tf.zeros_initializer()
训练神经网络
训练过程采用小批量梯度下降。在每个 epoch 中,我们将训练数据打乱,然后分成多个批次送入网络进行前向传播和反向传播,更新权重和偏置。
# Number of epochs and batch size
epochs = 10
batch_size = 256
# Initialize all variables
init = tf.global_variables_initializer()
# Start training session
with tf.Session() as sess:
sess.run(init)
for e in range(epochs):
# Shuffle training data
shuffle_indices = np.random.permutation(np.arange(len(y_train)))
X_train_shuffled = X_train[shuffle_indices]
y_train_shuffled = y_train[shuffle_indices]
# Minibatch training
for i in range(0, len(y_train_shuffled) // batch_size):
start = i * batch_size
batch_x = X_train_shuffled[start:start + batch_size]
batch_y = y_train_shuffled[start:start + batch_size]
# Run optimizer with batch
sess.run(opt, feed_dict={X: batch_x, Y: batch_y})
# Show progress periodically
if np.mod(i, 5) == 0:
# Prediction on test set
pred = sess.run(out, feed_dict={X: X_test})
# Calculate and print current MSE (optional)
# mse_test = sess.run(mse, feed_dict={X: X_test, Y: y_test})
# print(f'Epoch {e}, Batch {i}, Test MSE: {mse_test:.6f}')
在训练过程中,可以定期在测试集上评估模型性能,并可视化预测结果与真实值的对比。经过训练,模型应能学习到时间序列的基本模式。
总结与改进方向
本文演示了如何使用 TensorFlow 1.x 构建一个全连接神经网络进行时间序列预测。虽然模型结构相对基础,但它涵盖了定义计算图、初始化参数、设置损失函数与优化器、以及训练循环等核心步骤。
若要提升模型性能,可以考虑以下方向:
- 使用更先进的网络架构,如 LSTM 或 GRU,它们专为序列数据设计。
- 尝试不同的超参数(层数、神经元数、学习率)。
- 引入 Dropout 等正则化技术防止过拟合。
- 使用更早的数据划分或滚动窗口验证来模拟真实预测场景。
- 将代码迁移至 TensorFlow 2.x,利用其 Eager Execution 和 Keras 高级 API 简化开发流程。
TensorFlow 作为一个强大的框架,为深度学习研究和应用提供了极大的灵活性。对于初学者而言,理解这些基础概念是迈向更复杂模型的第一步。