深度神经网络DNN

深度神经网络(Deep Neural Networks,以下简称DNN)是深度学习的基础,而要理解DNN,首先我们要理解DNN模型,下面我们就对DNN的模型、前向传播算法与反向传播算法做一个总结。

从感知机到神经网络

感知机原理小结中,作者介绍过感知机的模型,它是一个有若干输入和一个输出的模型,如下图:

输出和输入之间学习到一个线性关系,得到中间输出结果:

接着是一个神经元激活函数:

从而得到我们想要的输出结果1或者-1。 这个模型只能用于二元分类,且无法学习比较复杂的非线性模型,因此在工业界无法使用。

而神经网络则在感知机的模型上做了扩展,总结下主要有三点:

1) 加入了隐藏层,隐藏层可以有多层,增强模型的表达能力,如下图实例,当然增加了这么多隐藏层模型的复杂度也增加了好多。

2) 输出层的神经元也可以不止一个输出,可以有多个输出,这样模型可以灵活的应用于分类回归,以及其他的机器学习领域比如降维和聚类等。多个神经元输出的输出层对应的一个实例如下图,输出层现在有4个神经元了。

由该图可以看出,该输出层输出的维度为4,由$z=\sum\limits_{i=1}^mw_ix_i + b$可得输出层由4个偏置,即等于输出层神经元的数目。同理,对于图中的hidden layer层,一个神经元对应一个偏置。

3)对激活函数做扩展,感知机的激活函数是$sign(z)$,虽然简单但是处理能力有限,因此神经网络中一般使用的其他的激活函数,比如我们在逻辑回归里面使用过的Sigmoid函数,即:

还有后来出现的tanx, softmax,和ReLU等。通过使用不同的激活函数,神经网络的表达能力进一步增强。对于各种常用的激活函数,我们在后面再专门讲。

DNN的基本结构

上一节我们了解了神经网络基于感知机的扩展,而DNN可以理解为有很多隐藏层的神经网络。这个很多其实也没有什么度量标准,多层神经网络和深度神经网络DNN其实也是指的一个东西,当然,DNN有时也叫做多层感知机(Multi-Layer perceptron,MLP),名字实在是多。后面我们讲到的神经网络都默认为DNN。

从DNN按不同层的位置划分,DNN内部的神经网络层可以分为三类,输入层,隐藏层和输出层,如下图示例,一般来说第一层是输入层,最后一层是输出层,而中间的层数都是隐藏层。

层与层之间是全连接的,也就是说,第$i$层的任意一个神经元一定与第$i+1$层的任意一个神经元相连。虽然DNN看起来很复杂,但是从小的局部模型来说,还是和感知机一样,即一个线性关系$z=\sum\limits w_ix_i + b$加上一个激活函数$\sigma(z)$。

由于DNN层数多,则我们的线性关系系数$w$和偏置$b$的数量也就是很多了。具体的参数在DNN是如何定义的呢?

首先我们来看看线性关系系数$w$的定义。以下图一个三层的DNN为例,第二层的第4个神经元到第三层的第2个神经元的线性系数定义为$w_{24}^3$。上标3代表线性系数$w$所在的层数,而下标对应的是输出的第三层索引2和输入的第二层索引4。你也许会问,为什么不是$w_{42}^3$,而是$w_{24}^3$呢?这主要是为了便于模型用于矩阵表示运算,如果是$w_{42}^3$而每次进行矩阵运算是$w^Tx+b$,需要进行转置。将输出的索引放在前面的话,则线性运算不用转置,即直接为$wx+b$。证明将在下面给出。总结下,第$l-1$层的第k个神经元到第$l$层的第j个神经元的线性系数定义为$w_{jk}^l$。注意,输入层是没有$w$参数的

再来看看偏置$b$的定义。我们使用$b^l_j$代表第$l$层中第$j$个元素的偏置,而$a^l_j$第$l$层中第$j$个元素的激活值。还是以这个三层的DNN为例,第二层的第三个神经元对应的偏倚定义为$b_3^{2}$。其中,上标2代表所在的层数,下标3代表偏倚所在的神经元的索引。同样的道理,第三个的第一个神经元的偏倚应该表示为$b_1^{3}$。同样的,输入层是没有偏置参数$b$的

DNN前向传播算法数学原理

在上一节,我们已经介绍了DNN各层线性关系系数$w$,偏置$b$的定义。假设我们选择的激活函数是$\sigma(z)$,隐藏层和输出层的输出值为$a$,则对于下图的三层DNN,利用和感知机一样的思路,我们可以利用上一层的输出计算下一层的输出,也就是所谓的DNN前向传播算法。

