手撕多层LSTM含代码python实现(正反传播)+股票预测

一、前言

整理自己学习过程中的相关知识内容,并且尽可能使用代码实现。提高自己的工程能力。

二、LSTM原理

请添加图片描述

2.1LSTM的门结构(公式简略版)

2.1.1遗忘门

在这里插入图片描述
这部分叫做遗忘门的原因是,经过sigmoid函数后的结果ft处于[0,1],之后ft与Ct-1按照元素乘,其实是哈达玛积(Hadamard product)。这样ft矩阵中值越接近0,相对应Ct-1矩阵中的值越小越应该遗忘。反之亦然。

2.1.2输入门

在这里插入图片描述
用于更新状态。
it来决定矩阵不同位置对于更新的重要性,将前一层隐藏状态ht-1和当前输入xt通过sigmoid函数映射到[0,1],以此来决定要更新的内容,越接近0,表示越不重要。反之亦然。
t将前一层隐藏状态ht-1和当前输入xt通过tanh激活函数,创造新的候选值向量。最后与it相乘。这样it矩阵中值越接近0,相对应C̅t矩阵中的值越小越不重要,越不需要更新。反之亦然。

2.1.3输出门

请添加图片描述
上述为lstm公式的简略版本

2.3LSTM的门结构(公式完全版)

以下来自李沐大神ppt
请添加图片描述
请添加图片描述
请添加图片描述请添加图片描述请添加图片描述

2.4避免长期记忆遗忘问题

因为LSTM中有两个通道,短期记忆通道h(保持非线性操作)和长期记忆通道c(保持线性操作),线性比较稳定,所以C变化比较稳定。

三、代码实现(注释后续慢慢加)

这里的代码公式实现的是完全版
第一步就是实现,lstm块

import torch
import numpy as np
from torch import nn

import tushare as ts
import pandas as pd
import matplotlib.pyplot as plt
#正常显示画图时出现的中文
from pylab import mpl
#这里使用微软雅黑字体
mpl.rcParams['font.sans-serif']=['SimHei']
#画图时显示负号
mpl.rcParams['axes.unicode_minus']=False
import seaborn as sns  #画图用的
from sklearn.preprocessing import MinMaxScaler
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import fetch_openml


import datetime
import time

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=0, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

