LSTM and GRU

介绍循环神经网络结构单元 LSTM 和 GRU

循环神经网络(RNN)

原理介绍

循环神经网络的主要用途是处理和预测序列数据,所以比较适合语音序列数据,也就是和自然语言处理相结合。在之前的全连接网络和卷积神经网络中,网络结构都是从输入层到隐层到输出层,层与层之间是连接的,但是每层之间的节点是无连接的。在自然语言处理中,预测句子的下一个单词是什么,一般需要用到当前的单词和之前的单词通过语义进行预测。循环神经网络就是为了刻画一个序列当前的输出和之前信息的关系。从网络结构上说,循环神经网络会记忆之前的信息,并利用之前的信息影响后面节点的输出。

在每一个时刻t,循环神经网络会针对该时刻的输出结合当前模型的状态给出一个输出,并更新模型的状态。对于循环神经网络的主体A,输入除了来自输入层$X_t$, 还有上一个时刻的隐藏状态(hidden state)$c_{t-1}$。A在读取了$X_t$ 和$c_{t-1}$之后会生成新的隐藏状态$c_{t}$,并产生本时刻的输出$h_t$。

循环神经网络可以看作是同一个神经网络结构被无限复制的结果。如果我们说卷积神经网络是在不同的空间位置上共享参数,那么循环神经网络就是在不同时间位置共享参数。循环神经网络对长度为N的序列上展开之后,可以被视作一个有N个中间层的前馈神经网络。(下图来自colah’s blog)

传统RNN使用隐藏状态,也就是一个向量来表示目前状态,这个向量的维度也被称为循环神经网络隐藏层的大小,假设其为n。假设输入向量的维度为x(也就是$X_t$的向量大小),那么每个A中全连接层神经网络的输入大小为n+x。因为该全连接层的输出为当前时刻的状态,所以也为n长度的向量,所以循环体中的参数个数为$(n+x)\times n + n$。注意这时候输出的向量长度为n,这个代表的是当前时刻的状态,但是我们的输入是x长度,输出值也应该保持一致是x长度的向量,这一步通过一个额外的全连接神经网络完成转化,这和卷积神经网络中最后的全连接层的意义是一样的。类似的,不同时刻用于输出的全连接神经网络中的参数也是一致的。同时由于循环神经网络每时每刻都有一个输出,所以最后的总损失是所有时刻(或者规定的部分时刻)上的损失函数的总和。

但是传统RNN中对于一个循环体A,里面的隐藏层大小n是固定的,也就是隐藏状态的长度表示是固定的,意味着每次都会记忆之前n长度的值。以语言预测为例子,有些时候推测下一个单词可能需要结合很久之前的语义,n需要特别大,这就带来了长期依赖的问题(long-term dependencies)问题。但在有些简单的情况下,推测下一个单词仅仅需要前几个单词就够了。在复杂的语言场景中,有用信息的间隔有大有小、长短不一,不够灵活的传统循环神经网络的表现会受到限制。

长短时记忆网络(LSTM)

这时候一个重要的结构,长短时记忆网络(long short-term memory, LSTM)被提出来优化RNN, 对于这部分可以参考

colah’s blog.

其余参考链接

结合medium上一篇文章的图,捋一捋LSTM中的结构和输入输出,这篇博客其实讲的很清楚。

上图是一个LSTM单元的完整的构造,这幅图比较侧重于表述流程,也比较容易理解。LSTM主要有三个门结构,遗忘门和输入门和输出门,使得它能够根据上一个时间点的memory信息和当前的输入有选择性的构建出当前时间点的memory信息和输出值。门结构是一个使用单层神经网络(sigmoid函数作激活值)和一个按位乘法的操作,因为sigmoid激活函数的值在0-1之间,相当于描述了当前输入有多少信息量可以通过这个结构。

