长短期记忆(LSTM)-tensorflow代码实现

作者:Jason
时间:2017.10.17

    长短期记忆(LSTM)神经网络是之前讲的RNN的一种升级版本,我们先来聊聊RNN的弊端。

RNN的弊端

《长短期记忆(LSTM)-tensorflow代码实现》

之前我们说过, RNN 是在有顺序的数据上进行学习的. 为了记住这些数据, RNN 会像人一样产生对先前发生事件的记忆. 不过一般形式的 RNN 就像一个老爷爷, 有时候比较健忘. 为什么会这样呢?

《长短期记忆(LSTM)-tensorflow代码实现》

想像现在有这样一个 RNN, 他的输入值是一句话: ‘我今天要做红烧排骨, 首先要准备排骨, 然后…., 最后美味的一道菜就出锅了’, shua ~ 说着说着就流口水了. 现在请 RNN 来分析, 我今天做的到底是什么菜呢. RNN可能会给出“辣子鸡”这个答案. 由于判断失误, RNN就要开始学习 这个长序列 X 和 ‘红烧排骨’ 的关系 , 而RNN需要的关键信息 ”红烧排骨”却出现在句子开头.

只需当权重w稍微小于1时(反向传播的误差越来越小,
梯度弥散

《长短期记忆(LSTM)-tensorflow代码实现》

只需当权重w稍微大于1时(反向传播的误差越来越大,
梯度爆炸

《长短期记忆(LSTM)-tensorflow代码实现》

具体example:

图1.1

《长短期记忆(LSTM)-tensorflow代码实现》

  如上图,所示普通的RNN情况下,再来看看 RNN是怎样学习的吧. 红烧排骨这个信息原的记忆要进过长途跋涉才能抵达最后一个时间点. 然后我们得到误差, 而且在 反向传递 得到的误差的时候, 他在每一步都会 乘以一个自己的参数 W. 如果这个 W 是一个小于1 的数, 比如0.99. 这个0.99 不断乘以误差, 误差传到初始时间点也会是一个接近于零的数, 所以对于初始时刻, 误差相当于就消失了. 我们把这个问题叫做梯度消失或者梯度弥散 Gradient vanishing. 反之如果 W 是一个大于1 的数, 比如1.01 不断累乘, 则到最后变成了无穷大的数, RNN被这无穷大的数撑死了, 这种情况我们叫做剃度爆炸(Gradient exploding)。
  如图1.1所示当依次输入1,0,0,0时,当W=0.99时,序列很长的情况下,最终的值为W的999次方,约等于0,远远小于1,LOSS/w=梯度也会变得特别小,当W=1.01时,序列很长的情况下,最终的值为W的999次方,约等于+00,远远大于1,LOSS/w=梯度也会变得特别大 。
  这就是普通 RNN 没有办法回忆起久远记忆的原因.

RNN —>LSTM

《长短期记忆(LSTM)-tensorflow代码实现》

LSTM 就是为了解决这个问题而诞生的. LSTM 和普通 RNN 相比, 多出了三个控制器. (输入控制, 输出控制, 忘记控制). 现在, LSTM RNN 内部的情况是这样.

他多了一个 控制全局的记忆, 我们用粗线代替. 为了方便理解, 我们把粗线想象成电影或游戏当中的 主线剧情. 而原本的 RNN 体系就是 分线剧情. 三个控制器都是在原始的 RNN 体系上, 我们先看 输入方面 , 如果此时的分线剧情对于剧终结果十分重要, 输入控制就会将这个分线剧情按重要程度 写入主线剧情 进行分析. 再看 忘记方面, 如果此时的分线剧情更改了我们对之前剧情的想法, 那么忘记控制就会将之前的某些主线剧情忘记, 按比例替换成现在的新剧情. 所以 主线剧情的更新就取决于输入 和忘记 控制. 最后的输出方面, 输出控制会基于目前的主线剧情和分线剧情判断要输出的到底是什么.基于这些控制机制, LSTM 就像延缓记忆衰退的良药, 可以带来更好的结果.

具体事例
根据paper里面给出的公式:

《长短期记忆(LSTM)-tensorflow代码实现》

  如上图所示,有4个输入端,1个输出端,明显看出那些门(gate)所控制的方式就是乘上原先RNN上的值就可以达到控制输入,控制储存在cell的state和输出值了。
  正如上面所提到的那样,RNN问题就出在W不断连乘积的问题上,所以我们要避免W不断相乘,所以我们把memory复制到下一阶段的方式改成不断相加的方式,同时forget gate大多数情况下是接近1的(偏置设大些),允许add来避免y过小。来解决这一个问题。而梯度爆炸不是个严重的问题,我们可以通过clip(梯度)来限制梯度过大。
接下来我们来看看最简单的LSTM的演算过程

《长短期记忆(LSTM)-tensorflow代码实现》

  如上图所示:4个输入端都输入一个3维的元素,偏置项的权重为1——[3,1,0,1],分别与他们相对应的门的权重参数相乘,然后再经过激活函数(f表示sigmod激活,h表示线性激活),sigmod激活值属于0到1之间。分别得到logits值为3,90,-10,110,通过激活函数激活得到3,~1,0,~1,(~表示约等于)。然后开始演算一下过程,3×1等于3—>3+0x1=3—>3×0=0。所以输入[3,1,0]时,最终得到0。依次类推,依次输入[4,1,0],[2,0,0],[1,0,1],[3,-1,0],最终得到的序列[0,0,0,7,0]
  所以LSTM的可训练参数是RNN的4倍

实际上:LSTM的输入值应该还包含了上一时期的cell状态值和隐藏层输出值

《长短期记忆(LSTM)-tensorflow代码实现》

LSTM的变体—GRU
GRU:通过将输入门和忘记门联动起来合并成一个门,意思就是旧的不去,新的不来,为了理解GRU的设计思想,我们再一次运用“三次简化一张图”的方法来进行分析:

《长短期记忆(LSTM)-tensorflow代码实现》

– 第一次简化: 忽略门控单元z, r的来源。

– 第二次简化: 考虑一维情况。   

– 第三次简化: 各门控单元0/1输出。这里和LSTM略有不同的地方在于,GRU需要引入一个”单刀双掷开关”。

《长短期记忆(LSTM)-tensorflow代码实现》

  与LSTM相比,GRU将输入门it和遗忘门ft融合成单一的更新门zt,并且融合了记忆单元ct和隐层单元ht,所以结构上比LSTM更简单一些。根据这张图,我们可以对GRU的各单元作用进行分析:
  重置门rtrt用于控制前一时刻隐层单元ht-1对当前词xt的影响。如果ht-1对xt不重要,即从当前词xt开始表述了新的意思,与上文无关, 那么rt开关可以打开, 使得ht-1对xt不产生影响。
  更新门ztzt用于决定是否忽略当前词xt。类似于LSTM中的输入门it, zt可以判断当前词xt对整体意思的表达是否重要。当zt开关接通下面的支路时,我们将忽略当前词xt,同时构成了从ht-1到ht的”短路连接”,这梯度得已有效地反向传播。和LSTM相同,这种短路机制有效地缓解了梯度消失现象, 这个机制于highwaynetworks十分相似。
  这样一来可以减少模型的可训练参数,提高模型的鲁棒性。
  

  
tensorflow代码实现LSTM

""" 用自己创建的 sin 曲线预测一条 cos 曲线 PS:深度学习中经常看到epoch、 iteration和batchsize,下面按自己的理解说说这三个的区别: (1)batchsize:批大小。在深度学习中,一般采用SGD训练,即每次训练在训练集中取batchsize个样本训练; (2)iteration:1个iteration等于使用batchsize个样本训练一次; (3)epoch:1个epoch等于使用训练集中的全部样本训练一次; 举个例子,训练集有1000个样本,batchsize=10,那么: 训练完整个样本集需要: 100次iteration,1次epoch。 """
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# 定义超参数
BATCH_START = 0     # 建立 batch data 时候的 index
TIME_STEPS = 20     # time_steps也就是n_steps,等于序列的长度
BATCH_SIZE = 50     # 批次的大小
INPUT_SIZE = 1      # sin 数据输入 size
OUTPUT_SIZE = 1     # cos 数据输出 size
CELL_SIZE = 10      # 隐藏层规模
LR = 0.006          # 学习率

# 数据生成
# 生成一个批次大小的数据的 get_batch function:
def get_batch():
    global BATCH_START, TIME_STEPS
    # xs的shape是(50batch,20steps)
    xs = np.arange (BATCH_START, BATCH_START + TIME_STEPS * BATCH_SIZE).reshape((BATCH_SIZE , TIME_STEPS))/(10*np.pi)#定义x
    seq=np.sin(xs)
    res=np.cos(xs)
    BATCH_START+=TIME_STEPS
    #返回seq,res 的shape(batch,step,1),xs的shape为(batch_size,time_steps)
    #一般像这种[:,:,np.newaxis]叫做扩维技术,从2为变成3维,扩的维数为1。
    #seq[:, :, np.newaxis].shape=(50,20,1)
    # plt.plot (xs, seq, 'r-', xs, res, 'b-')
    # plt.show ()
    return [seq[:,:,np.newaxis],res[:,:,np.newaxis],xs]

#1. 使用tf.Variable()的时候,tf.name_scope()和tf.variable_scope() 都会给 Variable 和 op 的 name属性加上前缀。
#2. 使用tf.get_variable()的时候,tf.name_scope()就不会给 tf.get_variable()创建出来的Variable加前缀。

#定义 LSTM 的主体结构
class LSTM(object):
    def __init__(self,n_steps,input_size,output_size,cell_size,batch_size):
        self.n_steps=n_steps
        self.input_size=input_size
        self.cell_size=cell_size
        self.batch_size=batch_size
        self.output_size=output_size
        #初始化几个函数
        with tf.name_scope("inputs"):
            self.xs=tf.placeholder(tf.float32,[None,n_steps,self.input_size],name="xs")
            self.ys=tf.placeholder(tf.float32,[None,n_steps,self.output_size],name="ys")
        with tf.variable_scope("in_hidden"):
            self.add_input_layer()
        with tf.variable_scope("LSTM"):
            self.add_cell()
        with tf.variable_scope("out_hidden"):
            self.add_output_layer()
        with tf.name_scope("cost"):
            self.compute_cost()
        with tf.name_scope('train'):
            self.train_op=tf.train.AdamOptimizer(LR).minimize(self.cost)
    #定义三个变量
    def ms_error(self,labels,logits):
        return tf.square(tf.subtract(labels,logits))

    def _weight_variable(self,shape,name="weights"):
        initializer=tf.random_normal_initializer(mean=0,stddev=1,)
        return tf.get_variable(shape=shape,initializer=initializer,name=name)

    def _bias_variable(self,shape,name="biases"):
        initializer=tf.constant_initializer(0.1)
        return tf.get_variable(name=name,shape=shape,initializer=initializer)
    #接下来定义几个函数
    def add_input_layer(self):
        #应该我们只能在二维数据上矩阵相乘,计算logits,之后在reshape成3维。以下同理
        l_in_x=tf.reshape(self.xs,[-1,self.input_size],name='2_2D')#输入shape(batch_size*n_step,input_size)
        #权重的shape(in_size,cell_size)
        Ws_in=self._weight_variable([self.input_size,self.cell_size])
        #偏置的shape (cell_size,)
        bs_in=self._bias_variable([self.cell_size,])
        #l_in_y = (batch * n_steps ,cell_size)
        with tf.name_scope("Wx_puls_b"):
            l_in_y=tf.matmul(l_in_x,Ws_in)+bs_in
        #reshape l_in_y-->>=(batch,n_steps,cell_size)
        self.l_in_y=tf.reshape(l_in_y,[-1,self.n_steps,self.cell_size],name='2_3D')

    def add_cell(self):
        lstm_cell=tf.contrib.rnn.BasicLSTMCell(self.cell_size,forget_bias=1.0,state_is_tuple=True)
        with tf.name_scope("initial_state"):
            self.cell_init_state=lstm_cell.zero_state(self.batch_size,dtype=tf.float32,)
        self.cell_outputs,self.cell_final_state=tf.nn.dynamic_rnn(lstm_cell,
            self.l_in_y,initial_state=self.cell_init_state,time_major=False
        )#如果l_in_y的shape是(n_steps,batch,cell_size)的话,则对应的time_major=True

    def add_output_layer(self):
        #shape=(batch * n_steps,cell_size)
        l_out_x=tf.reshape(self.cell_outputs,[-1,self.cell_size],name="2_2D")
        Ws_out=self._weight_variable([self.cell_size,self.output_size])
        bs_out=self._bias_variable([self.output_size,])
        with tf.name_scope("Wx_plus_b"):
            self.pred=tf.matmul(l_out_x,Ws_out)+bs_out #shape=(batch*n_steps,output_size)

    def compute_cost(self):
        #计算一个batch内每一样本的loss
        losses=tf.contrib.legacy_seq2seq.sequence_loss_by_example(
            [tf.reshape(self.pred,[-1],name="reshape_pred")],#平铺一下维数
            [tf.reshape(self.ys,[-1],name="reshape_target")],
            [tf.ones([self.batch_size*self.n_steps],dtype=tf.float32)],
            average_across_timesteps=True,
            softmax_loss_function=self.ms_error,
            name="losses"
        )
        with tf.name_scope("average_cost"):
            #计算每一个batch的平均loss,因为梯度更新是在计算一个batch的平均误差的基础上进行更新的
            self.cost=tf.div(tf.reduce_sum(losses,name="losses_sum"),self.batch_size,name="average_cost")
            tf.summary.scalar("cost",self.cost)

if __name__=="__main__":
    model= LSTM(TIME_STEPS,INPUT_SIZE,OUTPUT_SIZE,CELL_SIZE,BATCH_SIZE)
    sess=tf.Session()
    merged=tf.summary.merge_all()
    writer=tf.summary.FileWriter("logs",sess.graph)
    #版本控制
    # tf.initialize_all_variables() no long valid from
    # 2017-03-02 if using tensorflow >= 0.12
    if int((tf.__version__).split('.')[1]) < 12 and   int((tf.__version__).split('.')[0]) < 1:
        init=tf.initialize_all_variables()
    else:
        init=tf.global_variables_initializer()
    sess.run(init)
    # relocate to the local dir and run this line to view it on
    # 在terminal中输入$ tensorboard --logdir='logs',让后在浏览器中Chrome (http://0.0.0.0:6006/)查看tensorboard
    plt.ion()
    plt.show()
    for i in range(200):#训练200次,训练一次一个batch
        seq,res,xs=get_batch()#此时的seq,res都是3维数组,shape=(batch,time_steps,1),这里的1就是input_size
        if i==0:
            feed_dict={model.xs:seq,
                       model.ys:res
                       #创建初始状态,这里就开始体现类的优势了,直接调用里面的xs,ys,
                       }
        else:
            feed_dict={model.xs:seq,
                       model.ys:res,
                       model.cell_init_state:state#用最后的state代替初始化的state
            }
        _,cost,state,pred=sess.run(
            [model.train_op,model.cost,model.cell_final_state,model.pred],feed_dict=feed_dict)
    #输出值和带入的参数顺序一一对应,cost对应model.cost,等等

    #xs[0,:],表示的是一个batch里面的第一个序列,因为xs是由np.arange()函数生成的,
    # 所以xs在对于每一个batch来说,同一个batch里面的每个序列都是一样的
    #例如xs的batch_size=3,time_step=4,[[0,1,2,3],
    # [0,1,2,3],
    # [0,1,2,4]],shape=(3,4)
    #res[0].flatten()表示的是一个batch里面的第一个序列,序列长度为time_steps * 1
        plt.plot(xs[0,:],res[0].flatten(),"r",xs[0,:],pred.flatten()[:TIME_STEPS],"b--")
        plt.ylim((-1.2,1.2))
        plt.draw()
        plt.pause(0.3)

        if i % 20 ==0:#每训练20个批次来打印一次当时的cost
            print("cost:",round(cost,4))#输出每一个batch的平均cost,约到零后面4位小数点
            result=sess.run(merged,feed_dict)
            writer.add_summary(result,i)

结果展示
《长短期记忆(LSTM)-tensorflow代码实现》

点赞