反向传播算法(UFLDL版)
1. UFLDL中的一些术语
-
表示输出层的层数(数量),用表示第二层,表示第三层,表示输出层。无论是Nielsen版还是,coursera版都是用“L”表示神经网络的层数(总层数)
小写的经常来表示层数,大写的加角标经常表示第几层, - 表示第l层神经元的个数。
- 表示第层的第个神经元上的偏置
2 证明步骤
2.1 代价函数
假设我们有一个固定样本集,它包含 m 个样例。我们利用批量梯度下降法来求解神经网络。
2.1.2 单个样例的代价函数
对于单个的样例,其代价函数为:
\begin{align}
J(W,b; x,y) = \frac{1}{2} \left| h_{W,b}(x) - y \right|^2 \tag1\
\end{align}
2.1.2 整体代价函数
而整体代价函数为:
KaTeX parse error: No such environment: align at position 8:
\begin{̲a̲l̲i̲g̲n̲}̲
J(W,b)
&= \lef…
以上关于定义中的第一项是一个均方差项。第二项是一个规则化项(也叫权重衰减项),其目的是减小权重的幅度,防止过度拟合。
怎么理解正则化项,这项这么多个求和符号堆在一起,看上去实在很吓人,它其实表示了整个神经网络上的所有弧上的权重,你不要看这个项有很多,其实多层的求和公式放在一起相当于多层循环,$ \sum_{l=1}^{n_l-1} ; \sum_{i=1}^{s_l} ; \sum_{j=1}^{s_{l+1}} \left( W{(l)}_{ji}\right)2$相当于一个三层嵌套的循环:
: 1 ->
: 1 ->
: 1 ->
用语言来描述就是,先从第一层开始,一层一层往后直到倒数第二层,对于每一层执行下面的操作:
从第一个神经元开始到这层最后一个神经元(从上往下数),对于每个神经元执行下面的操作:
每个神经元与下一层所有相连接的的神经元之间连线上的权重,把这个上面的权重罗列出来进行累加。
其实这个累加的部分,不仅表示出这张图所有的权重,而且也指明了找出这个权重的方法。
2.2 梯度下降法
我们的目标是利用梯度下降法来求得参数W和b以使得函数最小。
梯度下降法中每一次迭代都按照如下公式对参数 和 进行更新:
KaTeX parse error: No such environment: align at position 8:
\begin{̲a̲l̲i̲g̲n̲}̲
W_{ij}^{(l)} &…
其中 $\textstyle \alpha $是学习速率。
和代价函数一样,我们也是先求出单个样例代价函数 的偏导数 和 ,之后再推导出整体代价函数 $\textstyle J(W,b) $的偏导数:
KaTeX parse error: No such environment: align at position 8:
\begin{̲a̲l̲i̲g̲n̲}̲
\frac{\partial…
而求单个样例偏导采用的是反向传播算法
2.2.1 针对单个样例的反向传播算法
在深入到具体的推导之前,先说一下大体的思路,先说目标吧:我们是利用整个神经网络的“残差”来计算偏导的,或者说代价函数对参数(权重或者偏置)的偏导可以由当前参数所处层次的临近层的残差来表示。对于最后一层我们可以用神经元的输出减去样本中的y得到最后一层的残差,但是对于隐藏层的残差如何计算呢?我们主要采用了这样几个技术来计算隐藏层的残差:
- 重新定义残差
定义残差为: $ {\delta^{(l)}_i} = \frac{\partial J(W,b;x,y)}{\partial z^{(l)}_i} $ - 隐藏层的残差都用相邻层的残差来表示
- 充分利用相邻两层神经元输入的关系(和之间的数量关系)建立偏导和残差之间的关系以及相邻两层残差之间的关系。
2.2.1.1 具体步骤
-
进行前馈传导计算,利用前向传导公式,得到 $\textstyle L_2, L_3, \ldots $ 直到输出层 的**值。
-
利用重新定义的残差计算公式来计算输出层每个单元的残差
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ \delta^{(n_l)}…
证明过程:
$ \delta^{(n_l)}_i = \frac{\partial}{\partial z^{n_l}i}J(W,b;x,y)= \frac{\partial}{\partial z^{n_l}i}\frac{1}{2} \left|y - h{W,b}(x)\right|^2 $ ,而对于最后一层来说又可以写成,于是$\frac{\partial}{\partial z^{n_l}i}\frac{1}{2} \left|y - h{W,b}(x)\right|^2 = \frac{\partial}{\partial z^{n_l}i}\frac{1}{2} \sum{j=1}^{S{n_l}} (y_j-a_j{(n_l)})2 a_j^{(n_l)} = f(z_j^{(n_l)})\frac{\partial}{\partial z^{n_l}i}J(W,b;x,y) =\frac{\partial}{\partial z^{n_l}i}\frac{1}{2} \sum{j=1}^{S{n_l}} (y_j-f(z_j{(n_l)}))2 J(W,b;x,y)z_j^{(n_l)}j=ij \neq i- (y_i - f(z_i^{(n_l)})) \cdot f’(z^{(n_l)}_i) - (y_i - a^{(n_l)}_i) \cdot f’(z^{(n_l)}_i)$) -
利用相邻两层残差的关系来计算隐藏层的各节点的残差
我们依然从重新定义的残差公式开始推导,$ {\delta^{(l)}_i} = \frac{\partial J(W,b;x,y)}{\partial z^{(l)}i} ll+1\frac{\partial J(W,b;x,y)}{\partial z^{(l)}i} = \sum{j=1}^{s{l+1}} \frac{\partial J}{\partial z^{(l+1)}_j} \frac{\partial z^{(l+1)}_j}{\partial z^{(l)}i}\frac{\partial J}{\partial z{(l+1)}_j}$根据我们之前的定义就是${\delta{(l+1)}j} \sum{j=1}^{s{l+1}} \delta^{(l+1)}_j \frac{\partial z^{(l+1)}j}{\partial z{(l)}_i}$,又因为$z{(l+1)}j = \sum{k=1}{s_l}W{(l)}{jk}a^{(l)}_k + b{(l+1)}_j=\sum_{k=1}{s_l}W{(l)}_{jk}f(z{(l)}_k) + b^{(l+1)}j\frac{\partial z^{(l+1)}j}{\partial z^{(l)}i} = W^{(l)}{ji}f’( z{(l)}_i)$,(这里涉及到累和求导的问题,求导后只留下一项),于是最后的结果是${\delta{(l)}i} = \sum{j=1}^{s{l+1}} W^{(l)}{ji} \delta^{(l+1)}_j f’(z^{(l)}_i)$,整理一下为
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ \delta^{(l)}_i…
注:原文是利用最后一层和倒数第二层之间的关系来证明的,但是我觉得利用任何中间任何两个相邻层来证明才更有一般性,这有点像数学归纳法。 -
利用残差计算代价函数对权重的偏导
我们依然利用相邻两层输入之间的关系:
$ \frac{\partial}{\partial W_{ij}^{(l)}} J(W,b; x, y) = \frac{\partial J}{\partial z^{(l+1)}_j} \frac{\partial z^{(l+1)}j}{\partial W{ij}^{(l)}} $ ,而,用这个公式提现的关系求导得$a^{(l)}j $,于是
\begin{align}
\frac{\partial}{\partial W{ij}^{(l)}} J(W,b; x, y) = a^{(l)}_j \delta_i^{(l+1)} \tag{10}\
\end{align} -
利用残差计算代价函数对偏置的偏导
和上一步的过程一模一样,只是最后一步对偏置进行求导。于是结果为:
\begin{align}
\frac{\partial}{\partial b_{i}^{(l)}} J(W,b; x, y) &= \delta_i^{(l+1)}. \tag{11}\
\end{align}
2.2.1.2 矩阵形式
上面是的推导结果是分量形式的,如果利用矩阵形式的,则公式(8)、(9)、(10)、(11)则表示为:
- 输出层残差计算公式:
\begin{align}
\delta^{(n_l)}= - (y - a^{(n_l)}) \bullet f’(z^{(n_l)}) \tag{12}\
\end{align} - 相邻两层残差的关系:
\begin{align}
\delta^{(l)} = \left((W{(l)})T \delta^{(l+1)}\right) \bullet f’(z^{(l)}) \tag{13}\
\end{align} - 代价函数对权重的偏导
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ \nabla_{W^{(l)… - 代价函数对偏置的偏导
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ \nabla_{b^{(l)…
2.2.2 梯度下降的实现
上面花大量篇幅阐述的反向传播算法只是梯度下降这个整体的一部分,而这个整体又长什么样呢?
2.2.2.1 首先我们再回顾一下利用批量梯度下降法来求参数的整体框架
梯度下降法主要是用来求函数的极值,求参数的问题转化为求“使得函数得到极值的那些变量的值”的问题(转化的方式就是引入“代价函数”的概念)。
简单来说用梯度下降法求极值的方式是一个通过试探的方式试出极值的过程,但是这个试探是不是瞎试探而是有套路的,这个套路就是沿着代价函数自变量(原函数的参数)梯度下降的方向,小步的试探(随便定一些初始值,之后在这个值的基础上做一些增量得到新的值),如果代价函数是凸函数,则通过这种办法一定能收敛。
我们来详细阐述一下什么叫做小步的试探的过程,因为这个我比较模糊,首先代价函数的自变量(原函数的参数)随便取一些初始值(一般为了简单就是全0),之后会在这个初始值的基础上减去一个增量,这个增量的取值方法是有讲究的,对于代价函数的第个自变量来说,这个增量的值等于代价函数在当前这个点(如果是一开始执行这个步骤就是初始值)对这个自变量的偏导数乘以学习率(一般是一个0到1之间的一个数)。由于学习率一般都是直接给出,一般来说梯度下降的难点在求增量上,而求增量的的难点在求梯度上(为什么叫梯度呢,因为一般来说未知的参数有很多个,如果把每个参数看成一个向量中的分量,那么偏导其实就是梯度),而求梯度就是求偏导,一般我们是求出代价函数对每一个自变量的偏导的一般形式,之后再往这一般公式里代数。而对于批量梯度下降法来说,代价函数是由全部样本构成的,或者从统计学的角度来说,它是一个统计量,从形式上来说比较复杂,但是利用微积分的工具进行处理是没有问题的。一般情况下,代价函数一般是一次性定义的(由全部样本的二阶矩构成),但是在神经网络里面代价函数特别一点,它是单个样本的二阶矩组合而成。那么对于神经网络的梯度来说也得是各个样本的代价函数的梯度组合而成。好在尽管需要求每一个样本的梯度,但是所有样本的梯度可以用同一个公式代表,只是往里面带入的值不一样。
2.2.2.2 用自然语言描述迭代的实现细节
了解了批量梯度下降的整体思路,我们再反推为了实现批量梯度下降需要求那些东西,先用自己的想法去求,再和现有的算法对接
为了便于思考我们举一个具体的例子,如下图
这个神经网络的参数包括两个权重矩阵(与)以及两个偏置向量(和)
为了存储权重,其实是需要两个矩阵的和,其中是一个4X3的矩阵,是一个2X4的矩阵分别为:$ W^{(1)} $ = $ \begin{pmatrix} {W^{(1)}}{11} & {W^{(1)}}{12} & {W^{(1)}}{13} \ {W^{(1)}}{21} & {W^{(1)}}{22} & {W^{(1)}}{23} \ {W^{(1)}}{31} & {W^{(1)}}{32} & {W^{(1)}}{33}\ {W^{(1)}}{41} & {W^{(1)}}{42} & {W^{(1)}}{43} \end{pmatrix} $以及 = $ \begin{pmatrix} {W^{(2)}}{11} & {W^{(2)}}{12} & {W^{(2)}}{13} & {W^{(2)}}{14} \ {W^{(2)}}{21} & {W^{(2)}}{22} & {W^{(2)}}{23} & {W^{(2)}}{24} \end{pmatrix}$
而偏置则是向量和第二层神经元的个数一样,和第三层神经元的个数一样,$b^{(1)} = \begin{pmatrix} b^{(1)}_1 \ b^{(1)}_2 \ b^{(1)}_3 \ b^{(1)}_4 \end{pmatrix} b^{(2)} = \begin{pmatrix} b^{(2)}_1 \ b^{(2)}_2 \end{pmatrix} $
我们再假设样本集里有三个样本(小一点的数方便观察规律):
以下是我用我自己的语言描述的梯度下降的实现过程
-
首先所有的参数肯定还是以矩阵的方式存储(向量化运算嘛),定义两个矩阵和以及两个向量和
然后对这些矩阵(为了描述简便两个偏置向量和权重矩阵统称为矩阵)进行初始化,初始化的结果是所有的矩阵里面的分量是一个接近于0的小数; -
之后我们就要对这些参数(对代价函数而言这些是自变量而样本则是参数)进行迭代了,我们先描述第一步迭代
我们先处理权重矩阵再处理偏置矩阵,而在处理权重矩阵内部过程中按照层数递增的顺序来处理,也就是按照,的顺序来处理,同理处理偏置向量的内部过程中也是按照层数递增的顺序来处理,也就是按照,的顺序。- 先看权重矩阵:
- 对于,更新公式为$ W_{ij}^{(1)} = (1 - \lambda){W_{ij}^{(1)}} - \left[ \frac{\alpha}{3} \sum_{i=1}^3 \frac{\partial}{\partial W_{ij}^{(1)}} J(W,b; x^{(i)}, y^{(i)}) \right]
\frac{\partial}{\partial
W_{ij}^{(1)}} J(W,b; x^{(1)}, y^{(1)})\frac{\partial}{\partial
W_{ij}^{(1)}} J(W,b; x^{(2)}, y^{(2)})\frac{\partial}{\partial
W_{ij}^{(1)}} J(W,b; x^{(3)},
y^{(3)}) W_{ij}^{(1)}$; - 对于,更新公式为$ W_{ij}^{(2)} = (1 - \lambda){W_{ij}^{(2)}} - \left[ \frac{\alpha}{3} \sum_{i=1}^3 \frac{\partial}{\partial W_{ij}^{(2)}} J(W,b; x^{(i)}, y^{(i)}) \right]
\frac{\partial}{\partial W_{ij}^{(2)}}
J(W,b; x^{(1)}, y^{(1)})\frac{\partial}{\partial W_{ij}^{(2)}}
J(W,b; x^{(2)}, y^{(2)})\frac{\partial}{\partial W_{ij}^{(2)}}
J(W,b; x^{(3)}, y^{(3)})
W_{ij}^{(2)}$;
- 对于,更新公式为$ W_{ij}^{(1)} = (1 - \lambda){W_{ij}^{(1)}} - \left[ \frac{\alpha}{3} \sum_{i=1}^3 \frac{\partial}{\partial W_{ij}^{(1)}} J(W,b; x^{(i)}, y^{(i)}) \right]
- 再看偏置矩阵:
- 对于,更新公式为$ b_{i}^{(1)} ={b_{i}^{(1)}} - \left[ \frac{1}{3} \sum_{i=1}^3 \frac{\partial}{\partial b_{i}^{(1)}}
J(W,b; x^{(i)}, y^{(i)}) \right]
\frac{\partial}{\partial
b_{i}^{(1)}} J(W,b; x^{(1)}, y^{(1)})\frac{\partial}{\partial
b_{i}^{(1)}} J(W,b; x^{(2)}, y^{(2)})\frac{\partial}{\partial
b_{i}^{(1)}} J(W,b; x^{(3)},
y^{(3)}) b_{i}^{(1)}$; - 对于,更新公式为$ b_{i}^{(2)} ={b_{i}^{(2)}} - \left[ \frac{1}{3} \sum_{i=1}^3 \frac{\partial}{\partial b_{i}^{(2)}}
J(W,b; x^{(i)}, y^{(i)}) \right]
\frac{\partial}{\partial b_{i}^{(2)}}
J(W,b; x^{(1)}, y^{(1)})\frac{\partial}{\partial b_{i}^{(2)}}
J(W,b; x^{(2)}, y^{(2)})\frac{\partial}{\partial b_{i}^{(2)}}
J(W,b; x^{(3)}, y^{(3)})
b_{i}^{(2)}$;
- 对于,更新公式为$ b_{i}^{(1)} ={b_{i}^{(1)}} - \left[ \frac{1}{3} \sum_{i=1}^3 \frac{\partial}{\partial b_{i}^{(1)}}
这样,参数第一次迭代就结束了;
- 先看权重矩阵:
-
之后我们将第一次参数迭代的结果和样本都放到代价函数里计算得到代价函数的一个值,这个值主要是用来监控是代价函数的值是否收敛
-
重复上面的操作若干次(以往的作业里是400次),对于少数据量的案例差不多就能收敛了
2.2.2.3 伪代码描述迭代过程
上面的实现过程用自然语言描述的目的主要是为了方便理解,如果要用伪代码描述,要考虑一些实现的工具和优化,比如用考虑如何精简存储空间,或者用较少的循环次数一次性计算出更多的结果。
第一次优化
上面是从参数更新公式的角度去思考的,公式需要什么数,我就去计算什么数,虽然思路很清晰但是效率不高,比如说我每次需要用偏导数的时候都要调用m次(样本数量)反向传播算法,如果神经网络一共有层,那么一共有个参数矩阵(包括个权重矩阵以及个偏置矩阵),那么就要调用次反向传播算法。效率很低。
其实每一次调用反向传播算法的时候是可以把代价函数在当前样本下对所有参数(包括权重和偏置)的梯度计算出来的,我们可以把这些值都保存下来,因为之后都要用的,而不是到用到的时候才去计算,这样我们只需要调用m次(样本数量)反向传播算法,就可以把所有的值计算出来,之后用的时候直接拿过来用就可以了。伪代码描述如下:
{begin}
先进行m次循环:
第次调用反向传播算法的结果保存在4个单元里
从第1层到第层
求代价函数对权重矩阵的偏导,同时更新权重参数
求代价函数对偏置矩阵的偏导,同时更新偏置参数
{end}
这个方法有一个问题就是比较耗费存储空间,假设一个矩阵用一个存储单元来计算的话,那么一个层的神经网络,将每一次调用反向传播算法的结果保存就需要个存储单元,m个样本就需要个存储单元,这是典型的拿空间换时间,正好和上面时空开销相反。
真的需要那么多存储单元吗?我们再仔细观察一下,运算的过程,为了便于观察,我们就用这三个样本来进行观察。“以调用m次反向传播算法为主线的”的思路可以用下面这张图来描述。为了方便指代,我们把代价函数在样本处对参数矩阵的偏导数用一个记号来表示,比如A1,A2,B1,C3等等,如下图:
先进行3次循环
第一次调用反向传播算法产生的结果保存在A1,A2,A3,A4里,
第二次调用反向传播算法产生的结果保存在B1,B2,B3,B4里,
第三次调用反向传播算法产生的结果保存在C1,C2,C3,C4里,
对于神经元的第1层:
求代价函数对的偏导:;之后更新权重矩阵:$ W_{ij}^{(1)} = (1 - \lambda){W_{ij}^{(1)}} - \frac{\alpha}{3} \frac{\partial}{\partial W_{ij}^{(1)}} J(W,b) $
求代价函数对的偏导;之后再更新偏置矩阵:$ b_{i}^{(1)} ={b_{i}^{(1)}} - \frac{1}{3} \frac{\partial}{\partial b_{i}^{(1)}} J(W,b) $
对于神经元的第2层:
求代价函数对的偏导:;之后更新权重矩阵:$ W_{ij}^{(2)} = (1 - \lambda){W_{ij}^{(2)}} - \frac{\alpha}{3} \frac{\partial}{\partial W_{ij}^{(2)}} J(W,b) $
求代价函数对的偏导;之后再更新偏置矩阵:$ b_{i}^{(2)} ={b_{i}^{(2)}} - \frac{1}{3} \frac{\partial}{\partial b_{i}^{(2)}} J(W,b) $
其实我们可以发现,求,,,的过程是四个相对独立的过程,虽然在每次调用反向传播算法的时候都会分别的为这四个过程提供计算材料,但是这四个过程所需要的材料彼此之间没有什么关联,是独立的,因此为了研究优化方式我们只看一个过程就可以了,把其他的都盖上,如下图。
我们可把这个过程抽象为为了计算一个值S,需要调用三次函数f(x),每次传给参数的值是a,b,c,S = f(a)+f(b)+f©。这个过程其实可以看成一个累和的过程,f(a),f(b),f©在计算中只用了一次就不再使用,如果我们并不要求一次性求出S,而是分几次求出S,那么我们可以不要求那么多存储空间,如果次数等于3,那么我们只需要一个存储空间就可以了,
for i in [a,b,c] :
s = s + f(i)
这个模式是不是特别基础,回忆一下我们刚开始学编程的时候在举例for的用法的时候,总会举这样一个例子——求从1到100的自然数的和。我们采用的方法是并不是把从1到100这100个数都到100个单元里然后在同一次里把这100个单元加在一起,而是利用一个单元通过累加的方式(迭代100次)得到。当时觉得这样不是理所应当的吗,但是现在我觉得要好好想想,凭什么不能把从1到100这100个数存到100个单元里之后在一次性的运算出结果,从结果上也能实现啊。为什么这两种都能实现的方法,前者(单存储多次运算)比后者(多存储一次运算)要好呢?我是这么理解的,其实这里隐含这一个评价体系,就是看程序运行的开销,平均开销小的为好,用一个比较严谨的说法是算法复杂度。从时间开销来看,对于100个数这样一个数量级来说,O(1)和O(100)是一个数量级的事情,没有区别,但是1个存储单元和100个存储单元还是有点区别的,特别是单个单元存储的数据量比较大,是一个大数的时候(参考文献2 fristn的例子),在计算机早期内存又小又昂贵的时候更是这样。其实计算机是擅长做循环的,因为存储资源,计算速度是富裕的资源,所以尽量把大任务拆成小任务多次运算是一个常见的思考范式。
再回到梯度下降的实现上来,其实我们可以发现为了最后计算出整体代价函数对参数矩阵的偏导,我们只需要预先申请个存储单元(和参数矩阵的总数一样)就可以了。
第二次优化的想法
我们需要的存储单元包括下面的部分:
- 个参数矩阵,包括个权重矩阵以及个偏置矩阵,这些矩阵一开始要进行随机初始化,而不是全零初始化
- 在调用反向传播算法计算单个样例代价函数对参数矩阵的偏导所需要的存储空间也是个,其中存储单个样例代价函数对权重矩阵的偏导数的存储单元有个,存储单个样例代价函数对偏置矩阵的偏导数的存储单元有个
- 保存整体代价函数对参数矩阵的偏导数也需要个,其中需要个,其中也需要个
下面是算法部分:
- 从1到,对所有的及进行随机初始化;对所有及进行全零初始化
- 对于到
a. 使用反向传播算法计算出所有的和;
b. 计算$\nabla_{W^{(l)}}J(W,b) := \nabla_{W^{(l)}}J(W,b) + \nabla_{W{(l)}}J(W,b;x{(i)},y^{(i)}) $
c. 计算$\nabla_{b^{(l)}}J(W,b) := \nabla_{b^{(l)}}J(W,b) + \nabla_{b{(l)}}J(W,b;x{(i)},y^{(i)}) $ - 更新权重参数:
a. $W^{(l)} := W^{(l)} - \alpha \left[ \left(\frac{1}{m} \nabla_{W^{(l)}}J(W,b) \right) + \lambda W^{(l)}\right] $
b.
这样参数就完成了一次迭代
3. 补充说明
3.1 为什么权重衰减项不不包括偏置项$ \textstyle b^{(l)}_i$
因为对最终的神经网络产生的影响实在是太小了。
3.2 权重衰减项的本质
权重衰减实际上是贝叶斯规则化方法的变种,在贝叶斯规则化方法中,我们将高斯先验概率引入到参数中计算极大后验估计(MAP)而不是极大似然估计(maximum likelihood estimation)。
3.3 随机初始化
我们需要将每一个参数 和 初始化为一个很小的、接近零的随机值(比如说,使用正态分布 $\textstyle {Normal}(0,\epsilon^2) $生成的随机值,其中 设置为 ),我们之所以这么做(并非全部初始化为0,而是初始化为接近与0的小数)是因为,如果我们用相同e值作为初始值,那么所有隐藏层单元最终会得到与输入值有关的,相同的函数,(也就是说,对于所有 都会取相同的值,那么对于任何输入$ \textstyle x \textstyle a^{(2)}_1 = a^{(2)}_2 = a^{(2)}_3 = \ldots $),这种情况叫做对称,而随机初始化的目的就是破除这种现象,也就是对称失效。