上篇介绍了RNN循环神经网络,上篇在最后说明了RNN有梯度爆炸和梯度消失的问题,也就是说RNN无法处理长时间依赖性问题,本篇介绍的LSTM(长短时记忆网络)是应用最多的循环神经网络,当提到循环神经网络时一般都特指LSTM,如果以将RNN视为一种思想,那么LSTM是循环神经网络的具体实现。通过‘门’运算引入细胞状态的概念(Cell state),LSTM可以较好的利用历史记录信息。
一、lstm前向传播
lstm的模型类似于数字电路,lstm按时间维度展开后模型如下图所示:
lstm比起其他类型神经网络多出了一个‘门’的概念,在数字电路中通过'与门'、‘或门’、‘异或门’等有机结合可以组成具有复杂功能的电路,lstm借鉴了这种思想,只不过是通过软件实现这些门电路,在刘慈欣的小说《三体》中,牛顿和冯.诺依曼利用3000千万士兵组成一台有CPU、内存、硬盘的人肉电脑,这与lstm的设计思想其实有异曲同工之妙。
lstm中有满足不同需求的'与门',在神经网络中'与门'是一个值在0到1之间向量,门向量与具体信息一般是以按位相乘的方式运算(哈达玛积),当门向量元素值为0时代表抑制该位置的信息,而向量元素值为1时代表让该位置的信息通过,首先看下lstm中的遗忘门,我们用符号ft表示:
上图中σ代表sigmod函数,ht-1是上一个序列的输出,xt为本次输入,可以看出遗忘门其实一个简单全连接神经网络,该神经网络是训练出各种具有过滤功能'门',与此类似还有'输入门'it、'候选门'
:
有了这几个门之后就可以引入LTSM的核心:细胞状态Ct
每个时刻的细胞状态Ct分为两个部分,其中一部分有取舍的选择了上一次细胞状态值Ct-1,另外一部分来自本次的输入,更新完Ct后即可通过输出门ot得到此时的输出ht:
由于引入了数字电路模型形式,lstm的模型比起之前介绍神经网络稍微复杂些。lstm原理类似于中国古代的‘万年历’,‘万年历’是古代人记录的自然界规律信息,这与lstm需要解决的时间序列问题很相似,有了'万年历’后,根据最近已发生的现象即可按图索骥定位到'万年历’对应的部分,这样就可根据'万年历’预测接下来的走势。再引用一下《三体》小说里情节,小说中三体世界里的墨子总结出一套'万年历’,他通过长期观察并记录三个太阳的运动轨迹数据,结合已经发生事件即可预测'恒纪元'与'乱纪元'更迭。
有了以上类比后再来看lstm前向传播过程,输出ht代表了一个需要预测信息,而ht由输出门和Ct运算得到,Ct公式:
Ct其中包含历史信息的Ct-1和本次输入
,这与查找定位'万年历’过程是一致的:通过Ct-1和
定位到'万年历’相应的部分即可得到预测数据ht,输出门ot的作用是提取细胞状态主要信息后输出,ot增强了模型的非线性拟合能力。
再来看细胞状态Ct,Ct含上一次信息Ct-1,与此类似Ct-1含Ct-2的信息,递归的存在导致Ct的输入中含有0到t-1时刻的全部的细胞状态信息,这些累积的信息不一定对此时t时刻预测都有用处,对Ct-1信息应有适当的取舍,如同做英语完形填空时,通过语义、语境的分析后,空缺处单词与段落中几个单词有关系,而与另外语句、单词没有任何关系,lstm对信息的取舍是通过遗忘门ft来实现的,前面说过,包括遗忘门在内所有门本质是全连接神经网络,以遗忘门为例:
ht-1含有历史的输出信息,ht-1与xt合并为一个向量作为ft输入,可以理解ht-1与xt组成了一个t时刻上下文,类似于完形填空中空缺处的上下文语境,通过全连接神经网络训练后,ft知道如何选择性的利用上下文信息推导出预测值ht,输入门it的作用也一样,通过训练后,可以选择性提取输入信息的主要特征推导出预测值ht,输入门与输出门互相配合后更新细胞状态Ct。
以一个例子说明lstm的运行过程,下面代码利用lstm预测A股上证指数走势,选择70%的样本数据作为训练集用于学习lstm网络,剩下30%作为测试集验证模型正确率。
走势数据下载地址:上证指数走势数据
import osimport 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函数测试模型效果如下:
蓝色线是实际的上证指数走势,红色线是lstm的预测走势图,模型基本上模拟出了实际数据走势,如果利用GPU并增加迭代次数模型效果会更好一些。
二、lstm反向传播推导
首先列出lstm前向传播中公式组,包含四个门以及两个输出:
(1)为推导方便,对带激活函数的公式定义其输入部分,如
定义为遗忘门ft在t时刻输入:
可将权重矩阵Wf拆解为Wfh、Wfx两个矩阵,Wfh、Wfx分别与ht-1和xt相乘:
类似的,可以定义以下的输入公式组:
(2)
假设已知t时刻误差δt,由于lstm的输出ht没有使用激活函数,δt定义为:
根据链式求导法则,已知δt后上一时刻误差δt-1为:
(3)求δt-1核心是求出
,比起RNN,lstm求解这一层次梯度稍微复杂些,观察(1)公式组,ht等式右侧ot和Ct都含有ht-1,而Ct中ft、it、
都含有ht-1,逐项使用链式求导法则得: