文章 12
评论 4
浏览 20910
神经网络反向传播算法的推导

神经网络反向传播算法的推导

概述

  在深度学习领域的研究中,神经网络有着举足轻重的地位,神经网络相关的理论也是整个深度学习研究的基石。其中,神经网络的反向传播算法又是神经网络理论的核心所在。如今各种深度学习框架百花齐放,相关的开源社区也蓬勃发展,这些深度学习框架大部分都为我们封装了神经网络的基本操作以及自动求导的功能,这样我们只需用几十行代码就可以训练一个简单的神经网络而不需要明白其内部的原理。当然,深度学习框架的发展有利亦有弊。一方面,使用深度学习框架就可以不用重复造轮子,让我们可以专注于算法本身的研究上,不需要编写过多的额外代码。另一方面,深度学习框架的出现大大降低了深度学习的门槛,这导致了广大所谓的“研究者”只知其然不知其所以然,所谓的深度学习研究就退化成了“调参”,这恐怕也是深度学习被广为诟病的特点之一。

  基于上述观点,我认为一个合格的深度学习研究者在数学上一定要过关,事实上任何理工科的科研脱离了数学都是扯淡。我很早就想学习一下神经网络反向传播算法的具体内容,但却一直拖到了现在。这几天趁着矩阵论刚考完还热乎着,准备结合网上的一些资料尝试一下推导反向传播算法,就写下了这篇博客,本文只讨论全连接神经网络和卷积神经网络基于梯度下降的反向传播算法。

全连接神经网络反向传播算法

  全连接神经网络的结构比较简单,它由若干层神经元节点相连而成,其中每一层的神经元节点都与前后层的每一个神经元直接相连,每一条连接都是一个特定的权重,如下图所示(图源网络)
dnn.png

简单来说就是,上一层的每个神经元所产生的信号都会乘以一个权重传入到本层的神经元之中,而神经元本身的作用是在输入信号作用一个非线性函数然后输出,这个非线性函数称为激活函数。不难发现神经网络的计算可以写成矩阵的形式,即

y=Wx+b

其中 x 表示上一层的输入向量(长度为上一层节点个数),W 表示权重矩阵,y 为输出向量(长度为本层节点数),权重矩阵的元素 \omega_{ij} 表示上一层第 i 个节点和本层第 j 个节点的连接权重,b 表示一个偏置向量。

  现在考虑一般的情况,假设神经网络共有 n_l 层,l 层权重矩阵为 W^{(l)},偏置向量为 b^{(l)},输出向量为 x^{(l)},设激活函数为 f,记

u^{(l)}=W^{(l)}x^{(l-1)}+b^{(l)}
x^{(l)}=f(u^{(l)})

问题的关键在于求 x^{(l)} 对权重和偏置的梯度,在计算梯度前先考虑一个简化的情况,设

u=Wx

函数 f(u) 输出值为标量,求输出值对权重的梯度,根据链式法则有

\frac {\partial f} {\partial \omega_{ij}}=\sum_{k=1}^{m}\frac {\partial f} {\partial u_{k}}\frac {\partial u_k} {\partial \omega_{ij}}=\sum_{k=1}^{m}\frac {\partial f} {\partial u_{k}}\frac {\partial (\sum_{l=1}^n\omega_{kl}x_l)} {\partial \omega_{ij}}

注意到 f\omega_{ij} 的偏导数只和 W 的第 i 行元素相关,于是将上面的式子化简为

\frac {\partial f} {\partial \omega_{ij}}=\frac {\partial f} {\partial u_{i}}\frac {\partial (\sum_{l=1}^n\omega_{il}x_l)} {\partial \omega_{ij}}=\frac {\partial f} {\partial u_{i}}x_j

根据标量对矩阵变量求导的法则可得

