下面介绍一下全连接层的反向传播算法的推导。首先对全连接层的前向过程进行一下介绍。
我们把全连接层的每一层神经元都表示为一个列向量。上一层的神经元的输出,通过乘上当前层的权重矩阵加上列向量形式的偏置项,得到激活前的输出值,最后通过激活函数得到当前层的输出,公式如下:
由于反向传播链式传导的规律,为了避免重复计算,我们引入中间量$\delta^l$,我们称它为第$l$层的误差,具体含义为误差函数对于神经网络第$l$层未经激活函数的输出值的偏导数,即$\delta^l=\frac{\partial C}{\partial z^l}$,输出层的网络误差$\delta ^L$ 为:
接下来求$W$矩阵的导数,应用链式法则,得:
def full_connect(self,input_data,fc,front_delta=None,deriv=False):
N=input_data.shape[0]
if deriv==False:
output_data=np.dot(input_data.reshape(N,-1),fc)
return output_data
else:
back_delta=np.dot(front_delta,fc.T).reshape(input_data.shape)
fc+=self.lr*np.dot(input_data.reshape(N,-1),front_delta)
return back_delta,fc
①卷积层通过张量的卷积,或者说是多个矩阵卷积求和得到的输出,这和全连接层是不同的,所以在反向传播的时候,上一层的$\delta^{l-1}$递推计算方法是不同的。
②在卷积运算的过程中,从$\delta^l$推导出$W$、$b$的方式也是不同的。
各个符号所代表的数学意义和上一节全连接层相同。
卷积层的前向传播过程如下:
前向传播的公式为:
$$a^l=\sigma(z^l)=\sigma(a^{l-1}W^l+b^l)$$
在全连接中,$\sigma^l$和$\sigma^{l+1}$的关系为:
$$\delta^l=\frac{\partial C}{\partial z^l}=\frac{\partial C}{\partial z^{l+1}}\frac{\partial z^{l+1}}{\partial z^l}=\delta^{l+1}\frac{\partial z^{l+1}}{\partial z^l}$$
同样应用在卷积层中,但如上面①提到的,$\frac{\sigma z^{l+1}}{\sigma z^l}$在全连接层和卷积层的计算方法不同。我们通过一个简单的例子来进行一下分析。
假设我们$l$层的输入$a^{l}$是一个$3\times3$的矩阵,第$l+1$层的卷积核$W^l$是一个$2\times2$的矩阵,步长为1,则输出$z^{l+1}$为:
$$z^{l+1}=a^lW^{l+1}$$
$\begin{pmatrix}
a_{11}&a_{12}&a_{13}\
a_{21}&a_{22}&a_{23}\
a_{31}&a_{32}&a_{33}\
\end{pmatrix}$*$\begin{pmatrix}
w_{11}&w_{12}\
w_{21}&w_{22}\
\end{pmatrix}$=$\begin{pmatrix}
z_{11}&z_{12}\
z_{21}&z_{22}\
\end{pmatrix}$
根据卷积的计算公式,得
表示为矩阵协相关的形式表示(其实卷积层的卷积实际上是数学的协相关):
$\begin{pmatrix}
\nabla a_{11}&\nabla a_{12}&\nabla a_{13}\
\nabla a_{21}&\nabla a_{22}&\nabla a_{23}\
\nabla a_{31}&\nabla a_{32}&\nabla a_{33}\end{pmatrix}$=$\begin{pmatrix}
0&0&0&0\
0&\delta_{11}&\delta_{12}&0\
0&\delta_{21}&\delta_{22}&0\
0&0&0&0\end{pmatrix}$*$\begin{pmatrix}
w_{22}&w_{21}\
w_{12}&w_{11}
\end{pmatrix}$
为了符合梯度计算,我们在误差矩阵周围填充了一圈0,此时我们将卷积核反转180度之后和本层的梯度误差进行卷积,就可以得到下一层的梯度误差。
现在我们推导完了误差的反向传播关系,现在我们根据梯度误差来对$W$、$b$进行更新。
$$z^{l+1}=a^{l}W^{l+1}+b$$ $$\frac{\partial C}{\partial W^{l+1}}=a^l\delta ^{l+1}$$
但是卷积层输入的是矩阵,还是根据上述那个例子来进行分析,可得:
$$\frac{\partial C}{\partial W^{l+1}{11}}=a{11}\delta_{11}+a_{12}\delta_{12}+a_{21}\delta_{21}+a_{22}\delta_{22} $$ $$\frac{\partial C}{\partial W^{l+1}{12}}=a{12}\delta_{11}+a_{13}\delta_{12}+a_{22}\delta_{21}+a_{23}\delta_{22} $$ $$\frac{\partial C}{\partial W^{l+1}{21}}=a{21}\delta_{11}+a_{22}\delta_{12}+a_{31}\delta_{21}+a_{33}\delta_{22} $$ $$\frac{\partial C}{\partial W^{l+1}{12}}=a{22}\delta_{11}+a_{23}\delta_{12}+a_{32}\delta_{21}+a_{33}\delta_{22} $$
对于$b$,因为$\delta^{l+1}$是高维张量,$b$是一个向量,在这将$\delta^{l+1}$的各个子矩阵的项相加,得到一个误差向量,即为$b$的梯度。
def convolution(self,input_data,kernel,front_delta=None,deriv=False):
N,C,W,H=input_data.shape
K_NUM,K_C,K_W,K_H=kernel.shape
if(deriv==False):
output_data=np.zeros((N,K_NUM,W-K_W+1,H-K_H+1))
for imgID in range(N):
for Kid in range(K_C):
for Cid im range(C):
output_data+=convolve2d(input_data[imgID][Cid],kernel[Kid][Cid],mode='valid')
return output_data
else:
back_delta=np.zeros((N,C,W,H))
kernel_gradient=np.zeros((K_NUM,K_C,K_W,K_H))
padded_front_delta=np.pad(front_delta,[(0,0), (0,0), (K_W-1, K_H-1), (K_W-1, K_H-1)], mode='constant', constant_values=0)
for imgId in range(N):
for cId in range(C):
for kId in range(K_NUM):
back_delta[imgId][cId] += convolve2d(padded_front_delta[imgId][kId], kernal[kId,cId,::-1,::-1], mode='valid')
kernal_gradient[kId][cId] += convolve2d(front_delta[imgId][kId], input_map[imgId,cId], mode='valid')
# update weights
kernal += self.lr * kernal_gradient
return back_delta, kernal
池化层一般没有参数,所以池化层在反向传播的过程中,并不需要进行参数的更新,只需要将梯度误差继续传递下去即可。但是池化操作使得特征图的尺寸发生了变化,这使得梯度误差无法对位的进行传递下去。所以我们采取了保持梯度误差总和不变的原则,例如在$2\times2$的池化层中,我们把1个像素的梯度传递给4个梯度。
把某个像素的梯度误差平均分配给前一次
在最大池化中也要满足梯度误差总和不变的原则,在反向传播的过程中,我们直接把该元素的梯度误差传递给上一层的最大像素,其他像素梯度误差为0,不接受梯度误差。所以我们需要一个额外的变量记录最大值像素所在的坐标。
def mean_pool(self,input_map,pool,front_delta,deriv=False):
N,C,W,H=ipnput_map.shape
P_W,P_H=tuple(pool)
if(deriv=False):
feature_map=np.zeros((N,C,W/P_W,H/P_H))
feature_map=block_reduce(input_map,tuple((1,1,P_W,P_H)),func=np.mean)
return feature_map
else:
back_delta=np.zeros((N,C,W,H))
back_delta=front_delta.repaet(P_W,axis=2).repeat(P_H,axis=3)
back_delta/=(P_W*P_H)
return back_delta
def relu(self,x,front_delta=None,deriv=False):
if(deriv==False):
return x*(x>0)
else:
return front_delta*1.0*(x>0)