LSTM网络

[TOC]

LSTM的网络结构

  • 上面是一层LSTM网络的结构,我们把它称为一个Cell。上面的图只是展示了不同时刻这个Cell的状态,但本质上还是同一个Cell,同一个Cell在不同时刻共享权值。
  • 我们可以把每个小黄框看成一个神经网络,输入会经过小黄框,转化为新的数据输出。num_units指定了每个小黄框的输出维度,如num_units=50,则神经网络输出维度为50。
  • 可以看到这个Cell里面有两根线,上面这根线代表长时记忆,下面这根线代表短时记忆
  • 长时记忆先对元素点乘,即忘记了某些东西(也保留了某些东西);后面又经过一个对元素的加法,即学到了某些东西,体现了基于当前输入对远距离记忆的忘记和保留。
  • 短时记忆主要在第四个小黄框之后,对长时记忆的输出又进行了一次元素点乘,加强了当前时刻输入的影响。

遗忘门

  • $x_t$代表t时刻的输入,$h_{t-1}$代表t-1时刻的短时输出(也叫隐藏状态),两者会进行简单的连接,形成新的向量。如$x_t$是个8维的向量,而$h_{t-1}$是个50维的向量,则拼接后新的向量为58维的向量。
  • 新的向量会输入到小黄框代表的神经网络中,激活该网络的50个神经元(num_units=50时),然后经由sigmoid函数作用后,映射到[0, 1]的范围内,作为$f_t$输出。(顺便计算一个这个小黄框需要训练的参数,每个神经元对58维向量的每个维度都有一个权重,因此每个神经元有58个权重,外加一个偏置,那么50个神经元则要训练50*59个参数)
  • $f_t$这个向量中每个值都为0~1之间的数,与$C_{t-1}$(t-1时刻的长时输出)做元素的点乘,即控制了$C_{t-1}$每个维度数据的通过与否。0代表了完全不通过(遗忘),1代表完全通过(保留)。

输入门

  • 输入门首先要理解的是:$i_t$和遗忘门的$f_t$虽然计算公式一样,但两者其实是独立的,是由不同的神经网络训练出来的结果。
  • 遗忘门中sigmoid函数的作用是选择性地获取长时记忆$C_{t-1}$中的信息,而这时的sigmoid函数的作用是选择性的获取隐藏状态$h_{t-1}$和输入$x_t$中的信息。

输出门

  • 同样地,这里的$o_t$和$i_t$、$f_t$也是独立的,这里的sigmoid函数的作用是选择性地获取当前时刻$C_t$中的信息。

激活函数

可以看出,三个门中都有sigmoid函数,sigmoid函数就是把数据映射为[0, 1]的范围,来控制其他数据的通过程度,这就是”门”的作用。生成候选记忆的时候才会用到tanh,把数据映射到[-1, 1]的以0为均值的范围。

Keras

LSTM的输入数据维度是形如(batch, timesteps, feature)的3D张量。

  • 举个简单的例子:下图是北京5年的空气质量数据,我们的目的是通过空气质量数据(有8列)预测PM2.5值。当我们指定timesteps=3时,即表示我们会取三行数据作为一个样本,如红框所示,完成第一次取样,该样本的标签为181。然后红框向下移动一行,如绿框所示,完成第二次取样,该样本标签为138。通过不断地滑动窗口来不断取样,而timesteps指定了这个窗口的高,feature指定了这个窗口的宽,也代表着我们用过去三小时的8个特征来预测未来一小时的PM2.5的值。
  • 还不明白的可参考Keras中的输入输出,上面举的例子对应着Multiple Input那种情况。

