该系列的Jupyter放在下方:
RNN入门(五)解决短期记忆问题
RNN的短期记忆问题
因为数据在流过RNN的时候会经过变换,所以每个时间步长都会丢失一定量的信息。一段时间以后,RNN的状态几乎没有任何输入的痕迹了。为了解决这个问题,引入了LSTM单元和GRU单元。
LSTM
优点:
- 把LSTM单元视作黑盒,则LSTM可以像基本单元一样使用,其性能比基本单元更好。
- 训练的时候收敛速度会更快
- 能检测数据中的长期依赖性。
使用方法:
在keras中,有两种方法调用LSTM单元:
使用LSTM层
model = keras.models.Sequential([ keras.layers.LSTM(20, return_sequences=True, input_shape=[None, 1]), keras.layers.LSTM(20, return_sequences=True), keras.layers.TimeDistributed(keras.layers.Dense(10)) ])
使用
keras.layers.RNN
层model = keras.models.Sequential([ keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True, input_shape=[None, 1]), keras.layers.RNN(keras.layers.LSTMCell(20), return_sequences=True), keras.layers.TimeDistributed(keras.layers.Dense(10)) ])
用LSTM训练上一节提到的时间序列预测模型。训练方法:在每个时间步长预测下一个10个值。这样损失会包含每个时间步长的RNN输出项,即有更多的误差梯度流经模型,使模型更加稳定。
# 用于计算上一轮的均方误差
def last_time_step_mse(Y_true, Y_pred):
return keras.metrics.mean_squared_error(Y_true[:, -1], Y_pred[:, -1])
np.random.seed(42)
n_steps = 50
series = generate_time_series(10000, n_steps + 10)
X_train = series[:7000, :n_steps]
X_valid = series[7000:9000, :n_steps]
X_test = series[9000:, :n_steps]
Y = np.empty((10000, n_steps, 10))
for step_ahead in range(1, 10 + 1):
Y[..., step_ahead - 1] = series[..., step_ahead:step_ahead + n_steps, 0]
Y_train = Y[:7000]
Y_valid = Y[7000:9000]
Y_test = Y[9000:]
np.random.seed(42)
tf.random.set_seed(42)
model = keras.models.Sequential([
keras.layers.LSTM(20, return_sequences=True, input_shape=[None, 1]),
keras.layers.LSTM(20, return_sequences=True),
keras.layers.TimeDistributed(keras.layers.Dense(10))
])
model.compile(loss="mse", optimizer="adam", metrics=[last_time_step_mse])
history = model.fit(X_train, Y_train, epochs=20,
validation_data=(X_valid, Y_valid))
上面的代码训练很快,用自己三年的笔记本1050Ti显卡,每轮差不多3秒就跑完了,训练速度远大于SimpleRNN。
到网上查了一下发现,这并不是LSTM收敛快,而是Tensorflow的历史遗留问题。我使用的是Tensorflow2.x版本,LSTM在版本迭代中被大幅度优化。而Simplef库因为不常用,其底层实现依然停留在Tensorflow1.x的版本,没有进行优化。如果使用Tensorflow1.x版本的LSTM,会发现他的训练速度比SimpleRNN要慢很多。
拟合时间序列结果如下:

LSTM原理
作为黑盒来看,LSTM单元的状态被分为两个向量:$h_{(t)}$和$c_{(t)}$。($c$代表cell,$h_{(t)}$可看做短期状态,$c_{(t)}$可看做长期状态)

LSTM单元图示,图源网络,侵删
LSTM的关键思想是网络可以学习长期状态下存储的内容、丢弃的内容和从中读取的内容。
长期状态$c_{(t)}$(图片靠上的一条横线):
- $c_{(t-1)}$从左到右遍历网络时,首先经过一个遗忘门丢掉一些记忆
- 再通过加法操作添加一下由输入门选择的记忆。
- 然后直接送$c_{(t)}$出来,无需任何变换。
因此在每个时间步长中,都会丢掉一些记忆并添加一些记忆。此外,在加法运算后,长期状态被复制,并通过$tanh$函数传输,其结果被输出门滤波,输出短期状态$h(t)$,$h(t)$等同于该时间步长的单元输出$y_{(t)}$。
门的运作方式
首先将输入向量$x_{(t)}$和上一个的短期状态$h_{(t-1)}$输入4个不同的全连接层。
$g_{(t)}$:分析当前输入$x_{(t)}$和上一个的短期状态$h_{(t-1)}$。
- 基本单元里,$g_{(t)}$直接作为$y_{(t)}$和$h_{(t)}$输出。
- LSTM单元里,$g_{(t)}$不直接输出,而是将自身最重要的部分存储在长期状态中。
$FC$:是门控制器,使用逻辑激活函数,所以输出范围是0-1。三个$FC$的输出送到各元素乘法计算。输出为0,则门关闭;输出为1,则门打开。
- 遗忘门$f_{(t)}$:控制长期状态里应该被删除的元素。
- 输入门$i_{(t)}$:控制$g_{(t)}$中应当添加进长期状态的元素。
- 输出门$o_{(t)}$:两个作用:(1)在该时间步长内,选择读取长期状态的那一部分。(2)在该时间步长内,选择输出输出长期状态的哪一部分到$h_{(t)}$和$y_{(t)}$。
总结
LSTM单元的优越性:
- 能够识别重要而定输入(通过输入门)
- 能够长期保留重要的输入(通过遗忘门)
- 并在需要时将其提取出来(通过输出门)
GRU
门控循环单元(Gated Recurrent Unit, GRU)是LSTM的简化版,性能也挺好的。主要进行了三方面简化:
- 两个状态向量合并为一个向量$h_{(t)}$。
- 单个门控制器$z_{(t)}$控制遗忘门和输入门。若门控制器输出1,则遗忘门打开($=1$),输入门关闭($1-1=0$)。输出0则相反。即:当必须要存储某些记忆时,对应位置上的原先记忆必须要被删除。
- 没有输出门,每个时间步长内,都输出完整的状态向量。
- 有一个新的门控制器$r_{(t)}$,用于选择上一状态中需要显示给主要层$g_{(t)}$的部分。
调用方式
Keras提供了keras.layers.GRU
层。
model = keras.models.Sequential([
keras.layers.GRU(20, return_sequences=True, input_shape=[None, 1]),
keras.layers.GRU(20, return_sequences=True),
keras.layers.TimeDistributed(keras.layers.Dense(10))
])
老问题:用GRU预测时间序列的最后10个输出(跟LSTM一样,每个时间步长内都有所有的RNN输出项流经模型):
评估均方误差:
model.evaluate(X_valid, Y_valid)
输出:
last_time_step_mse: 0.0103
损失曲线:

绘制预测值与真实值的比较:

可以看出GRU的预测更倾向于稳定。
Comments | NOTHING