上一篇《Python预测基金净值:keras神经网络》搭建了一个简单神经网络用来预测基金净值。
最后得出什么结论?
有点臭。
看看训练情况:
Epoch 1/100
156/156 [= = = = =] - 0s 3ms/step - loss: 0.0450 - val_loss: 0.1114
…
Epoch 100/100
156/156 [= = = = =] - 0s 2ms/step - loss: 0.0299 - val_loss: 0.1056
经过100次训练,训练集的loss已经从0.0450降到0.0299,但是验证集的val_loss几乎么有改善(说明训练集的“改善”只是过拟合)。这对于一个神经网络而言,相当于死刑判决:模型或参数有大问题。
事实上,对于股票/指数/基金净值这些“有序”数据而言,应该采用循环神经网络来搭建,本篇就此试一试。
初步结论是:有点香。
循环神经网络(Recurrent Neural Network, RNN)是以序列数据为输入,在序列的演进方向进行循环递归的递归神经网络。RNN适用于处理时间顺序、逻辑顺序或其他序列特性顺序的数据。
RNN的单元结构如下图(左)所示,上一时刻输出ht-1与当前时刻输入Xt进行拼接,然后由神经网络(以tanh为激活函数)进行处理,得到输出ht。
后续研究发现RNN存在“长期依赖问题”(long-term dependencies problem),亦即在学习过程中,会出现梯度消失或梯度爆炸现象。
LSTM比较好地解决了这个问题,其单元结构如上图(中)所示,增加了一个t时刻的细胞状态Ct,并且增加了三个门(忘记门、输入门、输出门),即图中标注为σ(sigma)的3个以sigmoid作为激活函数的神经网络。如果去掉Ct和三个门,如上图(右)所示,则与上图(左)的RNN等效。
关于LSTM,可参见:理解 LSTM 网络 by 朱小虎
关于keras,可参见:keras中文文档
import requests
import time
import execjs
fileTrain = './data/accTrain.csv'
jjTrain = ['004609', '004853', '005524', '005824', '007749']
fileTest = './data/accTest.csv'
jjTest = '007669'
def getUrl(fscode):
head = 'http://fund.eastmoney.com/pingzhongdata/'
tail = '.js?v='+ time.strftime("%Y%m%d%H%M%S",time.localtime())
return head+fscode+tail
# 根据基金代码获取净值
def getWorth(fscode):
content = requests.get(getUrl(fscode))
jsContent = execjs.compile(content.text)
#累计净值走势
ACWorthTrend = jsContent.eval('Data_ACWorthTrend')
ACWorth = []
for dayACWorth in ACWorthTrend:
ACWorth.append(dayACWorth[1])
return ACWorth
ACWorthFile = open(fileTrain, 'w')
for code in jjTrain:
try:
ACWorth = getWorth(code)
except:
continue
if len(ACWorth) > 0:
ACWorthFile.write(",".join(list(map(str, ACWorth))))
ACWorthFile.write("\n")
print('{} data downloaded'.format(code))
ACWorthFile.close()
ACWorthTestFile = open(fileTest, 'w')
ACWorth = getWorth(jjTest)
if len(ACWorth) > 0:
ACWorthTestFile.write(",".join(list(map(str, ACWorth))))
ACWorthTestFile.write("\n")
print('{} data downloaded'.format(jjTest))
ACWorthTestFile.close()
如上一篇所述,‘004609’, ‘004853’, ‘005524’, ‘005824’, '007749’是5只目前收益较稳定的偏债型混合基金。爬取每日净值数据,作为训练集和验证集(通过设置validation_split=0.25)的数据文件。'007669’也是一只同类型的基金,上一篇没选它,是因为它目前在支付宝基金里面暂停代购。这次用作测试集的数据文件。
注意,和上一篇相比,代码改动了一点:基金数据按日期正序保存。
import numpy as np
import pandas as pd
import csv
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from matplotlib import pyplot as plt
plt.rcParams['font.sans-serif']='SimHei'
plt.rcParams['axes.unicode_minus']=False
batch_size = 4
epochs = 50
time_step = 6 #用多少组天数进行预测
input_size = 6 #每组天数,亦即预测天数
look_back = time_step * input_size
showdays = 120 #最后画图观察的天数(测试天数)
X_train = []
y_train = []
X_validation = []
y_validation = []
testset = [] #用来保存测试基金的近期净值
#忽略掉最近的forget_days天数据(回退天数,用于预测的复盘)
forget_days = 0
def create_dataset(dataset):
dataX, dataY = [], []
print('len of dataset: {}'.format(len(dataset)))
for i in range(0, len(dataset) - look_back, input_size):
x = dataset[i: i + look_back]
dataX.append(x)
y = dataset[i + look_back: i + look_back + input_size]
dataY.append(y)
return np.array(dataX), np.array(dataY)
def build_model():
model = Sequential()
model.add(LSTM(units=128, input_shape=(time_step, input_size)))
model.add(Dense(units=input_size))
model.compile(loss='mean_squared_error', optimizer='adam')
return model
# 设定随机数种子
seed = 7
np.random.seed(seed)
# 导入数据(训练集)
with open(fileTrain) as f:
row = csv.reader(f, delimiter=',')
for r in row:
dataset = []
r = [x for x in r if x != 'None']
#涨跌幅是2天之间比较,数据会减少1个
days = len(r) - 1
#有效天数太少,忽略
if days <= look_back + input_size:
continue
for i in range(days):
f1 = float(r[i])
f2 = float(r[i+1])
if f1 == 0 or f2 == 0:
dataset = []
break
#把数据放大100倍,相当于以百分比为单位
f2 = (f2 - f1) / f1 * 100
#如果涨跌幅绝对值超过15%,基金数据恐有问题,忽略该组数据
if f2 > 15 or f2 < -15:
dataset = []
break
dataset.append(f2)
n = len(dataset)
#进行预测的复盘,忽略掉最近forget_days的训练数据
n -= forget_days
if n >= look_back + input_size:
#如果数据不是input_size的整数倍,忽略掉最前面多出来的
m = n % input_size
X_1, y_1 = create_dataset(dataset[m:n])
X_train = np.append(X_train, X_1)
y_train = np.append(y_train, y_1)
# 导入数据(测试集)
with open(fileTest) as f:
row = csv.reader(f, delimiter=',')
#写成了循环,但实际只有1条测试数据
for r in row:
dataset = []
#去掉记录为None的数据(当天数据缺失)
r = [x for x in r if x != 'None']
#涨跌幅是2天之间比较,数据会减少1个
days = len(r) - 1
#有效天数太少,忽略,注意:测试集最后会虚构一个input_size
if days <= look_back:
print('only {} days data. exit.'.format(days))
continue
#只需要最后画图观察天数的数据
if days > showdays:
r = r[days-showdays:]
days = len(r) - 1
for i in range(days):
f1 = float(r[i])
f2 = float(r[i+1])
if f1 == 0 or f2 == 0:
print('zero value found. exit.')
dataset = []
break
#把数据放大100倍,相当于以百分比为单位
f2 = (f2 - f1) / f1 * 100
#如果涨跌幅绝对值超过15%,基金数据恐有问题,忽略该组数据
if f2 > 15 or f2 < -15:
print('{} greater then 15 percent. exit.'.format(f2))
dataset = []
break
testset.append(f1)
dataset.append(f2)
#保存最近一天基金净值
f1=float(r[days])
testset.append(f1)
#测试集虚构一个input_size的数据(若有forget_days的数据,则保留)
if forget_days < input_size:
for i in range(forget_days,input_size):
dataset.append(0)
testset.append(np.nan)
else:
dataset = dataset[:len(dataset) - forget_days + input_size]
testset = testset[:len(testset) - forget_days + input_size]
if len(dataset) >= look_back + input_size:
#将testset修正为input_size整数倍加1
m = (len(testset) - 1) % input_size
testset = testset[m:]
m = len(dataset) % input_size
#将dataset修正为input_size整数倍
X_validation, y_validation = create_dataset(dataset[m:])
#将输入转化成[样本数,时间步长,特征数]
X_train = X_train.reshape(-1, time_step, input_size)
X_validation = X_validation.reshape(-1, time_step, input_size)
#将输出转化成[样本数,特征数]
y_train = y_train.reshape(-1, input_size)
y_validation = y_validation.reshape(-1, input_size)
print('num of X_train: {}\tnum of y_train: {}'.format(len(X_train), len(y_train)))
print('num of X_validation: {}\tnum of y_validation: {}'.format(len(X_validation), len(y_validation)))
# 训练模型
model = build_model()
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=1, validation_split=0.25, shuffle=True)
# 评估模型
train_score = model.evaluate(X_train, y_train, verbose=0)
validation_score = model.evaluate(X_validation, y_validation, verbose=0)
# 预测
predict_validation = model.predict(X_validation)
#将之前虚构的最后一组input_size里面的0涨跌改为NAN(不显示虚假的0)
if forget_days < input_size:
for i in range(forget_days,input_size):
y_validation[-1, i] = np.nan
print('Train Set Score: {:.3f}'.format(train_score))
print('Test Set Score: {:.3f}'.format(validation_score))
print('未来{}天实际百分比涨幅为:{}'.format(input_size, y_validation[-1]))
print('未来{}天预测百分比涨幅为:{}'.format(input_size, predict_validation[-1]))
#进行reshape(-1, 1)是为了plt显示
y_validation = y_validation.reshape(-1, 1)
predict_validation = predict_validation.reshape(-1, 1)
testset = np.array(testset).reshape(-1, 1)
# 图表显示
fig=plt.figure(figsize=(15,6))
plt.plot(y_validation, color='blue', label='基金每日涨幅')
plt.plot(predict_validation, color='red', label='预测每日涨幅')
plt.legend(loc='upper left')
plt.title('关联组数:{}组,预测天数:{}天,回退天数:{}天'.format(time_step, input_size, forget_days))
plt.show()
代码稍稍比上一篇复杂一点,建议先看懂上一篇(让上一篇“臭文”发挥余热)。
几个参数介绍一下:
input_size:即作为时间窗口大小,同时也是LSTM神经网络的输入参数之一,以及输出参数(多对多的模式)。
time_step:时间窗口组数,即用多少组维度为input_size的数据,来预测下一个input_size的数据。
look_back:等于time_step * input_size,相当于使用多少个现有数据来预测,LSTM网络的input_shape是也。
showdays:其实就是测试集大小,按说可以大一些,但太大了画图看不清,建议用120吧。
forget_days:回退天数,用于复盘前forget_days天的预测。
以上代码采用:time_step=6,input_size=6(因此LSTM的units设为128,通常建议为input_shape的2-3倍),运行之:
训练速度很快,打印信息参考如下:
num of X_train: 434 num of y_train: 434
num of X_validation: 15 num of y_validation: 15
Epoch 1/50
56/56 [= = = = =] - 0s 20ms/step - loss: 0.0482 - val_loss: 0.0956
…
Epoch 50/50
56/56 [= = = = =] - 0s 4ms/step - loss: 0.0100 - val_loss: 0.0431
Train Set Score: 0.018
Test Set Score: 0.045
未来6天预测百分比涨幅为:[-0.05394792 -0.4340353 0.20469473 0.15502352 0.0738658 0.1280248 ]
和上一篇“臭文”相比,至少val_loss训练有效果了。而且最后得分也不错。
当然,结果到底如何,还得看图说话。
凭直觉,look_back为30多天(相当于1.5个月的交易日)较为合理。采用不同的参数组合(time_step/input_size),跑几次,可见预测图形和实际图形的拟合度,和上一篇相比,有了极大改善。
感觉“有点香”了。
最后再加一段看净值图形的代码:(上一篇这一段写差了点,这次改了)
# 实际净值、预测净值
y_validation_plot = np.empty_like(testset)
predict_validation_plot = np.empty_like(testset)
y_validation_plot[:, :] = np.nan
predict_validation_plot[:, :] = np.nan
y = testset[look_back, 0]
p = testset[look_back, 0]
for i in range(look_back, len(testset)-1):
y *= (1 + y_validation[i-look_back, 0] / 100)
p *= (1 + predict_validation[i-look_back, 0] / 100)
#print('{:.4f} {:.4f} {:.4f}'.format(testset[i+1,0], y, p))
y_validation_plot[i, :] = y
predict_validation_plot[i, :] = p
# 图表显示
fig=plt.figure(figsize=(15,6))
plt.plot(y_validation_plot, color='blue', label='基金每日净值')
plt.plot(predict_validation_plot, color='red', label='预测每日净值')
plt.legend(loc='upper left')
plt.title('关联组数:{}组,预测天数:{}天,回退天数:{}天'.format(time_step, input_size, forget_days))
plt.show()
由于基金净值是连续乘出来的,10天里面就算9天预测相同,但另一天有些差异(比如实际爆涨1.3%,而预测只是大涨0.5%),那么净值就会一直有较大的差异了------差不多要一直差1分多钱了,所以只贴一张图作为示意(这一段其实是为了验证代码是否正确计算了净值------把print语句打开)。
LSTM模型效果有了明显改进。
当然,这里面有一些“作弊”成分:作为测试集的007669基金,由于跟训练集的5个基金相似(都是稳健的偏债混合基金),因此大的起伏基本一致,能拟合也不足为奇(既然验证集的val_loss经训练有了明显改进,测试集007669当然也可以表现不错------事实上,测试集分数,与验证集的val_loss分数相当)。
但是,如果认为LSTM模型只是靠“作弊”得分,那么这篇文章我就白写,您也白看了。LSTM本身是有“预测”能力的:不妨设置forget_days进行回退,复盘之前的预测。
1、该模型可以用来预测股票指数吗?
当然可以,只需改动两行代码------先把测试基金007669改为股票指数基金比如005918(天弘沪深300ETF联接C),再把训练集数据放大100倍的地方,改为放大400倍(训练集是偏债基金,股票占比大约四分之一,涨跌幅度大约是测试集股指基金的四分之一),就可以啦。效果如下图所示:
2、time_step和input_size建议都用6天。
原因:一是基金持有7天(亦即5个交易日)则卖出手续费较低,预测后续6天走势作为参考是合理的(再长则没意义);二是感觉这种时间序列,time_step和input_size相接近是合理的。
3、进一步改进,采用层叠LSTM(第一层的ht作为第二层的Xt,需设参数return_sequences=True),例如改这几行代码:
#epochs = 50
epochs = 200
#model.add(LSTM(units=128, input_shape=(time_step, input_size)))
#model.add(Dense(units=input_size))
model.add(LSTM(units=128, return_sequences=True, input_shape=(time_step, input_size)))
model.add(LSTM(units=32))
model.add(Dense(units=input_size))
又或者这样:
#epochs = 50
epochs = 200
#model.add(LSTM(units=128, input_shape=(time_step, input_size)))
#model.add(Dense(units=input_size))
model.add(LSTM(units=128, return_sequences=True, input_shape=(time_step, input_size)))
model.add(LSTM(units=32, return_sequences=True))
model.add(Dense(units=1))
注意:后者这个模型仅当time_step = input_size才可以,因为它最后全连接层的输出维数是(time_step, 1),不是(input_size)。感觉该模型不够合理。
4、将LSTM模型改为GRU(门控循环单元),也是可以的。改两处代码:
#from keras.layers import LSTM
from keras.layers.recurrent import GRU
#model.add(LSTM(units=128, input_shape=(time_step, input_size)))
model.add(GRU(units=128, input_shape=(time_step, input_size)))
测试结果,层叠LSTM似有提高,而GRU没什么改善。
5、由于基金净值通常在工作日19:00—21:00进行更新,因此需避免在傍晚下载基金净值进行训练。
6、当然,不可以用于预测具体股票,切记切记!勿谓言之不预也。
这三篇就告一段落了,有缘人请耐心研究收获(请勿剽窃,更勿招摇),实现小目标,呵呵。