\frac {\partial f} {\partial W}=\begin{pmatrix} \frac {\partial f} {\partial \omega_{11}} &\cdots &\frac {\partial f} {\partial \omega_{1n}} \\\\ \cdots & \cdots & \cdots \\\\ \frac {\partial f} {\partial \omega_{m1}} & \cdots & \frac {\partial f} {\partial \omega_{mn}} \end{pmatrix}=\begin{pmatrix} \frac {\partial f} {\partial u_{1}}x_1 &\cdots &\frac {\partial f} {\partial u_{1}}x_n \\\\ \cdots & \cdots & \cdots \\\\ \frac {\partial f} {\partial u_{m}}x_1 & \cdots & \frac {\partial f} {\partial u_{m}}x_n \end{pmatrix}

将其整理成另一种形式为

\nabla_W f=(\nabla_uf)x^T

其中 \nabla_uf=(\frac {\partial f} {\partial u_{1}},\cdots,\frac {\partial f} {\partial u_{n}})^Tx=(x_1,x_2,\cdots,x_n)^T。现在考虑更复杂的一种情况,给原式添加一个偏置项,即

u=W x+b

易知此时 f\omega_{ij} 的偏导数不变,计算 f 对偏置向量的梯度

\frac {\partial f} {\partial b_i}=\sum_{k=1}^{m}\frac {\partial f} {\partial u_{k}}\frac {\partial u_k} {\partial b_i}=\sum_{k=1}^{m}\frac {\partial f} {\partial u_{k}}\frac {\partial (\sum_{l=1}^n\omega_{kl}x_l + b_k)} {\partial b_i}

观察上面这个式子不难推出,f 对偏置向量的梯度

\nabla_b f=\nabla_uf

现在考虑神经网络的输出层,一般来说我们需要计算输出与真实值之间的损失函数值(标量),设损失函数为 L,根据上面的结论有

\nabla_{W^{(l)}} f=(\nabla_{u^{(l)}} L)(x^{(l-1)})^T
\nabla_{b^{(l)}} f=\nabla_{u^{(l)}} L

到这里,问题就转化为如何求 \nabla_{u^{(l)}} L,这一项的计算需要分为输出层和隐藏层两种情况讨论。首先看输出层,计算损失函数时需要计算各个分量的损失函数值,由于损失函数独立地作用于输出向量的各个分量,因此有

\frac {\partial f} {\partial u_i}=\frac {\partial f} {\partial x_i}\frac {\partial x_i} {\partial u_i}

将计算结果写成矩阵形式为

\nabla_{u^{(l)}} L=\nabla_{x^{(l)}} L\bigodot f^\prime(u^{(l)})

其中 \bigodot 表示两个向量对应位置元素相乘。如果当前层为隐藏层,不妨设第 l+1 层的梯度已经算出,根据全连接神经网络的特点有

u^{(l+1)}=W^{(l+1)}x^{(l)}+b^{(l+1)}=W^{(l+1)}f(u^{(l)})+b^{(l+1)}

这里我们需要计算损失函数 Lu^{(l)} 的梯度,由于 \nabla_{u^{(l+1)}} L 已知,因此很容易想到的一个思路是利用链式法则,在计算之前先考虑一个简化的情况,对于

u=W x

标量函数为 f(u),已知 \nabla_{u} f 来计算 \nabla_{x} f,考虑到 x 的每一个分量都和 y 有关,根据链式法则有

\frac {\partial f} {\partial x_i}=\sum_{j=1}^{m}\frac {\partial f} {\partial u_{j}}\frac {\partial u_j} {\partial x_i}=\sum_{j=1}^{m}\frac {\partial f} {\partial u_{j}}\frac {\partial (\sum_{l=1}^{n}\omega_{jl} x_l)} {\partial x_i}=\sum_{j=1}^{m}\frac {\partial f} {\partial u_{i}} \omega_{ji}

上面的结果可以写成矩阵形式,即

\nabla_{x} f=W^T\nabla_{u} f

现在我们回过头看隐藏层梯度的计算,根据上面的结论有

\nabla_{u^{(l)}} L=\nabla_{x^{(l)}} L\bigodot f^\prime(u^{(l)})=(W^T\nabla_{u^{(l+1)}} f)\bigodot f^\prime(u^{(l)})

将两种情况放在一起,写成下面的式子