对于第二层的的输出$a_1^2,a_2^2,a_3^2$,我们有:

对于第三层的的输出$a_1^3$,我们有:

将上面的例子一般化,假设第$l-1$层共有m个神经元,则对于第$l$层的第j个神经元的输出$a_j^l$,我们有:

其中,如果$l=2$,则对于的$a_k^1$即为输入层的$x_k$。

从表达式中可以看出来,在DNN中每一个输出都对应一个偏置,偏置的数量与该层输出的神经元数目有关。而在CNN中,输入特征经过一个卷积核输出一个特征图,每一个特征图对应一个偏置,而一个batch之内的相同位置特征图的偏置是同一个偏置。之所以这样,跟偏置的作用是增加表达式的非线性有关。思考,对于DNN或者CNN而言,每一个输出都是线性组合,为了增加非线性,直接在输出增加非线性即可。若对于每一个权重都增加非线性,会浪费计算量,并且偏置都是常数,一个常数可以看成是由多个常数之和。

从上面可以看出,使用代数法一个个的表示输出比较复杂,而如果使用矩阵法则比较的简洁。假设第$l-1$层共有$m$个神经元,而第$l$层共有$n$个神经元,则第$l$层的线性系数$w$组成了一个$n\times m$的矩阵$W^l$, 第$l$层的偏置$b$组成了一个$n \times 1$的向量$b^l$ ,第$l-1$层的的输出$a$组成了一个$m \times 1$的向量$a^{l-1}$,第$l$层的的未激活前线性输出$z$组成了一个$n \times 1$的向量$z^{l}$, 第$l$层的的输出$a$组成了一个$n \times 1$的向量$a^{l}$。最后我们需要对$\sigma$完成向量化,记做$\sigma(v)$,其中$\sigma(v)_j = \sigma(v_j)$。例如我们的函数为$f(x) = x^2$,向量化$f$有如下表达式:

将式子$({1})$用矩阵的形式表示出来。

这个表示方法简洁漂亮,后面我们的讨论都会基于上面的这个矩阵法表示来。另外,这个表达式为我们提供了一种更全面的思考一层中的激活值与上一层中的激活值之间的关系的方法:我们只需将权重矩阵与上一层激活值相乘,然后添加偏置向量,最后应用$σ$函数。这样就可以使用矩阵形式进行运算,编程中可以使用大量的库。

关于上面所说的为什么不使用$w^l_{kj}$。如果我们使用的是$w^l_{kj}$,假设第$l-1$层的维度为$m$,而第$l$层的维度为$n$,则$W$的维度为$m \times n$,第$a^{l-1}$层的维度为$m \times 1$,我们写成矩阵形式的化就需要写成$a^{l} = \sigma((W^l)^T a^{l-1}+b^l)$,即对$W$加上转置。

当计算$a^l$的时候,我们需要计算中间变量$z^l \equiv W^l a^{l-1}+b^l$,我们称$z^l$为层$l$中神经元的加权输入。$z^l$也可以拆开为$z^l_j = \sum_k w^l_{jk} a^{l-1}_k+b^l_j$。$z^l_j$含义为第$l$层第$j$个加权输入。

DNN前向传播算法

有了上一节的数学推导,DNN的前向传播算法也就不难了。所谓的DNN的前向传播算法也就是利用我们的若干个权重系数矩阵$W$,偏置向量$b$来和输入值向量$x$进行一系列线性运算和激活运算,从输入层开始,一层层的向后计算,一直到运算到输出层,得到输出结果为值。

输入: 总层数L,所有隐藏层和输出层对应的矩阵$W$,偏置向量$b$,输入值向量$x$

输出:输出层的输出$a^L$

1) 初始化$a^1 = x $

2) for $l = 2$ to $L$, 计算:
最后的结果即为输出$a^L$。

DNN反向传播算法要解决的问题

在上一节中,我们对DNN的模型和前向传播算法做了总结,这里我们更进一步,对DNN的反向传播算法(Back Propagation,BP)做一个总结。

在了解DNN的反向传播算法前,我们先要知道DNN反向传播算法要解决的问题,也就是说,什么时候我们需要这个反向传播算法?

回到我们监督学习的一般问题,假设我们有$m$个训练样本:$(x_1,y_1), (x_2,y_2), …,(x_m,y_m)$,其中$x$为输入向量,特征维度为$n_{in}$,而$y$为输出向量,特征维度为${n_{out}}$。我们需要利用这$m$个样本训练出一个模型,当有一个新的测试样本$(x_{test},?)$来到时,我们可以预测$y_{test}$向量的输出。

