编写C语言版本的卷积神经网络CNN之二:CNN网络的总体结构
上一节我们总体介绍项目并说明minst手写数字的使用,这一节我们将重点介绍cnn网络总体结构。
上图我们已经非常熟悉,其为yann在1998年介绍的lenet-5网络的结构,其刚被提出,就在学术和工业领域上得到广泛应用,而本文的cnn卷积网络却是如下图所示(博主自己画的,画这个图还是挺麻烦的:l,不清晰请原谅),和lenet-5相比主要有以下三点不同:
(1)lenet-5给输入图像增加了一圈黑边,使输入图像大小变成了32x32,这样的目的是为了在下层卷积过程中保留更多原图的信息。
(2)lenet-5的卷积层c3只有16个模板,得到16个输出,而本文的卷积层c3由于是全连接,所以有6*12个模板,得到12个输出图像
(3)lenet-5多了两种,分别是c5到f6的全连接神经网络层,和f6到output高斯连接网络层。而本文的直接由采样层s4直接经过一层全连接神经网络层到output。
下面我们将重点介绍各层的结构及数据的前向传播。
一、各层的解释
(1)卷积层c1
输入为28x28的灰度图像,灰度图像分别同6个5x5的模板进行卷积操作,分别得到了6个24x24的卷积图像,图像里的每个像素加上一个权重,并经过一个激活函数,得到该层的输出。
所以该层的相关参数为:6个5x5的模板参数w,6个模板对应的权重参数b,共6x5x5+6个参数
tips:
关于激活函数:激活函数我们在学习神经网络时就已经接触过了,其主要有两个目的,第一是将数据钳制在一定范围内(如sigmoid函数将数据压缩在-1到1之间),不太高也不太低,第二是用来加入非线性因素的,因为线性模型的表达能力不够。传统神经网络中最常用的两个激活函数sigmoid系和tanh系,而sigmoid系(logistic-sigmoid、tanh-sigmoid)被视为神经网络的核心所在。本文的例子就是sigmoid系。
近年来,在深度学习领域中效果最好,应用更为广泛的是relu激活函数,其相较于sigmoid系,主要变化有三点:①单侧抑制 ②相对宽阔的兴奋边界 ③稀疏激活性。特别是在神经科学方面,除了新的激活频率函数之外,神经科学家还发现了的稀疏激活性广泛存在于大脑的神经元,神经元编码工作方式具有稀疏性和分布性。大脑同时被激活的神经元只有1~4%。从信号方面来看,即神经元同时只对输入信号的少部分选择性响应,大量信号被刻意的屏蔽了,这样可以提高学习的精度,更好更快地提取稀疏特征。而在经验规则的初始化w之后,传统的sigmoid系函数同时近乎有一半的神经元被激活,这不符合神经科学的研究,而且会给深度网络训练带来巨大问题。softplus照顾到了新模型的前两点,却没有稀疏激活性。因而,校正函数max(0,x)即relu函数成了最大赢家。
(2)采样层s2及s4(pooling层)
采样层s又名pooling层,pooling主要是为了减少数据处理的维度,常见的pooling方法有max pooling和average pooling等。
max pooling 就是选择当前块内最大像素值来表示当前局部块
average pooling 就是选择当前块的像素值平均值来代替
本文的选择pooling方法是average pooling,而使用广泛效果较好的方法却是max pooling。(看到这里,你可能会吐槽,为什么不用效果好,因为平均计算相比而言,有那么一丢丢简单!)
(3)卷积层c3
这里的卷积层是一个全连接的卷积层。输出的卷积公式如下,这里i表示图像,w表示卷积模板,b表示偏重,φ表示激活函数,i表示输入图像序号(i=1~6),j表示该层输出图像序号(j=1~12)
由此可以看到在卷积层c3中输入为6个12x12的图像,输出为12个8x8的图像
所需要训练的参数有6x12个5x5的卷积模板w和12个偏重b(每个模板对应的偏重都是相同的)
而实际上由于神经网络的稀疏结构和减少训练时间的需要,该卷积层一般不是利用全连接的,就比如前面介绍lenet-5网络,只需要利用16个卷积模板就可以了,而不是全连接的6x12个,其连接方法如下,其最终得到16个输出图像。
这里x表示选择卷积,比如第0张输出图像是由第0、1、2张输入图像分别同第0个卷积模板卷积相加,再加上偏重,经过激活函数得到的。而第15张图像是由第0、1、2、3、4、5张输入图像分别同第15个卷积模板卷积相加得到的。
(4)输出层o5:
采样层s4后,我们将得到12张4*4的图像,将所有图像展开成一维,就得到了12*4*4=192位的向量。
输出层是由输入192位,输出10位的全连接单层神经网络,共有10个神经元构成,每个神经元都同192位输入相连,即都有192位的输入和1位输出,其处理公式如下,这里j表示输出神经元的序号,i表示输入的序号。
所以该层参数共有192*10个权重w,和10个偏重b
二、卷积神经网络的相关数据结构
这个卷积网络主要有五层网络,主要结构是卷积层、采样层(pooling)、卷积层、采样层(pooling)和全连接的单层神经网络层(输出层),所以我们建立了三个基本层的结构及一个总的卷积网络结构。
这里结构内除了必要的权重参数,而需要记录该层输入输出数据y,及需要传递到下一层的局部梯度d。
(1)卷积层
// 卷积层 typedef struct convolutional_layer{ int inputwidth; //输入图像的宽 int inputheight; //输入图像的长 int mapsize; //特征模板的大小,模板一般都是正方形 int inchannels; //输入图像的数目 int outchannels; //输出图像的数目 // 关于特征模板的权重分布,这里是一个四维数组 // 其大小为inchannels*outchannels*mapsize*mapsize大小 // 这里用四维数组,主要是为了表现全连接的形式,实际上卷积层并没有用到全连接的形式 // 这里的例子是deaplearningtoolboox里的cnn例子,其用到就是全连接 float**** mapdata; //存放特征模块的数据 float**** dmapdata; //存放特征模块的数据的局部梯度 float* basicdata; //偏置,偏置的大小,为outchannels bool isfullconnect; //是否为全连接 bool* connectmodel; //连接模式(默认为全连接) // 下面三者的大小同输出的维度相同 float*** v; // 进入激活函数的输入值 float*** y; // 激活函数后神经元的输出 // 输出像素的局部梯度 float*** d; // 网络的局部梯度,δ值 }covlayer;
(2)采样层
// 采样层 pooling typedef struct pooling_layer{ int inputwidth; //输入图像的宽 int inputheight; //输入图像的长 int mapsize; //特征模板的大小 int inchannels; //输入图像的数目 int outchannels; //输出图像的数目 int pooltype; //pooling的方法 float* basicdata; //偏置 float*** y; // 采样函数后神经元的输出,无激活函数 float*** d; // 网络的局部梯度,δ值 }poollayer;
(3)全连接的单层神经网络
// 输出层 全连接的神经网络 typedef struct nn_layer{ int inputnum; //输入数据的数目 int outputnum; //输出数据的数目 float** wdata; // 权重数据,为一个inputnum*outputnum大小 float* basicdata; //偏置,大小为outputnum大小 // 下面三者的大小同输出的维度相同 float* v; // 进入激活函数的输入值 float* y; // 激活函数后神经元的输出 float* d; // 网络的局部梯度,δ值 bool isfullconnect; //是否为全连接 }outlayer;
(4)各层共同组成一个完整的卷积网络
typedef struct cnn_network{ int layernum; covlayer* c1; poollayer* s2; covlayer* c3; poollayer* s4; outlayer* o5; float* e; // 训练误差 float* l; // 瞬时误差能量 }cnn;
(5)另外还有一个用于存放训练参量的结构
typedef struct train_opts{ int numepochs; // 训练的迭代次数 float alpha; // 学习速率 }cnnopts;
三、卷积神经网络的初始化
卷积神经网络的初始化主要包含了各数据的空间初始化及权重的随机赋值,没有什么复杂,按照结构分配空间就可以了,这里不再详细赘述了,可以直接参考代码内cnnsetup()函数
四、卷积神经网络的前向传播过程
前向传播过程实际上就是指输入图像数据,得到输出结果的过程,而后向传播过程就是将输出结果的误差由后向前传递给各层,各层依次调整权重的过程。所以前向传播过程相比而是比较直观,而且简单的。
前向传播过程在项目中主要是由cnnff函数完成,下面我们将按层介绍其过程
(1)卷积层c1
卷积层c1共有6个卷积模板,每个模板同输入图像卷积将会得到一个输出,即共6个输出,以下是图像的卷积公式:
c1层的相关代码,这里cov函数是卷积函数,在mat.cpp是具体的定义,activation_sigma是激活函数
int outsizew=cnn->s2->inputwidth; int outsizeh=cnn->s2->inputheight; // 第一层的传播 int i,j,r,c; // 第一层输出数据 nsize mapsize={cnn->c1->mapsize,cnn->c1->mapsize}; nsize insize={cnn->c1->inputwidth,cnn->c1->inputheight}; nsize outsize={cnn->s2->inputwidth,cnn->s2->inputheight}; for(i=0;i<(cnn->c1->outchannels);i++){ for(j=0;j<(cnn->c1->inchannels);j++){ float** mapout=cov(cnn->c1->mapdata[j][i],mapsize,inputdata,insize,valid); addmat(cnn->c1->v[i],cnn->c1->v[i],outsize,mapout,outsize); for(r=0;rc1->y[i][r][c]=activation_sigma(cnn->c1->v[i][r][c],cnn->c1->basicdata[i]); }
(2)采样层s2,avgpooling是平均pooling函数
// 第二层的输出传播s2,采样层 outsize.c=cnn->c3->inputwidth; outsize.r=cnn->c3->inputheight; insize.c=cnn->s2->inputwidth; insize.r=cnn->s2->inputheight; for(i=0;i<(cnn->s2->outchannels);i++){ if(cnn->s2->pooltype==avepool) avgpooling(cnn->s2->y[i],outsize,cnn->c1->y[i],insize,cnn->s2->mapsize); }
(3)卷积层c3,同c1很类似
// 第三层输出传播,这里是全连接 outsize.c=cnn->s4->inputwidth; outsize.r=cnn->s4->inputheight; insize.c=cnn->c3->inputwidth; insize.r=cnn->c3->inputheight; mapsize.c=cnn->c3->mapsize; mapsize.r=cnn->c3->mapsize; for(i=0;i<(cnn->c3->outchannels);i++){ for(j=0;j<(cnn->c3->inchannels);j++){ float** mapout=cov(cnn->c3->mapdata[j][i],mapsize,cnn->s2->y[j],insize,valid); addmat(cnn->c3->v[i],cnn->c3->v[i],outsize,mapout,outsize); for(r=0;rc3->y[i][r][c]=activation_sigma(cnn->c3->v[i][r][c],cnn->c3->basicdata[i]); }
(4)采样层s4,同s2很类似
// 第四层的输出传播 insize.c=cnn->s4->inputwidth; insize.r=cnn->s4->inputheight; outsize.c=insize.c/cnn->s4->mapsize; outsize.r=insize.r/cnn->s4->mapsize; for(i=0;i<(cnn->s4->outchannels);i++){ if(cnn->s4->pooltype==avepool) avgpooling(cnn->s4->y[i],outsize,cnn->c3->y[i],insize,cnn->s4->mapsize); }
(5)输出层o5
// 输出层o5的处理 // 首先需要将前面的多维输出展开成一维向量 float* o5indata=(float*)malloc((cnn->o5->inputnum)*sizeof(float)); for(i=0;i<(cnn->s4->outchannels);i++) for(r=0;rs4->y[i][r][c]; nsize nnsize={cnn->o5->inputnum,cnn->o5->outputnum}; nnff(cnn->o5->v,o5indata,cnn->o5->wdata,cnn->o5->basicdata,nnsize); for(i=0;io5->outputnum;i++) cnn->o5->y[i]=activation_sigma(cnn->o5->v[i],cnn->o5->basicdata[i]); free(o5indata); }