\delta^{(l)}=\nabla_{u^{(l)}} L=\begin{cases} \nabla_{x^{(l)}} L\bigodot f^\prime(u^{(l)}), & l=n_l \\\\ (W^T\nabla_{u^{(l+1)}} f)\bigodot f^\prime(u^{(l)}),&l \neq n_l \end{cases}

上面这个式子意味着,可以通过迭代的方式计算损失函数对各层权重和偏置的梯度。输出层的梯度可以直接计算得到,由于 \delta^{(l)} 依赖于 \delta^{(l+1)},因此可以通过输出层的梯度计算上一个隐藏层的梯度。以此类推,从后往前逐层计算损失函数对权重和偏置的梯度后更新权重和偏置,反向传播也因此得名。具体来说,损失函数对某一层的权重的梯度为

\nabla_{W^{(l)}} f=\delta^{(l)}(x^{(l-1)})

对偏置的梯度为

\nabla_{b^{(l)}} f=\delta^{(l)}

按照梯度下降的方法更新的公式为

W^{(l)}=W^{(l)}-\eta \nabla_{W^{(l)}} L
b^{(l)}=b^{(l)}-\eta \nabla_{b^{(l)}} L

上面的一组公式就是全连接神经网络反向传播算法的主要内容,其中 \eta 称为学习率,可以理解为每次学习的步长。基于梯度下降的反向传播算法,简单来说就是每次训练的迭代结束后,计算损失函数对各层权重和偏置的梯度,然后让这些参数减去梯度乘以学习率。我们知道,对于一个可微的函数来说,梯度是它的函数值变化最快的方向,而训练的目标是寻找一组参数使得损失函数值最小,因此很自然的一个想法是让自变量沿着梯度反方向变化。可以设想一下,我们处于一个崎岖不平的山地之中,在不知道具体地形的前提下我们希望前往海拔最低点,梯度下降的思想就是在任意一个位置确定一个当前位置向下最陡的方向,然后沿这个方向移动一个特定的步长,重复这个操作我们必然能够到达一个极小值点。值得一提的是,神经网络的训练并不是一个凸优化问题,它可能存在很多极小值点,因此很多时候按照梯度下降算法训练会陷入局部最优,这显然不是我们希望看到的。深度学习发展到现在,已经产生了很多优化的算法,可以有效防止陷入局部最优,具体的算法内容可以参考相关论文。

卷积神经网络反向传播算法

卷积层

  卷积神经网络的运算远比全连接神经网络要复杂,因为卷积过程中卷积核会重复作用于每个点,卷积的详细操作见下图(图源网络)

cnn.gif

不难看出,卷积运算就是让一个卷积核在输入图像上按照特定步长进行滑动,每次计算卷积核上各个元素与图像对应元素的乘积相加,这个值就是卷积后图像某个像素的值。大多数情况下,卷积后的图像尺寸与输入图像的尺寸是不一样的,根据卷积运算的特点,对于 N \times N 的输入图像,卷积核尺寸为 F \times F,步长为 S,则输出图像尺寸为

O=\frac {N-F} {S} +1

现在来讨论一般的情况,设第 l 层卷积核尺寸为 s*s,卷积核元素为 k_{pq}^{(l)},偏置为 b^{( l )},输出图像像素值为 x_{ ij }^{( l )},则卷积运算可以写成下面的表达式

x_{ij}^{(l)}=f(u_{ij}^{(l)})=f(\sum_{p=1}^{s}\sum_{q=1}^{s}x_{i+p-1, j+q-1}^{(l-1)} \times k_{pq}^{(l)}+b^{(l)})

其中 f 为本层的激活函数,i,j 分别为输出图像的行标和列标。对于损失函数 L,不妨设 \frac {\partial L} {\partial x_{(ij)}^{(l)}} 已经求出,仍然使用链式法则计算它对卷积核的梯度,即

\frac {\partial L} {\partial k_{pq}^{(l)}}=\sum_i \sum_j \frac {\partial L} {\partial x_{ij}^{(l)}}\frac {\partial x_{ij}^{(l)}} {\partial u_{ij}^{(l)}}\frac {\partial u_{ij}^{(l)}} {\partial k_{pq}^{(l)}}