如果我们采用DNN的模型,即我们使输入层有$n_{in}$个神经元,而输出层有$n_{out}$个神经元。再加上一些含有若干神经元的隐藏层。此时我们需要找到合适的所有隐藏层和输出层对应的线性系数矩阵$W$,偏置向量$b$,让所有的训练样本输入计算出的输出尽可能的等于或很接近样本输出。怎么找到合适的参数呢?

如果大家对传统的机器学习的算法优化过程熟悉的话,这里就很容易联想到我们可以用一个合适的损失函数来度量训练样本的输出损失,接着对这个损失函数进行优化求最小化的极值,对应的一系列线性系数矩阵$W$,偏置向量$b$即为我们的最终结果。对于DNN而言,我们无法找到精确地数值解,所以在DNN中,损失函数优化极值求解的过程最常见的一般是通过梯度下降法来一步步迭代完成的,当然也可以是其他的迭代方法比如牛顿法与拟牛顿法。如果大家对梯度下降法不熟悉,建议先阅读一个大神写的梯度下降(Gradient Descent)小结

对DNN的损失函数用梯度下降法进行迭代优化求极小值的过程即为我们的反向传播算法。

可以理解为反向传播算法将损失函数值反向传播到每一个权重和偏置上,即计算当我们改变权重和偏差时,损失函数的变化有多快,对应的数学表达式为$\partial J / \partial W$与$\partial J / \partial b$。其中$J$是损失函数,而$W$是权重,$b$为偏置。而梯度下降法、SGD等是如何将计算出的$\partial J / \partial W$与$\partial J / \partial b$的值进行权重和偏置更新的。

损失函数的两个假设

在进行DNN反向传播算法前,我们需要选择一个损失函数,来度量训练样本计算出的输出和真实的训练样本输出之间的损失。你也许会问:训练样本计算出的输出是怎么得来的?这个输出是随机选择一系列$W,b$,用我们上一节的前向传播算法计算出来的。即通过一系列的计算:$a^l = \sigma(z^l) = \sigma(W^la^{l-1} + b^l)$。计算到输出层第$L$层对应的$a^L$即为前向传播算法计算出来的输出。

这里我们定义激活函数:

其中,$m$是训练样本的总个数。我们关于损失函数的第一个假设是$J$可以写成$J = \frac{1}{n} \sum_x J_x$,$J_x$为每个独立训练样本$x$的损失值,具体表达式为$J_x = \frac{1}{2} |y-a^L |_2^2$。之所以做这样的假设,是因为反向传播中使用梯度下降法进行优化,经常让求每个训练样本的偏导数$\partial J_x / \partial W$和$\partial J_x / \partial b$。通过对$\partial J_x / \partial W$和$\partial J_x / \partial b$求平均,我们可以得到$\partial J / \partial W$和$\partial J / \partial b$。为了表达的简便,我们使用$J$代替$J_x$。

我们关于损失函数的第二个假设是,它可以写成神经网络输出的函数:

上图中使用了$C$表示误差,在我们文中使用$J$表示误差。

例如,我们这里的对于一个样本的平方误差函数就可以满足条件:

我们使用$y(x)$表示样本$x$的输出,而使用$y_j$表示某一个特定样本的输出中的第$j$个维度。

这里不把损失函数看成是$y$的函数,是因为$x$固定的那么$y$也是固定的,也就是说对于神经网络这不是一个可以学习的参数。因此,把损失函数看做是$a^L$的函数更有意义。而$y$仅仅是确定损失函数值的一个参数。

DNN反向传播算法的基本思路

DNN可选择的损失函数有不少,通过上节关于损失函数两个假设的讨论,这里我们使用最常见的均方差来度量损失。即对于每个样本,我们期望最小化下式:

其中,$a^L$和$y$为特征维度为$n_{out}$的向量,而$||a^L-y||_2$为$a^L-y$的L2范数。

反向传播实际上计算的是$\partial J / \partial W^l_{jk}$与$\partial J / \partial b^l_j$。在这之前,我们需要计算中间量$\delta^l_j$,它是第$l$层第$j$个神经元的损失值。

中间量$\delta^l_j$

为了理解中间量$\delta^l_j$是如何定义的,想象一下我们的神经网络中有一个恶魔:

