![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/41770ad68f1ed636778bdb0d4fb3cbba.png)
【1】导论
首先最需要明确的一点就是,卷积神经网络,也就是convolutional neural networks (简称CNN),现在已经被用来应用于各个领域,物体分割啦,风格转换啦,自动上色啦blahblah,但是!!CNN真正能做的,只是起到一个特征提取器的作用!所以这些应用,都是建立在CNN对图像进行特征提取的基础上进行的。
我们先用例子引入:
拿到一张图片,要对它进行识别,最简单的栗子是,这张图是什么?
比如,我现在要训练一个最简单的CNN,用来识别一张图片里的字母是X还是O。
我们人眼一看,很简单嘛,明显就是X啊,但是计算机不知道,它不明白什么是X。所以我们给这张图片加一个标签,也就是俗称的Label,Label=X,就告诉了计算机这张图代表的是X。它就记住了X的长相。
但是并不是所有的X都长这样呀。比如说…
这四个都是X,但它们和之前那张X明显不一样,计算机没见过它们,又都不认识了。
(这里可以扯出机器学习中听起来很高冷的名词 “ 欠拟合 ”)
不认识了怎么办,当然是回忆看看是不是见过差不多的呀。这时候CNN要做的,就是如何提取内容为X的图片的特征。
我们都知道,图片在计算机内部以像素值的方式被存储,也就是说两张X在计算机看来,其实是这样子的。
其中1代表白色,-1代表黑色。
如果按照每像素逐个比较肯定是不科学的,结果不对而且效率低下,因此提出其他匹配方法。
我们称之为patch匹配。
观察这两张X图,可以发现尽管像素值无法一一对应,但也存在着某些共同点。
如上图所示,两张图中三个同色区域的结构完全一致!
因此,我们就考虑,要将这两张图联系起来,无法进行全体像素对应,但是否能进行局部地匹配?
答案当然是肯定的。
相当于如果我要在一张照片中进行人脸定位,但是CNN不知道什么是人脸,我就告诉它:人脸上有三个特征,眼睛鼻子嘴巴是什么样,再告诉它这三个长啥样,只要CNN去搜索整张图,找到了这三个特征在的地方就定位到了人脸。
同理,从标准的X图中我们提取出三个特征(feature)
我们发现只要用这三个feature便可定位到X的某个局部。
feature在CNN中也被成为卷积核(filter),一般是3X3,或者5X5的大小。
阅读到这里不需要你明白太多东西,只需要你能对CNN识别图片原理大概了解。
应用中CNN要做的事情是:给定一张图片,是车还是马未知,是什么车也未知,现在需要模型判断这张图片里具体是一个什么东西,总之输出一个结果:如果是车 那是什么车
- 最左边是数据输入层,对数据做一些处理,比如去均值(把输入数据各个维度都中心化为0,避免数据过多偏差,影响训练效果)、归一化(把所有的数据都归一到同样的范围)、PCA/白化等等。CNN只对训练集做“去均值”这一步。
- CONV:卷积计算层,线性乘积 求和。
- RELU:激励层(激活层),上文2.2节中有提到:ReLU是激活函数的一种
- POOL:池化层,简言之,即取区域平均或最大
- FC:全连接层
这些概念我们下面一一讲解
【2】卷积运算
什么是卷积?
对图像(不同的数据窗口数据)和滤波矩阵(一组固定的权重:因为每个神经元的多个权重固定,所以又可以看做一个恒定的滤波器filter)做内积(逐个元素相乘再求和)的操作就是所谓的『卷积』操作,也是卷积神经网络的名字来源。
非严格意义上来讲,下图中红框框起来的部分便可以理解为一个滤波器,即带着一组固定权重的神经元。多个滤波器叠加便成了卷积层。
名词过多不易理解我们下面简单地说。
四个字:对应相乘。
取 feature里的(1,1)元素值,再取图像上蓝色框内的(1,1)元素值,二者相乘等于1。把这个结果1填入新的图中。
同理再继续计算其他8个坐标处的值
9个都计算完了就会变成这样。
接下来的工作是对右图九个值求平均,得到一个均值,将均值填入一张新的图中。(注意,其实求取平均值的一步可以不做,只求和填入特征图,最后多一步函数计算归一处理)
这张新的图我们称之为 feature map (特征图)
下面这张图可以更加直观的看到卷及计算的过程。
滤波器一样的东西就是我们的 feature特征,也叫卷积核,像是人类眼睛。
每次看图片由于图片像素过大,眼睛的可视范围较小,因此识别整张图片需要扫描。
这个蓝色框我们称之为 “眼睛”,眼睛的特性呢,就是要会扫描。
其实最开始,它应该在起始位置。
进行卷积对应相乘运算并求得均值后,滑动窗便开始向右边滑动。根据步长的不同选择滑动幅度。
比如,若 步长 stride=1,就往右平移一个像素。
若 步长 stride=2,就往右平移两个像素。
就这么移动到最右边后,返回左边,开始第二排。同样,若步长stride=1,向下平移一个像素;stride=2则向下平移2个像素。
好了,经过一系列卷积对应相乘,求均值运算后,我们终于把一张完整的feature map填满了。
feature map是每一个feature从原始图像中提取出来的“特征”。其中的值,越接近为1表示对应位置和feature的匹配越完整,越是接近-1,表示对应位置和feature的反面匹配越完整,而值接近0的表示对应位置没有任何匹配或者说没有什么关联。
一个feature作用于图片产生一张feature map,对这张X图来说,我们用的是3个feature,因此最终产生3个 feature map。
图像上的卷积
我们由浅入深。
在下图对应的计算过程中,输入是一定区域大小(width*height)的数据,和滤波器filter(带着一组固定权重的神经元)做内积后等到新的二维数据。
具体来说,左边是图像输入,中间部分就是滤波器filter(带着一组固定权重的神经元),不同的滤波器filter会得到不同的输出数据,比如颜色深浅、轮廓。相当于如果想提取图像的不同特征,则用不同的滤波器filter,提取想要的关于图像的特定信息:颜色深浅或轮廓。(注意结合上面X的例子)
- a. 深度depth:神经元个数,决定输出的depth厚度。同时代表滤波器(filter)个数。
- b. 步长stride:决定滑动多少步可以到边缘。
- c.填充值zero-padding:在外围边缘补充若干圈0,方便从初始位置以步长为单位可以刚好滑倒末尾位置,通俗地讲就是为了总长能被步长整除。
注意c,填充值的引入是为了更好地采集图片边缘信息,这应该不难理解。
- 两个神经元,即depth=2,意味着有两个滤波器。
- 数据窗口每次移动两个步长取3*3的局部数据,即stride=2。
- zero-padding=1。
- 左边是输入(773中,7*7代表图像的像素/长宽,3代表R、G、B 三个颜色通道)
- 中间部分是两个不同的滤波器Filter w0、Filter w1
- 最右边则是两个不同的输出
三个滤波器是如何计算的呢?
我们细究下上图的具体计算过程。即上图中的输出结果1具体是怎么计算得到的呢?其实,类似wx + b,w对应滤波器Filter w0,x对应不同的数据窗口,b对应Bias b0,相当于滤波器Filter w0与一个个数据窗口相乘再求和后,最后加上Bias b0得到输出结果1,如下过程所示:
【3】非线性激活
ReLU激励层
激活函数:ReLU,其图形表示如下
直观看到f(x)=max(0,x) ,对于输入的负值,输出全为0,对于正值,原样输出。
卷积层对原图运算多个卷积产生一组线性激活响应,而非线性激活层是对之前的结果进行一个非线性的激活响应。
现在我们要知道,在进行池化操作前需要对filter map进行非线性激活操作。
为什么要这么做呢?上面说到,卷积后产生的特征图中的值,越靠近1表示与该特征越关联,越靠近-1表示越不关联,而我们进行特征提取时,为了使得数据更少,操作更方便,就直接舍弃掉那些不相关联的数据。
生硬的文字直观的图。
x
i
>
=
0
x_i>=0
xi>=0的值不变
x
i
<
0
x_i<0
xi<0的值为0
【4】pooling池化层
卷积操作后,我们得到了一张张有着不同值的feature map,尽管数据量比原图少了很多,但还是过于庞大(比较深度学习动不动就几十万张训练图片),因此接下来的池化操作就可以发挥作用了,它的作用就是减少数据量。
池化分为两种,Max Pooling 最大池化、Average Pooling平均池化。顾名思义,最大池化就是取最大值,平均池化就是取平均值
拿最大池化举例:选择池化尺寸为2x2,因为选定一个2x2的窗口,在其内选出最大值更新进新的feature map。
最终得到池化后的feature map。可明显发现数据量减少了很多。
因为最大池化保留了每一个小块内的最大值,所以它相当于保留了这一块最佳匹配结果(因为值越接近1表示匹配越好)。这也就意味着它不会具体关注窗口内到底是哪一个地方匹配了,而只关注是不是有某个地方匹配上了。这也就能够看出,CNN能够发现图像中是否具有某种特征,而不用在意到底在哪里具有这种特征。这也就能够帮助解决之前提到的计算机逐一像素匹配的死板做法。
到这里就介绍了CNN的基本配置—卷积层、Relu层、池化层。
在常见的几种CNN中,这三层都是可以堆叠使用的,将前一层的输入作为后一层的输出。比如:
【5】全连接层
经过卷积层,激活层,池化层,我们得到了2*2的矩阵,这跟我们的分类似乎还差点联系。
全连接层要做的,就是对之前的所有操作进行一个总结,给我们一个最终的结果。
它最大的目的是对特征图进行维度上的改变,来得到每个分类类别对应的概率值。
得到了2X2的特征图后,对其应用全连接网络,再全连接层中有一个非常重要的函数----Softmax,它是一个分类函数,输出的是每个对应类别的概率值。比如:
【0.5,0.03,0.89,0.97,0.42,0.15】就表示有6个类别,并且属于第四个类别的概率值0.89最大,因此判定属于第四个类别。
注意:本例中因为只有两个类别X和O,而且数据量到此已经非常少了,而且进行卷积运算时进行了取平均,因此也不需用softmax函数了。因此直接将三个特征图改变维度直接变成一维的数据。(相当于全连接层的每个参数均为1)
展开的数据即为属于类别X的概率值,值大小也在对应X的线条粗细中表现出来了。
以上所有的操作都是对标准的原图X来进行的,因此最终分类显示即为X毋庸置疑。
【6】神经网络的训练与优化
学习过神经网络基础应该知道神经网络参数具体是多少是需要训练的,卷积神经网络网络同样如此,很明显我们的参数就是卷积核(filter)。
针对这个识别X的例子,我们可以人为定义三个3X3的卷积核,便可实现对X的特征提取。但是在实际运用中,比如识别手写字母,几乎不可能存在标准的写法,每个人的字迹都完全不同,因此原来的那三个标准的卷积核就变得不再适用了,为了提高CNN模型的通用性(机器学习中的“泛化能力”),就需要对卷积核进行改写。经过成千上万的训练集来训练,每一次加入新的数据,都有可能对卷积核里的值造成影响。
训练方法依旧是BackProp反向传播算法。
在训练时,我们采用的训练数据一般都是带有标签label的图片。如果图片中的字母是X,则label=x,如果图片中的字母是A,则label=A。 标签能直观地反映图片。
在最开始,训练前,我们定义一个大小为3X3的卷积核,那么里面具体的值是多少,我们都不知道,但又不能为0吧,所以就用随机初始化法来进行赋值,卷积核获取到了一个随机值,便可以开始工作。
卷积神经网络便可以开始工作了,输入一张带有标签的图片(假设图片内容是字母X)。经网络识别后判断是X的概率为0.3。本来应该是1.0的概率,现在只有0.3,问题就很明显了,存在了很大的误差。
一种简单定义误差error的计算公式为 e r r o r = ( r e s u l t − l a b e l ) 2 error=(result-label)^2 error=(result−label)2
训练的终极目的就是使得这个误差最小,常用的方法是 梯度下降法。
附上一篇代码:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_dat
mnist = input_data.read_data_sets('C:/Users/zhen/MNIST_data_bak/', one_hot=True)
sess = tf.InteractiveSession()
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
x = tf.placeholder(tf.float32, [None, 784])
y = tf.placeholder(tf.float32, [None, 10])
x_image = tf.reshape(x, [-1, 28, 28, 1])
# 第一层卷积核
W_conv = weight_variable([5, 5, 1, 16])
b_conv = bias_variable([16])
h_conv = tf.nn.relu(conv2d(x_image, W_conv) + b_conv)
h_pool = max_pool_2x2(h_conv)
# 第二层卷积核
W_conv2 = weight_variable([5, 5, 16, 32])
b_conv2 = bias_variable([32])
h_conv2 = tf.nn.relu(conv2d(h_pool, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
# 全连接层
W_fc = weight_variable([7 * 7 * 32, 512])
b_fc = bias_variable([512])
h_pool_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 32])
h_fc = tf.nn.relu(tf.matmul(h_pool_flat, W_fc) + b_fc)
# 防止过拟合,使用Dropout层
keep_prob = tf.placeholder(tf.float32)
h_fc_drop = tf.nn.dropout(h_fc, keep_prob)
# Softmax分类
W_fc2 = weight_variable([512, 10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc_drop, W_fc2) + b_fc2)
# 定义损失函数
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y * tf.log(y_conv), reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 训练
tf.global_variables_initializer().run()
for i in range(20):
batch = mnist.train.next_batch(50)
train_step.run(feed_dict={x:batch[0], y:batch[1], keep_prob:0.5})
print("test accuracy %g" % accuracy.eval(feed_dict={x:mnist.test.images, y:mnist.test.labels, keep_prob:1.0}))