zoukankan      html  css  js  c++  java
  • lstm-bp过程的手工源码实现

        近些年来,随着深度学习的崛起,RNN模型也变得非常热门。如果把RNN模型按照时间轴展开,它也类似其它的深度神经网络模型结构。因此,我们可以参照已有的方法训练RNN模型。

        现在最流行的一种RNN模型是LSTM(长短期记忆)网络模型。

        尽管我们可以借助Tensorflow、Torch、Theano等深度学习库轻松地训练模型,而不再需要推导反向传播的过程,但是逐步推导LSTM模型的梯度并用反向传播算法来实现,对我们深刻地理解模型是大有裨益的。

        因此,我们首先按照LSTM的公式实现正向传播计算过程,然后推导网络模型的梯度计算过程,最后用numpy来实现模型的求解。

    LSTM正向传播

    用代码可以表示为:

    H = 128 # LSTM 层神经元的数量
    D = ... # 输入数据的维度 == 词表的大小
    Z = H + D # 因为需要把LSTM的状态与输入数据拼接
    model = dict(
        Wf=np.random.randn(Z, H) / np.sqrt(Z / 2.),
        Wi=np.random.randn(Z, H) / np.sqrt(Z / 2.),
        Wc=np.random.randn(Z, H) / np.sqrt(Z / 2.),
        Wo=np.random.randn(Z, H) / np.sqrt(Z / 2.),
        Wy=np.random.randn(H, D) / np.sqrt(D / 2.),
        bf=np.zeros((1, H)),
        bi=np.zeros((1, H)),
        bc=np.zeros((1, H)),
        bo=np.zeros((1, H)),
        by=np.zeros((1, D))
    )


    在上面,我们定义了LSTM单元的结构。上述公式需要注意的一点是,我们把隐藏层上一步的状态h与当前的输入x相连接,因此LSTM单元的输入是 Z = H + D。另外,我们LSTM单元的输出层有H个神经元,因此每个权重矩阵的维度是 ZxH,偏置向量的维度是 1xH。
    W

    y

    和 b

    y

    略有不同,这两项是全连接层的参数,它们的下一级是softmax层。最终的输出结果将是词表中每个词语出现的概率分布,维度为 1xD。因此,W

    y

    的维度必须是 HxD,b

    y

    的维度必须是 1xD。
    def lstm_forward(X, state):
        m = model
        Wf, Wi, Wc, Wo, Wy = m['Wf'], m['Wi'], m['Wc'], m['Wo'], m['Wy']
        bf, bi, bc, bo, by = m['bf'], m['bi'], m['bc'], m['bo'], m['by']
    
        h_old, c_old = state
    
        # One-hot 编码
        X_one_hot = np.zeros(D)
        X_one_hot[X] = 1.
        X_one_hot = X_one_hot.reshape(1, -1)
    
        # 上一步状态与当前输入值连接
        X = np.column_stack((h_old, X_one_hot))
      hf = sigmoid(X @ Wf + bf)
        hi = sigmoid(X @ Wi + bi)
        ho = sigmoid(X @ Wo + bo)
        hc = tanh(X @ Wc + bc)
    
        c = hf * c_old + hi * hc
        h = ho * tanh(c)
    
        y = h @ Wy + by
        prob = softmax(y)
    
        cache = ... # 存储所有的中间变量结果
    
        return prob, cache
    

    上面的代码表示了单个LSTM单元的前向传播过程,与公式表示的基本一致,多了one-hot编码的步骤。

    LSTM反向传播

    接下来,我们进入到本篇文章的要点:LSTM反向传播计算。我们假设可以调用函数计算sigmoid和tanh函数的导数。

    def lstm_backward(prob, y_train, d_next, cache):
        # 取出前向传播步骤中存储的中间状态变量
        ... = cache
        dh_next, dc_next = d_next
    
        # Softmax loss gradient
        dy = prob.copy()
        dy[1, y_train] -= 1.
    # 隐藏层到输出层的导数 dWy = h.T @ dy dby = dy # 注意加上dh_next这一项 dh = dy @ Wy.T + dh_next # h = ho * tanh(c),计算ho的偏导数 dho = tanh(c) * dh dho = dsigmoid(ho) * dho # h = ho * tanh(c), 计算c的偏导数 dc = ho * dh * dtanh(c) dc = dc + dc_next # c = hf * c_old + hi * hc,计算hf的偏导数 dhf = c_old * dc dhf = dsigmoid(hf) * dhf # c = hf * c_old + hi * hc,计算hi的偏导数 dhi = hc * dc dhi = dsigmoid(hi) * dhi # c = hf * c_old + hi * hc,计算hc的偏导数 dhc = hi * dc dhc = dtanh(hc) * dhc # 各个门的偏导数 dWf = X.T @ dhf dbf = dhf dXf = dhf @ Wf.T dWi = X.T @ dhi dbi = dhi dXi = dhi @ Wi.T dWo = X.T @ dho dbo = dho dXo = dho @ Wo.T dWc = X.T @ dhc dbc = dhc dXc = dhc @ Wc.T # 由于X参与多个门的计算,因此偏导数需要累加 dX = dXo + dXc + dXi + dXf # 计算h_old的偏导数 dh_next = dX[:, :H] # c = hf * c_old + hi * hc,计算dc_next的偏导数 dc_next = hf * dc grad = dict(Wf=dWf, Wi=dWi, Wc=dWc, Wo=dWo, Wy=dWy, bf=dbf, bi=dbi, bc=dbc, bo=dbo, by=dby) state = (dh_next, dc_next) return grad, state

    在推导的过程中,不太容易理解地方的有如下几点:

    1. 计算dh时需要加上dh_next,因为在前向过程中,h不仅出现在y = h @ Wy + by,还与下一步计算有关。因此,这里不要忘记加上它。
    2. 计算dc时加上dc_next,理由同上。
    3. 计算dX时,需要累加dXo + dXc + dXi + dXf,理由与上面类似,因为X在多个计算步骤中都有用到。
    4. 因为X = [h_old, x],所以从dx可以得到dh_next。

    既然正向和反向传播计算都已经实现,我们就可以合并两者来训练模型。

    LSTM训练步骤

    训练的过程分为三步:正向计算,计算损失值,反向计算。

    python
    def train_step(X_train, y_train, state):
        probs = []
        caches = []
        loss = 0.
        h, c = state
    
        # 正向计算
    
        for x, y_true in zip(X_train, y_train):
            prob, state, cache = lstm_forward(x, state, train=True)
            loss += cross_entropy(prob, y_true)
    
            # 保存正向计算的结果
            probs.append(prob)
            caches.append(cache)
    
        # 损失值采用交叉熵
        loss /= X_train.shape[0]
    
        # 反向过程
    
        # 在最后一步, dh_next 和 dc_next 的值等于0。
        d_next = (np.zeros_like(h), np.zeros_like(c))
        grads = {k: np.zeros_like(v) for k, v in model.items()}
    
        # 按照从后到前的时间顺序
        for prob, y_true, cache in reversed(list(zip(probs, y_train, caches))):
            grad, d_next = lstm_backward(prob, y_true, d_next, cache)
    
            # 累加各个步骤的梯度值
            for k in grads.keys():
                grads[k] += grad[k]
    
        return grads, loss, state

    在一个完整的训练步骤中,我们首先进行前向计算,保存softmax层的概率分布结果以及每一步的中间结果,因为在反向过程中还会用到。

    接着,我们在每一步都能计算交叉熵损失值(因为采用softmax方法)。然后,累加每一步的损失值,并求平均值。

    最后,基于前向传播的结果进行反向传播运算,需要注意的是数据遍历的方向与之前相反。

    另外,在反向传播的第一步,dh_next和dc_next的值等于0.为什么呢?这是因为在正向计算的最后一步,h和c不会参与下一步的计算,因为不存在下一步!因此,在最后一步h和c的偏导数可以直接推导,不需要考虑dh_next和dc_next。

    一旦实现了这个函数,我们稍加修改就可以把它嵌入到任何优化算法中,比如RMSProp、Adam等等。

    一切搞定!我们可以尝试训练一个LSTM模型

    测试结果

    使用Adam优化算法,我从维基百科上复制了一段关于文字。每一个字符表示一个数据。训练目标是预测文章的下一个字符。每隔100轮迭代,我们会检查一下模型的效果。下面是截取到的训练结果:

    =========================================================================
    Iter-100 loss: 4.2125
    =========================================================================
    best c ehpnpgteHihcpf,M tt" ao tpo Teoe ep S4 Tt5.8"i neai   neyoserpiila o  rha aapkhMpl rlp pclf5i
    =========================================================================
    
    ...
    
    =========================================================================
    Iter-52800 loss: 0.1233
    =========================================================================
    tary shoguns who ruled in the name of the Uprea wal motrko, the copulation of Japan is a sour the wa
    =========================================================================

    模型果然学到了一些知识!

    小结

    在本文中,我们介绍了LSTM的通用公式,并基于此实现了前向计算过程。然后,我们推导了反向计算的过程,尽管加入了一些小技巧,但是整个过程还是非常直截了当。接着,我们将两者结合构建了完整的训练步骤,并用真实的数据训练和测试模型。

  • 相关阅读:
    如何为Android写一个PhoneGap插件
    Javascript高性能动画与页面渲染
    jquery mobile Popup
    android学习资料免费下载
    锋利的jquery第2版高清 pdf
    android获取sd卡最后一张照片
    mongodb 基础
    django 实现读写分离
    docker 部署django方式
    mysql 主从读写
  • 原文地址:https://www.cnblogs.com/Libo-Master/p/7725017.html
Copyright © 2011-2022 走看看