恶魔所在的位置是第$l$层的第$j$个神经元位置,当神经元的输入进来时,恶魔就会扰乱神经元的运作,对于神经元的加权输入增加变化$\Delta z^l_j$,则我们需要计算的是$\sigma(z^l_j+\Delta z^l_j)$而不是$\sigma(z^l_j)$。这种变化会在网络的后续层中传播,最终导致总损失函数值的变化$\frac{\partial J}{\partial z^l_j} \Delta z^l_j$。

现在假设恶魔是好的,他试图帮助你降低损失函数值。假设$\frac{\partial J}{\partial z^l_j}$有一个比较大的值,无论是正还是负,只要$\Delta z^l_j$的符号与$\frac{\partial J}{\partial z^l_j}$相反,总损失函数值的变化$\frac{\partial J}{\partial z^l_j} \Delta z^l_j$就会小于零,神经网络就会得到优化。当$\frac{\partial J}{\partial z^l_j}$的值接近零的时候,通过对$z^l_j$干扰,不能使得损失函数值下降。这时神经网络已经取得最优解。受到这个启发,使用$\frac{\partial J}{\partial z^l_j}$度量神经元的损失值。

第$l$层第$j$个神经元的损失值$\delta^l_j$可以如下定义:

我们定义$\delta^l$为层$l$误差的向量形式。

之所以恶魔选择改变$z^l_j$和$\frac{\partial J}{\partial z^l_j}$,而不是$a^l_j$和$\frac{\partial J}{\partial a^l_j}$,是因为选择后者可以得到相同的结论,但是会使得反向传播公式比较复杂。

现在,损失函数$J$有了,中间量$\delta^l_j$来源介绍了。现在我们开始用梯度下降法迭代求解每一层的$W,b$。

Hadamard积

假设$s$和$t$是两个维度相同的向量,我们使用$s \odot t$表示两个向量之间的点乘,也就是说$(s \odot t)_j = s_j t_j$。例如:

输出层的$\delta^L$

首先写出$\delta^L$离散形式

$\partial J / \partial a^L_j$衡量损失函数随第$j$次输出的激活函数值而变化的速度有多快。第二个$\sigma’(z^L_j)$衡量激活函数$σ$在$z^L_j$处变化的速度。

这个式子中$z^L_j$在前向传播过程中已经被计算,而之前假设了损失函数可以写成输出的形式,$\partial J / \partial a^L_j$也就很好计算。例如损失函数为$J = \frac{1}{2} \sum_j (y_j-a^L_j)^2$,则$\partial J / \partial a^L_j = (a_j^L-y_j)$。这个也很好计算。

因此,第一个方程可以进一步写作:

为了方便计算,我们将公式$({BP1})$写成矩阵形式

其中,$\nabla_a J$是$\partial J / \partial a^L_j$向量形式。又因为对于均方差误差函数

因此对于均方误差函数$({BP1})$的完整矩阵形式

式子$({BP1})$证明

这里我们使用下标$j$代表第$L$层中间输出$z$的第$j$元素;下标$k$代表第$L$层输出$a$的第$k$元素。

因为$k \neq j$的时候,$\partial a^L_k / \partial z^L_j$消失。所以上式可以写为:

因为$a^L_j = \sigma(z^L_j)$,所以右边第二项可以写为$\sigma’(z^L_j)$,可得下式得证。

式子$({BP11})$证明

关于式子$({BP11})$的推导,可以将分子写成$\frac{1}{2} \left\{(y_1-a^L_1)^2+(y_2-a^L_2)^2+\dots \right\}$,这是一个标量,接着将式子按照分母布局运算格式展开。对于第$j$项,

可以看出来,分子还是求和形式,但是只有$\frac{1}{2}(y_j-a^L_j)^2$与$a^L_j$有关,因此求导可得 $(a_j^L-y_j)$。带入式子$({BP11})$中第三项即可得证。

式子$({BP1aa})$证明

注意到式${(5)}$中$\frac{1}{2}|| \sigma(z^L)-y ||_2^2=\frac{1}{2} \sum_{i=1}^{n_{out}}(\sigma_i(z^L)-y_i)^2$,那么可得:

而在上式中,$\frac{1}{2} \sum_{i=1}^{n_{out}}(\sigma(z_i^L)-y_i)^2$为标量,$\partial z^L$为向量,按照分母布局,根据标量对向量的求导公式,可以得到下式:

其中,$\sigma^{‘}(z^L)$也是一个向量,可以这么计算。例如这里假设$\sigma=ln(x)$,将$z^L$向量拆成$z_i^L$的标量,带入到$\sigma^{‘}=\frac{1}{x}$即可。