class lstm_cell(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.w_xf = nn.Parameter(torch.randn(self.input_size, self.hidden_size))
        self.w_xi = nn.Parameter(torch.randn(self.input_size, self.hidden_size))
        self.w_xo = nn.Parameter(torch.randn(self.input_size, self.hidden_size))
        self.w_xc = nn.Parameter(torch.randn(self.input_size, self.hidden_size))

        self.w_hf = nn.Parameter(torch.randn(self.hidden_size, self.hidden_size))
        self.w_hi = nn.Parameter(torch.randn(self.hidden_size, self.hidden_size))
        self.w_ho = nn.Parameter(torch.randn(self.hidden_size, self.hidden_size))
        self.w_hc = nn.Parameter(torch.randn(self.hidden_size, self.hidden_size))

        self.w_hq = nn.Parameter(torch.randn(self.hidden_size, self.hidden_size))
        self.b_q = nn.Parameter(torch.zeros(self.hidden_size))

        self.b_f = nn.Parameter(torch.zeros(self.hidden_size))
        self.b_i = nn.Parameter(torch.zeros(self.hidden_size))
        self.b_o = nn.Parameter(torch.zeros(self.hidden_size))
        self.b_c = nn.Parameter(torch.zeros(self.hidden_size))

    def forward(self,input_seq, h_prev, c_prev):
        outputs=[]
        input_seq_copy = input_seq.transpose(1, 0)
            
        #input_seq = input_seq.view(input_seq.shape[1], input_seq.shape[0]*input_seq.shape[2])
        for i,x_t in enumerate(input_seq_copy):
            #forget
            f_t = torch.sigmoid(x_t@self.w_xf + h_prev@self.w_hf + self.b_f)
            i_t = torch.sigmoid(x_t@self.w_xi + h_prev@self.w_hi + self.b_i)
            o_t = torch.sigmoid(x_t@self.w_xo + h_prev@self.w_ho + self.b_o)
    
            cct= torch.tanh(x_t@self.w_xc + h_prev@self.w_hc + self.b_c)
            c_prev = f_t*c_prev + i_t*cct
            
    
            h_prev = o_t*torch.tanh(c_prev)
            #y_t = torch.softmax((h_prev@self.w_hq).squeeze()+self.b_q,dim=0)
            y_t = (h_prev@self.w_hq)+self.b_q
            #y_t = softmax(h_prev@self.w_hq + self.b_q)
            
            
            outputs.append(y_t)
        outputs = torch.stack(outputs)
        outputs.transpose_(1, 0)
        #torch.cat(outputs, dim=0)
        return outputs, h_prev, c_prev

跑个例子验证一下

#其中输入的特征维度是13,隐藏层是32,时间步为1
batch_size = 506
x = torch.randn(batch_size,1,13)
lstm=lstm_cell(13,32)
h_0=torch.randn(batch_size,32)
c_0=torch.randn(batch_size,32)
outputs, h, c = lstm(x,h_0,c_0)

print('outputs.shape:',outputs.shape)

'''输出
outputs.shape: torch.Size([506, 1, 32])
'''

下一步实现多层堆叠

class LSTM_layers(nn.Module):
    def __init__(self, input_size, hidden_size,num_layers=1):
        super().__init__()
        self.input_size=input_size
        self.hidden_size=hidden_size
        self.num_layers=num_layers
        self.lstm_cell = nn.ModuleList([lstm_cell(self.input_size, self.hidden_size)])
        for i in range(1, num_layers):
            self.lstm_cell.append(lstm_cell(self.hidden_size, self.hidden_size))

    def forward(self, input_seq, h_0, c_0):
        
        
        out_put, h, c = self.lstm_cell[0](input_seq, h_0[0], c_0[0])
        for i in range(1,self.num_layers):
            out_put, h, c = self.lstm_cell[i](out_put, h_0[i], c_0[i])
            
        return out_put, h, c

    def __repr__(self):
        return f'LSTM_layers(input_size={self.input_size}, hidden_size={self.hidden_size}, num_layers={self.num_layers})'
      
#使用下面代码测试,和上面一样,只不过多了一个堆叠的层数,这里设置为了2
x = torch.randn(506,1,13)
lstm=LSTM_layers(13,32,num_layers=2)
h_0=torch.randn(2,x.shape[0],32)
c_0=torch.randn(2,x.shape[0],32)
out_put, h, c = lstm(x, h_0, c_0)
print('out_put.shape:',out_put.shape)
#查看训练参数
lstm_model=LSTMNet(10, 32, 1, 2)
print(lstm_model)
# 定义损失函数和优化器
for name, param in lstm_model.named_parameters():
    print(f"{name}: {param.size()}")

'''输出
out_put.shape: torch.Size([506, 1, 32])
LSTMNet(
  (lstm): LSTM_layers(input_size=10, hidden_size=32, num_layers=2)
  (fc): Linear(in_features=32, out_features=1, bias=True)
)
lstm.lstm_cell.0.w_xf: torch.Size([10, 32])
lstm.lstm_cell.0.w_xi: torch.Size([10, 32])
lstm.lstm_cell.0.w_xo: torch.Size([10, 32])
lstm.lstm_cell.0.w_xc: torch.Size([10, 32])
lstm.lstm_cell.0.w_hf: torch.Size([32, 32])
lstm.lstm_cell.0.w_hi: torch.Size([32, 32])
lstm.lstm_cell.0.w_ho: torch.Size([32, 32])
lstm.lstm_cell.0.w_hc: torch.Size([32, 32])
lstm.lstm_cell.0.w_hq: torch.Size([32, 32])
lstm.lstm_cell.0.b_q: torch.Size([32])
lstm.lstm_cell.0.b_f: torch.Size([32])
lstm.lstm_cell.0.b_i: torch.Size([32])
lstm.lstm_cell.0.b_o: torch.Size([32])
lstm.lstm_cell.0.b_c: torch.Size([32])
lstm.lstm_cell.1.w_xf: torch.Size([32, 32])
lstm.lstm_cell.1.w_xi: torch.Size([32, 32])
lstm.lstm_cell.1.w_xo: torch.Size([32, 32])
lstm.lstm_cell.1.w_xc: torch.Size([32, 32])
lstm.lstm_cell.1.w_hf: torch.Size([32, 32])
lstm.lstm_cell.1.w_hi: torch.Size([32, 32])
lstm.lstm_cell.1.w_ho: torch.Size([32, 32])
lstm.lstm_cell.1.w_hc: torch.Size([32, 32])
lstm.lstm_cell.1.w_hq: torch.Size([32, 32])
lstm.lstm_cell.1.b_q: torch.Size([32])
lstm.lstm_cell.1.b_f: torch.Size([32])
lstm.lstm_cell.1.b_i: torch.Size([32])
lstm.lstm_cell.1.b_o: torch.Size([32])
lstm.lstm_cell.1.b_c: torch.Size([32])
fc.weight: torch.Size([1, 32])
fc.bias: torch.Size([1])
'''

在LSTM_layers上封装一层全连接网络,来把output转成我们需要的形状。

#来个全连接层控制输出
class LSTMNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size,num_layers=1):
        super().__init__()
        self.input_size=input_size
        self.hidden_size=hidden_size
        self.num_layers=num_layers
        self.output_size=output_size
        self.lstm = LSTM_layers(self.input_size, self.hidden_size,self.num_layers)
        self.fc = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input_seq):
        h_0=torch.zeros(self.num_layers,input_seq.shape[0],self.hidden_size)
        c_0=torch.zeros(self.num_layers,input_seq.shape[0],self.hidden_size)
        out, h, c = self.lstm(input_seq, h_0.detach(), c_0.detach())
        b, s, f = out.shape
        out = out.view(b*s, f)
        out = self.fc(out)
        out = out.view(b, s, -1)
        return out

