算法学习笔记3:RNN和LSTM
学习参考:Tensorflow实战Google深度学习框架
1 循环神经网络简介
循环神经网络(RNN)在挖掘数据中的时序信息以及语义信息的深度表达能力很强,在语音识别、语言模型、机器翻译以及时序分析等方面应用广泛。
在全链接神经网络或者卷积神经网络中,网络结构都是从输入层到隐含层再到输出层,层与层之间是全链接或者部分链接的,但每层之间的节点是无链接的。RNN的来源就是为了刻画一个序列当前的输出与之前信息的关系的,从结构上,循环神经网络会记忆之前的信息,并利用之前的信息影响后面节点的输出。也就是说,RNN的隐藏层之间的节点是有链接的,隐藏层的输入不仅包括输入层的输出,还包括上一时刻的输出。
对于RNN,一个很重要的概念就是时刻,循环神经网络会对于每一个时刻的输入结合当前模型的状态给出一个输出。如下图,RNN的主题结构A的输出除了来自输入层Xt,还有一个循环的边来提供上一时刻的隐藏状态ht-1。在每一个时刻,循环神经网络在读取了Xt和ht-1后会生产新的隐藏状态ht,并产生本时刻的输出Ot。由于模块A中的运算和变量在不同的时刻是相同的,因此RNN理论上可以被看做是同一神经网络结构被无限复制的结果。正如卷积神经网络在不同的空间位置共享参数,RNN网络是在不同时间位置共享参数,从而能够适用有限的参数处理任意长度的序列。
从循环神经网络的结构特征可以很容易看出它最擅长解决与时间序列相关的问题。对于一个序列数据,可以将这个 序列上不同时刻的数据依次传入循环神经网络的输入层,而输出可以是对序列中下 一个时 刻的预测,也可以是对当前时刻信息的处理结果(比如语音识别结果) 。循环神经网络要求 每一个时刻都有一个输入,但是不一定每个时刻都需要有输出。
循环神经网络可以看作是同 一神经网络结构在时间序列上被复制多次 的结果,这个被复制多次的结构被称之为循环体。如何设计循环体的网络结构是循环神经 网络解决实际问题的关键。下图展示了一个最简单的循环体结构。这个循环体中只使用 了一个类似全链接层的神经网络结构 。
循环神经网络中的状态是通过一个向量来表示的,这个向量的维度也称为循环神经网 络隐藏层的大小,假设其为 n,假设输入向量的维度为 x ,循环体的全连接层神经 网络的输入大小为 n勺。也就是将上一时刻的状态与当前时刻的输入拼接成一个大的向量 作为循环体中神经网络的输入。因为该全连接层的输出为当前时刻的状态,于是输出层的 节点个数也为 n,循环体中的参数个数为 (n+x)×n+n 个
下图展示了一 个循环神经网络前向传播的具体计算过程。
假设状态的维度为 2,输入、输出的维度都为 1,而且循环体中的全连接层中权重为 :
偏置项的大小为brnn= [O.l,一0l],用于输出的全连接层权重为:
偏置项大小为 bo111put = 0.1 。 那么在时刻印,因为没有上 一 时刻,所以将状态初始化为 h;,,;1= [0,0],而当前的输入为 l,所以拼接得到的向量为[0,0,1],通过循环体中的全连接层 神经网络得到的结果为:
这个结果将作为下 一 时刻的输入状态,同时循环神经网络也会使用该状态生成输出。 将该向盘作为输入提供给用于输出的全连接神经网络可以得到 to时刻的最终输出:
使用 to时刻的状态可以类似地推导得出 t, 时刻的状态为 [0.860, 0.884],而 t1 时刻的输 出为 2.73。在得到循环神经网络的前向传播结果之后,可以和其他神经网络类似地定义损 失函数。循环神经网络唯 一 的区别在于因为它每个时刻都有 一个输出,所以循环神经网络 的总损失为所有时刻(或者部分时刻向 上的损失函数的总和。
循环神经网络前向传播的过程:
import numpy as np X = [1,2] state = [0.0,0.0] # 分开定义不同输入部分的权重 w_cell_state = np.asarray([[0.1,0.2],[0.3,0.4]]) w_cell_input = np.asarray([0.5,0.6]) b_cell = np.asarray([0.1,-0.1]) # 定义用于输出的全链接层参数 w_output = np.asarray([[1.0],[2.0]]) b_output = 0.1 # 按照时间顺序执行循环神经网络的前向传播过程 for i in range(len(X)): # 计算循环体中的全链接层神经网络 before_activation = np.dot(state, w_cell_state) + X[i]*w_cell_input + b_cell state = np.tanh(before_activation) # 根据当前状态计算最终输出 final_output = np.dot(state, w_output) + b_output # 输出每一个时刻的信息 print('before activation:',before_activation) print('state:', state) print('output:', final_output)
理论上循环神 经网络可以支持任意长度的序列,然而在实际训练过程中,如果序列过长, 一方面会导致优 化时出现梯度消散和梯度爆炸的问题,另一方面,展开后的前馈神经网络会占用过大的内存,所以实际中一般会规定一个最大长度,当序列长度超过规定长度之后会对序列进行截断。
2 长短时记忆网络(LSTM)
在复杂语言场景中 ,有用 信息的间隔有大有小、长 短不 一 ,循环神经网络的性能也会受到限制。长短时记忆网络( long short-term memory, LSTM)的设计就是为了解决这个问题.与单一 tanh 循环体结构不同 , LSTM 是一种拥有三个“门”结构的 特殊网络结构。
LSTM 靠一些 “门”的结构让信息有选择性地影响循环神经网络中每个时刻的状态。 所谓“门”的结构就是一个使用 sigmoid 神经网络和 一个按位做乘法的操作,这两个操作 合在一起就是一个“门”的结构 。
为了使循环神经网更有效的保存长期记忆,上图中“遗忘门"和“输入门” 至关重要,它们是 LSTM 结构的核心。“遗忘门”的作用是让循环神经网络“忘记”之前没有用的信息。“遗忘门”会根据当前的输入 X1和上一时刻输出 h,_J决定哪一部分记忆需要被遗 忘。假设状态 c 的维度为 n。“遗忘门”会根据当前的输入 x,和上一时刻输出 ht-1计算一个 维度为 n的向量f=sigmoid(W1x +W2h),它在每一维度上的值都在(0,1)范围内。再将上一 时刻的状态 Ci-I与f向量按位相乘,那么 f取值接近 0 的维度上的信息就会被“忘记”,而f 取值接近1的维度上的信息会被保留 。
在循环神经网络“忘记”了部分之前的状态后,它还需要从当前的输入补充最新的记忆。这个过程就是“输入门”完成的。
LSTM 结构在计算得到新的状态 C1后需要产生当前时刻的输出,这个过程是通过“输 出门”完成的。“输出门”会根据最新的状态 C1、上一时刻的输出 ht-1和当前的输入 Xt来决 定该时刻的输出。
在 TensorFlow 中 实现使用 LSTM 结构的循环神 经网络 的前向传播过程 。
# 定义一个 LSTM 结构 。在 TensorFlow 中通过一句简单的命令就可以实现一个完整 LSTM 结构 。 # LSTM 中使用的变量也会在该函数中自动被声明。 lstm = tf.nn.rnn cell.BasicLSTMCell(lstm hidden size) # 将 LSTM 中的状态初始化为全 0 数组。 BasicLSTMCell 类提供了 zero state 函数 # 来生成 #全零的初始状态 。 state 是一个包含两个张量的 LSTMStateTuple 类, # 其中 state.c 和 # state.h分别对应了图 8-7 中的 c状态和 h状态。 # 和其他神经网络类似,在优化循环神经网络时,每次也会使用一个 batch 的训练、样本。 # #以下代码中, batch size 给出了一个 batch 的大小。 state= lstm.zero state(batch size, tf.float32) # 定义损失函数 。 loss = 0.0 # 虽然在测试时循环神经网络可以处理任意长度的序列,但是在训练中为了将俯环网络展开成 # 前馈神经网络,我们需要知道训练数据的序列长度 。在以下代码巾,用 num_steps 米表示这个长度。 for i in range (num steps): # 在第一个时刻声明 LSTM结构中使用的变量, if i > 0: tf.get variable scope().reuse variables() # 每一步处理时间序列中的一个时刻。将当前输入 current_input (上图中的 Xt) # 和前一时刻状态 state (ht-l 和 Ct-1)传入定义的 LSTM 结构可以得到当前 LSTM的输出 # lstm_output (ht)和更新后状态 state (ht和 Ct)。 lstm_output 用于输出给其他层, # state用于输出给下一时刻,它们在 dropout等方面可以有不问的处理方式。 lstm一output, state = lstm(current_input, state) #将当前时刻 LSTM 结构的输出传入一个全连接层得到最后的输出 。 final_output = fully_connected(lstm_output) #计算当前时刻输出的损失 。 loss+= calc_loss(final output, expected output)
3、循环神经网络的变种
3.1 双向循环神经网络和深层循环神经网络
有些问题中, 当 前时刻的输出不仅和之前的状态有关系,也和之后的状态相关。这时就需要使用双 向循环 神经网络(bidirectionalRNN)来解决这类问题。双向循环神经网络是由两个独立的循环神经网络叠加在一起组成的 。 输出由这两个循环神经网 络的输出拼接而成。如下图
深层循环神经经网络( Deep RNN 〉是循环神经网络的另外 一种变种。为了增强模型的表 达能力,可以在网络中设置多个循环层,将每层循环网络的输出传给下一层进行处理。
3.2 循环神经网络的dropout
循环神经网络一般只在不同层循环 体结构之间使用 dropout,而不在同一层的循环体结构之间使用。 也就是说从时刻 t-1 传递 到时刻 t时,循环神经网络不会进行状态的 dropout;而在同一个时刻 t 中,不同层循环体 之间会使用 dropout。
实线箭头表示不使用dropout,虚线表示使用dropout。
4、循环神经网络应用
利用循环神经网络实现对函数sinx取值的预测。
因为循环神经网络模型预测的是离散时刻的取值 , 所以在程序中需要将连续的 sin 函数曲线离散化 。 所谓离散化就是在一个给定的区间 [O, MAX]内,通过有限个采样点模拟一个连续的曲线 。 比如在以下程序中每隔SAMPLE ITERVAL对sin函数进行一次来样, 来样得到的序列就是sin 函数离散化之后的结果 。 以下程序为预测离散化之后的 sin 函数 。
import numpy as np import tensorflow as tf import matplotlib as mpl mpl.use('TKAgg') from matplotlib import pyplot as plt HIDDEN_SIZE = 30 # LSTM中隐藏节点的个数 NUM_LAYERS = 20 # LSTM的层数 TIMESTEPS = 10 # 循环神经网络的训练序列长度 TRAINING_STEPS = 10000 # 循环次数 BATCH_SIZE = 32 # batch大小 TRAINING_EXAMPLES = 10000 # 训练数据个数 TESTING_EXAMPLES = 1000 # 测试数据个数 SAMPLE_GAP = 0.01 # 采样间隔 def gennerate_data(seq): X = [] y = [] # 序列的第i项和后面的TIMESTEPS-1项合在一起作为输入; # 第i+timesteps项作为输出。即用sin函数前面的timesteps个点的信息,预测第i+timesteps个点的函数值 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) ]) # 使用TensorFlow接口将多层的LSIM结构链接成RNN网络并计算其前向传播结果 outputs, _ = tf.nn.dynamic_rnn(cell, X, dtype= tf.float32) # outputs;在顶层LSTM在每一步的输出结果,它的维度是[batch size, time,HIDDEN SIZE] output = outputs[:,-1,:] # 对LSTM网络的的输出再做一层全链接层并计算损失,这里的损失为平均损失 predictions = tf.contrib.layers.fully_connected(output,1,activation_fn=None) if not is_training: return predictions,None,None # 计算损失函数 loss = tf.losses.mean_squared_error(labels=y,predictions=predictions) # 创建模型优化器并得到优化步骤 train_op = tf.contrib.layers.optimize_loss( loss, tf.train.get_global_step(),optimizer="Adagrad",learning_rate=0.1 ) return predictions,loss,train_op def train(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() # 调用模型得到计算结果,这里不需要输入真实的y值 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) # 对预测的sin函数曲线进行绘图 plt.figure() plt.plot(predictions,label='predictions') plt.plot(labels,label='real_sin') plt.legend() plt.show() # 用正弦函数生成训练集和测试集 # numpy. linspace FJ(1数可以创建一个等差序列的数组 test_start = (TRAINING_EXAMPLES + TIMESTEPS) * SAMPLE_GAP test_end = test_start + (TESTING_EXAMPLES + TIMESTEPS) * SAMPLE_GAP train_X, train_y = gennerate_data(np.sin(np.linspace( 0, test_start, TRAINING_EXAMPLES + TIMESTEPS, dtype=np.float32 ))) test_X,test_y = gennerate_data(np.sin(np.linspace( test_start, test_end, TESTING_EXAMPLES+TIMESTEPS,dtype=np.float32 ))) with tf.Session() as sess: train(sess, train_X,train_y) run_eval(sess, test_X,test_y)
运行程序会得到以下输出:
不过这里预测失败了,暂时还没弄明白那里出了问题,记录下