已知$\delta^{l+1}$推导$\delta^l$

这里我们用数学归纳法,第$L$层的$\delta^{L}$上面我们已经求出,假设第$l+1$层的$\delta^{l+1}$已经求出来了,那么我们如何求出第$l$层的$\delta^{l}$呢?我们先给出结论。

首先写出$\delta^l$的离散形式

为了计算方便,我们把上式表示成矩阵的形式

式子$({BP2})$证明

这里我们使用下标$j$代表第$l$层第$j$元素;下标$k$代表第$l+1$层第$k$元素。我们假设第$l$层的维度为$n \times 1$,而第$l+1$层的维度为$m \times 1$

在这里,因为对于特定的$j,k$,$\delta^{l+1}_k$与$\frac{\partial z^{l+1}_k}{\partial z^l_j}$均是标量,因此前后顺序无所谓。

又因为

所以

按照分母布局,最终$\frac{\partial z^{l+1}_k}{\partial z^l_j}$的维度为$m \times n$,我们按照分母布局对上式进行分析,分析过程如下:

对于结果中第一行,即当$j=1$的时候,取$k=1$,求导结果为$w_{11}^{l+1}\sigma’(z^l_1)$;取$k=2$,求导结果为$w_{21}^{l+1}\sigma’(z^l_1)$;取$k=n$,求导结果为$w_{n1}^{l+1}\sigma’(z^l_1)$。

对于结果中第二行,即当$j=2$的时候,取$k=1$,求导结果为$w_{12}^{l+1}\sigma’(z^l_2)$;取$k=2$,求导结果为$w_{22}^{l+1}\sigma’(z^l_2)$;取$k=n$,求导结果为$w_{n2}^{l+1}\sigma’(z^l_2)$。

对于结果中第m行,即当$j=m$的时候,取$k=1$,求导结果为$w_{1m}^{l+1}\sigma’(z^l_m)$;取$k=2$,求导结果为$w_{2m}^{l+1}\sigma’(z^l_m)$;取$k=n$,求导结果为$w_{nm}^{l+1}\sigma’(z^l_m)$。

综合上述分析,可以得到最终的结果

对于特定的$j,k$,$W^{l+1}_{kj}$与$ \sigma’(z^l_j)$均是标量,而$\delta^{l+1}_k$也是一个标量,因此可以互换顺序,代回方程$({17})$,即可得证。

式子$({BP2a})$证明

这里我们假设所有的向量均为列向量,而所有的标量对向量求导均采取分母布局的形式,假设$z^{l+1}$的维度为$n \times 1$,$z^l$的维度为$m \times 1$。根据分母布局,$\frac{\partial z^{l+1}}{\partial z^l}$的维度为$m \times n$,而$\delta^{l+1}$的维度为$n \times 1$。为了保证矩阵可以正常相乘,这里需要将$\frac{\partial z^{l+1}}{\partial z^{l}} $放到前面,将$\delta^{l+1}$放到后面。

可见,用归纳法递推$\delta^{l+1}$和$\delta^{l}$的关键在于求解$\frac{\partial z^{l+1}}{\partial
z^{l}}$。 而$z^{l+1}$和$z^{l}$的关系其实很容易找出:

注意到这里的$z^{l+1}$是$n \times 1$的向量,而$z^l$是$m \times 1$的向量,$w^{l+1}$是$n \times m$的向量,$n^{l+1}$是$n \times 1$的向量。则$z^{l+1}$可以表示如下形式:

在上式中,这是一个$n \times 1$的列向量,而$z^l$是$m \times 1$的列向量,这里用到了列向量对列向量求导,我们采用分母布局计算此式,即标量对列向量求导为列向量,列向量对列向量求导为行向量。所以可得下式:

这样也就很容易写成下式:

将上式带入上面$\delta^{l+1}$和$\delta^{l}$关系式我们可以得到下式:

所以得证。

现在我们得到了$\delta^{l}$的递推关系式,只要求出了某一层的$\delta^{l}$,求解$W^l,b^l$的对应梯度就很简单的。

损失函数对$b$的偏导

首先写出离散形式:

为了计算方便,我们把上式表示成矩阵的形式:

式子$({BP3})$证明

因为存在

所以

带入到式子$({25})$

所以得证

式子$({BP3a})$证明