加载数据集

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)  



DAYS_FOR_TRAIN = 10  # 数据集采样天数
t0 = time.time() #这段代码的作用是记录当前时间并将其赋值给变量t0。通常情况下,可以通过将代码结束时的当前时间减去t0的值来测量代码的执行时间。

pro = ts.pro_api('d0c109f1982cd156bf4737cd2be4d48552084535bec71893cb26e552')
df = pro.daily(ts_code='000001.SZ', start_date='20190101')
'获取原始数据'
data_close = df['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) #将数据绘制成折线图

'数据预处理:将价格标准化到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 读入的数据维度是 (batch_size, seq_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)
print(train_x.shape)
print(train_y.shape)

定义训练函数

def train(X, y, num_epochs, num_layers, input_size, hidden_size, output_size, lr):
    device = torch.device("cpu")
    if (torch.cuda.is_available()):
        device = torch.device("cuda:0")
        print('Training on GPU.')
    else:
        print('No GPU available, training on CPU.')

    lstm_model=LSTMNet(input_size, hidden_size, output_size, num_layers)


    #print(lstm_model)
    # 定义损失函数和优化器
    criterion = nn.MSELoss()
    optimizer = optim.Adam(lstm_model.parameters(), lr=lr)

    
    loss_list = []
    
    for epoch in range(num_epochs):
        optimizer.zero_grad()
        outputs = lstm_model(X)
        loss = criterion(outputs, y)
        loss.backward()
        optimizer.step()
    
        if epoch % 5 == 0:
            loss_list.append(loss.item())
            print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')

    # 可视化损失曲线
    plt.plot(range(num_epochs//5), loss_list)
    plt.xlabel('num_epochs')
    plt.ylabel('loss of LSTM Training')
    plt.show()
    return lstm_model

进行训练

input_size = train_x.shape[2]
hidden_size = 32
output_size = 1
num_layers = 3
num_epochs = 2000
lr=0.001
model = train(train_x,train_y,num_epochs,num_layers, input_size, hidden_size, output_size,lr)

请添加图片描述
定义测试函数

def test(model ,dataset_x):
    # 预测新数据
    #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)  # (batch_size, seq_size, feature_size)
    dataset_x = torch.from_numpy(dataset_x)
    print(dataset_x.shape)
    pred_test = model(dataset_x) # 全量训练集的模型输出 (batch_size, seq_size, output_size)
    print(pred_test.shape)
    pred_test = pred_test.view(-1).data.numpy()
    pred_test = np.concatenate((np.zeros(DAYS_FOR_TRAIN), pred_test))  # 填充0 使长度相同
    
    print(len(data_close))
    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')

测试

test(model ,dataset_x)

测试结果:
请添加图片描述

代码小结

敲代码过程中遇到的问题:

1、公式的简化版和完全版本的理解。在实现简化版本时,输入x和引状态h只需要在列方向上进行叠加,组成一个新的矩阵即可,这时[x,h]套用一个w和一个b即可。完全版中x和h有其单独的w,将x和h矩阵形状变化为相同的形状再相加,多了参数。

2、公式中参数w和b共用问题,同一层的w和b是共用的,但是不同层之间是不同的w、b参数。

3、lstm层的叠加,这点开始比较困惑,上一层的输出是下一层的输入,但是输出输入的矩阵格式不同,不清楚怎么转换,之后把除最后一层外的所有输出大小设置为hidden_size作为下一层输入,只把最后一层的输出大小设置为output_size。

4、实现bitch计算,开始时用for循环计算,时间太慢。所以把输入变下形状,由input_size=(batch_size,time_steps,input_size),变为由input_size=(time_steps,batch_size,input_size),循环time_steps,这样矩阵乘法就可以一次性计算batch_size个输入了,得到的outputs再将0维和1维转换回来,input_size=(batch_size,time_steps,input_size)

5、自己实现网络一定要注意nn.Parameter和nn.ModuleList,前者将参数注册到nn.Module,后者将模型注册到nn.Module。如果没有这两个,反向传播时参数不会更新!
训练过程中loss在收敛,但是收敛不到0附近,甚至最后收敛到的值很大,还以为是公式复现问题,原因其实是自己实现的模型和参数没有加nn.Parameter和nn.ModuleList,训练过程中loss在降,纯粹靠out = self.fc(out)这个全连接层在往回拉。。

总结

原理看着简单,但是自己动手实现的时候很多不理解的小细节就暴露出来了。本篇代码的实现是根据公式和通过查找资料互补实现的,有错误的地方欢迎大家指正。

股票预测部分,参考这位博主的博文LSTM-代码讲解(股票预测),十分详细

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值