等式右边乘积项的第二个因子很容易就可以求出,因为它表示经过激活函数后的值对原值的偏导数,即

\frac {\partial x_{ij}^{(l)}} {\partial u_{ij}^{(l)}}=f^{\prime}(u_{ij}^{(l)})

第三个因子表示的是经过卷积运算后的值对原值的偏导数,它的计算就复杂得多,将上面提到的卷积运算的表达式代入,注意到求和项中只有一项对 k_{(pq)}^{(l)} 的偏导数不为 0,于是

\frac {\partial u_{ij}^{(l)}} {\partial k_{pq}^{(l)}}=\frac {\partial (\sum_{p=1}^{s}\sum_{q=1}^{s}x_{i+p-1, j+q-1}^{(l-1)} \times k_{pq}^{(l)}+b^{(l)})} {\partial k_{pq}^{(l)}}=x_{i+p-1, j+q-1}^{(l-1)}

结合上面几步的推导,可以求出损失函数对卷积核的偏导数为

\frac {\partial L} {\partial k_{pq}^{(l)}} = \sum_i \sum_j \frac {\partial L} {\partial x_{ij}^{(l)}}f^{\prime}(u_{ij}^{(l)})x_{i+p-1, j+q-1}^{(l-1)}

利用同样的原理也可以计算损失函数对偏置项(注意,各个像素的偏置相同,因此偏置项可以看作一个标量)的偏导数,即

\frac {\partial L} {\partial b^{(l)}} =\sum_i \sum_j \frac {\partial L} {\partial x_{ij}^{(l)}}\frac {\partial x_{ij}^{(l)}} {\partial u_{ij}^{(l)}}\frac {\partial u_{ij}^{(l)}} {\partial b^{(l)}}= \sum_i \sum_j \frac {\partial L} {\partial x_{ij}^{(l)}}f^{\prime}(u_{ij}^{(l)})

观察上面两个式子,我们可以仿照前面全连接神经网络一样定义其误差项为

\delta_{ij}^{(l)}=\frac {\partial L} {\partial x_{ij}^{(l)}}f^{\prime}(u_{ij}^{(l)})

需要指出的是,每层的误差项都是一个矩阵,这个矩阵的尺寸等于卷积层输出图像的尺寸,这样损失函数对卷积核的偏导数就可以写成

\frac {\partial L} {\partial k_{pq}^{(l)}} = \sum_i \sum_j \frac {\partial L} {\partial x_{ij}^{(l)}}f^{\prime}(u_{ij}^{(l)})x_{i+p-1, j+q-1}^{(l-1)}=\sum_i \sum_j \delta_{ij}^{(l)}x_{i+p-1, j+q-1}^{(l-1)}

对照这个式子和前面的卷积运算表达式的形式不难看出,这也是一个卷积运算,表示以误差矩阵对输入图像的卷积,且没有偏置项,那么损失函数对卷积核的梯度就可以写作

\nabla_{K^{(l)}} L=conv(X^{(l-1)}, \delta^{(l)})

至此,问题就转化成了如何求解误差矩阵 \delta^{(l)},和全连接网络类似,输出层的误差项可以直接通过损失函数计算求得。如果当前层并非输出层,需要根据下一层分类讨论,如果下一层是全连接层,那么可以根据上一节讨论的全连接层误差传递方式得到其误差项,池化层的传播在后面讨论。现在需要讨论的是,卷积层如何将误差传播到上一层。假设我们已经求得 \delta_{ij}^{(l+1)},现在需要根据它来计算 \delta_{ij}^{( l )},由定义和链式法则有

\delta_{ij}^{( l )}=\frac {\partial L} {\partial x_{ij}^{(l)}}f^{\prime}(u_{ij}^{(l)})=(\sum_m \sum_n (\frac {\partial L} {\partial u_{mn}^{(l+1)}}\frac {\partial u_{mn}^{(l+1)}} {\partial x_{ij}^{(l)}}))f^{\prime}(u_{ij}^{(l)})

这个式子中 \frac {\partial L} {\partial u_{rs}^{(l+1)}} 就是 l+1 层的误差项,为了便于计算另一个乘积项,不妨将偏置忽略,代入卷积运算表达式得到

