LSTM-循环神经网络(下)

    上篇介绍了RNN循环神经网络,上篇在最后说明了RNN有梯度爆炸和梯度消失的问题,也就是说RNN无法处理长时间依赖性问题,本篇介绍的LSTM(长短时记忆网络)是应用最多的循环神经网络,当提到循环神经网络时一般都特指LSTM,如果以将RNN视为一种思想,那么LSTM是循环神经网络的具体实现。通过‘门’运算引入细胞状态的概念(Cell state),LSTM可以较好的利用历史记录信息。

一、lstm前向传播

    lstm的模型类似于数字电路,lstm按时间维度展开后模型如下图所示:

ltsm.png

lstm比起其他类型神经网络多出了一个‘门’的概念,在数字电路中通过'与门'、‘或门、‘异或门’等有机结合可以组成具有复杂功能的电路,lstm借鉴了这种思想,只不过是通过软件实现这些门电路,在刘慈欣的小说《三体》中,牛顿和冯.诺依曼利用3000千万士兵组成一台有CPU、内存、硬盘的人肉电脑,这与lstm的设计思想其实有异曲同工之妙。

    lstm中有满足不同需求的'与门',在神经网络中'与门'是一个值在0到1之间向量,门向量与具体信息一般是以按位相乘的方式运算(哈达玛积),当门向量元素值为0时代表抑制该位置的信息,而向量元素值为1时代表让该位置的信息通过,首先看下lstm中的遗忘门,我们用符号ft表示:

遗忘们.png

上图中σ代表sigmod函数,ht-1是上一个序列的输出,xt为本次输入,可以看出遗忘门其实一个简单全连接神经网络,该神经网络是训练出各种具有过滤功能'门',与此类似还有'输入门'it、'候选门'ctt.png:

输入门.png

有了这几个门之后就可以引入LTSM的核心:细胞状态Ct

细胞状态.png

每个时刻的细胞状态Ct分为两个部分,其中一部分有取舍的选择了上一次细胞状态值Ct-1,另外一部分来自本次的输入,更新完Ct后即可通过输出门ot得到此时的输出ht

输出.png

    由于引入了数字电路模型形式,lstm的模型比起之前介绍神经网络稍微复杂些。lstm原理类似于中国古代的‘万年历’,‘万年历’是古代人记录的自然界规律信息,这与lstm需要解决的时间序列问题很相似,有了'万年历’后,根据最近已发生的现象即可按图索骥定位到'万年历’对应的部分,这样就可根据'万年历’预测接下来的走势。再引用一下《三体》小说里情节,小说中三体世界里的墨子总结出一套'万年历’,他通过长期观察并记录三个太阳的运动轨迹数据,结合已经发生事件即可预测'恒纪元'与'乱纪元'更迭。

    有了以上类比后再来看lstm前向传播过程,输出ht代表了一个需要预测信息,而ht由输出门和Ct运算得到,Ct公式:

输出门1.png

Ct其中包含历史信息的Ct-1和本次输入ctt.png,这与查找定位'万年历’过程是一致的:通过Ct-1ctt.png定位到'万年历’相应的部分即可得到预测数据ht,输出门ot的作用是提取细胞状态主要信息后输出,ot增强了模型的非线性拟合能力。

    再来看细胞状态CtCt含上一次信息Ct-1,与此类似Ct-1Ct-2的信息,递归的存在导致Ct的输入中含有0到t-1时刻的全部的细胞状态信息,这些累积的信息不一定对此时t时刻预测都有用处,对Ct-1信息应有适当的取舍,如同做英语完形填空时,通过语义、语境的分析后,空缺处单词与段落中几个单词有关系,而与另外语句、单词没有任何关系,lstm对信息的取舍是通过遗忘门ft来实现的,前面说过,包括遗忘门在内所有门本质是全连接神经网络,以遗忘门为例:

ft.png

ht-1含有历史的输出信息,ht-1与xt合并为一个向量作为ft输入,可以理解ht-1与xt组成了一个t时刻上下文,类似于完形填空中空缺处的上下文语境,通过全连接神经网络训练后,ft知道如何选择性的利用上下文信息推导出预测值ht,输入门it的作用也一样,通过训练后,可以选择性提取输入信息的主要特征推导出预测值ht,输入门与输出门互相配合后更新细胞状态Ct。

    以一个例子说明lstm的运行过程,下面代码利用lstm预测A股上证指数走势,选择70%的样本数据作为训练集用于学习lstm网络,剩下30%作为测试集验证模型正确率。

走势数据下载地址:上证指数走势数据

import os
import numpy as np
import matplotlib.pyplot as plt
import torch
from torch import nn, optim
import  sys
import json
import pandas as pd
dataPath = 'dataset'
savePath='model/lstmmodel.pkl'
datainterval=3