首先将$z^l= W^la^{l-1} + b^l$式子进行展开,这里表示方便,假设$W$的维度为$n \times m$,$a^{l-1}$的维度为$m \times 1$,$b^l$和$z^l$的维度均为$n \times 1$。所以:

接着按照分母布局,得到列向量对列向量求导,如下:

得到的是$n \times n$的单位矩阵。带入到式子$({BP3a})$中得到:

损失函数对$W$的偏导

首先写出离散形式:

为了计算方便,我们把上式表示成矩阵的形式

式子$({BP4})$证明

这里我们使用下标$j$代表第$l$层$z$的第$j$元素;下标$k$代表第$l-1$层$a$第$k$元素。

因为存在

所以有如下式子,其中分子始终是一个求和的形式,只是求和的项与$j$取值、$k$的个数有关。

当$j=1$,$k$依次从1取到第$l-1$层总个数,上式的结果依次为$a_1^{l-1},a_2^{l-1},\dots$

当$j=2$,$k$依次从1取到第$l-1$层总个数,上式的结果依次为$a_1^{l-1},a_2^{l-1},\dots$

可以看出来,结果只和$k$的取值有关,而和$j$的取值无关。所以可得

式子$({BP4a})$证明

式子中第一项是显而易见的,这里对主要是对第二项$\frac{\partial z^l}{\partial W^l}= (a^{l-1})^T$进行证明,不论是依据行向量对矩阵求导还是列向量对矩阵求导,其结果矩阵不可能是$a^{l-1}$的维度大小,肯定大很多。这里正确的做法是将矩阵形式拆开成每个分量进行计算,推导如下:

首先将$z^l= W^la^{l-1} + b^l$式子进行展开,这里表示方便,假设$W$的维度为$n \times m$,$a^{l-1}$的维度为$m \times 1$,$b^l$和$z^l$的维度均为$n \times 1$。所以:

将$\frac{\partial z^l}{\partial W^l}$写成离散的形式,第$l$层使用$j$下标进行索引,而$l-1$层使用$k$下标进行索引。可以得到下式

上式中最终分子和分母均为标量,分子始终是一个求和的形式,只是求和的项与$j$取值、$k$的个数有关。$W$的维度为$n \times m$,因此可以依次固定$j$,再依次取$k$。具体分析如下:

当$j=1$,即分母取$z^l$的第1行,$k$依次从1取到第$l-1$层总个数,上式的结果依次为$a_1^{l-1},a_2^{l-1},\dots,a_m^{l-1}$

当$j=2$,即分母取$z^l$的第2行,$k$依次从1取到第$l-1$层总个数,上式的结果依次为$a_1^{l-1},a_2^{l-1},\dots,a_m^{l-1}$

当$j=n$,即分母取$z^l$的第n行,$k$依次从1取到第$l-1$层总个数,上式的结果依次为$a_1^{l-1},a_2^{l-1},\dots,a_m^{l-1}$

可以看出来,结果只和$k$的取值有关,而和$j$的取值无关。

所以可得,下式中$a^{l-1}$取转置,是因为它是一个列向量,而由上面的分析,当$j$固定的时候,结果是一个行向量。

通过以上分析,可以发现,之所以使用矩阵求导法则会出现漏洞,并不是因为矩阵求导法则出错了,而是$\frac{\partial z_j^l}{\partial W_{jk}^l}$中分子和分母具有绑定关系,即当分子中$j=1$的时候,在运算中只存在对于$W_{1k}^l$的导数;而且,最终得到的结论是求导的是结果和$j$的取值无关,可以进一步简化结果,所以最终得到了上式结果。

另外,矩阵求导与BP的证明的建议中说到,不论是依据行向量对矩阵求导还是列向量对矩阵求导,其结果矩阵不可能是$a^{l-1}$的维度大小,肯定大很多。但是由$\frac{\partial z^l}{\partial W}$可以推出来等于${\sigma}’\cdot (a^{l-1})^T$,其实这一部分推导时候不能用矩阵求导方法,而是直接拆分。

所以

总结

离散形式的四大方程

若我们选择均方误差函数,则第一个方程可以进一步写作:

从这4个式子中,我们考虑$({BP1})$中的$\sigma’(z^L_j)$,当$\sigma$为sigmoid函数的时候,$\sigma(z^L_j)$的值接近与0或1,这时偏导数$\sigma’(z^L_j) \approx 0$。在这种情况下,通常会说输出神经元已经饱和,这时,权重停止了学习(或学习缓慢)。类似的观点也适用于输出神经元的偏差。