首选介绍遗忘门,下图就是流程图中属于遗忘门的部分。遗忘门的作用是让循环神经网络“忘记”之前没有用信息。它根据当前的输入$X_t$和上一个时刻的输出$h_{t-1}$计算一个维度为n的向量 $f = sigmoid(W_f[h_{t-1},x_t]) + b0$ ,它在每一个维度上的值都在(0,1)范围内。这边图中的 + 是联结,就是把$X_t$和$h_{t-1}$拼接成一个更长的向量。之后再将上一个时刻的memory状态$c_{t-1}$ 与 $f$ 向量按位相乘,那么$f$取值接近0的维度上的信息就会被忘记,而取值接近1的维度上的信息会被保留。

forgetdoor

其次是输入门,在循环神经网络“忘记”了部分之前的状态后,它还需要从当前的输入补充最新的记忆。这个过程就是输入门完成的。值得注意的输入门和遗忘门都是以sigmoid为激活函数,但是需要写入的新memory信息是有另一个单独的单层神经网络生成的,并且使用tanh作为激活函数。之后这两个向量进行按位相乘来决定新生成的memory有多少需要添加。

inputdoor

在这两部分计算完成后我们就可以计算出$c_t$的值。这边的+是按位加操作。

这时候我们已经计算出了此刻新的memory数值$c_t$, 需要计算新的输出值是什么,这部分需要输出门进行计算。输出门决定刚刚计算出的新memory信息有多少需要被输出到下一个LSTM单元。

inputdoor

LSTM的TensorFlow实现和其他演变模型

LSTM的TensorFlow实现

在TensorFlow中,LSTM结构可以被很简单地实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#定义一个LSTM结构
lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_hidden_size)

#将LSTM中的状态初始化为全0的数组。BasicLSTMCell类提供了 zero_state 函数来生成全零的初始状态。
#state是一个包含两个张量的LSTMStateTurple类,其中state.c 和 state.h 分别对应上文的 c 状态 和 h 状态。

state = lstm.zero_state(batch_size, tf.float32)

#定义损失函数

loss = 0.0

#用num_steps来表示循环深度,将循环神经网络展开成n层的前馈神经网络。

for i in range(nums_steps):
if i > 0: tf.get_variable_scope().reuse_variables()

lstm_output, state = lstm(current_input, state)
final_output = fully_connected(lstm_output)

loss += calc_loss(final_output, expected_output)

具体会在后面一个详细的例子里展示。

双向循环神经网络和深层循环神经网络

在传统RNN中,都是从前向后传输状态,也就是预测下文的时候会用到上文的信息。但实际上就像翻译的时候我们会结合上下文,一些问题中,当前时刻的输出不仅仅需要根据前文来判断,也需要根据后面的内容。这时候就需要双向循环神经网络(bidirectional RNN)。

双向循环网络的最终输出是这两个单向循环神经网络的输出的简单拼接。两个循环神经网络除了方向不同,其余结构完全对称。每一层网络中的循环体可以自由选择结构,如RNN或者LSTM。

深层循环神经网络(Deep RNN)的概念也很简单,之前描述的LSTM和传统RNN结构中,基本上每个小结构内部都只设置了一层全连接层,这是一个很浅的网络,特征提取能力并不强。深度循环神经网络就是加深了这部分网络的深度。

在TensorFlow中,提供了MultiRNNCell类来实现深层循环神经网络的前向传播过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#定义一个基本的LSTM结构最为循环体的基本结构。

lstm_cell = tf.nn.rnn_cell.BasicLSTMCell

#通过MultiRNNCell类实现深层循环神经网络中每一时刻的前向传播,number_of_layers代表深度

stacked_lstm = tf.nn.rnn_cell.MultiRNNCell(
[lstm_cell(lstm_size) for _ in range(number_of_layers)])

state = stacked_lstm.zero_state(batch_size, tf.float32)

for i in range(nums_steps):
if i > 0: tf.get_variable_scope().reuse_variables()

stacked_lstm_output, state = stacked_lstm(current_input, state)
final_output = fully_connected(stacked_lstm_output)

loss += calc_loss(final_output, expected_output)

RNN的dropout

dropout一般只在全连接层使用,对于深度循环神经网络来讲,从时刻t-1传递到时刻t时,深度循环神经网络不会进行状态的dropout;在同一时刻t中,不同循环体之间会使用dropout。也就是dropout只会用在非循环链接上,即下图的虚线。粗线是LSTM中使用了dropout之后一个可能的信息流。

