RNN入门(五)LSTM与GRU


该系列的Jupyter放在下方:

原码点击下载

RNN入门(五)解决短期记忆问题

RNN的短期记忆问题

因为数据在流过RNN的时候会经过变换,所以每个时间步长都会丢失一定量的信息。一段时间以后,RNN的状态几乎没有任何输入的痕迹了。为了解决这个问题,引入了LSTM单元和GRU单元。

LSTM

优点:

  • 把LSTM单元视作黑盒,则LSTM可以像基本单元一样使用,其性能比基本单元更好。
  • 训练的时候收敛速度会更快
  • 能检测数据中的长期依赖性。

使用方法:
在keras中,有两种方法调用LSTM单元:

  1. 使用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))
    ])
  2. 使用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)}$。

门的运作方式

  1. 首先将输入向量$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的简化版,性能也挺好的。主要进行了三方面简化:

  1. 两个状态向量合并为一个向量$h_{(t)}$。
  2. 单个门控制器$z_{(t)}$控制遗忘门和输入门。若门控制器输出1,则遗忘门打开($=1$),输入门关闭($1-1=0$)。输出0则相反。即:当必须要存储某些记忆时,对应位置上的原先记忆必须要被删除。
  3. 没有输出门,每个时间步长内,都输出完整的状态向量。
  4. 有一个新的门控制器$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的预测更倾向于稳定。

声明:奋斗小刘|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - RNN入门(五)LSTM与GRU


Make Everyday Count