class lstm(nn.Module):
    def __init__(self, input_size , hidden_size , output_size , num_layer,dropout=0.5 ):
        super(lstm, self).__init__()
        self.layer1 = nn.LSTM(input_size, hidden_size, num_layer  )
        self.layer2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.layer1(x)
        s, b, h = x.size()
        x = x.view(s * b, h)
        x = self.layer2(x)
        x = x.view(s, b, -1)
        return x
#默认以前三天数据作为输入,第4天数据作为标签,interval为输入天数
def create_dataset(dataset, interval=3):
    dataX, dataY = [], []
    for i in range(len(dataset) - interval):
        a = dataset[i:(i + interval)]
        dataX.append(a)
        dataY.append(dataset[i + interval])
    return np.array(dataX), np.array(dataY)

def loaddata( ):
    dataset =loadjson()
    dataset = dataset.astype('float32')
    max_value = np.max(dataset)
    min_value = np.min(dataset)
    scalar = max_value - min_value
    dataset = list(map(lambda x: (x-min_value)  / scalar, dataset))  # 归一化
    data_X, data_Y = create_dataset(dataset,datainterval)
    #选择70%作为训练集用于学习模型,30%数据作为测试集
    train_size = int(len(data_X) * 0.7)
    train_X,train_Y,test_X, test_Y= data_X[:train_size],data_Y[:train_size],data_X[train_size:],data_Y[train_size:]

    train_X = train_X.reshape(-1, 1, datainterval)
    train_Y = train_Y.reshape(-1, 1, 1)
    test_X = test_X.reshape(-1, 1, datainterval)
    test_Y = test_Y.reshape(-1, 1, 1)

    train_x = torch.from_numpy(train_X)
    train_y = torch.from_numpy(train_Y)
    test_x = torch.from_numpy(test_X)
    test_y = torch.from_numpy(test_Y)
    return train_x, train_y, test_x, test_y

def train(train_x, train_y):
    model = lstm(datainterval, 10, 1, 2)
    model.train()
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
    iternum=20000
    for e in range(iternum):
        # 前向传播
        out = model(train_x)
        loss = criterion(out, train_y)
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (e + 1) % 100 == 0:  # 每 100 次输出结果
            print('迭代数: {}, 损失值: {:.6f}'.format(e + 1, loss.data.item()),end='\r',flush=True)
    print('\r')
    torch.save(model, savePath)

def test(test_x, test_y,modefilepath=savePath):
    model = torch.load(modefilepath)
    model.eval()
    pred_test = model(test_x)  # 测试集的预测结果
    # 改变输出的格式
    pred_test = pred_test.view(-1).data.numpy()
    test_y = test_y.view(-1).data.numpy()
    plt.plot(test_y ,color='blue',label='predict')
    plt.plot(pred_test, color='red',label='real')
    plt.legend(['real','predict'])
    plt.show()

def loadjson():
    with open("dataset/stock.txt", "r") as f:  # 打开文件
        data = f.read()  # 读取文件
        stocks=json.loads(data)
        trades= np.array(stocks['data']['sh000001']['day'])
        trades= [float(x) for x  in trades[:,[2]]]
        return  np.array(trades).reshape(-1,1)

if __name__=='__main__':
    #选择当前3天的数据作为输入,预测第4天走势
    datainterval=3
    train_x, train_y, test_x, test_y = loaddata()
    train(train_x, train_y)
    test(test_x, test_y  )

上述代码是利用CPU运算训练模型,训练时间较长,有条件的读者可将示例代码改为GPU模式运行,训练得到lstm模型后,利用test函数测试模型效果如下:

1616160975145029165.png

蓝色线是实际的上证指数走势,红色线是lstm的预测走势图,模型基本上模拟出了实际数据走势,如果利用GPU并增加迭代次数模型效果会更好一些。

二、lstm反向传播推导

首先列出lstm前向传播中公式组,包含四个门以及两个输出:

公式组.png (1)

为推导方便,对带激活函数的公式定义其输入部分,如遗忘门输出.png定义为遗忘门ft在t时刻输入:

遗忘门输入1.png

输入遗忘门.png

可将权重矩阵Wf拆解为Wfh、Wfx两个矩阵,Wfh、Wfx分别与ht-1和xt相乘:

遗忘门输入2.png

类似的,可以定义以下的输入公式组:

公式组2.png(2)

假设已知t时刻误差δt,由于lstm的输出ht没有使用激活函数,δt定义为

t时刻误差.png

根据链式求导法则,已知δt后上一时刻误差δt-1

t-1.png    (3)

求δt-1核心是求出天天.png,比起RNN,lstm求解这一层次梯度稍微复杂些,观察(1)公式组,ht等式右侧ot和Ct都含有ht-1,而Ct中ft、itctt.png都含有ht-1,逐项使用链式求导法则得:

lht2.png

-请登陆后阅读余下文章-
登录|注册
上一篇  RNN-循环神经网络(上) 下一篇 生成对抗网络
评论区