同样的情况也适用于$({BP2})$中的$\sigma’(z^l)$,这个时候,$\delta^l_j$会变得很小,这又意味着,输入到饱和神经元的任何权值都会慢慢地学习

当然,上述讨论不适用于当${w^{l+1}}^T \delta^{l+1}$特别大可以弥补$\sigma’(z^l_j)$的小的情形。上述讨论只是一种总体趋势。

综上所述,我们了解到,如果输入神经元处于低激活状态,或者输出神经元处于饱和状态,则权值的学习速度较慢。这上面的观察结果可以帮助我们设计较好的激活函数。

矩阵形式的四大方程

因为在计算的时候,矩阵形式可以利用线性代数的一些库,使得运算更加方便,因此,在这里我们把矩阵形式的四大方程列出来:

若我们选择均方误差函数,则第一个方程可以进一步写作:

反向传播方程的另外一种形式

$({BP1a})$和$({BP2a})$含有Hadamard积,然而当对Hadamard积不是很了解的时候,可以写成矩阵的形式方便大家理解。

$({BP1a})$可以写成下面的形式:

其中,$\Sigma’(z^L)$方阵,对角元素是$\sigma’(z^L_j)$,非对角元素值为零。

$({BP2a})$可以写成下面的形式:

通过以上两个式子,可以得到

上述这种矩阵的表示方式可能更好理解,并且数值计算的时候,方便。

DNN反向传播算法过程

现在我们总结下DNN反向传播算法的过程。由于梯度下降法有批量(Batch),小批量(mini-Batch),随机三个变种,为了简化描述,这里我们以最基本的随机梯度下降法和批量梯度下降法为例来描述反向传播算法。实际上在业界使用最多的是mini-Batch的梯度下降法。不过区别仅仅在于迭代时训练样本的选择而已。

随机梯度下降法

1) 输入$x$:即$a^1$

2) 前向计算:对于每一个$l = 2, 3, \ldots, L$,计算$z^{l} = W^l a^{l-1}+b^l$,并且$a^{l} = \sigma(z^{l})$。

3) 计算$\delta^L$:$\delta^{L} = \nabla_a J \odot \sigma’(z^L)$

4) 损失反向传播:对于每一个$l = L-1, L-2, \ldots, 2$,计算$\delta^{l} = ((W^{l+1})^T \delta^{l+1}) \odot
\sigma’(z^{l})$

5) 输出:计算梯度,$\frac{\partial J}{\partial W^l_{jk}} = a^{l-1}_k \delta^l_j$和$\frac{\partial J}{\partial b^l_j} = \delta^l_j$。

批量梯度下降法

输入:总层数L,以及各隐藏层与输出层的神经元个数,激活函数,损失函数,迭代步长$\alpha$,最大迭代次数MAX与停止迭代阈值$\epsilon$,输入的m个训练样本$(x_1,y_1),(x_2,y_2), …, (x_m,y_m)$

输出:各隐藏层与输出层的线性关系系数矩阵$W$和偏倚向量$b$

1)初始化各隐藏层与输出层的线性关系系数矩阵$W$和偏倚向量$b$的值为一个随机值。

2)for iter to 1 to MAX:

  2-1) for i =1 to m:

    a) 将DNN输入$a^1$设置为$x_i$

    b) for $l$=2 to L,进行前向传播算法计算$a^{i,l} = \sigma(z^{i,l}) = \sigma(W^la^{i,l-1} + b^l)$

    c) 通过损失函数计算输出层的$\delta^{i,L}= \frac{\partial J}{\partial a^{i,L}}\odot \sigma’(z^{i,L}) $

    d) for $l$= L-1 to 2, 进行反向传播算法计算$\delta^{i,l} = (W^{l+1})^T\delta^{i,l+1}\odot \sigma^{‘}(z^{i,l})$

  2-2) for $l$ = 2 to L,更新第$l$层的$W^l,b^l$:

  2-3)如果所有$W,b$的变化值都小于停止迭代阈值$\epsilon$,则跳出迭代循环到步骤3。

3)输出各隐藏层与输出层的线性关系系数矩阵$W$和偏倚向量$b$。

DNN代码实现

Python2:github链接

Python3:github链接

这里为了理解,贴出Python3的实现过程,注意下面使用的损失函数为平方误差损失函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# %load network.py

"""
network.py
~~~~~~~~~~
IT WORKS

A module to implement the stochastic gradient descent learning
algorithm for a feedforward neural network. Gradients are calculated
using backpropagation. Note that I have focused on making the code
simple, easily readable, and easily modifiable. It is not optimized,
and omits many desirable features.
"""

