该文档将以含有两个隐藏层的神经网络为基础进行正向和反向的公式推导,因为一个隐藏层的网络太简单,多个隐藏层与两个隐藏层在推导上没有本质区别,所以综合平衡知识性与文档撰写的便捷性,两个隐藏层比较合适。
整个文档主要包含以下内容或者特点:
- 符号表示要足够清晰
- 中间步骤尽量详细
- 把batch_size引入进来
- 以向量化的方式推导
- 使用两个Loss:L2和Cross Entropy Loss
符号表示
全连接神经网络的正反传都不难,但是一定要有清晰的,好的符号表示系统,否则容易出错。有些符号可能开始不易被理解,但是结合图例和公式推导过程,后面慢慢应该就会感受到其中的用处。
推导过程中要特别注意矩阵的shape,因为矩阵乘法占据了绝大部分的运算,所以搞清楚矩阵乘法对shape的要求,即前矩阵的第二个维度应等于后矩阵的第一个维度,就可以有效地避免犯错。
图1. 全连接神经网络结构图
- 第 l 层的节点数用 nl 表示;
-
zi[l] 表示第 l 层第 i 个节点的值,z[l] 的shape为 (b, nl),其中 b 表示batch_size;
-
ai[l] 表示与 zi[l] 对应的**值,**函数记为 f(z) 其导数记为 f′(z) ,其shape与 zi[l] 相同;输入用 a 表示,整个公式流程会更统一;
-
w[l], b[l] 表示第 l 层的weight和bias,w[l] 的shape为(nl−1, nl),b[l] 的shape为(1, nl);
- 以 x 表示任意变量,包括 z,a,w,b 等,那么 dx 表示该变量的梯度,shape与 x 相同,计算公式为 dx=∂Loss/∂x
根据上述符号表示方法(主要是看shape的对应关系),用于全连接的线性运算应当写为:a[l−1] ∗w[l] +b[l] ,可以看到是 a 在前 w 在后,而不是我们熟悉的 w 在前 a 在后。这种表示至少有以下两方面好处:
- 公式与结构图的对应更加顺滑,因为在图中,a[l−1] 的位置在 w[l] 的前面;
- 可以把batch_size放在第一个维度,这也是各个深度学习框架的通用做法。
公式推导
虽然前面我们讲了要推导两个Loss,但是看完反向过程就会知道,我们并不用针对每一个Loss都重写一遍推导流程,只需要在反向传播的初期把相应的Loss梯度公式带进去即可。
前向过程
请注意,虽然下面的前向公式里写了Loss,但实际上只有Loss的梯度参与反向过程,Loss本身不参与计算,写下来以示敬意而已。
z[1]a[1]z[2]a[2]z[3]Loss=a[0]∗w[1]+b[1]=f(z[1])=a[1]∗w[2]+b[2]=f(z[2])=a[2]∗w[3]+b[3]=L(z[3],y)
反向过程
反向过程要详细写,dz[3] 暂时不写具体表达式,针对不同的Loss,将其带入即可。xT 表示变量 x 的转置。
乘法表示:两个变量中间为点表示矩阵乘法;multiply(x,y) 表示element-wise乘法;mean(x,axis=0)表示对 x 在axis=0的维度上做平均。
从下面推导过程体现出以下特点:
- 传递性:在链式法则的作用下,每一个变量的梯度仅跟上一层变量的梯度有关,与更早的梯度无关(当然,本质上是有关的,但是由于传递性的存在,从计算公式上来看是无关的),其他参与计算的都是正向过程中缓存下来的变量。
-
z[l] 在反向过程中不需要用到,虽然multiply()函数中用到了f′(z[l]),但是现在常用的**函数的导数如sigmoid和relu等都不包含 z[l] 本身。这就意味着,z[l] 仅在前向过程中用一下就好了,因此前向过程中**函数可以做原位运算(in-place),即 a[l] 和 z[l] 可以使用同一个变量,这样可以节省一些显存。但是为了清晰起见,我们不使用原位运算的方式做公式推导。
dz[3]dw[3]db[3]da[2]dz[2]dw[2]db[2]da[1]dz[1]dw[1]db[1]=∂z[3]∂Loss=∂w[3]∂Loss=∂z[3]∂Loss⋅∂dw[3]∂dz[3]=a[2]T⋅dz[3]=∂b[3]∂Loss=∂z[3]∂Loss⋅∂db[3]∂dz[3]=mean(dz[3], axis=0)=∂a[2]∂Loss=∂z[3]∂Loss⋅∂da[2]∂dz[3]=dz[3]⋅w[3]T=∂z[2]∂Loss=∂a[2]∂Loss⋅∂dz[2]∂da[2]=multiply(da[2], f′(z[2]))=∂w[2]∂Loss=∂z[2]∂Loss⋅∂dw[2]∂dz[2]=a[1]T⋅dz[2]=∂b[2]∂Loss=∂z[2]∂Loss⋅∂db[2]∂dz[2]=mean(dz[2], axis=0)=∂a[1]∂Loss=∂z[2]∂Loss⋅∂da[1]∂dz[2]=dz[2]⋅w[2]T=∂z[1]∂Loss=∂a[1]∂Loss⋅∂dz[1]∂da[1]=multiply(da[1], f′(z[1]))=∂w[1]∂Loss=∂z[1]∂Loss⋅∂dw[1]∂dz[1]=a[0]T⋅dz[1]=∂b[1]∂Loss=∂z[1]∂Loss⋅∂db[1]∂dz[1]=mean(dz[1], axis=0)
下面是反向过程中所有变量的shape,带 d 和不带 d 的shape一样,因为现在是反向过程,所以不带 d 的变量就不再写了。配合等式左右两边变量的shape来看反向过程更易理解,特别是矩阵乘法中变量的前后顺序的确定。
dz[3]da[2]=dz[2]da[1]=dz[1]da[0]dw[3]dw[2]dw[1]db[3]db[2]db[1]=(b,n3)=(b,n2)=(b,n1)=(b,n0)=(n2,n3)=(n1,n2)=(n0,n1)=(1,n3)=(1,n2)=(1,n1)
梯度更新
仅需要更新网络参数,即weight和bias,α 表示学习率。
for w[l]b[l]all l:=w[l]−α⋅dw[l]=b[l]−α⋅db[l]
Loss层的梯度
L2 Loss的梯度比较容易理解。Cross Entropy Loss的梯度推导可以参考另一篇文档《Softmax以及Cross Entropy Loss求导》。
一般来讲L2 Loss多用于回归任务,而Cross Entropy Loss多用于分类任务。
L2 Loss
Loss(z[3], y)dz[3]=0.5∗(z[3]−y)2=∂z[3]∂Loss=z[3]−y
Cross Entropy Loss
这部分可参考:
Softmax以及Cross Entropy Loss求导
Softmax(zi[3])Loss(z[3], y)dz[3]=S(zi[3])=∑kezk[3]ezi[3]=−k∑yk⋅lnS(zk[3])=∂z[3]∂Loss=S(z[3])−y
注意,Cross Entropy Loss的梯度看起来与L2 Loss的梯度有点像,但其实差别还是挺大的,Cross Entropy Loss的 z[3] 外面还包了个Softmax,另外因为针对任务的不同,两个Loss的标签 y 一般也不太一样。