深度学习系列卷积神经网络详解二自己手写一个卷积神经网络.docx
《深度学习系列卷积神经网络详解二自己手写一个卷积神经网络.docx》由会员分享,可在线阅读,更多相关《深度学习系列卷积神经网络详解二自己手写一个卷积神经网络.docx(14页珍藏版)》请在冰豆网上搜索。
深度学习系列卷积神经网络详解二自己手写一个卷积神经网络
【深度学习系列】卷积神经网络详解
(二)——自己手写一个卷积神经网络
作者:
Charlotte77数学系的数据挖掘民工
博客专栏:
个人公众号:
Charlotte数据挖掘(ID:
CharlotteDataMining)
精彩回顾:
【深度学习系列】卷积神经网络CNN原理详解
(一)——基本原理【深度学习系列】PaddlePaddle之数据预处理?
?
上篇文章中我们讲解了卷积神经网络的基本原理,包括几个基本层的定义、运算规则等。
本文主要写卷积神经网络如何进行一次完整的训练,包括前向传播和反向传播,并自己手写一个卷积神经网络。
如果不了解基本原理的,可以先看看上篇文章:
?
?
?
?
【深度学习系列】卷积神经网络CNN原理详解
(一)——基本原理卷积神经网络的前向传播 首先我们来看一个最简单的卷积神经网络:
1.输入层---->卷积层 以上一节的例子为例,输入是一个4*4的image,经过两个2*2的卷积核进行卷积运算后,变成两个3*3的feature_map以卷积核filter1为例(stride=1):
计算第一个卷积层神经元o11的输入:
神经元o11的输出:
(此处使用Relu激活函数)
其他神经元计算方式相同 2.卷积层---->池化层 计算池化层m11的输入(取窗口为2*2),池化层没有激活函数 3.池化层---->全连接层 池化层的输出到flatten层把所有元素“拍平”,然后到全连接层。
4.全连接层---->输出层 全连接层到输出层就是正常的神经元与神经元之间的邻接相连,通过softmax函数计算后输出到output,得到不同类别的概率值,输出概率值最大的即为该图片的类别。
卷积神经网络的反向传播 传统的神经网络是全连接形式的,如果进行反向传播,只需要由下一层对前一层不断的求偏导,即求链式偏导就可以求出每一层的误差敏感项,然后求出权重和偏置项的梯度,即可更新权重。
而卷积神经网络有两个特殊的层:
卷积层和池化层。
池化层输出时不需要经过激活函数,是一个滑动窗口的最大值,一个常数,那么它的偏导是1。
池化层相当于对上层图片做了一个压缩,这个反向求误差敏感项时与传统的反向传播方式不同。
从卷积后的feature_map反向传播到前一层时,由于前向传播时是通过卷积核做卷积运算得到的feature_map,所以反向传播与传统的也不一样,需要更新卷积核的参数。
下面我们介绍一下池化层和卷积层是如何做反向传播的。
在介绍之前,首先回顾一下传统的反向传播方法:
1.通过前向传播计算每一层的输入值neti,jneti,j(如卷积后的feature_map的第一个神经元的输入:
neti11neti11) 2.反向传播计算每个神经元的误差项δi,jδi,j,δi,j=?
E?
neti,jδi,j=?
E?
neti,j,其中E为损失函数计算得到的总体误差,可以用平方差,交叉熵等表示。
3.计算每个神经元权重wi,jwi,j的梯度,ηi,j=?
E?
neti,j?
?
neti,j?
wi,j=δi,j?
outi,jηi,j=?
E?
neti,j?
?
neti,j?
wi,j=δi,j?
outi,j
4.更新权重wi,j=wi,j?
λ?
ηi,jwi,j=wi,j?
λ?
ηi,j(其中λλ为学习率) 卷积层的反向传播 由前向传播可得:
所以上一层的输出也就是这一层的输入,即:
outi11=activators(neti11)=i11outi11=activators(neti11)=i11 首先计算输入层的误差项δ11δ11:
(注意这里是neti11neti11,代表的是上一层的输入,不是neto11neto11) 先计算?
E?
i11?
E?
i11 此处我们并不清楚?
E?
i11?
E?
i11怎么算,那可以先把input层通过卷积核做完卷积运算后的输出feature_map写出来:
然后依次对输入元素ii,jii,j求偏导 i11i11的偏导:
观察一下上面几个式子的规律,归纳一下,可以得到如下表达式:
图中的卷积核进行了180°翻转,与这一层的误差敏感项矩阵deltai,j)deltai,j)周围补零后的矩阵做卷积运算后,就可以得到?
E?
i11?
E?
i11,即?
E?
ii,j=∑m?
∑nhm,nδi+m,j+n?
E?
ii,j=∑m?
∑nhm,nδi+m,j+n 第一项求完后,我们来求第二项?
i11?
neti11 此时我们的误差敏感矩阵就求完了,得到误差敏感矩阵后,即可求权重的梯度。
由于上面已经写出了卷积层的输入neto11neto11与权重hi,jhi,j之间的表达式,所以可以直接求出:
推论出权重的梯度:
偏置项的梯度:
可以看出,偏置项的偏导等于这一层所有误差敏感项之和。
得到了权重和偏置项的梯度后,就可以根据梯度下降法更新权重和梯度了。
池化层的反向传播 池化层的反向传播就比较好求了,看着下面的图,左边是上一层的输出,也就是卷积层的输出feature_map,右边是池化层的输入,还是先根据前向传播,把式子都写出来,方便计算:
假设上一层这个滑动窗口的最大值是outo11
这样就求出了池化层的误差敏感项矩阵。
同理可以求出每个神经元的梯度并更新权重。
手写一个卷积神经网络 1.定义一个卷积层 首先我们通过ConvLayer来实现一个卷积层,定义卷积层的超参数classConvLayer(object):
'''参数含义:
input_width:
输入图片尺寸——宽度input_height:
输入图片尺寸——长度channel_number:
通道数,彩色为3,灰色为1filter_width:
卷积核的宽filter_height:
卷积核的长filter_number:
卷积核数量zero_padding:
补零长度stride:
步长activator:
激活函数learning_rate:
学习率'''def__init__(self,input_width,input_height,channel_number,filter_width,filter_height,filter_number,zero_padding,stride,activator,learning_rate):
self.input_width=input_widthself.input_height=input_heightself.channel_number=channel_numberself.filter_width=filter_widthself.filter_height=filter_heightself.filter_number=filter_numberself.zero_padding=zero_paddingself.stride=strideself.output_width=\ConvLayer.calculate_output_size(self.input_width,filter_width,zero_padding,stride)self.output_height=\ConvLayer.calculate_output_size(self.input_height,filter_height,zero_padding,stride)self.output_array=np.zeros((self.filter_number,self.output_height,self.output_width))self.filters=[]foriinrange(filter_number):
self.filters.append(Filter(filter_width,filter_height,self.channel_number))self.activator=activatorself.learning_rate=learning_rate
其中calculate_output_size用来计算通过卷积运算后输出的feature_map大小
@staticmethod
defcalculate_output_size(input_size,
filter_size,zero_padding,stride):
return(input_size-filter_size+5
2*zero_padding)/stride+1 2.构造一个激活函数 此处用的是RELU激活函数,因此我们在activators.py里定义,forward是前向计算,backforward是计算公式的导数:
classReluActivator(object):
defforward(self,weighted_input):
#returnweighted_inputreturnmax(0,weighted_input)
defbackward(self,output):
return1ifoutput>0else0
其他常见的激活函数我们也可以放到activators里,如sigmoid函数,我们可以做如下定义:
classSigmoidActivator(object):
defforward(self,weighted_input):
return1.0/(1.0+np.exp(-weighted_input))
#thepartialofsigmoid
defbackward(self,output):
returnoutput*(1-output) 如果我们需要自动以其他的激活函数,都可以在activator.py定义一个类即可。
3.定义一个类,保存卷积层的参数和梯度classFilter(object):
def__init__(self,width,height,depth):
#初始权重self.weights=np.random.uniform(-1e-4,1e-4,(depth,height,width))#初始偏置self.bias=0self.weights_grad=np.zeros(self.weights.shape)self.bias_grad=0
def__repr__(self):
return'filterweights:
\n%s\nbias:
\n%s'%(repr(self.weights),repr(self.bias))
defget_weights(self):
returnself.weights
defget_bias(self):
returnself.bias
defupdate(self,learning_rate):
self.weights-=learning_rate*self.weights_gradself.bias-=learning_rate*self.bias_grad 4.卷积层的前向传播 1).获取卷积区域#获取卷积区域defget_patch(input_array,i,j,filter_width,filter_height,stride):
'''从输入数组中获取本次卷积的区域,自动适配输入为2D和3D的情况'''start_i=i*stridestart_j=j*strideifinput_array.ndim==2:
input_array_conv=input_array[start_i:
start_i+filter_height,start_j:
start_j+filter_width]print'input_array_conv:
',input_array_convreturninput_array_conv
elifinput_array.ndim==3:
input_array_conv=input_array[:
start_i:
start_i+filter_height,start_j:
start_j+filter_width]print'input_array_conv:
',input_array_convreturninput_array_conv 2).进行卷积运算
defconv(input_array,kernel_array,output_array,stride,bias):
'''计算卷积,自动适配输入为2D和3D的情况'''channel_number=input_array.ndimoutput_width=output_array.shape[1]output_height=output_array.shape[0]kernel_width=kernel_array.shape[-1]kernel_height=kernel_array.shape[-2]foriinrange(output_height):
forjinrange(output_width):
output_array[i][j]=(get_patch(input_array,i,j,kernel_width,kernel_height,stride)*kernel_array).sum()+bias
3).增加zero_padding
#增加Zeropaddingdefpadding(input_array,zp):
'''为数组增加Zeropadding,自动适配输入为2D和3D的情况'''ifzp==0:
returninput_arrayelse:
ifinput_array.ndim==3:
input_width=input_array.shape[2]input_height=input_array.shape[1]input_depth=input_array.shape[0]padded_array=np.zeros((input_depth,input_height+2*zp,input_width+2*zp))padded_array[:
zp:
zp+input_height,zp:
zp+input_width]=input_arrayreturnpadded_arrayelifinput_array.ndim==2:
input_width=input_array.shape[1]input_height=input_array.shape[0]padded_array=np.zeros((input_height+2*zp,input_width+2*zp))padded_array[zp:
zp+input_height,zp:
zp+input_width]=input_arrayreturnpadded_array
4).进行前向传播
defforward(self,input_array):
'''计算卷积层的输出输出结果保存在self.output_array'''self.input_array=input_arrayself.padded_input_array=padding(input_array,self.zero_padding)forfinrange(self.filter_number):
filter=self.filters[f]conv(self.padded_input_array,filter.get_weights(),self.output_array[f],self.stride,filter.get_bias())element_wise_op(self.output_array,self.activator.forward) 其中element_wise_op函数是将每个组的元素对应相乘#对numpy数组进行elementwise操作,将矩阵中的每个元素对应相
defelement_wise_op(array,op)
foriinnp.nditer(array
op_flags=['readwrite']):
i[...]=op(i) 5.卷积层的反向传播 1).将误差传递到上一层defbp_sensitivity_map(self,sensitivity_array,activator):
'''计算传递到上一层的sensitivitymapsensitivity_array:
本层的sensitivitymapactivator:
上一层的激活函数'''#处理卷积步长,对原始sensitivitymap进行扩展expanded_array=self.expand_sensitivity_map(sensitivity_array)#full卷积,对sensitivitiymap进行zeropadding#虽然原始输入的zeropadding单元也会获得残差#但这个残差不需要继续向上传递,因此就不计算了expanded_width=expanded_array.shape[2]zp=(self.input_width+self.filter_width-1-expanded_width)/2padded_array=padding(expanded_array,zp)#初始化delta_array,用于保存传递到上一层的#sensitivitymapself.delta_array=self.create_delta_array()#对于具有多个filter的卷积层来说,最终传递到上一层的#sensitivitymap相当于所有的filter的#sensitivitymap之和forfinrange(self.filter_number):
filter=self.filters[f]#将filter权重翻转180度flipped_weights=np.array(map(lambdai:
np.rot90(i,2),filter.get_weights()))#计算与一个filter对应的delta_arraydelta_array=self.create_delta_array()fordinrange(delta_array.shape[0]):
conv(padded_array[f],flipped_weights[d],delta_array[d],1,0)self.delta_array+=delta_array#将计算结果与激活函数的偏导数做element-wise乘法操作derivative_array=np.array(self.input_array)element_wise_op(derivative_array,activator.backward)self.delta_array*=derivative_array
2).保存传递到上一层的sensitivitymap的数组defcreate_delta_array(self):
returnnp.zeros((self.channel_number,
self.input_height,self.input_width)) 3).计算代码梯度
defbp_gradient(self,sensitivity_array):
#处理卷积步长,对原始sensitivitymap进行扩展expanded_array=self.expand_sensitivity_map(sensitivity_array)forfinrange(self.filter_number):
#计算每个权重的梯度filter=self.filters[f]fordinrange(filter.weights.shape[0]):
conv(self.padded_input_array[d],expanded_array[f],filter.weights_grad[d],1,0)#计算偏置项的梯度filter.bias_grad=expanded_array[f].sum()
4).按照梯度下降法更新参数defupdate(self):
'''
按照梯度下降,更新权重
'''
forfilterinself.filters:
filter.update(self.learning_rate) 6.MaxPooling层的训练 1).定义MaxPooling类classMaxPoolingLayer(object):
def__init__(self,input_width,input_height,channel_number,filter_width,filter_height,stride):
self.input_width=input_widthself.input_height=input_heightself.channel_number=channel_numberself.filter_width=filter_widthself.filter_height=filter_heightself.stride=strideself.output_width=(input_width-filter_width)/self.stride+1self.output_height=(input_height-filter_height)/self.stride+1self.output_array=np.zeros((self.channel_number,self.output_height,self.output_width)) 2).前向传播计算
#前向传播defforward(self,input_array):
fordinrange(self.channel_number):
foriinrange(self.output_height):
forjinrange(self.output_width):
self.output_array[d,i,j]=(get_patch(input_array[d],i,j,self.filter_width,self.filter_height,self.stride).max())
3).反向传播计算
#反向传播defbackward(self,input_array,sensitivity_array):
self.delta