#### Libraries
# Standard library
import random

# Third-party libraries
import numpy as np

class Network(object):

def __init__(self, sizes):
"""The list ``sizes`` contains the number of neurons in the
respective layers of the network. For example, if the list
was [2, 3, 1] then it would be a three-layer network, with the
first layer containing 2 neurons, the second layer 3 neurons,
and the third layer 1 neuron. The biases and weights for the
network are initialized randomly, using a Gaussian
distribution with mean 0, and variance 1. Note that the first
layer is assumed to be an input layer, and by convention we
won't set any biases for those neurons, since biases are only
ever used in computing the outputs from later layers."""
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]

def feedforward(self, a):
"""Return the output of the network if ``a`` is input."""
for b, w in zip(self.biases, self.weights):
a = sigmoid(np.dot(w, a)+b)
return a

def SGD(self, training_data, epochs, mini_batch_size, eta,
test_data=None):
"""Train the neural network using mini-batch stochastic
gradient descent. The ``training_data`` is a list of tuples
``(x, y)`` representing the training inputs and the desired
outputs. The other non-optional parameters are
self-explanatory. If ``test_data`` is provided then the
network will be evaluated against the test data after each
epoch, and partial progress printed out. This is useful for
tracking progress, but slows things down substantially."""

training_data = list(training_data)
n = len(training_data)

if test_data:
test_data = list(test_data)
n_test = len(test_data)

for j in range(epochs):
random.shuffle(training_data)
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
for mini_batch in mini_batches:
self.update_mini_batch(mini_batch, eta)
if test_data:
print("Epoch {} : {} / {}".format(j,self.evaluate(test_data),n_test));
else:
print("Epoch {} complete".format(j))

def update_mini_batch(self, mini_batch, eta):
"""Update the network's weights and biases by applying
gradient descent using backpropagation to a single mini batch.
The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``
is the learning rate."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
for x, y in mini_batch:
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
self.weights = [w-(eta/len(mini_batch))*nw
for w, nw in zip(self.weights, nabla_w)]
self.biases = [b-(eta/len(mini_batch))*nb
for b, nb in zip(self.biases, nabla_b)]

def backprop(self, x, y):
"""Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``."""
nabla_b = [np.zeros(b.shape) for b in self.biases]
nabla_w = [np.zeros(w.shape) for w in self.weights]
# feedforward
activation = x
activations = [x] # list to store all the activations, layer by layer
zs = [] # list to store all the z vectors, layer by layer
for b, w in zip(self.biases, self.weights):
z = np.dot(w, activation)+b
zs.append(z)
activation = sigmoid(z)
activations.append(activation)
# backward pass
delta = self.cost_derivative(activations[-1], y) * \
sigmoid_prime(zs[-1])
nabla_b[-1] = delta
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# Note that the variable l in the loop below is used a little
# differently to the notation in Chapter 2 of the book. Here,
# l = 1 means the last layer of neurons, l = 2 is the
# second-last layer, and so on. It's a renumbering of the
# scheme in the book, used here to take advantage of the fact
# that Python can use negative indices in lists.
for l in range(2, self.num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
nabla_b[-l] = delta
nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
return (nabla_b, nabla_w)

def evaluate(self, test_data):
"""Return the number of test inputs for which the neural
network outputs the correct result. Note that the neural
network's output is assumed to be the index of whichever
neuron in the final layer has the highest activation."""
test_results = [(np.argmax(self.feedforward(x)), y)
for (x, y) in test_data]
return sum(int(x == y) for (x, y) in test_results)

def cost_derivative(self, output_activations, y):
"""Return the vector of partial derivatives \partial C_x /
\partial a for the output activations."""
return (output_activations-y)

#### Miscellaneous functions
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
return sigmoid(z)*(1-sigmoid(z))

主函数:

1
2
3
4
5
6
# 
# - network.py example:
import network

net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)

DNN反向传播算法小结

有了DNN反向传播算法,我们就可以很方便的用DNN的模型去解决第一节里面提到了各种监督学习的分类回归问题。当然DNN的参数众多,矩阵运算量也很大,直接使用会有各种各样的问题。有哪些问题以及如何尝试解决这些问题并优化DNN模型与算法,我们在下一篇讲。

参考

深度神经网络(DNN)模型与前向传播算法
深度神经网络(DNN)反向传播算法(BP)
BP推导——续
矩阵求导与BP的证明的建议
How the backpropagation algorithm works

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道