本文是对 RNN 循环神经网络中的每个神经元进行反向传播求导的数学推导过程,下面还使用 PyTorch
对导数公式进行编程求证。php
一个普通的 RNN 神经网络以下图所示:html
其中 \(x^{\langle t \rangle}\) 表示某一个输入数据在 \(t\) 时刻的输入;\(a^{\langle t \rangle}\) 表示神经网络在 \(t\) 时刻时的hidden state,也就是要传送到 \(t+1\) 时刻的值;\(y^{\langle t \rangle}\) 则表示在第 \(t\) 时刻输入数据传入之后产生的预测值,在进行预测或 sampling 时 \(y^{\langle t \rangle}\) 一般做为下一时刻即 \(t+1\) 时刻的输入,也就是说 \(x^{\langle t \rangle}=\hat{y}^{\langle t \rangle}\) ;下面对数据的维度进行说明。python
下图所示的是一个特定的 RNN 神经元:web
上图说明了在第 \(t\) 时刻的神经元中,数据的输入 \(x^{\langle t \rangle}\) 和上一层的 hidden state \(a^{\langle t \rangle}\) 是如何通过计算获得下一层的 hidden state 和预测输出 \(\hat{y}^{\langle t \rangle}\) 。编程
下面是对五个参数的维度说明:网络
计算 \(t\) 时刻的 hidden state \(a^{\langle t \rangle}\) :架构
预测 \(t\) 时刻的输出 \(\hat{y}^{\langle t \rangle}\) :框架
在当今流行的深度学习编程框架中,咱们只须要编写一个神经网络的结构和负责神经网络的前向传播,至于反向传播的求导和参数更新,彻底由框架搞定;即使如此,咱们在学习阶段也要本身动手证实一下反向传播的有效性。ide
下图是 RNN 神经网络中的一个基本的神经元,图中标注了反向传播所需传来的参数和输出等。函数
就如一个全链接的神经网络同样,损失函数 \(J\) 的导数经过微积分的链式法则(chain rule)反向传播到每个时间轴上。
为了方便,咱们将损失函数关于神经元中参数的偏导符号简记为 \(\mathrm{d}\mathit{parameters}\) ;例如将 \(\frac{\partial J}{\partial W_{ax}}\) 记为 \(\mathrm{d}W_{ax}\) 。
上图的反向传播的实现并无包括全链接层和 Softmax 层。
计算损失函数关于各个参数的偏导数以前,咱们先引入一个计算图(computation graph),其演示了一个 RNN 神经元的前向传播和如何利用计算图进行链式法则的反向求导。
由于当进行反向传播求导时,咱们须要将整个时间轴的输入所有输入以后,才能够从最后一个时刻开始往前传进行反向传播,因此咱们假设 \(t\) 时刻就为最后一个时刻 \(T_x\) 。
若是咱们想要先计算 \(\frac{\partial\ell}{\partial W_{ax}}\) 因此咱们能够从计算图中看到,反向传播的路径:
咱们须要循序渐进的分别对从 \(W_{ax}\) 计算到 \(\ell\) 一路相关的变量进行求偏导,利用链式法则,将红色路线上一路的偏导数相乘到一块儿,就能够求出偏导数 \(\frac{\partial\ell}{\partial W_{ax}}\) ;因此咱们获得:
在上面的公式中,咱们仅须要分别求出每个偏导便可,其中红色的部分就是关于 \(\mathrm{Softmax}\) 的求导,关于 \(\mathrm{Softmax}\) 求导的推导过程,能够看本人的另外一篇博客: 关于 Softmax 回归的反向传播求导数过程
关于 \(\mathrm{tanh}\) 的求导公式以下:
因此上面的式子就获得:
咱们就能够获得在最后时刻 \(t\) 参数 \(W_{ax}\) 的偏导数。
关于上面式子中的偏导数的计算,除了标量对矩阵的求导,在后面还包括了两个一个矩阵或向量对另外一个矩阵或向量中的求导,实际上这是很是麻烦的一件事。
好比在计算 \(\frac{\partial z1^{\langle t\rangle}}{\partial W_{ax}}\) 偏导数的时候,咱们发现 \(z1^{\langle t\rangle}\) 是一个 \(\mathbb{R}^{n_a\times m}\) 的矩阵,而 \(W_{ax}\) 则是一个 \(\mathbb{R}^{n_a\times n_x}\) 的矩阵,这一项就是一个矩阵对另外一个矩阵求偏导,若是直接对其求导咱们将会获得一个四维的矩阵 \(\mathbb{R}^{n_a\times n_x\times n_a\times m}\) (雅可比矩阵 Jacobian matrix);只不过这个高维矩阵中偏导数的值有不少 \(0\) 。
在神经网络中,若是直接将这个高维矩阵直接生搬硬套进梯度降低里更新参数是不可行,由于咱们须要获得的梯度是关于自变量同型的向量或矩阵并且咱们还要处理更高维度的矩阵的乘法;因此咱们须要将结果进行必定的处理获得咱们仅仅须要的信息。
通常在深度学习框架中都会有自动求梯度的功能包,这些包(好比
PyTorch
)中就只容许一个标量对向量或矩阵求导,其余状况是不容许的,除非在反向传播的函数里传入一个同型的权重向量或矩阵才能够获得导数。
咱们先简单求出一个偏导数 \(\frac{\partial\ell}{\partial W_{ax}}\) 咱们下面使用 PyTorch
中的自动求梯度的包进行验证咱们的公式是否正确。
import torch
# 这是神经网络中的一些架构的参数 n_x = 6 n_y = 6 m = 1 T_x = 5 T_y = 5 n_a = 3
# 定义全部参数矩阵 # requires_grad 为 True 代表在涉及这个变量的运算时创建计算图 # 为了以后反向传播求导 W_ax = torch.randn((n_a, n_x), requires_grad=True) W_aa = torch.randn((n_a, n_a), requires_grad=True) ba = torch.randn((n_a, 1), requires_grad=True) W_ya = torch.randn((n_y, n_a), requires_grad=True) by = torch.randn((n_y, 1), requires_grad=True)
# t 时刻的输入和上一时刻的 hidden state x_t = torch.randn((n_x, m), requires_grad=True) a_prev = torch.randn((n_a, m), requires_grad=True) y_t = torch.randn((n_y, m), requires_grad=True)
# 开始模拟一个神经元 t 时刻的前向传播 # 从输入一直到计算出 loss z1_t = torch.matmul(W_ax, x_t) + torch.matmul(W_aa, a_prev) + ba z1_t.retain_grad() a_t = torch.tanh(z1_t) a_t.retain_grad() z2_t = torch.matmul(W_ya, a_t) + by z2_t.retain_grad() y_hat = torch.exp(z2_t) / torch.sum(torch.exp(z2_t), dim=0) y_hat.retain_grad() loss_t = -torch.sum(y_t * torch.log(y_hat), dim=0) loss_t.retain_grad()
# 对最后的 loss 标量开始进行反向传播求导 loss_t.backward()
# 咱们就能够获得 W_ax 的导数 # 存储在后缀 _autograd 变量中,代表是由框架自动求导获得的 W_ax_autograd = W_ax.grad
# 查看框架计算获得的导数 W_ax_autograd
tensor([[ 0.5252, 1.1938, -0.2352, 1.1571, -1.0168, 0.3195], [-1.0536, -2.3949, 0.4718, -2.3213, 2.0398, -0.6410], [-0.0316, -0.0717, 0.0141, -0.0695, 0.0611, -0.0192]])
# 咱们对本身推演出的公式进行手动计算导数 # 存储在后缀 _manugrad 变量中,代表是手动由公式计算获得的 W_ax_manugrad = torch.matmul(torch.matmul((y_hat - y_t).T, W_ya).T * (1 - torch.square(torch.tanh(z1_t))), x_t.T) #torch.matmul(torch.matmul(W_ya.T, y_hat - y_t) * (1 - torch.square(torch.tanh(z1_t))), x_t.T)
# 输出手动计算的导数 W_ax_manugrad
tensor([[ 0.5195, 1.1809, -0.2327, 1.1447, -1.0058, 0.3161], [-1.0195, -2.3172, 0.4565, -2.2461, 1.9737, -0.6202], [-0.0309, -0.0703, 0.0138, -0.0681, 0.0599, -0.0188]], grad_fn=<MmBackward>)
# 查看两种求导结果的之差的 L2 范数 torch.norm(W_ax_manugrad - W_ax_autograd)
tensor(0.1356, grad_fn=<CopyBackwards>)
经过上面的编程输出能够看到,咱们手动计算的导数和框架本身求出的导数虽然有必定的偏差,可是一一对照能够大致看到咱们手动求出来的导数大致是对的,并无说错的很是离谱。
但上面只是当 \(t=T_x\) 即 \(t\) 时刻是最后一个输入单元的时候,也就是说所求的关于 \(_W{ax}\) 的导数只是所有导数的一部分,由于参数共享,因此每一时刻的神经元都有对 \(W_{ax}\) 的导数,因此须要将全部时刻的神经元关于 \(W_{ax}\) 的导数所有加起来。
若 \(t\) 不是最后一时刻,多是神经网络里的中间的某一时刻的神经元;也就是说,在进行反向传播的时候,想要求 \(t\) 时刻的导数,就得等到 \(t+1\) 时刻的导数值传进来,而后根据链式法则才能够计算当前时刻参数的导数。
下面是一个简易的计算图,只绘制出了 \(W_ax\) 到 \(\ell\) 的计算中,共涉及到哪些变量(在整个神经网络中的 \(W_{ax}\) 的权重参数是共享的):
下面使用一个视频展现整个神经网络中从 \(W_{ax}\) 到一个数据批量的损失值 \(\ell\) 的大致流向:
计算完 \(\ell\) 以后就能够计算 \(\frac{\partial\ell}{\partial W_{ax}}\) 的导数值,可是 RNN 神经网络的反向传播区别于全链接神经网络的。
而后,咱们演示一下如何进行反向传播的,注意看每个时刻的 \(a^{\langle t\rangle}\) 的计算都是等 \(a^{\langle t+1\rangle}\) 的导数值传进来才进行计算的;一样地,\(W_{ax}\) 导数的计算也不是一步到位的,也是须要等到全部时刻的 \(a\) 的值所有传到才计算完。
因此对于神经网络中间某一个单元 \(t\) 咱们有:
关于红色的部分的意思是须要等到 \(t+1\) 时刻的导数值传进来,而后才能够进行对 \(t+1\) 时刻关于当前时刻 \(t\) 的参数求导,最后获得参数梯度的一个份量。其实若仔细展开每个偏导项,就像是一个递归同样,每次求某一时刻的导数老是要从最后一时刻往前传到当前时刻才能够进行。
多元复合函数的求导法则
若是函数 \(u=\varphi(t)\) 及 \(v=\psi(t)\) 都在点 \(t\) 可导,函数 \(z=f(u,v)\) 在对应点 \((u,v)\) 具备连续偏导数,那么复合函数 \(z=f[\varphi(t),\psi(t)]\) 在点 \(t\) 可导,且有
\[\frac{\mathrm{d}z}{\mathrm{d}t}=\frac{\partial z}{\partial u}\frac{\mathrm{d}u}{\mathrm{d}t}+\frac{\partial z}{\partial v}\frac{\mathrm{d}v}{\mathrm{d}t} \]
下面使用一张计算图说明 \(a^{\langle t\rangle}\) 到 \(\ell\) 的计算关系。
也就是说第 \(t\) 时刻 \(\ell\) 关于 \(a^{\langle t\rangle}\) 的导数是由两部分相加组成,也就是说是由两条路径反向传播,这两条路径分别是 \(\ell\to\ell^{\langle t\rangle}\to\hat{y}^{\langle t\rangle}\to z2^{\langle t\rangle}\to a^{\langle t\rangle}\) 和 \(\ell\to\ell^{\langle t+1\rangle}\to\hat{y}^{\langle t+1\rangle}\to z2^{\langle t+1\rangle}\to a^{\langle t+1\rangle}\to z1^{\langle t+1\rangle}\to a^{\langle t\rangle}\) ,咱们将这两条路径导数之和使用 \(\mathrm{d}a_{\mathrm{next}}\) 表示。
因此咱们能够获得在中间某一时刻的神经单元关于 \(W_{ax}\) 的导数为:
经过一样的方法,咱们就能够获得其它参数的导数:
除了传递参数的导数,在第 \(t\) 时刻还须要传送 \(\ell\) 关于 \(z1^{\langle t\rangle}\) 的导数到 \(t-1\) 时刻,将须要传送到上一时刻的导数记做为 \(\mathrm{d}a_{\mathrm{prev}}\) 咱们获得:
能够看到,一个循环神经网络的反向传播其实是很是复杂的,由于每一时刻的神经元都与参数有计算关系,因此反向传播时的路径很是杂乱,其中还涉及到了高维的矩阵,因此在计算时须要对高维矩阵进行必定的矩阵代数转换才方便导数和更新参数的计算。