目录
前言:
学习LSTM代码之前要先了解LSTM模型解决什么问题,简而言之,LSTM是RNN的升级版,擅长挖掘时序数据中的信息。本模型对ZHW_AI课题组.2021的LSTM(股票预测)代码进行讲解。本文对代码的难以理解的、重点的部分进行了注释,并对需要注意的点进行了文字提醒。同时,本文最大的特点,是结合本人自制的数据集制作图、LSTM网络结构图,深入浅出的对'数据-->网络'的具体过程进行讲解演示。
一、代码讲解
基本逻辑:导入相关资源包-->定义模型结构-->制作数据集-->模型训练-->测试与保存结果
1 导入相关资源包
这里除了安装深度学习框架pytorch之外,还需要安装matplotlib、numpy、pandas、tushare等库,安装步骤都很简单,只需输入pip3 install xxx(库名)即可。
import matplotlib.pyplot as plt
import numpy as np
import tushare as ts
import torch
from torch import nn
import datetime
import time
2 定义模型结构
模型架构图 |
class LSTM_Regression(nn.Module): #调用nn.Module的初始化函数,初始化LSTMRegression类。
"""
使用LSTM进行回归
参数:
- input_size: feature size
- hidden_size: number of hidden units
- output_size: number of output
- num_layers: layers of LSTM to stack
"""
def __init__(self, input_size, hidden_size, output_size=1, num_layers=2):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, num_layers) #定义一个LSTM层,其中输入的大小为inputsize,输出的大小为hiddensize,LSTM的层数为numlayers。
self.fc = nn.Linear(hidden_size, output_size) #定义一个全连接层,其中输入的大小为hiddensize,输出的大小为outputsize
def forward(self, _x):
x, _ = self.lstm(_x) # _x is input, size (seq_len, batch, input_size) 这行代码是在LSTMRegression类的forward函数中,将输入_x送入LSTM层中进行计算,并将输出赋值给变量x。其中x的形状为(seqlen, batch, inputsize),表示输入的序列长度为seqlen,batchsize为batch,每个时间步的特征维度为inputsize。在这里,由于不需要使用LSTM层的输出,所以将其赋值给了一个下划线变量。
s, b, h = x.shape # x is output, size (seq_len, batch, hidden_size)
x = x.view(s*b, h) #将x的形状改为(sb, h)。 在深度学习中,全连接层通常接受的输入是二维张量,即(batchsize, inputsize),其中batchsize表示批次大小,inputsize表示每个样本的特征维度。这个操作是为了将LSTM的输出送入全连接层中。
x = self.fc(x) #将x送入全连接层中,得到输出x。
x = x.view(s, b, -1) # 把形状改回来
return x
参照上面的LSTM网络模型架构图可知,self.lstm 表示input_layers和hidden_layers,self.fc表示output_layers,此二者即完成了整个LSTM网络模型的搭建。
提醒:
在深度学习中,全连接层通常接受的输入是二维张量,即(batch_size, input_size),其中batch_size表示批次大小,input_size表示每个样本的特征维度。
3 制作数据集
def create_dataset(data, days_for_train=5) -> (np.array, np.array):
"""
根据给定的序列data,生成数据集
数据集分为输入和输出,每一个输入的长度为days_for_train,每一个输出的长度为1。
也就是说用days_for_train天的数据,对应下一天的数据。
若给定序列的长度为d,将输出长度为(d-days_for_train+1)个输入/输出对
"""
dataset_x, dataset_y= [], []
for i in range(len(data)-days_for_train):
_x = data[i:(i+days_for_train)] #具体来说,i:(i+days_for_train)] 是一个左闭右开的区间
dataset_x.append(_x)
dataset_y.append(data[i+days_for_train])
return (np.array(dataset_x), np.array(dataset_y)) # 数据类型变换:dataset_x和data_set_y数据类型(type)由列表(list)->数组(numpy.nparray)
"""
为什么要将数据集由序列变为数组?序列和数据的联系和区别
为了更高效地进行数值计算,通常会使用数组(numpy.nparray)代替列表(list)。相比于列表,数组具有以下优势:
1. 数组支持向量化操作,即对整个数组进行操作,而不需要使用循环,这样可以大大提高计算速度;
2. 数组支持broadcasting操作,即不同形状的数组之间的算法,这样可以简化代码,提高可读性。
3.1 数据采样制作
原始数据和数据集的区别:
原始数据指的是要研究的时序数据,数据集并不是把整个原始时序数据给作为一个数据,而是要结合研究目的对原始时序数据进行采样从而制作。
案例演示:input数据集的采集:原始时序数据为(n,1)的序列,每k个打包作为一个输入数据,共能采集(n-k)个输入数据,数据集尺寸为(n-k,k,1),label数据(即output)的采集是同样的道理。
4 模型训练
if __name__ == '__main__':
DAYS_FOR_TRAIN = 10 # 数据集采样天数
t0 = time.time() #这段代码的作用是记录当前时间并将其赋值给变量t0。通常情况下,可以通过将代码结束时的当前时间减去t0的值来测量代码的执行时间。
'获取原始数据'
data_close = ts.get_k_data('000001', start='2019-01-01', index=True)['close'] # 取上证指数的收盘价 ,'index=True'表示获取的是指数数据,而不是股票数据。['close']表示只获取收盘价数据。
data_close.to_csv('000001.csv', index=False) #将下载的数据转存为.csv格式保存
data_close = pd.read_csv('000001.csv') #读取.csv文件
# df_sh = ts.get_k_data('sh', start='2019-01-01', end=datetime.datetime.now().strftime('%Y-%m-%d'))
# print(df_sh.shape)
# '绘制原始数据折线图'
#data_close = data_close.astype('float32').values # 转换数据类型:astype('float32')将数据类型转换为float32,values将其转换为numpy数组
#plt.plot(data_close) #将数据绘制成折线图
#plt.savefig('data.png', format='png', dpi=200) #将图像保存,名字”data.png“,格式为png,分辨率200
#plt.close() #关闭图像
'数据预处理:将价格标准化到0~1'
max_value = np.max(data_close)
min_value = np.min(data_close)
data_close = (data_close - min_value) / (max_value - min_value)
'制作数据集'
dataset_x, dataset_y = create_dataset(data_close, DAYS_FOR_TRAIN)
'划分训练集和测试集,70%作为训练集'
train_size = int(len(dataset_x) * 0.7)
train_x = dataset_x[:train_size]
train_y = dataset_y[:train_size]
'将数据改变形状,RNN 读入的数据维度是 (seq_size, batch_size, feature_size)'
train_x = train_x.reshape(-1, 1, DAYS_FOR_TRAIN) #这行代码的作用是将trainx数组的形状改变为(-1, 1, DAYSFORTRAIN)。其中,-1表示该维度的大小由程序自动计算得出,1表示batchsize为1,DAYSFORTRAIN表示featuresize为DAYSFORTRAIN。这是为了将数据转换为RNN读入的格式。
train_y = train_y.reshape(-1, 1, 1)
'转为pytorch的tensor对象'
train_x = torch.from_numpy(train_x) #这行代码的作用是将numpy数组trainx转换为PyTorch的tensor对象。这是因为在PyTorch中,神经网络的输入和输出必须是tensor对象。通过将trainx转换为tensor对象,可以将其作为神经网络的输入。
train_y = torch.from_numpy(train_y)
'模型训练'
model = LSTM_Regression(DAYS_FOR_TRAIN, 8, output_size=1, num_layers=2) # 导入模型并设置模型的参数输入输出层、隐藏层等
model_total = sum([param.nelement() for param in model.parameters()]) # 计算模型参数: 具体来说,它使用了PyTorch中的sum函数和nelement函数,对模型的所有参数进行求和并返回总参数数量。这个值可以用来评估模型的大小和复杂度。
print("Number of model_total parameter: %.8fM" % (model_total/1e6)) # 这行代码的作用是打印模型的总参数数量。我们使用了Python的百分号格式化字符串,其中%.8f表示将一个浮点数格式化为8位小数的字符串。我们将模型的总参数数量除以1e6,以将其从字节转换为兆字节。最后,我们将结果插入到字符串中,使用%f占位符。
train_loss = []
loss_function = nn.MSELoss() #损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2, betas=(0.9, 0.999), eps=1e-08, weight_decay=0) #优化器
for i in range(200): #迭代200次
out = model(train_x)
loss = loss_function(out, train_y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
train_loss.append(loss.item()) #为了在训练过程中访问损失函数的值,您可以使用loss.item()方法。该方法将损失函数的标量值作为Python浮点数返回。
# 将训练过程的损失值写入文档保存,并在终端打印出来
with open('log.txt', 'a+') as f: #语句打开一个名为log.txt的文档,如果该文档不存在则创建一个新的文档
f.write('{} - {}\n'.format(i+1, loss.item())) #将当前的迭代次数和损失值写入到文档中
if (i+1) % 1 == 0:
print('Epoch: {}, Loss:{:.5f}'.format(i+1, loss.item())) #在终端打印出当前的迭代次数和损失值。其中,{:.5f}表示将一个浮点数格式化为5位小数的字符串。
模型训练的准备工作:
(1)制作并处理数据集
1. 获取原始数据
2. 原始数据的预处理:标准化
3. 制作数据集:调用create_dataset( )函数制作数据集
4. 划分训练集和测试集
(2)网络输入维度和数据类型转换要求:
1. RNN 读入的数据维度是 (seq_size, batch_size, feature_size)
2.在PyTorch中,神经网络的输入和输出必须是tensor对象
模型训练完毕之后,想要对损失函数进行可视化操作、保存模型参数或计算训练时间的可进行以下操作。
# 画loss曲线 matplotlib.pyplot
plt.figure() # 先准备一个图板
plt.plot(train_loss, 'b', label='loss') # train_loss是要绘制的列表文件,b-颜色
plt.title("Train_Loss_Curve") # 标题
plt.ylabel('train_loss') # y轴名称
plt.xlabel('epoch_num') # x轴名称
plt.savefig('loss.png', format='png', dpi=200) #将图保存为图片:图片名,格式,分辨率
plt.close() #关闭图版
# torch.save(model.state_dict(), 'model_params.pkl') # 可以保存模型的参数供未来使用
t1 = time.time()
T = t1 - t0
print('The training time took %.2f' % (T / 60) + ' mins.')
tt0 = time.asctime(time.localtime(t0))
tt1 = time.asctime(time.localtime(t1))
print('The starting time was ', tt0)
print('The finishing time was ', tt1)
5 测试与保存结果
# for test
model = model.eval() # 转换成测试模式
# model.load_state_dict(torch.load('model_params.pkl')) # 读取参数
# 注意这里用的是全集 模型的输出长度会比原数据少DAYS_FOR_TRAIN 填充使长度相等再作图
dataset_x = dataset_x.reshape(-1, 1, DAYS_FOR_TRAIN) # (seq_size, batch_size, feature_size)
dataset_x = torch.from_numpy(dataset_x)
pred_test = model(dataset_x) # 全量训练集的模型输出 (seq_size, batch_size, output_size)
pred_test = pred_test.view(-1).data.numpy()
pred_test = np.concatenate((np.zeros(DAYS_FOR_TRAIN), pred_test)) # 填充0 使长度相同
assert len(pred_test) == len(data_close)
plt.plot(pred_test, 'r', label='prediction')
plt.plot(data_close, 'b', label='real')
plt.plot((train_size, train_size), (0, 1), 'g--') # 分割线 左边是训练数据 右边是测试数据的输出
plt.legend(loc='best')
plt.savefig('result.png', format='png', dpi=200)
plt.close()