• 自己动手实现深度学习框架-7 RNN层--GRU, LSTM


    目标

            这个阶段会给cute-dl添加循环层,使之能够支持RNN--循环神经网络. 具体目标包括:

    1. 添加激活函数sigmoid, tanh.
    2. 添加GRU(Gate Recurrent Unit)实现.
    3. 添加LSTM(Long Short-term Memory)实现.
    4. 使用基于GRU和LSTM的RNN模型拟合一个正余弦叠加函数.

    RNN原理

    原始的RNN

            RNN模型用来捕捉序列数据的特征. 给定一个长度为T的输入系列(X=(x_1, x_2, .., X_T)), RNN层输出一个长度为T的序列(H=(h_1, h_2, ..., H_T)), 对于任意时间步t, 可以表示为:

    [H_t = δ(X_tW_x + H_{t-1}W_h + b), quad t = 2, 3, .., T ]

            函数δ是sigmoid函数:

    [δ = frac{1}{1 + e^{-x}} ]

            (H_t)包含了前面第1到t-1步的所有信息。 和CNN层类似, CNN层在空间上共享参数, RNN层在时间步上共享参数(W_x, W_h, b).

            RNN层中隐藏层的数量为T-2, 如果T较大(超过10), 反向传播是很容易出现梯度爆炸. GRU和LSTM就是为了解决这个问题而诞生, 这两种模型,可以让RNN能够支持长度超过1000的输入序列。

    GRU

            GRU使用了不同功能的门控单元, 分别捕捉序列上不同时间跨度的的依赖关系。每个门控单元都会都有独立的参数, 这些参数在时间步上共享。

            GRU的门控单元有:

            (R_t = δ(X_tW^r_x + H_{t-1}W^r_h + b^r)), 重置门用于捕捉短期依赖关系.

            (U_t = δ(X_tW^u_x + H_{t-1}W^u_h + b^u)), 更新门用于捕捉长期依赖关系

            (ar{H}_t = tanh(X_tar{W}_x + (R_t * H_{t-1})ar{W}_h + ar{b}))

            除此之外, 还有一个输出单元:

            (H_t = U_t * H_{t-1} + (1-U_t)*ar{H}_t)

    LSTM

            LSTM的设计思路和GRU类似, 同样使用了多个门控单元:

            (I_t = δ(X_tW^i_x + H_{t-1}W^i_h + b^i)), 输入门,过滤记忆门的输出.

            (F_t = δ(X_tW^f_x + H_{t-1}W^f_h + b^f)), 遗忘门, 过滤前面时间步的记忆.

            (O_t = δ(X_tW^o_x + H_{t-1}W^o_h + b^o)), 输出门, 过滤当前时间步的记忆.

            (M_t = tanh(X_tW^m_x + H_{t-1}W^m_h + b^m)), 记忆门.

            它还有自己独有的记忆单元和输出单元:

            (ar{M}_t = F_t * ar{M}_{t-1} + I_t * M_t)

            (H_t = O_t * tanh(ar{M}_t))

    RNN实现

            设计要求:

    1. RNN层中的隐藏层的数量是基于序列长度的,输入序列有多长, RNN层应生成对应数量的隐藏层。
    2. RNN层在时间步上共享参数, 从前面的描述可以看出, 只有门控单元有参数,因此门控单元应独立实现。
    3. 任意一个时间步上的层都依赖上一个时间步的输出,在正向传播和反向传播过程中都需要上一个时间步的输出, 每个门控单元都使用栈保存上一个时间步的输出.
    4. 默认情况下RNN层输出所有时间步的输出。但有时只需要最后一个时间步的输出, 这种情况下使用过滤层, 只向下一层传播最后一个时间步的输出。
    5. 使用门控单元实现GRU和LSTM

    RNN基础类的实现

    RNN类

            文件: cutedl/rnn_layers.py, 类名: RNN

            这个类是RNN层基类, 它主要功能是控制向前传播和向后传播的主流程.

            初始化参数:

      '''
      out_units 输出单元数
      in_units 输入单元数
      stateful 保留当前批次的最后一个时间步的状态作为下一个批次的输入状态, 默认False不保留
    
      RNN 的输入形状是(m, t, in_units)
      m: batch_size
      t: 输入系列的长度
      in_units: 输入单元数页是输入向量的维数
    
      输出形状是(m, t, out_units)
      '''
      def __init__(self, out_units, in_units=None, stateful=False, activation='linear'):
    

            向前传播

    def forward(self, in_batch, training):
        m, T, n = in_batch.shape
        out_units = self.__out_units
        #所有时间步的输出
        hstatus = np.zeros((m, T, out_units))
        #上一步的输出
        pre_hs = self.__pre_hs
        if pre_hs is None:
            pre_hs = np.zeros((m, out_units))
    
        #隐藏层循环过程, 沿时间步执行
        for t in range(T):
            hstatus[:, t, :] = self.hiden_forward(in_batch[:,t,:], pre_hs, training)
            pre_hs = hstatus[:, t, :]
    
        self.__pre_hs = pre_hs
        #pdb.set_trace()
        if not self.stateful:
            self.__pre_hs = None
    
        return hstatus
    

            反向传播

    def backward(self, gradient):
          m, T, n = gradient.shape
    
          in_units = self.__in_units
          grad_x = np.zeros((m, T, in_units))
          #pdb.set_trace()
          #从最后一个梯度开始反向执行.
          for t in range(T-1, -1, -1):
              grad_x[:,t,:], grad_hs = self.hiden_backward(gradient[:,t,:])
              #pdb.set_trace()
              if t - 1 >= 0:
                  gradient[:,t-1,:] = gradient[:,t-1,:] + grad_hs
    
          #pdb.set_trace()
          return grad_x
    

    sigmoid和tanh激活函数

    sigmoid及其导数

    [sigmoid = frac{1}{1+e^{-x}} ]

    [frac{d}{dx}sigmoid = sigmoid(1-sigmoid) ]

    tanh及其导数

    [tanh = frac{e^x - e^{-x}}{e^x + e^{-x}} ]

    [frac{d}{dx}tanh = 1 - tanh^2 ]

    门控单元实现

            文件: cutedl/rnn_layers.py, 类名: GateUint

            门控单元是RNN层基础的参数单元. 和Dense层类似,它是Layer的子类,负责学习和使用参数。但在学习和使用参数的方式上有很大的不同:

    • Dense有两个参数矩阵, GateUnit有3个参数矩阵.
    • Dense在一次反向传播过程中只使用当前的梯度学习参数,而GateUnit会累积每个时间步的梯度。

            下面我们会主要看一下GateUnit特别之处的代码.

            在__ init__方法中定义参数和栈:

        #3个参数
        self.__W = None #当前时间步in_batch权重参数
        self.__Wh = None #上一步输出的权重参数
        self.__b = None #偏置量参数
    
        #输入栈
        self.__hs = []  #上一步输出
        self.__in_batchs = [] #当前时间步的in_batch
    

            正向传播:

      def forward(self, in_batch, hs, training):
          W = self.__W.value
          b = self.__b.value
          Wh = self.__Wh.value
    
          out = in_batch @ W + hs @ Wh + b
    
          if training:
              #向前传播训练时把上一个时间步的输出和当前时间步的in_batch压栈
              self.__hs.append(hs)
              self.__in_batchs.append(in_batch)
    
              #确保反向传播开始时参数的梯度为空
              self.__W.gradient = None
              self.__Wh.gradient = None
              self.__b.gradient = None
    
          return self.activation(out)
    

            反向传播:

    def backward(self, gradient):
        grad = self.activation.grad(gradient)
    
        W = self.__W.value
        Wh = self.__Wh.value
        pre_hs = self.__hs.pop()
        in_batch = self.__in_batchs.pop()
    
        grad_in_batch = grad @ W.T
        grad_W = in_batch.T @ grad
        grad_hs = grad @ Wh.T
        grad_Wh = pre_hs.T @ grad
        grad_b = grad.sum(axis=0)
    
        #反向传播计算
        if self.__W.gradient is None:
            #当前批次第一次
            self.__W.gradient = grad_W
        else:
            #累积当前批次的所有梯度
            self.__W.gradient = self.__W.gradient + grad_W
    
        if self.__Wh.gradient is None:
            self.__Wh.gradient = grad_Wh
        else:
            self.__Wh.gradient = self.__Wh.gradient +  grad_Wh
    
        if self.__b.gradient is None:
            self.__b.gradient = grad_b
        else:
            self.__b.gradient = self.__b.gradient + grad_b
    
        return grad_in_batch, grad_hs
    

    GRU实现

            文件: cutedl/rnn_layers.py, 类名: GRU

            隐藏单初始化:

    def set_parent(self, parent):
        super().set_parent(parent)
    
        out_units = self.out_units
        in_units = self.in_units
    
        #pdb.set_trace()
        #重置门
        self.__g_reset = GateUnit(out_units, in_units)
        #更新门
        self.__g_update = GateUnit(out_units, in_units)
        #候选输出门
        self.__g_cddout = GateUnit(out_units, in_units, activation='tanh')
    
        self.__g_reset.set_parent(self)
        self.__g_update.set_parent(self)
        self.__g_cddout.set_parent(self)
    
        #重置门乘法单元
        self.__u_gr = MultiplyUnit()
        #输出单元
        self.__u_out = GRUOutUnit()
    

            向前传播:

      def hiden_forward(self, in_batch, pre_hs, training):
          gr = self.__g_reset.forward(in_batch, pre_hs, training)
          gu = self.__g_update.forward(in_batch, pre_hs, training)
          ugr = self.__u_gr.forward(gr, pre_hs, training)
          cddo = self.__g_cddout.forward(in_batch, ugr, training)
    
          hs = self.__u_out.forward(gu, pre_hs, cddo, training)
    
          return hs
    

            反向传播:

    def hiden_backward(self, gradient):
    
        grad_gu, grad_pre_hs, grad_cddo = self.__u_out.backward(gradient)
        #pdb.set_trace()
        grad_in_batch, grad_ugr = self.__g_cddout.backward(grad_cddo)
    
        #计算梯度的过程中需要累积上一层输出的梯度
        grad_gr, g_pre_hs = self.__u_gr.backward(grad_ugr)
        grad_pre_hs = grad_pre_hs + g_pre_hs
    
        g_in_batch, g_pre_hs = self.__g_update.backward(grad_gu)
        grad_in_batch = grad_in_batch + g_in_batch
        grad_pre_hs = grad_pre_hs + g_pre_hs
    
        g_in_batch, g_pre_hs = self.__g_reset.backward(grad_gr)
        grad_in_batch = grad_in_batch + g_in_batch
        grad_pre_hs = grad_pre_hs + g_pre_hs
    
        #pdb.set_trace()
        return grad_in_batch, grad_pre_hs    
    

    LSTM实现

            文件: cutedl/rnn_layers.py, 类名: LSTM

            隐藏单元初始化:

    def set_parent(self, layer):
        super().set_parent(layer)
    
        in_units = self.in_units
        out_units = self.out_units
    
        #输入门
        self.__g_in = GateUnit(out_units, in_units)
        #遗忘门
        self.__g_forget = GateUnit(out_units, in_units)
        #输出门
        self.__g_out = GateUnit(out_units, in_units)
        #记忆门
        self.__g_memory = GateUnit(out_units, in_units, activation='tanh')
    
        self.__g_in.set_parent(self)
        self.__g_forget.set_parent(self)
        self.__g_out.set_parent(self)
        self.__g_memory.set_parent(self)
    
        #记忆单元
        self.__memory_unit =LSTMMemoryUnit()
        #输出单元
        self.__out_unit = LSTMOutUnit()
    

            向前传播:

    def hiden_forward(self, in_batch, hs, training):
        g_in = self.__g_in.forward(in_batch, hs, training)
        #pdb.set_trace()
        g_forget = self.__g_forget.forward(in_batch, hs, training)
        g_out = self.__g_out.forward(in_batch, hs, training)
        g_memory = self.__g_memory.forward(in_batch, hs, training)
    
        memory = self.__memory_unit.forward(g_forget, g_in, g_memory, training)
        cur_hs = self.__out_unit.forward(g_out, memory, training)
    
        return cur_hs
    

            反向传播:

    def hiden_backward(self, gradient):
        #pdb.set_trace()
        grad_out, grad_memory = self.__out_unit.backward(gradient)
        grad_forget, grad_in, grad_gm = self.__memory_unit.backward(grad_memory)
    
        grad_in_batch, grad_hs = self.__g_memory.backward(grad_gm)
        tmp1, tmp2 = self.__g_out.backward(grad_out)
        grad_in_batch += tmp1
        grad_hs += tmp2
    
        tmp1, tmp2 = self.__g_forget.backward(grad_forget)
        grad_in_batch += tmp1
        grad_hs += tmp2
    
        tmp1, tmp2 = self.__g_in.backward(grad_in)
        grad_in_batch += tmp1
        grad_hs += tmp2
    
        return grad_in_batch, grad_hs
    

    验证

            接下来, 验证示例将会构建一个简单的RNN模型, 使用该模型拟合一个正余弦叠加函数:

    #采样函数
    def sample_function(x):
        y = 3*np.sin(2 * x * np.pi) + np.cos(x * np.pi) + np.random.uniform(-0.05,0.05,len(x))
        return y
    

            训练数据集和测试数据集在这个函数的不同定义域区间内样. 训练数据集的采样区间为[1, 200.01), 测试数据集的采样区间为[200.02, 240.002). 模型任务是预测这个函数值的序列.

            示例代码在examples/rnn/fit_function.py文件中.

    使用GRU构建的模型

    def fit_gru():
        model = Model([
                    rnn.GRU(32, 1),
                    nn.Filter(),
                    nn.Dense(32),
                    nn.Dense(1, activation='linear')
                ])
        model.assemble()
        fit('gru', model)
    

    训练报告:

    使用LSTM构建的模型

    def fit_lstm():
        model = Model([
                    rnn.LSTM(32, 1),
                    nn.Filter(),
                    nn.Dense(2),
                    nn.Dense(1, activation='linear')
                ])
        model.assemble()
        fit('lstm', model)
    

    训练报告:

    总结

            这个阶段,框架新增了RNN的两个最常见的实现:GRU和LSTM, 相应地增加了它需要的激活函数. cute-dl已经具备了构建最基础RNN模型的能力。通过验证发现, GRU模型和LSTM模型在简单任务上都表现出了很好的性能。会添加嵌入层,使框架能够构建文本分类任务的模型,然后在imdb-review(电影评价)数据集上进行验证.

  • 相关阅读:
    1、1、2、3、5、8、13、21、34...... 求第X位数是多少,2种实现方式
    数据库设计的三大范式
    Parse与TryParse的区别
    常用Lambda范例
    【转载】说说JSON和JSONP,
    Lambda表达式
    Winform部署时强名称签名mshtml.dll无效的解决方案
    .net面试问答(大汇总)
    Asp.net MVC3 一语道破
    在asp.net中使用 log4net
  • 原文地址:https://www.cnblogs.com/brandonli/p/13063898.html
Copyright © 2020-2023  润新知