一、LSTM发展背景
LSTM (Long Short Term Memory Network)首先是由Hochreiter & Schmidhuber(引用文献[Hochreiter, S, and J. Schmidhuber. “Long short-term memory.” Neural Computation 9.8(1997):1735-1780])于1997年提出,经过了若干代学者(Felix Gers, Fred Cummins, Santiago Fernandez, Justin Bayer, Daan Wierstra, Julian Togelius, Faustino Gomez, Matteo Gagliolo, and Alex Gloves)的发展,由此形成了现在比较系统且完整的LSTM框架;本文所介绍的就是目前深度学习时代的LSTM,它不仅仅在处理序列数据上表现优异,而且能够解决普通循环神经网络(RNN)难以解决的长期依赖问题。
二、LSTM基本结构与原理
2.1 基本原理概述
其实LSTM的基本原理,说复杂确实复杂,说简单也挺简单,可以把它分成三个部分去理解:
A:这两个是LSTM的宏观结构(其实就是T个BP神经网络沿着时间轴T排列,然后隐藏层之间的状态(权重参数)可以传递)
1.垂直于时间轴方向的LSTM网络:在每一个时间步上都有一个由输入层--隐藏层--输出层组成的BP神经网络(好吧它其实就是一个简单的BP神经网络啦!)
2.沿时间轴方向的历史信息传递渠道:由隐藏层传递的隐藏层状态h(权重参数矩阵)(跟RNN一样的啦)和细胞状态c(权重参数矩阵)(这就是LSTM的特殊之处啦)(这一步就是LSTM这种循环神经网络能处理长期依赖的时间序列数据的原因之一)
B:这个是LSTM的单元结构
3.单个LSTM单元的结构:LSTM的最关键的功能全在这个小小的单元体内(也就是隐藏层的神经元里),这才是LSTM的精华之所在
下面我们就可以分别研究一下这三个部分:
2.2 LSTM的整体结构(宏观上看)
由下图LSTM的结构图可知:每一个time_steps上都有一个BP神经网络,由输入层+隐藏层+输出层构成,这也就是LSTM结构的关键部分了,我们结合代码进行剖析一下结构:详情可以看注释
这里的模型构建过程,用的是keras构建,这个我之前也不懂,参考的是这位大佬的博客:(已经注明来处,欢迎大家自行前往学习)
LSTM 网络结构剖析(Keras) - 知乎 (zhihu.com)
import tensorflow as tf
from tensorflow import keras
from keras.optimizers import Adam
# 双层单尾lstm
def model_tLSTM_h2(units):
model = keras.Sequential() #此处便是先建立一个空的Sequential模型,我们将在其中逐步添加层
#Input Layer
#这里就是利用keras的layers函数构建模型层
#keras的好处在于可以清晰显式的定义模型的结构以及各层的激活函数,输入输出格式,以及各种信息
model.add(keras.layers.LSTM( #你问我为啥这里用LSTM层,下面又是Dense层,我只能跟你说,
#LSTM跟普通ANN的区别就是这个layers.LSTM,LSTM那单元体的精华全在里面了
#普通ANN自然就是直接Dense层就好啦!
units=units, #定义此层的神经元熟数量
activation="relu", #定义激活函数,不定义就是默认"tanh",建议是要根据自己的需求和实验结果来定义
return_sequences=True, #默认"False",设置True意为此层会输出每个时间步的隐藏状态,这对于堆叠 LSTM 层是必要的
#大概意思就是,注意!!!假如你下一层还是LSTM层!为了保证层堆叠,这一层的每一个时间步都会输出作为下一个LSTM层的输入。
#这对于多层LSTM来说非常重要,将一个LSTM层连接到另一个LSTM层,确保信息可以在不同的时间步之间传递。
input_shape=(X_train.shape[1], X_train.shape[2]) #定义输入数据的格式(times_step, fetures),
#在这里X_train的数据格式是tensor, (n_samples, times_step, feture)
#所以X_train.shape[0]就是n_samples, 以此类推
#第一层定义了后面的就不需要定义了,它自己的会根据前面的输出形状来调整输入形状
))
model.add(keras.layers.Dropout(0.2)) #这里是定义了一个Dropout层,丢弃20%的神经元,防止过拟合用的
#Hidden Layer
#同理利用keras的layers进行构建模型层
model.add(keras.layers.LSTM(
units=units, #定义此层的神经元熟数量
activation="relu", #定义激活函数,不定义就是默认"tanh",建议是要根据自己的需求和实验结果来定义
return_sequences=False, #默认"False", 仅返回最后一个时间步的输出,这里因为下一层就是dense输出层了,Dense层期望接收一个固定大小的输入,
#而不是一个完整的序列,这样可以将其直接连接到下一层的Dense层。
))
model.add(keras.layers.Dropout(0.2)) #这里是定义了一个Dropout层,丢弃20%的神经元,防止过拟合用的
#Output Layer
#这里因为我的期望输出每一个times_steps是输出一个值(stress),所以我用一个Dense层,也就是最普通的神经网络层来接受这个输入,
#并直接输出我需要的那最后一个时间步的预测值即可
model.add(keras.layers.Dense(unit=1)) #这里设置的unit=1,是因为这个我用dense层作为输出层,直接就是输出一维的预测连续值,
#并且没有设置激活函数,这个是因为考虑我需要的是连续预测值,类似回归的效果,使用线性激活函数(即默认即可)
#如果是二分类问题,可以加一个sigmoid函数,多分类问题可以加一个softmax函数,具体的可以自行了解
model.compile(loss='mse', optimizer=Adam(learning_rate=0.0001, clipvalue = 0.2)) #这里就是将模型编译构建起来,
#损失函数为mse,用于回归问题比较多,
#优化器为 Adam 优化器,学习率为 0.0001,
#并使用梯度裁剪阈值为0.2来控制梯度的大小。
model.summary() #展示模型结构概要:包括每一层的名称、输出形状以及可训练参数的数量。
return model
以上代码是定义两层LSTM+一层Dense的LSTM模型结构,大概可以看看下面这个示意图,有一个直观的了解,具体有哪个参数不会,可以再细致去看(注意就是,千万不要乱搞,养成一个习惯,搭建完模型后要搞一个模型结构概要检查一下!所有的参数都要细致学习一遍其功能,并做好笔记记录!否则,就是懵懵逼逼的在那瞎调参数,不充分了解该模型的原理以及结构以及代码参数详解,你又怎么能去使用它呢?切记啊切记!)
然后从上图中我们也可以看到的是在LSTM随时间步(time steps)推进的同时,隐藏层(hidden layers)在推进方向上的输出不仅仅只有隐藏层状态h(此time steps的隐藏层权重向量),还有一个细胞状态c;而这里的细胞状态c便起着存储历史有效信息的作用。 这也就是LSTM能够处理长期依赖的序列数据的原因,具有长期记忆的原因啦!
下面我就来解释剖析一下这个神奇的单元体!
2.3 LSTM的单元体结构(微观上看)
下图就是LSTM单元体的内部结构示意图,可以看到的是:
细胞状态c(cell)类似一个存储历史信息的储存库,由三个门控机制来控制其信息的输入输出。这三个门分别是遗忘门(forget gate)、输入门(input gate)、输出门(output gate);大概简单化一下就是类似这个结构:
那么这三个开关是怎样在算法中实现的呢?这就用到了门(gate)的概念。门实际上就是一层全连接层,它的输入是一个向量,输出是一个0到1之间的实数向量。假设W是门的权重向量,b是偏置项,那么门可以表示为:
门的使用,就是用门的输出向量按元素乘以我们需要控制的那个向量。因为门的输出是0到1之间的实数向量,那么,当门输出为0时,任何向量与之相乘都会得到0向量,这就相当于啥都不能通过;输出为1时,任何向量与之相乘都不会有任何改变,这就相当于啥都可以通过。因为δ(也就是sigmoid函数)的值域是(0,1),所以门的状态都是半开半闭的。
在LSTM中,用两个门来控制单元状态c的内容,一个是遗忘门(forget gate),它决定了上一时刻的单元状态有多少保留到当前时刻ct;另一个是输入门(input gate),它决定了当前时刻网络的输入xt有多少保存到单元状态ct。LSTM则用输出门(output gate)来控制单元状态ct有多少输出到LSTM的当前输出值ht。所以说,两个门决定Ct的内容,一个门决定Ct能输出的内容,这两个概念可不一样喔!
2.3.1 单元体内部结构--动态图理解
1.遗忘门(forget gate)的作用就是决定上一时刻的隐藏层状态有多少能保留到当前时刻也就是 (保留过去的回忆);
2.输入门(input gate)的作用就是决定当前时刻网络的输入有多少能保存到单元状态 (珍惜现在的经历);输出门(output gate)则是用来控制当前时刻的单元状态 有多少可以保存到LSTM此刻的隐藏层状态 中(留点希望给未来)。
3.细胞状态cell更新(上一时刻的Ct-1结合遗忘门的点乘以及输入门的相加更新为现在时刻的Ct)
4.输出门(output gate)则是用来控制当前时刻的单元状态Ct有多少可以保存到LSTM此刻的隐藏层状态ht中(留点希望给未来)。
2.3.2 单元体内部结构--公式推导理解
下面就来逐步用数学公式图解推导这三条门以及LSTM单元体内的细节:
首先给出单元体内的公式:(其实就是典型的神经网络的输出:输出=激活函数(权重*[输入]))
其中:,,,分别表示当前时刻的遗忘门(forget)、输入门(input)、细胞状态(cell)、输出门(output)、隐藏单元向量(hidden);对应的W下标啥的就是啥的权重参数(补充一下:权重参数也就是大部分神经网络类模型用于学习的最重要参数,搞来搞去不就是为了一个学到位训到位调到位的权重矩阵参数)。
以上公式汇总为一张图:(这里引用的是某个大佬写的图解LSTM——一文吃透LSTM-CSDN博客,写的非常的好,已经注明来处,欢迎大家去源博客里面看!)
进行公式的拆分:
所以啊,显然遗忘门和输出门就是做点乘的门控操作,乘0就是遗忘它不要它/乘1就是输出它你去吧你去吧嘿嘿(遗忘门控制之前时刻的不必要的数据,输出门控制Ct能输出的数据);只有人家输入门就是玩真正的信息融合的将此时此刻的输入x和上一时刻的输出ht-1,处理一下然后相加融合在一起,perfect!
OKOK,到这里,我相信大家都已经能理解LSTM单元体内部结构的运行机制和底层数学逻辑了!wowowowowowo!
三、LSTM模型构建以及训练代码
在给出代码之前,我还想跟大家讨论一下LSTM的数据结构众所周知,LSTM的输入数据是3D的tensor(关于tensor是什么大家可以详细见这位博主的博客:笔记 | 什么是张量(tensor)& 深度学习 - 知乎 (zhihu.com)
总之lstm的数据格式是像下面这张图一样的:(n_samples,time_steps,features);由样本数量,时间步长度,样本特征数组成的一个3D的tensor。显然也容易想到,LSTM是如何逐步向前学习的啦。
为了方面大家理解,我用spyder里面的变量浏览器再给大家看看代码里面LSTM的数据结构:很显然,我的输入数据X全是三维的tensor,以x_test为例:样本数量为142336,时间步数为10,特征数为15,构成了我的lstm网络的输入数据,要预测的数据y我就是一维的tensor啦,因为我的预测结果就是一串数据啦。
关于lstm如何推进,我打开x_test数据结构大家看看就知道了:大家可以看到左下角圈红的地方,x_test的shape就是(142356,10,15)的三维tensor,轴可选0/1/2就是以这三个维度谁为轴展示该数据(毕竟三维数据在这里只能平面展开啦!);旁边那个就是在此轴展开数据的索引,这里是以样本数量为轴的,索引0就是第一个输入数据,1就是第二个输入数据,2就是第三个输入数据,可以很明显看到:对于lstm,每一个输入数据(也就是每一段时间步(时间步不是时刻!)的输入就是下面这样的数据,输入每一段时间步的BP网络里面,然后再继续向前推进,一次推一个时刻,以此类推,从而达到学习整个序列数据的能力,怎么样,是不是非常nice哈哈哈。
下面做了一个LSTM推进的输入数据结构示意图,希望大家能看懂吧!(我的测试数据比较特殊,前几百行数据只有第一列是不一样的,所以大家只能看到随着索引检索,只有第一列的数据在逐步推进变化)
2024-04-14 10-01-57 - Trim
这里的话呢,上面我已经详细给出了利用keras进行模型构建的代码,这里的话简单一点,补充一个训练代码就好:
import numpy as np
from numpy import savetxt
import pandas as pd
from pandas.plotting import register_matplotlib_converters
from pylab import rcParams
import matplotlib.pyplot as plt
from matplotlib import rc
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.metrics import r2_score
import tensorflow as tf
from tensorflow import keras
from keras.optimizers import Adam,RMSprop
import os
import tensorflow as tf
tf.config.set_visible_devices(tf.config.list_physical_devices('GPU'), 'GPU')
register_matplotlib_converters()
#plt.rcParams["font.sans-serif"] = [""]# 指定默认字体
pd.set_option('display.max_columns', None) # 结果显示所有列
pd.set_option('display.max_rows', None) # 结果显示所行行
#>>>>>>>>>>>>数据预处理
#1.训练集(New-train)数据处理
source = 'New-train.csv'
df_train = pd.read_csv(source, index_col=None)
df_train = df_train[['设置你的数据表头']]
source = 'New-test.csv'
df_test = pd.read_csv(source, index_col=None)
df_test = df_test[['设置你的数据表头']]
train_size = int(len(df_train))
test_size = int(len(df_test))
train = df_train.iloc[0:train_size]
test = df_test.iloc[0:test_size]
print(len(train), len(test))
def training_data(X, y, time_steps=1):
Xs, ys = [], []
for i in range(len(X) - time_steps):
v = X.iloc[i:(i + time_steps)].values
Xs.append(v)
ys.append(y.iloc[i + time_steps])
return np.array(Xs), np.array(ys)
time_steps = 10
X_train, y_train = training_data(train.loc[:, '设置你的数据表头'], train.*, time_steps)
x_test, y_test = training_data(test.loc[:,'设置你的数据表头'], test.*, time_steps)
#构建模型
def model_vLSTM_h2(units):
model = keras.Sequential()
model.add(keras.layers.LSTM(units=units, activation="relu", return_sequences=True,
input_shape=(X_train.shape[1], X_train.shape[2])))
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.LSTM(units=units, activation="relu")
model.add(keras.layers.Dropout(0.2))
model.add(keras.layers.Dense(1))#这行代码是在Keras模型中添加一个具有1个神经元的Dense层,用于生成单个连续的输出值(strain)。
model.compile(loss='mean_squared_error', optimizer=Adam(learning_rate=0.0001, clipvalue = 0.2))
return model
#训练模型
def fit_model(model):
#早停机制,防止过拟合
early_stop = keras.callbacks.EarlyStopping(
monitor='val_loss',
min_delta=0.0,#min_delta=0.0 表示如果训练过程中的指标没有发生任何改善,即使改善非常微小,也会被视为没有显著改善
patience=2000)#表示如果在连续的 2000 个 epoch 中,指标没有超过 min_delta 的改善,训练将被提前停止
history = model.fit( # 在调用model.fit()方法时,模型会根据训练数据进行参数更新,并在训练过程中逐渐优化模型的性能
X_train, y_train, # 当训练完成后,模型的参数就被更新为训练过程中得到的最优值
epochs=400, # 此时model已经是fit之后的model,直接model.predict即可(千万不要model=model.fit(),然后再model.predict)
validation_split=0.1,
batch_size=12600,
shuffle=False,
callbacks=[early_stop])
return history
LSTM = model_vLSTM_h2(64)
#这里只是写了训练模型的代码,预测的话要根据自己的数据结构以及想要的效果来写喔