卷积层的向前传播
def conv_forward(A_prev,W,b,hparameters): (m,n_H_prev,n_W_prev,n_C_prev)=A_prev.shape (f,f,n_C_prev,n_C)=W.shape
向前传播,参与运算的有A_prev,W,b,还有两个超参数,步长和填充大小。
先来看A_prev的结构,第一维表示的是样本,所以一会向前传播第一层循环就是
for i in range(m): a_prev=A_prev[i]
将四维降维成为三维,A_prev后面的三维分别是,当前的高度,当前的宽度,当前的通道数量。然后就是过滤器的结构,分析一下,过滤器的前三维表示的是过滤器的长和宽,然后第三维对应的就是A_prev的通道的数,前三维是针对一个过滤器来说的。第四维对应的就是这一层的过滤器的数量。所以对于一个单样本的A_prev来说,我们要让他经历每一个当前层的过滤器,所以由有了一个循环
for i in range(n_C):
分析完了重要参数的结构,然后就是正向传播的内容了
stride=hparameters["stride"] pad=hparameters["pad"]
传入了超参数,一个是步长,一个是填充的边界长度,分别获取就可以了
n_H=int((n_H_prev-f+2*pad)/stride)+1 n_W=int((n_W_prev-f+2*pad)/stride)+1 Z=np.zeros((m,n_H,n_W,n_C)) A_prev_pad=zero_pad(A_prev,pad)
在开始计算卷积之前,我们首先要了解一下当前层计算完毕之后的结构,首先是通道数,通道数就是过滤器的数量,也就是过滤器的那个参数的第四维(?,?,?,当前层过滤器的数量),然后第一维就是样本的数量,(样本的数量,?,?,当前层过滤器的数量),那么第二三维呢?,这个就需要了解我们的卷积计算过程了,产生的长宽高和步长以及填充的边有关系,通过计算可以知道,当前层的长和宽
n_H=int((n_H_prev-f+2*pad)/stride)+1 n_W=int((n_W_prev-f+2*pad)/stride)+1
然后就是创建当前层计算结果的结构
Z=np.zeros((m,n_H,n_W,n_C))
然后在就是填充传入的数据
A_prev_pad=zero_pad(A_prev,pad)
接下来就是正式的开始卷积的计算了
for i in range(m):#样本 #四维变成三维 a_prev_pad=A_prev_pad[i] for h in range(n_H): for w in range(n_W):#前两维就是长和宽 for c in range(n_C):# 每一个小块块都要经过当前层的所有的过滤器 vert_start=h*stride vert_end=vert_start+f horiz_start=w*stride horiz_end=horiz_start+f #每一个小快都要经历当前层的所有的过滤器 a_slice_prev=a_prev_pad[vert_start:vert_end,horiz_start:horiz_end,:] Z[i,h,w,c]=conv_single_step(a_slice_prev,W[:,:,:,c],b[0,0,0,c])
assert(Z.shape==(m,n_H,n_W,n_C)) cache=(A_prev,W,b,hparameters) return (Z,cache)
然后就是断言Z的结构是不是对的,还有就是向后面传播cache
池化层的向前传播
def pool_forward(A_prev,hparameters,mode="max"): (m , n_H_prev , n_W_prev , n_C_prev) = A_prev.shape f = hparameters["f"] stride = hparameters["stride"]
和卷积层的结构是一样的
n_H=int((n_H_prev-f)/stride)+1 n_W=int((n_W_prev-f)/stride)+1 n_C=n_C_prev A = np.zeros((m , n_H , n_W , n_C))
实际上公式是一样的,只不过这里没有使用到padding,然后在池化计算前后通道数是相同的,所以池化前后通道数没有发生变化,所以前后发生变化的只有第二维和第三维,和步长以及f相关
for i in range(m): for h in range(n_H): for w in range(n_W): for c in range(n_C): vert_start=h*stride vert_end=vert_start+f horiz_start=w*stride horiz_end=horiz_start+f a_slice_prev=A_prev[i,vert_start:vert_end,horiz_start:horiz_end,c] if mode=="max": A[i,h,w,c]=np.max(a_slice_prev) elif mode=="average": A[i,h,w,c]=np.mean(a_slice_prev) assert(A.shape==(m,n_H,n_W,n_C)) cache=(A_prev,hparameters) return A,cache
卷积层和池化层的区别
卷积计算之后厚度会发生变化,而池化层的厚度不会发生变化
卷积层的过滤器的值是要和上一层的结果进行运算的,而池化层的f完全就是用来计算当前层的长和宽的
卷基层的反向传播
def conv_backward(dZ,cache): (A_prev,W,b,hparameters)=cache
反向传播的推导一定需要一些正向传播时用到的参数,就像神经网络在反向传播中,在推导dA_prev时需要用到W等等。在这里我们需要获取的和神经网络是一样的,我们需要获取到dA_prev,dW,db,我们需要获取到A_prev,W,b
(m,n_H_prev,n_W_prev,n_C_prev)=A_prev.shape
在上一步中,我们已经获取到了上一层的结果,上一层的结果是四维的,如下(样本,上一层的高,上一层的宽,上一层的通道数)
(m,n_H,n_W,n_C)=dZ.shape
就像神经网络中的起点一样,我们的卷积神经网络的反向传播的起点也是这样的,dZ是我们的起点。
(f,f,n_C_prev,n_C)=W.shape
W的结构也是四维的,在正向传播中已经介绍了
pad=hparameters["pad"] stride=hparameters["stride"]
我们需要用到pad(填充)和stride(步长)
dA_prev=np.zeros((m,n_H_prev,n_W_prev,n_C_prev)) dW=np.zeros((f,f,n_C_prev,n_C)) db=np.zeros((1,1,1,n_C))
然后就是为了这一层的反向传播创建dA_prev,dW,db的空结构
A_prev_pad=zero_pad(A_prev,pad) dA_prev_pad=zero_pad(dA_prev,pad)
这里需要我们进行填充
for i in range(m):#第一维的循环,使得每一个处理都是针对单样本来说的 a_prev_pad=A_prev_pad[i] da_prev_pad=dA_prev_pad[i] for h in range(n_H): for w in range(n_W): for c in range(n_C): vert_start=h vert_end=vert_start+f horiz_start=w horiz_end=horiz_start+f #我们获取到了一个三维中的一个小块 a_slice=a_prev_pad[vert_start:vert_end,horiz_start:horiz_end,:] #一下代码的我们详细说明 da_prev_pad[vert_start:vert_end,horiz_start:horiz_start:horiz_end,:]+=W[:,:,:,c]*dZ[i,h,w,c] dW[:,:,:,c]+=a_slice*dZ[i,h,w,c] db[:,:,:,c]+=dZ[i,h,w,c] dA_prev[i,:,:,:]=da_prev_pad[pad:-pad,pad:-pad,:] assert(dA_prev.shape==(m,n_H_prev,n_W_prev,n_C_prev)) return (dA_prev,dW,db)
如下就是对反向传播的核心代码的详细分析
da_prev_pad[vert_start:vert_end,horiz_start:horiz_start:horiz_end,:]+=W[:,:,:,c]*dZ[i,h,w,c]
在神经网络中我们通过dz和当前层的w推出da_prev(z=a_prevw+b),和卷积神经网络类似,我们通过dz和w推出
da_prev_pad。大致的思路定下来了,然后就是细节上的处理。看上面的循环,在第三维的循环是针对每一个不同的过滤器的,*所以每一次的循环都是使用每一个不同的过滤器,所以一定就有一项W[:,:,:,c]。
然后dz,我们使用的dz是思维的唯一确定的一个点,所以四维都是固定的,也就是dZ[i,h,w,c]
W[:,:,:,c]*dZ[i,h,w,c]的运算相当于是一个点乘以一个矩阵,所以获取到的也是一个矩阵,也肯定是一个大矩阵中的小块
dW[:,:,:,c]+=a_slice*dZ[i,h,w,c] db[:,:,:,c]+=dZ[i,h,w,c]
然后就是推dW和db.
dW[:,:,:,c]表示的当前这个过滤器的梯度,c表示的是不同过滤器的数量的。它是有上次层a和Z推出来的。在上一层的a有多少个切片,就对应着计算出来的W的梯度,然后相加。还是一个值乘以一个小矩阵的结构
左边dW[:,:,:,c] 右边a_slice*dZ[i,h,w,c] -------> dW[:,:,:,c]+=a_slice*dZ[i,h,w,c]
推出了dW,然后也推出了db
db[:,:,:,c]+=d[i,h,w,c]
上面推出来的da_prev_pad是针对单样本来说的,我们现在要将它赋给多样本的单元
dA_prev[i,:,:,:]=da_prev_pad[pad:-pad,pad:-pad,:]
这个时候就要解除填充
池化层的反向传播
针对与最大池化和平均池化处理的方式是不同的,所以对于函数的参数有一项是mode看mode是最大还是平均
首先是两个预备的函数
def create_mask_from_window(x): mask=x==np.max(x) return mask
def distribute_value(dz,shape): (n_H,n_W)=shape average=dz/(n_H*n_W) a=np.ones(shape)*average return a
def pool_backward(dA,cache,mode="max"): (A_prev,hparameters)=cache
传入的参数不是dZ而是dA,说明的是池化层的过程,池化层的正向传播是A_prev转化为A
f=hparameters["f"] stride=hparameters["stride"]
(m,n_H_prev,n_W_prev,n_C_prev)=A_prev.shape (m,n_H,n_W,n_C)=dA.shape
dA_prev=np.zeros_like(A_prev)
for i in range(m): a_prev=A_prev[i] for h in range(n_H): for w in range(n_W): for c in range(n_C): vert_start=h vert_end=vert_start+f horiz_start=w horiz_end=horiz_start+f
if mode=="max": a_prev_slice=a_prev[vert_start:vert_end,horiz_start:horiz_end,c] mask=create_mask_from_window(a_prev_slice) dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]+=np.multiply(mask,dA[i,h,w,c]) elif mode=="average": da=dA[i,h,w,c] shape=(f,f) dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]+=distribute_value(da,shape) assert(dA_prev.shape==A_prev.shape) return dA_prev
dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]+=np.multiply(mask,dA[i,h,w,c])
dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]=np.multiply(mask,dA[i,h,w,c])
以上两种写法都是可以的
dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]=distribute_value(da,shape)
dA_prev[i,vert_start:vert_end,horiz_start:horiz_end,c]+=distribute_value(da,shape)
```