一、背景前言
先看下人眼看到物体的情况:
眼睛里一个神经元,它会盯着图像,但它并不会盯着图像的每一个像素,它只会盯着图像的一部分。盯的一部分,称为叫感受野,也就是感受的一部分。另外一个神经元它又会盯着另外的一部分。但是它们有重叠。这就相当于一些神经元看线, 另外一些神经元会看线的方向,然后组合底层的一些图案,所以底层看到的这个线、方向组合成一些更大的感受野,然后组成一些基础图像,基础图像再组合成一些更高级抽象的图像。
卷积神经网络(Convolutional Neural Networks) CNN,人工智能是拟人,卷积神经网络的图像识别也是拟人,拟人的眼睛。模仿人类的视觉皮层,这里面最早提出卷积的是在98年时,Yann LeCun等人推出了LeNet-5这样一个架构,它之所以叫LeNet-5,实际上其实在这里面就是 5层的这样一个神经网络,当时他推出LeNet-5架构的时候,它其实是广泛用于手写体数字识别,在这个LeNet-5架构里面,它包含Fc的全连接层,还有Sigmoid激活函数,他还提出了卷积层(Conv)和池化层(Pool)。
二、卷积层的理解
生物里面的神经元它并不像咱们之前去做全连接层的时候,比如DNN做MNIST的时候,它里面是一张图片,内容就是0到9的数字,每张图片相当于是28×28的大小,也就是总共会有784个像素,我们之前做全连接的时候,输入层在这里面就会输入784个像素点,相当于从X1~X784,第二层的时候,当时我们称为隐藏层,hide1这里面每一个神经元都会去连接我们之前的784个输入,另外的神经元也会去连接之前的784个输入,这样就构成级联与级联之间的全部的连接,我们叫做全连接层。
现在图像视频里面卷积就是这个感受野的概念:
比如说下图是我们之前的第一个隐藏层(h1),它里面的每个神经元不再是全连接到我们原来的每一个像素点,而是只连接到一个局部,
局部里面包含多少像素点,它就和前面连接有多少像素点。
实际上在卷积里面这个地方不叫隐藏层了,叫做卷积层(Conv1)。卷积层里面的一个神经元,它只会接收到前面输入层里面的其中某一部分,这就是我们所谓的局部连接,这样的话有什么样的好处 ?
好处就在于如果是全连接的话,假如中间w矩阵它前面有m个节点,当前这一层有多少个n节点,中间这个w矩阵的形状一定是m行n列的。
而局部连接会使w的个数相应减少,但是这样会不会影响我们的最后的准确率呢?事实上卷积神经网络,它是完全的一个拟人。人类的视觉每个神经元并不是看所有的像素点,而是每个神经元是看局部的一个像素点,神经元不单单只有一层,或者说神经网络,它不单单只有一层,实际上它有很多层,所以说卷积是通过靠层数的增加来弥补它里面层与层之间w个数的减少。这样的话当然不会影响准确率。神经网络里面隐藏层越多,就相当于是它越往前进行了推理和演绎的越多,尤其在图像识别这个地方更是通过层数进行推理和演绎,因为它需要从点成线,需要从线的成基本的形状,然后变成复杂的图形,然后最终根据这些抽象的高端的高级的这些特征再去来组合,再来判断它是什么样一个物体。
这里面我们说第一个卷积层它其实就是提取了一些像素点的特征,然后来进行组合,这种组合它并不是全部的组合,而是把一些局部里面像素点来进行组合。比如下图:
比如其中的一个神经元识别到局部像素点后画了一条竖杠,另一个神经元对局部像素进行特征提取,识别出来是一条横杠,第3个像素点,对局部像素点进行特征提取后画了一条竖杠,那这样的话这三个基本的点成线的抽象就会再一次的进行抽象,汇总到一个神经元后提取出来一个门(一竖一横一竖的门),而另一些神经元会把一个房顶识别出来,然后门加房顶不断的提取汇总,最终汇总出来一个房子。所以它就在不断抽象特征,因此卷积神经网络它更多的是在图像识别这一块来进行特征的提取、组合。
所以DNN里面其实就通过全连接层把这些特征进行组合抽象,CNN里面是通过卷积层把这些特征进行提取和组合来进行抽象。
其实CNN不局限于仅仅用于图像识别,现在可以用于其他的一些应用,所以说卷积这件事情其实也可以去做图像识别之外的东西。
CNN里面最重要的构建单元跟前面DNN相比,它就是多了卷积层。神经元在第一个卷积层不是连接输入图片的每一个像素,只是连接它们感受野的局部的那些感受野的像素,以此类推,第二个卷积层的每一个神经元它是连接位于第一个卷积层的一个小方块的神经元。
为什么是小方块?对于我们上方的房子的图来说,一个神经元,它拿一个圆形圈内的像素点,但对于计算机来说是拿一个方块的形状里面的像素点比较好取还是一个圆形里面的像素点比较好取呢,必然方块的。对于计算机来说更加容易,所以在真正去执行的时候,一个小方块就是一个局部。
以前我们做MNIST的时候,我们用DNN的时候,实际上是把MNIST的输入,变成一维的,比如一张图片里面有784个像素点,我们实际上是把784个像素点变成一个行向量,这个东西是DNN做全连接的时候去输入的数据。如果是CNN的时候,我们就要把这个输入数据变成二维的,也就是说我们的CNN里面,比如对于MNIST来说,我们的输入图像就应该是28×28的,DNN以前一条样本的输入形状是1行784列,多行的话就是M行784列,每个样本784个维度,整个我们S矩阵就是M行784列, 如果是CNN我们的输入每一张图片它的形状是28×28 ,如果我有M个图片,我们的输入就应该是m作为第一个维度,然后28作为第二个维度,28作为第三个维度,所以在这里面实际上我们的输入的S矩阵应该是(M,28,28)。
对于MNIST来,我们去做图像识别的时候,对于黑白的图片来说,它就是单通道。如果对于彩色图片,笼统的说RGB(红绿蓝)就是三个通道,这样的话我们原来DNN的时候我们的S输入矩阵,其实应该是M行,每一行图片784个维度,然后每张图片是一个通道Channel。
对于我们的CNN来说,我们整个输入S矩阵M的图片,每张图片是28×28的,所以写(M,28,28,1),如果是一个通道,这个地方就写1。如果你的图片不是MNIST而是一些彩色图片,比如说后面会用到Cifar10这样的一个经典数据集,这个数据集里面它其实都是彩色图片,我们的输入的形状S矩阵的形状就得写成(M,28,28,3),最后是三维的,三通道,所以这个地方是未来写代码的时候s矩阵设置上的区别。
但是说为什么要把784个维度一列变成28×28的方形呢?
因为我们做卷积的时候实际上是一个小格一个小格一个小格的去扫原来的图片, 它不再是原来的一个像素点。原来DNN相当于是一个像素点一个像素点的去扫,一个像素点一个像素点的去扫的时候,一维还是二维其实无所谓,所以简单来说就把它做成一维的,对于CNN来说,现在相当于是小块一个小块的去扫,如图:
每一个小格里面实际上就有高Height,宽width,有高有宽,所以就把原来的图片变成28×28,有高有宽的这样一个小格一个小格去取原来的像素点。
对于卷积层来说,我如果把最下面这个input layer看成是输入层,然后再往上Convolutional layer1是一个卷积层,它本质上其实就把图像里面扫描的这一个小格的像素点拿过来,进行计算,计算完之后得到一个结果。
然后另一个小格跟刚才小格的长和宽实际上是一样的,这个小格它又会把下面图的一些像素点拿出来,
然后根据卷积的计算,生成Convolutional layer1 中一个蓝点这样的一个结果,相当于是把很多像素点进行计算,然后抽象了一个最终的结果,这是第一个卷积层干的事情,相当于是特征的提取。
接着再往上如果你还有一个卷积层(Conv2),在这个卷积层里面相当于还会再拿一个相对于你设置的一个框的大小,然后刚才提取出来归纳的特征,就相当于蓝色这个圈的值再次当成图像里面的像素点再来进行卷积,得到Conv2结果里面这一个值。如图:
所以在这个地方就是卷积不断抽象,其实它还是抽象完之后会把这个图片变成一张新的图片,只不过那张图片到底是不是人能看着很舒服的,这个就不用管了,它只要包含着这个特征相应的信息量就足够了,它可能并不是像原始图像那样或者房子什么的,但是这个特征图其实也能画出来也能去看,但是不是特别的重要,就是不是人能看得懂这些特征图,接着再来提取更高级别的图像之后,这个地方提取之后的每一个图我们也称为叫feature map。
三、卷积的具体计算
卷积的计算可以把图层里面一个小方格的原始像素点经过某种计算最终变成一个值,如图:
对上图,比如最右边里面的小方格的结果4,
它就是把5行5列这样一个原始图片,先从左上角开始,进行框的扫描,这个地方为什么它是一个3×3的框来进行扫描?因为中间黄色部分我们指定的filter是3×3的这样一个大小,所以我们就拿黄色的3×3的 filter,也叫做卷积核,拿着3*3 的卷积核去原始图片来进行扫描,扫描相当于是把这3×3的小方格给它摞到原始图片左上角的地方,然后进行相对应的计算,我们通常把卷积核看成W矩阵,原始图像看成是X。
X跟W通常是怎么计算,相乘相加,所以在这个地方其实也是一样,如图:
我们把左上角的1对应到feature卷积核里面的1,它俩相乘就是1,然后对应位置相乘相即:1*1+1*0+1*1+0*0+1*1+1*0+0*1+0*0+1*1=4,我们前面讲多元线回归时候,通常y^=x1*w1+x2*w2+x3*w3+x4*w4...+w0,所以在卷积这个地方我们同样会有w0这个东西,截距通常写成bias。假设这个地方的截距为零,我们就相当于是把刚才相乘相加的这一部分4,然后再加上零能够得到最终这里面的结果,实际上就是4,这个是卷积卷一下左上角3×3的位置得到一个4。
这里卷积核里面的0和1可以理解为是画图的人随便写的。
既然原始图片相当于X,卷积核相当于W,既然是W,那就是需要调整,需要训练的。根据你真实的y然后正向传播,反向传播,反复的调整,就是说在这个地方实际上每一次正向传播是把这个图像传进来,根据w进行计算,就得到y^。然后又有真实的y,得到误差,然后反向传播回来去调w。调完w之后下一次再来进行正向传播,该卷积就卷积,该全连接就全连接,往后传播。所以这里面的黄色部分的卷积核里面的数值真正的情况下是我们计算出来的。
它和左边原始图片的零不一样,原始图片
左边0,1就代表这个图片里面非黑即白,而卷积里面其实就随便写的参数,为了好算。
接着如果我们要再往后来进行扫描的话,在这个地方会有一个概念叫做stride步长, 指的是你移动3×3小块的时候,往右移几个像素格,如果说stride步长等于1,那相当于它往右边移一个位置,这样的话下一次卷积的时候框跑到我现在画3×3这个位置来了,如下:
接着用黄色的3×3的 filter去卷积,结果仍是对应位置相乘相加然后再加上bias:
以此类推,步长为1的话,下一次卷积,再往右移一个格,然后再来进行一下卷积,就能得到最右边矩阵的第一行的位置。
我们的stride步长实际上有两个方向,一般写作stride=(h,w),h即表示height,w即表示width。一个是横着的方向,一个是竖着的方向,如果我把它俩都写成(1,1),就指的是你横着每一次扫的时候移一个像素点,你竖着每一次扫的时候也是移一个像素点,所以3×3的框扫了三次之后,下一次如果步长1,那它该向下走一行,如图:
然后和我们卷积核进行相乘相加,然后再加一个截距,就得到第二行第一个位置的结果。以此类推,下一次就是得到红色框中心点的位置,然后再一次扫描的时候右移一个得到第二行第三列这个位置,接着又到右边了,之后再往下一移一个格。假设你的stride步长是1×1的,原始图片是一个5×5的,然后我们这里面用了卷积核是3×3 ,发现最终得到的是3×3的feature map。
如果在这个地方我们说它是一个4×4的卷积核,步长不变,卷积得到的结果还是3×3的吗?实际上在这里面4×4这卷一下,然后往右一步又卷一下,最后就变成2×2的一个结果了。所以咱们会发现最终的卷积层输出的feature map的大小和原始图片大小有关系,和feature卷积核有关系,其次与步长有关系。
咱们来看一个特殊的例子,原图片是5×5,咱们的卷积核是4×4,stride步长是2×2,开始顶到左上是一个4×4的方框,如图:
下一次的时候走两步到这:
但是右面没有了。这时候已经到右边了,它不会做卷积。接着要往下走了,咱们步长设置成成2×2,这样就意味着往下走两步,再来一个4×4如图,下方也没有了,所以这个也不做卷积了。那最终的结果就是1*1的结果。就是一个值。
你会发现这样卷积只相当于是我原始图片这下面这一行、最右边这一行跟没有是一样的, 相当于把这信息给丢了。对于MNIST来说,它的图像被中心化了, 周围全部都是白的,那样的情况下,丢下面一行和右面一行没有关系,但如果你那个图片它不是周围都是白的,比如说图片里面全部都是彩色的各种物体,那下面这一行和右边这一行最好还是别丢掉,没有它就会影响你的图像识别判断。
如果说我们不想丢这个信息,比如刚才是4×4这个卷积核,这个步长可以小一点,如果不调这个步长,那怎么可以把这些信息不让它丢失?
实际上在这里面我们说除了有stride步长这个概念,还有一个叫做Zero Padding(0 填充)。比如原始的图片是5行5列,我们可以使用Zero Padding,它可以自动的帮助我们去在周边填充零,怎么去填补0呢?需要根据情形,比如原始图片是5*5的,卷积核是3*3,我们需要补几圈零才可以使得5×5的feature map?现在卷积核是3×3。 我们应该在周围都补上一圈0,如下:
补完一圈零之后,这样的图像变成7×7,7×7的原始图片,我们去使用3×3的卷积核,步长还是1×1,而现在是从新的7*7图片的左上角开始扫描,最终会得到5×5的feature map,请读者根据上述卷积过程自行验证。
比如5×5的原图用3×3的filter 去卷积想得到4×4的feacture 就需要补一列一行,是补左边还是右边呢?其实补右边和下边。所以补零也是需要根据原始图像和最终的feature决定。
再看一个Zero Padding例子:
对于原来5行7列的原始图片进行卷积,在外围添了一圈0后,使用3*3的卷积核,它最终的结果是3×4。原始图片是5×7,发现周遭添0 也没有使得它与原来的结果保持一致,那它周遭添0的目的是什么呢?事实上我们拿这张图来说,如果不添一圈0的话来进行卷积的时候,如图:
它是以第二行,第二列为中心去看这个东西了,就不太在乎左上角东西了。如果你要是添一下零,相当于是你上来就把第一行第一列原图片的像素点作为中心点去卷一个东西,如图:
表明你对原来的每一个像素点都很重视。
所以我们说Zero Padding有几个目的,第一点就是使得我们得到的卷积结果和原始图片的大小一样,有些时候会用Zero Padding。第二点就是对于周边的这一圈的像素点也很看重的情况下,我们会用Zero Padding。第三就是不想去丢掉右边和下边的像素点信息,我们会用Zero Padding。
所以Zero Padding这个东西实际上其实无非就是把原始数据里面相应的位置都插一些零就完事了。如果这个东西自己来写代码的话还挺费劲,既要找到一开始这个点在原始的图片里面什么位置,然后右上角在原始图片上面是什么位置,然后你这样给它补一圈。但是这个东西对于tensorflow来说,太常用了,所以人家就有相应的API你直接一调用,然后告诉你给你补全了。也不用自己去操心,是补一圈还是补两圈,你只需要告诉它我要的大小feacture map 和初始的大小是一样的,然后它就自动给我们去补了,就不用自己去算了。