\frac {\partial u_{mn}^{(l+1)}} {\partial x_{ij}^{(l)}}=\frac {\partial (\sum_{p=1}^{s}\sum_{q=1}^{s}x_{m+p-1, n+q-1}^{(l)} \times k_{pq}^{(l+1)})} {\partial x_{ij}^{(l)}}

和对卷积核的偏导数不同的是,对输入图片的偏导数会产生不止一个不为零的项,这是因为卷积操作会对很多像素进行不止一次运算,基于这个原理,可以将误差项化简为

\delta_{ij}^{( l )} = \sum_m \sum_n(\delta_{mn}^{( l+1 )}k_{i+1-m,j+1-n}^{(l+1)})

这个式子可以看作是 l+1 层卷积核旋转 180 度之后对误差矩阵做卷积运算,由此可以得到误差传递的公式为

\delta_{ij}^{( l )} =conv(\delta^{(l+1)}, rot180(K^{(l+1)})) \bigodot f^{\prime}(u_{ij}^{(l)})

池化层

  与卷积层不同的是,池化层没有参数,因此无需对池化层求权重和偏置的偏导数。换句话说,在反向传播时池化层的作用就是传递误差。正向传播过程中,池化层一般对图像进行下采样操作将图片压缩,那么在反向传播时应对误差做上采样操作。这里主要讨论平均池化和最大池化的反向传播,首先看平均池化,对于输入图像 s\times s 的子块,其输出为

y=\frac {1}{s\times s}\sum_i^k x_i

如果设这个位置上一层传递的误差值为 \delta,依据链式法则有

\frac {\partial L} {\partial x_i }=\frac {\partial L} {\partial y }\frac {\partial y} {\partial x_i }=\frac {\delta} {s\times s}

在传递误差时,应将 \delta^{( l )} 中每一项都扩充为 s\times s 的矩阵,形式为

\begin{pmatrix} \frac {\delta} {s\times s}& \cdots & \frac {\delta} {s\times s} \\\\ \cdots &\cdots &\cdots \\\\ \frac {\delta} {s\times s}&\cdots &\frac {\delta} {s\times s} \end{pmatrix}

接下来看最大池化的情况,最大池化的表达式为

y=max(x_1,x_2,\cdots ,x_k)

同样根据链式法则计算得到

\frac {\partial L} {\partial x_i }=\frac {\partial L} {\partial y }\frac {\partial y} {\partial x_i }=\delta \frac {\partial y} {\partial x_i }

由于最大池化只保留了最大的一项,因此 \frac {\partial y} {\partial x_i } 只在最大的位置处有值,因此传递误差时将 \delta^{( l )} 中每一项都扩充为 s\times s 的矩阵,除了原来最大位置的值为 \delta 以外,其余均为 0。

  至此,卷积神经网络的反向传播公式的推导全部完成,有了损失函数对每一层参数的梯度,就可以根据上一节最后的公式以梯度下降的方式更新参数。在实际应用中,图像和卷积核的通道数一般都大于 1,本节只是讨论了二维情况的反向传播算法,仿照二维情况做一些简单的推导就可以将其推广到多个通道的卷积上,这里不再赘述。

总结

  神经网络的反向传播算法原理上并不难,其核心思想就是利用导函数的链式法则计算各层参数的梯度,但中间涉及到的诸多矩阵运算着实给整个推导过程带来了不小的麻烦。现在主流的深度学习框架都提供了自动求导功能,只需要调用相关接口搭建好神经网络,程序就能自动计算反向传播过程中的梯度来更新参数。从本文的推导过程也可以看出,必须在正向传播过程中保存中间变量,这样反向时才能正常计算梯度,也正是这个原因,实际训练过程中模型所占用的显存会远大于模型参数理论上所需要的显存,其中大部分显存就是用于保存中间变量。


标题:神经网络反向传播算法的推导
作者:coollwd
地址:http://coollwd.top/articles/2019/12/31/1577771360985.html

Everything that kills me makes me feel alive

取消