在TensorFlow中,使用tf.nn.rnn_cell.DropoutWrapper类实现dropout的功能。

1
2
3
4
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell

stacked_lstm = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.DropoutWrapper(lstm_cell(lstm_size)) for _ in range(number_of_layers)])

RNN例子

书中利用LSTM来预测sin的数值

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import tensorflow as tf
import numpy as np

import matplotlib as mpl
mpl.use('TkAgg')
from matplotlib import pyplot as plt

HIDDEN_SIZE = 30 #lstm中隐藏节点个数
NUM_LAYERS =2 #lstm的层数

TIMESTEPS =10; #RNN训练序列长度
TRAINING_STEPS = 10000 #训练轮数
BATCH_SIZE= 32 #batch大小

TRAINING_EXAMPLES = 10000
TESTING_EXAMPLES =1000
SAMPLE_GAP = 0.01 #采样间隔

def generate_data(seq):
X=[]
y=[]
for i in range(len(seq)-TIMESTEPS):
X.append([seq[i:i+TIMESTEPS]])
y.append([seq[i+TIMESTEPS]])
return np.array(X,dtype=np.float32),np.array(y,dtype=np.float32)

def lstm_model(X,y,is_training):
#建立多层lstm
cell = tf.nn.rnn_cell.MultiRNNCell([
tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) for _ in range(NUM_LAYERS)
])
outputs,_ = tf.nn.dynamic_rnn(cell,X,dtype = tf.float32) #outputs 输出维度是[batch_size,time,hidden_size]
output = outputs[:,-1,:]

#再加一层全连接层
predictions = tf.layers.dense(output,1,activation=None)

if not is_training:
return predictions,None,None

loss = tf.losses.mean_squared_error(labels=y,predictions=predictions)

op = tf.train.AdamOptimizer(learning_rate=0.1)
train_op = op.minimize(loss,tf.train.get_global_step())
return predictions,loss,train_op

def trains(sess,train_X,train_y):
ds = tf.data.Dataset.from_tensor_slices((train_X,train_y))
ds = ds.repeat().shuffle(1000).batch(BATCH_SIZE)
X,y = ds.make_one_shot_iterator().get_next()

with tf.variable_scope("model"):
predictions,loss,train_op = lstm_model(X,y,True)

sess.run(tf.global_variables_initializer())
for i in range(TRAINING_STEPS):
_,l = sess.run([train_op,loss])
if i % 100 == 0:
print("train step: " + str(i)+ ", loss: "+str(l))

def run_eval(sess,test_X,test_y):
ds = tf.data.Dataset.from_tensor_slices((test_X, test_y))
ds = ds.batch(1)
X, y = ds.make_one_shot_iterator().get_next()

with tf.variable_scope("model",reuse=True):
prediction,_,_ = lstm_model(X,[0.0],False)

predictions =[]
labels = []
for i in range(TESTING_EXAMPLES):
p,l = sess.run([prediction,y])
predictions.append(p)
labels.append(l)

#计算rmse作为评价指标
predictions = np.array(predictions).squeeze()
labels = np.array(labels).squeeze()
rmse = np.sqrt(((predictions-labels)**2).mean(axis=0))
print("Mean Square Error is : %f"%rmse)

plt.figure()
plt.plot(predictions,label = "predictions")#labels="predictions"
plt.plot(labels,label='labels')
plt.legend()
plt.show()

def MAIN():
test_start = (TRAINING_EXAMPLES+TIMESTEPS)*SAMPLE_GAP
test_end = test_start+(TESTING_EXAMPLES+TIMESTEPS)*SAMPLE_GAP
train_X,train_y=generate_data(np.sin(np.linspace(0,test_start,TRAINING_EXAMPLES+TIMESTEPS,dtype=np.float32)))
test_X, test_y = generate_data(np.sin(np.linspace(test_start, test_end, TESTING_EXAMPLES + TIMESTEPS, dtype=np.float32)))

with tf.Session() as sess:
trains(sess,train_X,train_y)
run_eval(sess,test_X,test_y)

if __name__ == '__main__':
MAIN()

可以看到结果预测拟合的很好: