一、前言
这篇卷积神经网络是前面介绍的多层神经网络的进一步深入,它将深度学习的思想引入到了神经网络当中,通过卷积运算来由浅入深的提取图像的不同层次的特征,而利用神经网络的训练过程让整个网络自动调节卷积核的参数,从而无监督的产生了最适合的分类特征。这个概括可能有点抽象,我尽量在下面描述细致一些,但如果要更深入了解整个过程的原理,需要去了解DeepLearning。
这篇文章会涉及到卷积的原理与图像特征提取的一般概念,并详细描述卷积神经网络的实现。但是由于精力有限,没有对人类视觉的分层以及机器学习等原理有进一步介绍,后面会在深度学习相关文章中展开描述。
二、卷积
卷积是分析数学中一种很重要的运算,其实是一个很简单的概念,但是很多做图像处理的人对这个概念都解释不清,为了简单起见,这里面我们只介绍离散形式的卷积,那么在图像上,对图像用一个卷积核进行卷积运算,实际上是一个滤波的过程。我们先看一下卷积的基本数学表示:
f(x,y)°w(x,y)=∑s=−aa∑t=−bbw(s,t)f(x−s,y−t)f(x,y)°w(x,y)=∑s=−aa∑t=−bbw(s,t)f(x−s,y−t)
其中I=f(x,y)I=f(x,y)是一个图像,f(x,y)f(x,y)是图像I上面xx行yy列上点的灰度值。而w(x,y)w(x,y)有太多名字叫了,滤波器、卷积核、响应函数等等,而aa和bb定义了卷积核即w(x,y)w(x,y)的大小。
从上面的式子中,可以很明显的看到,卷积实际上是提供了一个权重模板,这个模板在图像上滑动,并将中心依次与图像中每一个像素对齐,然后对这个模板覆盖的所有像素进行加权,并将结果作为这个卷积核在图像上该点的响应。所以从整个卷积运算我们可以看到以下几点:
1)卷积是一种线性运算
2)卷积核的大小,定义了图像中任何一点参与运算的邻域的大小。
3)卷积核上的权值大小说明了对应的邻域点对最后结果的贡献能力,权重越大,贡献能力越大。
4)卷积核沿着图像所有像素移动并计算响应,会得到一个和原图像等大图像。
5)在处理边缘上点时,卷积核会覆盖到图像外层没有定义的点,这时候有几种方法设定这些没有定义的点,可以用内层像素镜像复制,也可以全设置为0。
三、卷积特征层
其实图像特征提取在博客里的一些其他文章都有提过,这里我想说一下图像特征提取与卷积的关系。其实大部分的图像特征提取都依赖于卷积运算,比如显著的边缘特征就是用各种梯度卷积算子对图像进行滤波的结果。一个图像里目标特征主要体现在像素与周围像素之间形成的关系,这些邻域像素关系形成了线条、角点、轮廓等。而卷积运算正是这种用邻域点按一定权重去重新定义该点值的运算。
水平梯度的卷积算子:
竖直梯度的卷积算子:
根据深度学习关于人的视觉分层的理论,人的视觉对目标的辨识是分层的,低层会提取一些边缘特征,然后高一些层次进行形状或目标的认知,更高层的会分析一些运动和行为。也就是说高层的特征是低层特征的组合,从低层到高层的特征表示越来越抽象,越来越能表现语义或者意图。而抽象层面越高,存在的可能猜测就越少,就越利于分类。
而深度学习就是通过这种分层的自动特征提取来达到目标分类,先构建一些基本的特征层,然后用这些基础特征去构建更高层的抽象,更精准的分类特征。
那整个卷积神经网络的结构也就很容易理解了,它在普通的多层神经网络的前面加了2层特征层,这两层特征层是通过权重可调整的卷积运算实现的。
四、卷积神经网络
在原来的多层神经网络结构中,每一层的所有结点会按照连续线的权重向前计算,成为下一层结点的输出。而每一条权重连结线都彼此不同,互不共享。每一个下一层结点的值与前一层所有结点都相关。
与普通多层神经网络不同的是,卷积神经网络里,有特征抽取层与降维层,这些层的结点连结是部分连接且,一幅特征图由一个卷积核生成,这一幅特征图上的所有结点共享这一组卷积核的参数。
这里我们设计一个5层的卷积神经网络,一个输入层,一个输出层,2个特征提取层,一个全连接的隐藏层。下面详细说明每一层的设计思路。
在一般的介绍卷积神经网络的文章中你可能会看到在特征层之间还有2层降维层,在这里我们将卷积与降维同步进行,只用在卷积运算时,遍历图像中像素时按步长间隔遍历即可。
输入层:普通的多层神经网络,第一层就是特征向量。一般图像经过人为的特征挑选,通过特征函数计算得到特征向量,并作为神经网络的输入。而卷积神经网络的输出层则为整个图像,如上图示例29*29,那么我们可以将图像按列展开,形成841个结点。而第一层的结点向前没有任何的连结线。
第1层:第1层是特征层,它是由6个卷积模板与第一层的图像做卷积形成的6幅13*13的图像。也就是说这一层中我们算上一个偏置权重,一共有只有(5*5+1)*6=156权重参数,但是,第二层的结点是将6张图像按列展开,即有6*13*13=1014个结点。
而第2层构建的真正难点在于,连结线的设定,当前层的结点并不是与前一层的所有结点相连,而是只与25邻域点连接,所以第二层一定有(25+1)*13*13*6=26364条连线,但是这些连结线共享了156个权值。按前面多层网络C++的设计中,每个连线对象有2个成员,一个是权重的索引,一个是上一层结点的索引,所以这里面要正确的设置好每个连线的索引值,这也是卷积神经网络与一般全连结层的区别。
第2层:第2层也是特征层,它是由50个特征图像组成,每个特征图像是5*5的大小,这个特征图像上的每一点都是由前一层6张特征图像中每个图像取25个邻域点最后在一起加权而成,所以每个点也就是一个结点有(5*5+1)*6=156个连结线,那么当前层一共有5*5*50=1250个结点,所以一共有195000个权重连结线,但是只有(5*5+1)*6*50=7800个权重参数,每个权重连结线的值都可以在7800个权重参数数组中找到索引。
所以第3层的关键也是,如何建立好每根连结线的权重索引与与前一层连结的结点的索引。
第3层:和普通多神经网络没有区别了,是一个隐藏层,有100个结点构成,每个结点与上一层中1250个结点相联,共有125100个权重与125100个连结线。
第4层:输出层,它个结点个数与分类数目有关,假设这里我们设置为10类,则输出层为10个结点,相应的期望值的设置在多层神经网络里已经介绍过了,每个输出结点与上面隐藏层的100个结点相连,共有(100+1)*10=1010条连结线,1010个权重。
从上面可以看出,卷积神经网络的核心在于卷积层的创建,所以在设计CNNLayer类的时候,需要两种创建网络层的成员函数,一个用于创建普通的全连接层,一个用于创建卷积层。
1 class CNNlayer 2 { 3 private: 4 CNNlayer* preLayer; 5 vector<CNNneural> m_neurals; 6 vector<double> m_weights; 7 public: 8 CNNlayer(){ preLayer = nullptr; } 9 // 创建卷积层 10 void createConvLayer(unsigned curNumberOfNeurals, unsigned preNumberOfNeurals, unsigned preNumberOfFeatMaps, unsigned curNumberOfFeatMaps); 11 // 创建 普通层 12 void createLayer(unsigned curNumberOfNeurals,unsigned preNumberOfNeurals); 13 void backPropagate(vector<double>& dErrWrtDxn, vector<double>& dErrWrtDxnm, double eta); 14 };
创建普通层,在前面介绍的多层神经网络中已经给出过代码,它接收两个参数,一个是前面一层结点数,一个是当前层结点数。
而卷积层的创建则复杂的多,所有连结线的索引值的确定需要对整个网络有较清楚的了解。这里设计的createConvLayer函数,它接收4个参数,分别对应,当前层结点数,前一层结点数,前一层特征图的个数和当前层特征图像的个数。
下面是C++代码,要理解这一部分可以会稍有点难度,因为特征图实际中都被按列展开了,所以邻域这个概念会比较抽象,我们考虑把特征图像还原,从图像的角度去考虑。
1 void CNNlayer::createConvLayer 2 (unsigned curNumberOfNeurals, unsigned preNumberOfNeurals, unsigned preNumberOfFeatMaps, unsigned curNumberOfFeatMaps) 3 { 4 // 前一层和当前层特征图的结点数 5 unsigned preImgSize = preNumberOfNeurals / preNumberOfFeatMaps; 6 unsigned curImgSize = curNumberOfNeurals / curNumberOfFeatMaps; 7 8 // 初始化权重 9 unsigned numberOfWeights = preNumberOfFeatMaps*curNumberOfFeatMaps*(5 * 5 + 1); 10 for (unsigned i = 0; i != numberOfWeights; i++) 11 { 12 m_weights.push_back(0.05*rand() / RAND_MAX); 13 } 14 // 建立所有连结线 15 16 for (unsigned i = 0; i != curNumberOfFeatMaps; i++) 17 { 18 unsigned imgRow = sqrt(preImgSize); //上一层特征图像的大小 ,imgRow=imgCol 19 // 间隙2进行取样,邻域周围25个点 20 for (int c = 2; c < imgRow-2; c= c+ 2) 21 { 22 for (int r = 2; r < imgRow-2; r =r + 2) 23 { 24 CNNneural neural; 25 for (unsigned k = 0; k != preNumberOfNeurals; k++) 26 { 27 for (int kk = 0; kk < (5*5+1); kk ++) 28 { 29 CNNconnection connection; 30 // 权重的索引 31 connection.weightIdx = i*(curNumberOfFeatMaps*(5 * 5 + 1)) + k*(5 * 5 + 1) + kk; 32 // 结点的索引 33 connection.neuralIdx = k*preImgSize + c*imgRow + r; 34 neural.m_connections.push_back(connection); 35 } 36 m_neurals.push_back(neural); 37 } 38 } 39 } 40 } 41 }
五、训练与识别
整个网络结构搭建好以后,训练只用按照多层神经网络那样训练即可,其中的权值更新策略都是一致的。所以总体来说,卷积神经网络与普通的多层神经网络,就是结构上不同。卷积神经网络多了特征提取层与降维层,他们之间结点的连结方式是部分连结,多个连结线共享权重。而多层神经网络前后两层之间结点是全连结。除了这以外,权值更新、训练、识别都是一致的。
训练得到一组权值,也就是说在这组权值下网络可以更好的提取图像特征用于分类识别。
关于源码的问题:个人非常不推荐直接用别人的源码,所以我的博客里所有文章不会给出整个工程的源码,但是会给出一些核心函数的代码,如果你仔细阅读文章,一定能够很好的理解算法的核心思想。尝试着去自己实现,会对你的理解更有帮助。有什么疑问可以直接在下面留言。