0
前言
当我们说起金融时间序列的预测,大家可能第一个想到的是预测股票价格。 然而,Chollet 的《Deep Learning with Python》一书强调,人们不应该尝试使用时间序列预测方法去预测股票价格。 他解释道,在股市中过去的数据并不是估计未来的一个好的基础。
那么,有没有可能用神经网络来预测股价呢?今天公众号带你来探讨。
1
案例
我们将训练一个神经网络,它将使用n个已知值(过去的价格)来预测(n 1)-th的价格。我们假设两次价格测量之间的时间是常数。
我们将使用前几天的收盘价来预测收盘价。使用yfinance Python包获取数据。
代码语言:javascript复制pip install yfinance
下一步:
代码语言:javascript复制import yfinance as yf
# create the object that represents Maersk stock data
# here MAERSK-B.CO -- is the Maerks's ticker
maersk = yf.Ticker('MAERSK-B.CO')
我们还没有下载任何数据,只创建了可以用来请求数据的对象。雅虎财经为Maersk提供了股息数据,我们知道,股息(股票的利息)会影响股票价格。因此,我们希望神经网络在预测价格时考虑股息。这意味着,当我们告诉网络使用前几天的一组价格来预测某一天的收盘价时,我们还需要为它提供一个标记,告诉它当天是否支付了股息。
要获得支付股息的日期,请查看maersk.dividends。为了得到股票价格,我们调用history方法。这个方法有几个参数,我们用到的是period和interval。
Period参数定义我们请求数据的时间段。该参数支持一些预定义的字符串值,我们将使用其中的一个。我们传递字符串'max’ ,它告诉我们所有可用的数据。使用开始和结束参数可以定义确切的周期。但是,因为我们将使用所有可用的数据,所以我们将使用 period 参数并传递'max'。
Interval 参数告诉方法两个后续值之间的间隔。 它取一个预先定义的值,我们将通过'1d' ,因为我们要使用每日价格。
代码语言:javascript复制history = maersk.history(period='max', interval='1d')
看看dataframe:
当设计一个神经网络来预测时间序列时,应该决定网络将有多少input。在我们的例子中,我们必须选择输入网络的价格数量来预测下一个价格。由于我们现在还不知道这个数字,所以最好能够生成具有不同数量输入的数据集。幸运的是,Keras开发人员已经考虑到了这一点,现在Keras提供了一个时间序列生成器,可以生成具有不同输入量的数据集。在时间序列预测的情况下,输入值和目标值都来自同一个序列。这意味着我们使用大小为j的移动窗口,其中j是我们用来预测(j 1)-th值的值的个数。
换句话说,我们获取时间序列的 j 后续元素({ x1,x2,... xj }) ,然后取(j 1) th元素(x₍ⱼ₊₁₎)并将它设置为目标值。这对(j,(j 1)-th)构成一个单独的训练示例。为了生成另一个训练示例,我们将移动窗口一个step,并使用{x₂x₃,……x₍ⱼ₊₁₎}作为输入,x₍ⱼ₊₂₎作为目标值。
Keras为我们提供了TimeseriesGenerator类,我们将使用这个类来生成训练集。
https://keras.io/preprocessing/sequence/
这里唯一的困难是我们还希望网络考虑股息。因此,我们必须编写一个函数,该函数使用TimeseriesGenerator类生成训练集,然后使用关于股息的信息丰富生成器的输出。
代码语言:javascript复制def generate_series(data, value_num):
close = data['Close']
dividends = data['Dividends']
tsg = TimeseriesGenerator(close, close,
length=value_num,
batch_size=len(close))
global_index = value_num
i, t = tsg[0]
has_dividends = np.zeros(len(i))
for b_row in range(len(t)):
assert(abs(t[b_row] - close[global_index]) <= 0.001)
has_dividends[b_row] = dividends[global_index] > 0
global_index = 1
return np.concatenate((i, np.transpose([has_dividends])),
axis=1), t
该函数接受两个参数:我们希望它处理的数据集(data参数)和序列应该具有的输入值的数量(value_num 参数)。
如你所知,神经网络是用梯度下降法训练的,采用梯度的成本函数。最简单的方法是假设我们使用整个数据集来计算成本函数梯度。然而,也有不利的一面。首先,数据集可能非常大,这使得计算梯度非常耗时。其次,如果数据集非常大,那么梯度值也可以非常大,非常大!以至于它根本不适合机器精度。一些人会指出,我们实际上并不需要确切的梯度值。我们只需要它的估计值来决定我们应该朝哪个方向移动,以最小化成本函数。因此,我们可以利用训练样本的一个小子集来估计梯度。当然,我们最终会遍历整个数据集,但是不需要一次计算整个数据集的梯度。我们可以将数据集划分为几个称为batch的子集,一次只处理一个batch。我们使用单批计算的梯度来更新网络的权值。一旦我们处理了所有的批次,我们可以说我们运行了一个单独的训练周期。在一次训练中,可能有多个epoch,具体的epoch数量取决于任务。同时,训练的例子必须打乱。这意味着随后的两个训练示例不能属于同一批。让我们测试这个函数并生成一个使用四个输入值的数据集。
代码语言:javascript复制inputs, targets = generate_series(history, 4)
让我们看一个例子:
代码语言:javascript复制# print(inputs[3818])
array([1.246046e 04, 1.232848e 04, 1.244496e 04, 1.274000e 04,
1.000000e 00])
我们可以看到,一个训练示例是一个带有4个价格和5个附加值的向量,该值表示当天是否支付股息。注意,值比较大。的确,价格范围从767.7到12740.0的神经网络在这样的范围内不能很好地工作,所以我们必须将数据归一化。我们将使用最简单的归一化方法:MinMax。
代码语言:javascript复制h_min = history.min()
normalized_h = (history - h_min) / (history.max() - h_min)
重新生成数据集:
代码语言:javascript复制inputs, targets = generate_series(normalized_h, 4)
看看数据归一化后的结果:
代码语言:javascript复制# print(inputs[3818])
array([0.9766511 , 0.96562732, 0.97535645, 1. , 1. ])
正如我们所看到的,这些值现在的范围是从0到1。这使得任务更容易了。然而,我们现在必须保留h.min()和h.max(),以便在预测价格时对网络输入进行规一化,并对其输出进行反规一化以获得准确的值。
最后,神经网络该出场了!网络将有(n 1)输入,n表示价格,1表示股息指标和一个输出。我们仍然需要确定n。为此,我们将编写一个函数来创建具有指定数量输入的神经网络。我们使用input_shape=(n 1,)表达式来包含股息指示器。
代码语言:javascript复制def create_model(n):
m = models.Sequential()
m.add(layers.Dense(64, activation='relu', input_shape=(n 1,)))
m.add(layers.Dense(64, activation='relu'))
m.add(layers.Dense(1))
return m
在训练网络之前,我们将数据集划分为两部分:训练集和测试集。在训练网络时,我们不会使用测试集的例子。
代码语言:javascript复制train_inputs = inputs[:-1000]
val_inputs = inputs[-1000:]
train_targets = targets[:-1000]
val_targets = targets[-1000:]
我们再写一个函数。这个函数将帮助我们决定网络应该有多少输入。这个函数接受输入的数量来检查要训练的epoch的数量。该函数将创建一个网络,为其准备数据,然后对网络进行训练,并在测试集中评估其性能。
代码语言:javascript复制def select_inputs(data, start, end, epochs):
models = {}
for inputs in range(start, end 1):
print('Using {} inputs'.format(inputs))
model_inputs, targets = generate_series(data, inputs)
train_inputs = model_inputs[:-1000]
val_inputs = model_inputs[-1000:]
train_targets = targets[:-1000]
val_targets = targets[-1000:]
m = create_model(inputs)
print('Training')
m.compile(optimizer='adam', loss='mse')
h = m.fit(train_inputs, train_targets,
epochs=epochs,
batch_size=32,
validation_data=(val_inputs, val_targets))
model_info = {'model': m, 'history': h.history}
models[inputs] = model_info
return models
现在,让我们用2到10个输入为20个epoch训练网络:
代码语言:javascript复制dtrained_models = select_inputs(normalized_h, 2, 10, 20)
当训练完成后,我们可以用下面的代码得到一个简短的总结:
代码语言:javascript复制model_stats = {}
for k, v in trained_models.items():
train_history = v['history']
loss = train_history['loss'][-1]
val_loss = train_history['val_loss'][-1]
model_stats[k] = {'inputs': k, 'loss': loss, 'val_loss': val_loss}
打印model_stats值,我们可以看到摘要:
代码语言:javascript复制{2: {'inputs': 2,
'loss': 6.159038594863468e-05,
'val_loss': 0.0006709674960002303},
3: {'inputs': 3,
'loss': 7.425233190960614e-05,
'val_loss': 0.00021176348975859583},
4: {'inputs': 4,
'loss': 7.471898652647588e-05,
'val_loss': 0.00022580388654023408},
5: {'inputs': 5,
'loss': 8.866131339595126e-05,
'val_loss': 0.00027424713294021784},
6: {'inputs': 6,
'loss': 7.322355930846842e-05,
'val_loss': 0.0003323734663426876},
7: {'inputs': 7,
'loss': 8.709070955596233e-05,
'val_loss': 0.0004295352199114859},
8: {'inputs': 8,
'loss': 8.170129280188121e-05,
'val_loss': 0.00024587249546311797},
9: {'inputs': 9,
'loss': 7.327485314296024e-05,
'val_loss': 0.0003118165017804131},
10: {'inputs': 10,
'loss': 8.064566193526276e-05,
'val_loss': 0.0003668071269057691}}
我们可以看到,使用测试集计算的错误总是略大于为训练集计算的值。这意味着网络处理已知数据比处理未知数据稍微好一些。
现在,我们可以根据网络的输入值来绘制测试误差图。
代码语言:javascript复制import matplotlib.pyplot as plt
val_loss = []
indices = []
for k, v in model_stats.items():
indices.append(k)
val_loss.append(v['val_loss'])
plt.plot(indices, val_loss)
通过这个图,我们可以看到哪个网络显示的测试错误最少。确切的结果可能会随着时间的推移而变化,这取决于雅虎财经历史数据的数量。
有一个有趣的现象。如果一个人运行这个脚本两次,那么他们将收到不同的结果。换句话说,最小的测试错误是由不同的网络产生的。由于网络之间的唯一区别是输入的数量,那么我们可以得出结论:测试误差并不依赖于输入的数量有多少。这反过来支持了最初的推测,即我们无法用神经网络预测股价。显然,网络训练忽略一些输入,结论是输出并不依赖于它们。
我们已经把数据进行了标准化。现在我们来计算网络的精确误差。
代码语言:javascript复制close_min = history['Close'].min()
close_max = history['Close'].max()
for k in model_stats:
e = ((close_max - close_min) * model_stats[k]['val_loss'] close_min)
print(k, e)
输出:
代码语言:javascript复制2 771.0400773414451
3 770.341964375037
4 771.6538168560887
5 771.9637314503287
6 770.3164239349957
7 771.5147973106168
8 778.0784490537151
9 779.7546236891968
10 770.8432766947052
误差非常大!即使对于测试误差最小的网络,精确误差也是非常大的。反正,我们是不会相信用这样一个网络去买卖股票。
现在,我们画一个图来比较精确的价格和预测的价格。
2
总结
对于未知数据,我们得到了较大的误差。这意味着该网络未能预测收盘价。或许,我们可以通过改变现有的网络结构来改善这一结果。但,我们不认为我们可以得到更多的数据,因为我们已经使用了所有可用的数据。使用不同公司的数据来训练一个网络是可能的,但是由于这些公司可能有不同的属性,它们的股价可能会根据不同的规律变化,这只会使网络混乱。
值得注意的是,网络本身并不能预测股价。相反,它它尝试使用给定的值猜测一个序列的下一个值是什么。这是因为价格没有编码下一次它会如何变化。如果价格上涨一段时间,就不能保证下一分钟不会下跌。价格受外部事件的影响很大,这是网络所不知的。
或许,我们将能够使用神经网络进行短期预测,来确定未来几分钟内的价格变化。这可能是因为我们预测的时间段越短,外部事件发生的变化就越小。然而,在这种情况下,最好使用线性回归模型来预测价格变化的方向。
如果我们仔细观察股价曲线,我们会发现它的变化是随机的。如果我们只有价格而不知道外部事件,这就是事实。因此,股价看起来就像一个鞅,这一个我们无法预测的过程。
鞅(Martingale)于博弈论中的表示公平博弈的数学模型,在概率论中是满足下述条件的随机过程:已知过去某一时刻s以及之前所有时刻的观测值,若某一时刻t的观测值的条件期望等于过去某一时刻s的观测值,则称这一随机过程是鞅。所以:
最好不要用神经网络来预测股价
参考资料
1、Bugorskij, V. Ispol’zovanie nejronnyh setej dlya modelirovaniya prognoza kotirovok cennyh bumag / V. Bugorskij, A. Sergienko // Prikladnaya informatika. — 2008. — T. № 3(15). (in Russian)
2、Chollet, F. Deep learning with python — 2017. — Manning Publications.
3、Elliot, A. Time Series Prediction : Predicting Stock Price / A. Elliot, C. H. Hsu // ArXiv e-prints. — 2017.
4、Ian Goodfellow. Deep Learning / Ian Goodfellow, Yoshua Bengio, Aaron Courville — MIT Press, 2016.