Narcissus's blog Stay Follish, Stay Hungry

RNN原理、公式推导及python实现

2019-07-28
Narcissus

RNN原理及pytorch实现

图像理解

注意细节
  • RNN只有这四个参数:$U,W​$,对于RNN和特定任务相结合,使用隐藏状态的输出,得到预测结果,也就是分类器部分,才有分类器的参数$V​$,并不是RNN的参数。
  • 横向展开的每个timestep共享权重。

  • pytorch中的激活函数:tanh 或者 relu,因为pytorch直接使用的cudnn接口,cudnn中只支持这两种。

  • pytorch中,RNN的默认输入形状是(seq_len, batch_size, input_size),batch_size位于第二维度,因为cuDNN中RNN的API就是batch_size在第二维度!把batch_first设置为True,可以将batch_size和序列长度seq_len对换位置。batch first意味着模型的输入(一个Tensor)在内存中存储时,先存储第一个sequence,再存储第二个… 而如果是seq_len first,模型的输入在内存中,先存储所有序列的第一个单元,然后是第二个单元.

公式理解

正向传播公式

RNN迭代式 分类器 损失:把整个timestep的数据当成一个训练数据。

反向求导(BPTT:back propagation through time)

BPTT是关于标准RNN的名称。对每个参数的求导,都要各个timestep加起来。即:

img

以$\frac{\partial{E_1}}{\partial{W}}$为例:

img

img

1)$V$的反向求导比较简单,,与等无时间序列无关

根据链式法则: $$ \begin{align} \frac{\partial{E_t}}{\partial{V}}&=\frac{\partial{E_t}}{\partial{\hat{y_t}}}·\frac{\partial{\hat{y_t}}}{\partial{z_t}}·\frac{\partial{z_t}}{\partial{V}}
&=(-y_t^T·\frac{1}{\hat{y_t}})(\frac{\partial{\hat{y_t}}}{\partial{z_t}})(s_t)
&=(\hat{y_t}-y_t^T)\otimes s_t

\end{align} $$ 注意:这里$z_t,y_t$都是向量,$\frac{\partial{\hat{y_t}}}{\partial{z_t}}$这个导数就是softmax的求导,因此是个分段函数,根据下面补充的结论,可推出最后的结果。最后的外积,是根据纬度推导出来的,比如有k个类,$\hat{y_t},y_t$都是k维的,$s_t$为隐藏状态输出,是h维的,而V是个矩阵,也就是$k\times h​$维的(左乘),因此,就是外积。

补充:

  • 注softmax求导

​ 推导过程:

  • cross entropy loss 求导

  • 总结:softmax+cross entropy loss ,对$x_j$(这里可以看成是$z_{tj}$)求导,就是t==j时,对应位置的softmax值(也就是预测分数)减1,其他的不变: $$ \hat{y_t}=g(x_t)\

    \frac{\partial{E_t}}{\partial{x_j}}=\left{ \begin{aligned} \hat{y_t}-1,t=j
    \hat{y_j},t\neq j \end{aligned} \right. \frac{\partial{E_t}}{\partial{x}}=\frac{\partial{E_t}}{\partial{z_t}}=(\hat{y_t}-y_t^T) $$

2)$W​$的反向求导:

  • 首先要得到$E_t$对$s_t$的导数($(\hat{y_t}-y_t^T)$是k维(分类类别数),V是k*h的):
  • $s_t​$对W求导 $$ s_t=tanh(Ws_{t-1}+Ux_t)\

    \frac{\partial s_t}{\partial W}=(1-s_t^2)s_{t-1}\

    \frac{\partial E_t}{\partial W}=V^T(\hat{y_t}-y_t^T)(1-s_t^2)s_{t-1} $$ 注:需要注意的是各部分之间的运算时element-wise dot ,outer times还是dot,这个需要根据纬度再来推导。

    • 补充:

3)$U$的反向求导:$\frac{\partial{E_t}}{\partial{U}}$,和W推导过程的一样:

实现

def bptt(self, x, y):

    T = len(y)

    # Perform forward propagation

    o, s = self.forward_propagation(x)

    # We accumulate the gradients in these variables

    dLdU = np.zeros(self.U.shape)

    dLdV = np.zeros(self.V.shape)

    dLdW = np.zeros(self.W.shape)

    delta_o = o

    delta_o[np.arange(len(y)), y] -= 1.

    # For each output backwards...

    for t in np.arange(T)[::-1]:

        dLdV += np.outer(delta_o[t], s[t].T)

        # Initial delta calculation: dL/dz

        delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))

        # Backpropagation through time (for at most self.bptt_truncate steps)

        for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:

            # print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)

            # Add to gradients at each previous step

            dLdW += np.outer(delta_t, s[bptt_step-1])              

            dLdU[:,x[bptt_step]] += delta_t#这个地方不容易想到,本来应该是delta_t与x_t的外积(delta_t是h维的,x_t是d维的,U是h*d的,因为U对x_t是左乘),但是这里省略了这个乘法.因为X是输入句子中,单词在字典中是否出现,那么,就是one-hot的表示,因此可以省略这个外积运算,直接对标签为1的+delta_t即可。

            # Update delta for next step dL/dz at t-1

            delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)/#求s_t对s_{t-1}的导数,下一次循环就是s_{t-1}对U,W的导数了.

    return [dLdU, dLdV, dLdW]

参考资料

https://zhuanlan.zhihu.com/p/32103001

https://zhuanlan.zhihu.com/p/32930648

https://github.com/pangolulu/rnn-from-scratch

https://www.cnblogs.com/wacc/p/5341670.html


Comments

Content