Bootstrap

LSTM-代码讲解(股票预测)

目录

前言:

一、代码讲解

1 导入相关资源包

2 定义模型结构

3 制作数据集

4 模型训练

5 测试与保存结果 


前言:

学习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_layershidden_layersself.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()

;