1、LSTM简介
在时间序列数据学习中,传统的循环神经网络(RNN)存在较多的学习瓶颈和技术缺陷,而长短时记忆(LSTM)神经网络克服了循环神经网络的缺陷,使其在长时间序列数据学习训练中能克服梯度爆炸和梯度消失的瓶颈,展现出超强的长系列数据学习能力。
2、LSTM原理
基础的 RNN 网络结构,LSTM 新增了一个状态向量𝑪𝑡,同时引入了门控(Gate)机制,通过门控单元来控制信息的遗忘和刷新。
在 LSTM 中,有两个状态向量𝒄和h,其中𝒄作为 LSTM 的内部状态向量,可以理解为LSTM 的内存状态向量 Memory,而h表示 LSTM 的输出向量。相对于RNN,LSTM 把内部 Memory 和输出分开为两个变量,同时利用三个门控:输入门(Input Gate)、遗忘门(Forget Gate)和输出门(Output Gate)来控制内部信息的流动。
门控机制可以理解为控制数据流通量的一种手段,在 LSTM 中,阀门开和程度利用门控值向量𝒈表示。通过𝜎(𝒈)激活函数将门控制压缩到[0,1]之间区间,当𝜎(𝒈) = 0时,门控全部关闭,输出𝒐 = 0;当𝜎(𝒈) = 1时,门控全部打开,输出𝒐 = 𝒙。通过门控机制可以较好地控制数据的流量程度。
.
2.1 遗忘门
遗忘门作用于 LSTM 状态向量𝒄上面,用于控制上一个时间戳的记忆 𝒄𝑡−1 对当前时间戳的影响。遗忘门的控制变量 𝒈𝑓 由格式产生:
其中 𝑾𝑓 和 𝒃𝑓 为遗忘门的参数张量,可由反向传播算法自动优化,𝜎为激活函数,一般使用 Sigmoid 函数。当门控 𝒈𝑓 = 1 时,遗忘门全部打开,LSTM 接受上一个状态 𝒄𝑡−1的所有信息;当门控 𝒈𝑓 = 0 时,遗忘门关闭,LSTM 直接忽略 𝒄𝑡−1,输出为 0的向量。这也是遗忘门的名字由来。经过遗忘门后,LSTM 的状态向量变为 𝒈𝑓 𝒄t−1。
.
2.2 输入门
输入门用于控制 LSTM 对输入的接收程度。首先通过对当前时间戳的输入 𝒙𝑡 和上一个时间戳的输出 h𝑡−1 做非线性变换得到新的输入向量
𝒄
t
~
\widetilde{𝒄_t}
ct
:
其中 𝑾𝑐 和 𝒃𝑐 为输入门的参数,需要通过反向传播算法自动优化,tanh 为激活函数,用于将输入标准化到[−1,1]区间。
𝒄
t
~
\widetilde{𝒄_t}
ct
并不会全部刷新进入 LSTM 的 Memory,而是通过输入门控制接受输入的量。输入门的控制变量同样来自于输入 𝒙𝑡 和输出 h𝑡−1:
其中 𝑾𝑖 和 𝒃𝑖 为输入门的参数,需要通过反向传播算法自动优化,𝜎为激活函数,一般使用Sigmoid 函数。输入门控制变量 𝒈𝑖 决定了 LSTM 对当前时间戳的新输入
𝒄
t
~
\widetilde{𝒄_t}
ct
的接受程度:当 𝒈𝑖 = 0时,LSTM 不接受任何的新输入
𝒄
t
~
\widetilde{𝒄_t}
ct
;当 𝒈𝑖 = 1时,LSTM 全部接受新输入
𝒄
t
~
\widetilde{𝒄_t}
ct
。经过输入门后,待写入 Memory 的向量为 𝒈𝑖
𝒄
t
~
\widetilde{𝒄_t}
ct
。
.
2.3 更新门
更新门用于刷新 Memory。在遗忘门和输入门的控制下,LSTM 有选择地读取了上一个时间戳的记忆 𝒄𝑡−1 和当前时间戳的新输入
𝒄
t
~
\widetilde{𝒄_t}
ct
,状态向量 𝒄𝑡 的刷新方式为:
得到的新状态向量 𝒄𝑡 即为当前时间戳的状态向量。
2.4 输出门
LSTM 的内部状态向量 𝒄𝑡 并不会直接用于输出,简单的 RNN 不一样。RNN 网络的状态向量 h 既用于记忆,又用于输出,所以 RNN 可以理解为状态向量 𝒄 和输出向量 h 是同一个对象。在 LSTM 内部,状态向量并不会全部输出,而是在输出门的作用下有选择地输出。输出门的门控变量 𝒈𝑜 为:
其中 𝑾𝑜 和 𝒃𝑜 为输出门的参数,同样需要通过反向传播算法自动优化,𝜎为激活函数,一般使用 Sigmoid 函数。当输出门 𝒈𝑜 = 0时,输出关闭,LSTM 的内部记忆完全被隔断,无法用作输出,此时输出为 0 的向量;当输出门 𝒈𝑜 = 1时,输出完全打开,LSTM 的状态向量 𝒄𝑡全部用于输出。LSTM 的输出由:
产生,即内存向量 𝒄𝑡 经过 tanh 激活函数后与输入门作用,得到 LSTM 的输出。由于𝒈𝑜 ∈[0,1],tanh(𝒄t) ∈ [−1,1],因此 LSTM 的输出 h𝑡 ∈ [−1,1]。
3、LSTM 情感分类问题实战
首先是 Cell 方式。LSTM 网络的状态 List 共有两个,需要分别初始化各层的 h 和 𝒄 向量。例如:
self.state0 = [tf.zeros([batchsz, units]),tf.zeros([batchsz, units])]
self.state1 = [tf.zeros([batchsz, units]),tf.zeros([batchsz, units])]
并将模型修改为 LSTMCell 模型。代码如下:
self.rnn_cell0 = layers.LSTMCell(units, dropout=0.5)
self.rnn_cell1 = layers.LSTMCell(units, dropout=0.5)
其它代码不需要修改即可运行。对于层方式,只需要修改网络模型一处即可,修改如下:
# 构建 RNN,换成 LSTM 类即可
self.rnn = keras.Sequential([
layers.LSTM(units, dropout=0.5, return_sequences=True),
layers.LSTM(units, dropout=0.5)
])
层方式完整代码
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Sequential, layers, optimizers, losses
import numpy as np
# 加载数据
# 批大小
batchsize = 128
# 词汇表大小
total_words = 10000
# 句子最大长度s,大于的句子部分将截断,小于的将填充
max_review_len = 80
# 词向量特征长度n
embedding_len = 100
# 加载IMDB数据集,此处的数据采用数字编码,一个数字代表一个单词
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
# 截断和填充句子,使得等长,此处长句子保留句子后面的部分,短句子在前面填充
x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len)
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len)
# 构建数据集,打散,批量,并丢掉最后一个不够batchsize的batch
db_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
db_train = db_train.shuffle(1000).batch(batchsize, drop_remainder=True)
db_test = tf.data.Dataset.from_tensor_slices((x_test, y_test))
db_test = db_test.batch(batchsize, drop_remainder=True)
# 网络模型
class MyRNN(keras.Model):
# Cell方式构建多层网络
def __init__(self, units):
super(MyRNN, self).__init__()
# [b, 64], 构建Cell初始化状态向量,重复使用
self.state0 = [tf.zeros([batchsize, units]), tf.zeros([batchsize, units])]
self.state1 = [tf.zeros([batchsize, units]), tf.zeros([batchsize, units])]
# 词向量编码[b, 80] => [b, 80, 100]
self.embedding = layers.Embedding(total_words, embedding_len, input_length=max_review_len)
# 构建RNN
self.rnn = keras.Sequential([
layers.LSTM(units, dropout=0.5, return_sequences=True),
layers.LSTM(units, dropout=0.5)
])
# 构建分类网络,用于将CELL的输出特征进行分类,2分类
# [b, 80, 100] => [b, 64] => [b, 1]
self.outlayer = Sequential([
layers.Dense(units),
layers.Dropout(rate=0.5),
layers.ReLU(),
layers.Dense(1)])
def call(self, inputs, training=None):
# [b, 80]
x = inputs
# 获取词向量:[b, 80] => [b, 80, 100]
x = self.embedding(x)
# 通过2个RNN CELL,[b, 80, 100] => [b, 64]
x = self.rnn(x)
# 末层最后一个输出作为分类网络的输入:[b, 64] => [b, 1]
x = self.outlayer(x, training)
# 通过激活函数,p(y is pos|x)
prob = tf.sigmoid(x)
return prob
def main():
units = 64
epochs = 20
model = MyRNN(units)
# 装配
model.compile(optimizer=optimizers.Adam(1e-3),
loss=losses.BinaryCrossentropy(), metrics=['accuracy'],
experimental_run_tf_function=False)
# 训练和验证
model.fit(db_train, epochs=epochs, validation_data=db_test)
# 测试
model.evaluate(db_test)
if __name__ == '__main__':
main()