PM2.5预测例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pandas as pd
from datetime import datetime
from sklearn.preprocessing import LabelEncoder
pd.set_option('display.width',1000)
# 读入数据
df_ds = pd.read_csv("beijingPM2.5.csv")
# 删除"No"列
df_ds.drop('No', axis=1, inplace=True)
# 修改日期类型
def parser(x): # 传入series,返回datetime
str_date = str(x[0])+' '+str(x[1])+' '+str(x[2])+' '+str(x[3])
return datetime.strptime(str_date, "%Y %m %d %H")
df_ds['datetime'] = df_ds[['year', 'month', 'day', 'hour']].apply(lambda x: parser(x), axis=1)
# 删除年月日时列
df_ds.drop(['year', 'month', 'day', 'hour'], axis=1, inplace=True)
# 以时间为索引
df_ds.index = df_ds['datetime']
df_ds.drop('datetime', axis=1, inplace=True)
# 给风向编码
encoder = LabelEncoder()
df_ds['cbwd'] = encoder.fit_transform(df_ds['cbwd'])
# 除去nan值
print("去除nan之前:", df_ds.shape) # 去除nan之前: (43824, 8)
df_ds.dropna(inplace=True)
print("去除nan之后:", df_ds.shape) # 去除nan之后: (41757, 8)
1
2
3
4
5
6
7
8
9
10
11
12
13
from keras.models import Sequential
from keras.layers import Dense, LSTM
timesteps = 3
# 搭建模型
model = Sequential()
model.add(LSTM(
output_dim=50, # 输出维度为50
input_shape=(timesteps, 8), # 特征为8
activation='relu'
))
model.add(Dense(1))
model.compile(loss='mae', optimizer='adam')
model.summary()

1
2
3
4
5
6
7
8
9
10
# 特征标签分别归一化
# 特征归一化
from sklearn.preprocessing import MinMaxScaler
ds_x = df_ds[df_ds.columns[:]].values
scaler_X = MinMaxScaler(feature_range=(0, 1))
ds_x_scaled = scaler_X.fit_transform(ds_x)
# 标签归一化
ds_y = df_ds[df_ds.columns[0]].values.reshape((-1, 1))
scaler_y = MinMaxScaler(feature_range=(0, 1))
ds_y_scaled = scaler_y.fit_transform(ds_y)
1
2
3
4
5
6
7
8
# 划分数据集
n_train_hours = 365*24*3
train_X = ds_x_scaled[:n_train_hours, :]
train_y = ds_y_scaled[:n_train_hours, :][timesteps:, :]
test_X = ds_x_scaled[n_train_hours:, :]
test_y = ds_y_scaled[n_train_hours:, :][timesteps:, :]
print('训练X:', train_X.shape, '训练y:', train_y.shape) # 训练X: (26280, 8) 训练y: (26277, 1)
print('测试X:', test_X.shape, '测试y:', test_y.shape) # 测试X: (15477, 8) 测试y: (15474, 1)
1
2
3
4
5
6
7
8
# 将特征转为LSTM的输入格式
import numpy as np
def parse(ds):
samples = []
for i in range(ds.shape[0]-timesteps):
samples.append(ds[i:i+timesteps, :].tolist())
samples = np.array(samples)
return samples
1
2
3
# 训练模型
train_X = parse(train_X)
history = model.fit(train_X, train_y, epochs=20, batch_size=32, validation_split=0.2, verbose=2, shuffle=False)
1
2
3
4
5
6
7
import matplotlib.pyplot as plt
%matplotlib inline
# 训练集loss变化
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['train', 'validation'])
plt.savefig('loss.jpg', dpi=800)

1
2
3
4
5
6
7
import numpy as np
# 使用测试集预测
test_X = parse(test_X)
predict_y = model.predict(test_X)
# 反归一化
actual_y = scaler_y.inverse_transform(test_y)
predict_y = scaler_y.inverse_transform(predict_y)
1
2
3
4
5
6
7
8
# 计算训练集RMSE
rmse = np.sqrt(np.mean((actual_y[:-1]-predict_y[1:])**2)) # 预测值滞后一步
print("Test RMSE: {:.3f}".format(rmse)) # Test RMSE: 10.295
# 画出预测曲线与实际曲线
plt.plot(actual_y[:100], 'r')
plt.plot(predict_y[1:101], 'g--')
plt.legend(['actual', 'predict'])
plt.savefig('result3h.jpg', dpi=800)