本系列将由浅入深的介绍卷积神经网络的前向传播与后向传播,池化层的前向传播与后向传播,并分别介绍在不同步长与不同填充的情况下卷积层和池化层的前向传播与后向传播又有哪些不同。最后给出完整的Python实现代码。
完整目录如下:
一、CNN ⇒ \Rightarrow ⇒步长为1,无填充的卷积神经网络
二、CNN ⇒ \Rightarrow ⇒步长为1,有填充的卷积神经网络
三、CNN ⇒ \Rightarrow ⇒步长不为1,无填充的卷积神经网络
四、CNN ⇒ \Rightarrow ⇒步长不为1,有填充的卷积神经网络
上一篇文章已经介绍了有填充,步长为1的卷积神经网络,本文将继续由浅入深的介绍无填充,步长不为1的卷积神经网络的前向传播与后向传播,有填充的池化层的前向传播与后向传播。
给定输入4x4的矩阵A,2x2的卷积核W,让A和W做无填充,步长为2的卷积,得到2x2的矩阵Z。示意图如下。
给定输入4x4的矩阵Ain,2x2的池化核,对Ain做步长为2的池化后可以得到2x2的矩阵Aout,示意图如下。
对于上述前向传播,我们将损失定义为loss,我们的目标是去minimize loss,记loss对某个参数x的偏导数为dx,先来看看池化层的反向传播。
loss对池化层输入 A 1 A^{1} A1的偏导数为 d A i n 1 dA^{1}_{in} dAin1,池化层的反向传播的数学表达式可写为 d A i n 1 = d A o u t 1 × d A o u t 1 d A i n 1 dA^{1}_{in}=dA^{1}_{out}\times \frac{dA^{1}_{out}}{dA^{1}_{in}} dAin1=dAout1×dAin1dAout1,其中最关键的一步是搞清楚 d A o u t 1 d A i n 1 \frac{dA^{1}_{out}}{dA^{1}_{in}} dAin1dAout1怎么求。我们由之前的介绍可知,池化层分为最大池化和均值池化,我们来分别探讨这两种情况。
假定给定2x2的池化核,设定步长为2,每个池化核区域内的最大值由彩色方框框出。则对输入矩阵Ain进行最大池化的示意图如下。
则我们可知
A 11 = 1 × a 11 + 0 × a 12 + 0 × a 21 + 0 × a 22 A11=1\times a11+0\times a12+0\times a21+0\times a22 A11=1×a11+0×a12+0×a21+0×a22
A 12 = 0 × a 13 + 0 × a 14 + 0 × a 23 + 1 × a 24 A12=0\times a13+0\times a14+0\times a23+1\times a24 A12=0×a13+0×a14+0×a23+1×a24
A 21 = 0 × a 31 + 0 × a 32 + 0 × a 41 + 1 × a 42 A21=0\times a31+0\times a32+0\times a41+1\times a42 A21=0×a31+0×a32+0×a41+1×a42
A 22 = 0 × a 33 + 1 × a 34 + 0 × a 43 + 0 × a 44 A22=0\times a33+1\times a34+0\times a43+0\times a44 A22=0×a33+1×a34+0×a43+0×a44
所以可得
d A 11 d a 11 = 1 , d A 11 d a 12 = 0 , d A 11 d a 21 = 0 , d A 11 d a 22 = 0 \frac{dA11}{da11}=1,\frac{dA11}{da12}=0,\frac{dA11}{da21}=0,\frac{dA11}{da22}=0 da11dA11=1,da12dA11=0,da21dA11=0,da22dA11=0
d A 12 d a 13 = 0 , d A 12 d a 14 = 0 , d A 12 d a 23 = 0 , d A 12 d a 24 = 1 \frac{dA12}{da13}=0,\frac{dA12}{da14}=0,\frac{dA12}{da23}=0,\frac{dA12}{da24}=1 da13dA12=0,da14dA12=0,da23dA12=0,da24dA12=1
d A 21 d a 31 = 0 , d A 21 d a 32 = 0 , d A 21 d a 41 = 0 , d A 21 d a 42 = 1 \frac{dA21}{da31}=0,\frac{dA21}{da32}=0,\frac{dA21}{da41}=0,\frac{dA21}{da42}=1 da31dA21=0,da32dA21=0,da41dA21=0,da42dA21=1
d A 22 d a 33 = 0 , d A 22 d a 34 = 1 , d A 22 d a 43 = 0 , d A 22 d a 44 = 0 \frac{dA22}{da33}=0,\frac{dA22}{da34}=1,\frac{dA22}{da43}=0,\frac{dA22}{da44}=0 da33dA22=0,da34dA22=1,da43dA22=0,da44dA22=0
即 d A i n 1 = u p s a m p l e ( d A o u t 1 ) dA^{1}_{in}=upsample(dA^{1}_{out}) dAin1=upsample(dAout1),其中upsample是指将 d A o u t 1 dA^{1}_{out} dAout1上采样至和 d A i n 1 dA^{1}_{in} dAin1一样的shape,而 d A o u t 1 dA^{1}_{out} dAout1里的每个值所在位置由 d A i n 1 dA^{1}_{in} dAin1里每个池化核区域内的最大值决定,同时池化核区域移动的步长由和正向传播时的步长一样。
可知,我们需要在正向传播时记住各个池化核大小区域内最大值的位置和步长,在反向传播时即可正确计算。
以下给出这一方法的python实现(为了简便易懂,假设只有一个输入样本且为单通道,但实际情况往往不是这样。)
def max_pool_backward(self,dAout,Ain,filterSize,strides):
#params说明
#params dAout为传入的dAout
#params Ain为我们记录的正向传播时当前层池化的输入,我们将用来它来计算每个池化核区域内的最大值
#params filterSize为池化核的大小
#params strides为池化层的步长
#return dAin
dAin=np.zeros_like(Ain) #初始化dAin为shape和Ain相同,值为0的矩阵
n_H,n_W=dAout.shape #n_H,n_W分别是dAout的高和宽
for h in range(n_H): #遍历行
for w in range(n_W): #遍历列
h_start=h*strides #当前池化区域行的开头
h_end=h_start+filterSize #当前池化区域行的结尾
w_start=w*strides #当前池化区域列的开头
w_end=w_start+filterSize #当前池化区域列的结尾
mask=(Ain[h_start:h_end,w_start:w_end]==np.max(Ain[h_start:h_end,w_start:w_end]))
#找到当前池化区域内最大值的位置,在mask内,最大值的位置将会等于True(1),其他位置都等于False(0)
dAin[h_start:h_end,w_start:w_end]=mask*dAout[h,w] #实现对dAout的upsample
return dAin
假定给定2x2的池化核,设定步长为2,以下是均值池化正向传播的示意图。
A 11 = ( a 11 + a 12 + a 21 + a 22 ) ÷ 4 A11=(a11+a12+a21+a22)\div4 A11=(a11+a12+a21+a22)÷4
A 12 = ( a 13 + a 14 + a 23 + a 24 ) ÷ 4 A12=(a13+a14+a23+a24)\div4 A12=(a13+a14+a23+a24)÷4
A 21 = ( a 31 + a 32 + a 41 + a 42 ) ÷ 4 A21=(a31+a32+a41+a42)\div4 A21=(a31+a32+a41+a42)÷4
A 22 = ( a 33 + a 34 + a 43 + a 44 ) ÷ 4 A22=(a33+a34+a43+a44)\div4 A22=(a33+a34+a43+a44)÷4
可得
d A 11 d a 11 = d A 11 d a 12 = d A 11 d a 21 = d A 11 d a 22 = 1 4 \frac{dA11}{da11}=\frac{dA11}{da12}=\frac{dA11}{da21}=\frac{dA11}{da22}=\frac{1}{4} da11dA11=da12dA11=da21dA11=da22dA11=41
d A 12 d a 13 = d A 12 d a 14 = d A 12 d a 23 = d A 12 d a 24 = 1 4 \frac{dA12}{da13}=\frac{dA12}{da14}=\frac{dA12}{da23}=\frac{dA12}{da24}=\frac{1}{4} da13dA12=da14dA12=da23dA12=da24dA12=41
d A 21 d a 31 = d A 21 d a 32 = d A 21 d a 41 = d A 21 d a 42 = 1 4 \frac{dA21}{da31}=\frac{dA21}{da32}=\frac{dA21}{da41}=\frac{dA21}{da42}=\frac{1}{4} da31dA21=da32dA21=da41dA21=da42dA21=41
d A 22 d a 33 = d A 22 d a 34 = d A 22 d a 43 = d A 22 d a 44 = 1 4 \frac{dA22}{da33}=\frac{dA22}{da34}=\frac{dA22}{da43}=\frac{dA22}{da44}=\frac{1}{4} da33dA22=da34dA22=da43dA22=da44dA22=41
因此我们可得步长为2的均值池化的反向传播为
即 d A i n 1 = u p s a m p l e ( d A o u t 1 ) dA^{1}_{in}=upsample(dA^{1}_{out}) dAin1=upsample(dAout1),这里的upsample和最大池化里的略有不同,是将 d A o u t 1 dA^{1}_{out} dAout1上采样至和 A i n 1 A^{1}_{in} Ain1一样的shape,每个位置上的值为对应的 d A o u t 1 dA^{1}_{out} dAout1的值除上池化核大小的平方。
可见我们只需要知道池化核的大小及步长,就能完成反向传播的计算。
以下给出这一方法的python实现(为了简便易懂,假设只有一个输入样本且为单通道,但实际情况往往不是这样。)
def average_pool_backward(self,dAout,Ain,filterSize,strides):
#params说明
#params dAout为传入的dAout
#params Ain为我们记录的正向传播时当前层池化的输入,我们将用来它来计算每个池化核区域内的平均值
#params filterSize为池化核的大小
#params strides为池化层的步长
#return dAin
dAin = np.zeros_like(Ain) #初始化dAin为shape和Ain相同,值为0的矩阵
n_H, n_W = dAout.shape #n_H,n_W分别是dAout的高和宽
for h in range(n_H): #遍历行
for w in range(n_W): #遍历列
h_start = h * strides #当前池化区域行的开头
h_end = h_start + filterSize #当前池化区域行的结尾
w_start = w * strides #当前池化区域列的开头
w_end = w_start + filterSize #当前池化区域列的结尾
dAin[h_start:h_end,w_start:w_end]+=np.ones((filterSize,filterSize))*(dAout[h,w]/(filterSize**2))
#1/(filterSize**2)为池化区域内对每个值的导数,再将它上采样至池化核的大小
return dAin
关于卷积层的反向传播,我们需要求三个值,一个是dW,第二个是dA(即上一池化层的输出,本层的输入),第三个是db,当求得dW,db后,就可以选用梯度下降法,Adam等优化算法来更新参数,直至满足停止条件最终得到模型。
首先来看看如何求dW。
求dW的反向传播的数学表达式可写为 d W 0 = d Z 1 × d Z 1 d W 0 dW^{0}=dZ^{1}\times \frac{dZ^{1}}{dW^{0}} dW0=dZ1×dW0dZ1,其中 d Z 1 dZ^{1} dZ1已由上面池化层的反向传播和relu的反向传播求得,关键就是求解 d Z 1 d W 0 \frac{dZ^{1}}{dW^{0}} dW0dZ1,先回顾一下卷积层的正向传播,给定4x4的输入矩阵A,2x2的卷积核W,W对A做步长为2无填充的卷积,可以得到2x2的矩阵Z。注意这里的A为上一层池化层的输出!!(如果为第一层的话,则为输入样本数据)
z 11 = a 11 × w 11 + a 12 × w 12 + a 21 × w 21 + a 22 × w 22 z11=a11\times w11+a12\times w12+a21\times w21+a22\times w22 z11=a11×w11+a12×w12+a21×w21+a22×w22
z 12 = a 13 × w 11 + a 14 × w 12 + a 23 × w 21 + a 24 × w 22 z12=a13\times w11+a14\times w12+a23\times w21+a24\times w22 z12=a13×w11+a14×w12+a23×w21+a24×w22
z 21 = a 31 × w 11 + a 32 × w 12 + a 41 × w 21 + a 42 × w 22 z21=a31\times w11+a32\times w12+a41\times w21+a42\times w22 z21=a31×w11+a32×w12+a41×w21+a42×w22
z 22 = a 33 × w 11 + a 34 × w 12 + a 43 × w 21 + a 44 × w 22 z22=a33\times w11+a34\times w12+a43\times w21+a44\times w22 z22=a33×w11+a34×w12+a43×w21+a44×w22
于是
d w 11 = d z 11 × a 11 + d z 12 × a 13 + d z 21 × a 31 + d z 22 × a 33 dw11=dz11\times a11+dz12\times a13+dz21\times a31+dz22\times a33 dw11=dz11×a11+dz12×a13+dz21×a31+dz22×a33
d w 12 = d z 11 × a 12 + d z 12 × a 14 + d z 21 × a 32 + d z 22 × a 34 dw12=dz11\times a12+dz12\times a14+dz21\times a32+dz22\times a34 dw12=dz11×a12+dz12×a14+dz21×a32+dz22×a34
d w 21 = d z 11 × a 21 + d z 12 × a 23 + d z 21 × a 41 + d z 22 × a 43 dw21=dz11\times a21+dz12\times a23+dz21\times a41+dz22\times a43 dw21=dz11×a21+dz12×a23+dz21×a41+dz22×a43
d w 22 = d z 11 × a 22 + d z 12 × a 24 + d z 21 × a 42 + d z 22 × a 44 dw22=dz11\times a22+dz12\times a24+dz21\times a42+dz22\times a44 dw22=dz11×a22+dz12×a24+dz21×a42+dz22×a44
将上述公式总结一下,给出一张示意图帮助理解。
对于A_ranged,我们可以由两种理解。
1.将A的第2列与第3列交换,在把A的第3行与第3行交换。更普遍的,将A的第j列与第j-1+strides列交换,在把A的第j行与第j-1+strides行交换,之后依此类推。
2.对于每个卷积区域的元素,我们可以这样得到,以第一个卷积区域为例,应该是对正常的A取(1,1),(1,1+strides),(1+strides,1),(1+strides,1+strides)这四个元素得到,取出来正好也等于a11,a13,a31,a33,之后的卷积区域依此类推。
由此可见,我们在正向传播时应把上一层池化后的输出A和步长记录下来,在反向传播求dW时,只需将记录的正向传播时的上一层池化层的输出A作为输入,对A按上述两种方法处理得到A_ranged,之后与dZ做步长为2(同正向传播时的步长相同),无填充的卷积,即可完成计算。
以下给出这一方法的python实现(为了简便易懂,假设只有一个输入样本且为单通道,但实际情况往往不是这样。)
def conv_backward(self,dZ,Aout,filterSize,strides): #卷积层的后向传播
#params说明
#params dZ传入的对当前卷积层输出的导数
#params Aout当前卷积层正向传播时的输入,我们用它来与dZ做卷积
#params filterSize卷积核的大小
#params strides卷积的步长
#return dW
dW=np.zeros((filterSize,filterSize)) #初始化dW
n_H,n_W=dZ.shape #dZ的高宽
for m in range(filterSize): #遍历行
for n in range(filterSize): #遍历列
for i in range(n_H):
for j in range(n_W):
h= i * strides + m
w= j * strides + n
dW[m,n]+=Aout[h,w]*dZ[i,j]
return dW
借助上述卷积层的正向传播,我们可以得到
d a 11 = d z 11 × w 11 , d a 12 = d z 11 × w 12 , d a 21 = d z 11 × w 21 , d a 22 = d z 11 × w 22 da11=dz11\times w11,da12=dz11\times w12,da21=dz11\times w21,da22=dz11\times w22 da11=dz11×w11,da12=dz11×w12,da21=dz11×w21,da22=dz11×w22
d a 13 = d z 12 × w 11 , d a 14 = d z 12 × w 12 , d a 23 = d z 12 × w 21 , d a 24 = d z 12 × w 22 da13=dz12\times w11,da14=dz12\times w12,da23=dz12\times w21,da24=dz12\times w22 da13=dz12×w11,da14=dz12×w12,da23=dz12×w21,da24=dz12×w22
d a 31 = d z 21 × w 11 , d a 32 = d z 21 × w 12 , d a 41 = d z 21 × w 21 , d a 42 = d z 21 × w 22 da31=dz21\times w11,da32=dz21\times w12,da41=dz21\times w21,da42=dz21\times w22 da31=dz21×w11,da32=dz21×w12,da41=dz21×w21,da42=dz21×w22
d a 33 = d z 22 × w 11 , d a 34 = d z 22 × w 12 , d a 43 = d z 22 × w 21 , d a 44 = d z 22 × w 22 da33=dz22\times w11,da34=dz22\times w12,da43=dz22\times w21,da44=dz22\times w22 da33=dz22×w11,da34=dz22×w12,da43=dz22×w21,da44=dz22×w22
我们整理一下,可以得到下图
即 d A = p a d ( i n s e r t ( d Z ) ) ∗ r o t 180 ( W ) dA=pad(insert(dZ))\ast rot180(W) dA=pad(insert(dZ))∗rot180(W),
我们可以看到对于求反向传播时的dA,我们首先需要对dZ做insert操作,即填充(strides-1)行和填充(strides-1)列,然后再对dZ做pad操作(填充),填充的大小为 p a d = ( n H − 1 ) − n H p r e v + f 2 = 4 − 1 − 3 + 2 2 = 1 pad=\frac{(n_{H}-1)-n_{Hprev}+f}{2}=\frac{4-1-3+2}{2}=1 pad=2(nH−1)−nHprev+f=24−1−3+2=1,然后与卷积核W做步长为1!!!步长为1!!!步长为1!!!的卷积操作(无论正向传播时strides是多少),即可完成计算。
以下给出这一方法的python实现(为了简便易懂,假设只有一个输入样本且为单通道,但实际情况往往不是这样。)
def zero_insert(self,dZ,insert_size):
H,W=dZ.shape
dZout=dZ
insert_values_h=np.zeros((insert_size,W))
insert_values_w=np.zeros((H,insert_size))
for h in range(H-1):
dZout=np.insert(dZout,h+1,values=insert_values_h,axis=0)
for w in range(W-1):
dZout=np.insert(dZout,w+1,values=insert_values_w,axis=1)
return dZout
def conv_backward(self,dZ,Aout,convFilter,filterSize,strides): #卷积层的后向传播
#params说明
#params dZ传入的对当前卷积层输出的导数
#params Aout正向传播时该层的输入,上层的输出
#params convFilter当前卷积层正向传播时的卷积核
#params filterSize卷积核的大小
#params strides卷积的步长
#return dA
convFilter=convFilter[:,:,::-1,::-1]
n_H,n_W=Aout.shape
dA=np.zeros_like(Aout) #初始化dA和Aout一样的shape,数值为0
dZ_inserted = self.zero_insert(dZ, strides - 1) #对dZ做insert操作
pad_size=filterSize-1 #填充的大小
dZ_paded=np.pad(dZ,(pad_size,pad_size),'constant') #对dZ做填充,填充大小为filterSize-1,填充的值为0
for h in range(n_H):
for w in range(n_W):
dA[h,w]=np.sum(np.multiply(dZ_paded[h:h+filterSize,w:w+filterSize],convFilter)) #卷积操作
return dA
对于求db比较简单,db=dZ,只是需要对dZ在某个维度上做累加运算即可得到。
以下给出求db的python实现(为了简便易懂,假设只有一个输入样本且为单通道,但实际情况往往不是这样。)
def conv_backward(self,dZ): #卷积层的后向传播
db=np.sum(dZ)
return db
以上就是无填充,步长不为1的卷积神经网络的原理,可以看到,步长是否为1的反向传播的区别就在于A是否要重排列和dZ是否要insert和pad,下一节我们将对有填充,步长为1的卷积神经网络进行讲解。
所有源代码在文末
码字作图实属不易,前前后后断断续续用了不少时间,希望大家看后能有所收获,觉得可以的给点赞赏,让我有走下去的动力。谢谢大家!
大家看完python实现的方法是否会发现效率过于低下,因为其中有大量的for循环,博主本人花费了一些时间优化了代码,使速度有了很大的提升(最快的由6个for循环降为2个for循环),有兴趣的小伙伴可以在打赏我后评论或私聊我,我将代码发给你。请见谅。
Github见所有Python代码