卷积神经网络(Convolutional Neural Network)是一个专门针对图像识别问题设计的神经网络。它模仿人类识别图像的多层过程:瞳孔摄入像素;大脑皮层某些细胞初步处理,发现形状边缘、方向;抽象判定形状(如圆形、方形);进一步抽象判定(如判断物体是美女还是恐龙)。
如果我们使用传统神经网络方式,对一张图片进行分类,那么,我们把图片的每个像素都连接到隐藏层节点上,那么对于一张1000x1000像素的图片,如果我们有1M隐藏层单元,那么一共有10^12个参数,这显然是不能接受的。
卷积网络通过一系列方法,成功将数据量庞大的图像识别问题不断降维,最终使其能够被训练。
近年来,卷积神经网络在语音识别、人脸识别、通用物体识别、运动分析、自然语言处理甚至脑电波分析方面均有突破,是目前深度学习研究领域的热点之一。
1. 卷积
卷积是CNN的核心,从数学表达上来看,卷积就是一种数学运算。
这个卷积操作背后的数学知识其实非常的简单。
要计算一个feature和其在原图上对应的某一小块的结果,只需要简单地将两个小块内对应位置的像素值进行乘法运算,然后将整个小块内乘法运算的结果累加起来,最后再除以小块内像素点总个数即可。
卷积的目的是简化更复杂的数据表达,过滤掉复杂数据中多余的噪声,提取出关键的特征。所以卷积应用经常被称作滤波,而卷积核经常被称作过滤器(或图像扫描器、特征扫描器、局部感受器)。
只需要调整卷积核的数值,就可以执行诸如边缘检测、锐化、模糊等效果,这说明不同的滤波器会从图片中探测到不同的特征,比如边缘、曲线等。
简而言之,卷积就是对提取特征。
传统的特征工程非常难,很少有资料告诉我们特征工程应该怎么做,这些都非常依赖经验。要做好特征工程,就需要长期的经验积累。例如,要识别猫和狗的图片,就得先人工提取出猫和狗的各种特征,比如猫和狗的眼睛大小、耳朵形状等,然后将这些提取好的特征给一个分类器(例如SVM),再进行分类。
在卷积神经网络中,通过对卷积核赋予参数,这些参数经过训练,即可完成特征学习。当我们在新的数据上训练时,就自动学习了新的特征。
这就是卷积神经网络如此强大的原因--不再需要繁重的特征工程了。
2. 局部感知野
在图像处理中,往往把图像表示为像素的向量,比如一个1000×1000的图像,可以表示为一个1000000的向量。在上一节中提到的神经网络中,如果隐含层数目与输入层一样,即也是1000000时,那么输入层到隐含层的参数数据为1000000×1000000=10^12,这样就太多了,基本没法训练。所以图像处理要想练成神经网络大法,必先减少参数加快速度。
卷积神经网络有两种神器可以降低参数数目,第一种神器叫做局部感知野。
一般认为人对外界的认知是从局部到全局的,而图像的空间联系也是局部的像素联系较为紧密,而距离较远的像素相关性则较弱。因而,每个神经元其实没有必要对全局图像进行感知,只需要对局部进行感知,然后在更高层将局部的信息综合起来就得到了全局的信息。网络部分连通的思想,也是受启发于生物学里面的视觉系统结构。视觉皮层的神经元就是局部接受信息的(即这些神经元只响应某些特定区域的刺激)。如下图所示:左图为全连接,右图为局部连接。
在上右图中,假如每个神经元只和10×10个像素值相连,那么权值数据为1000000×100个参数,减少为原来的千分之一。而那10×10个像素值对应的10×10个参数,其实就相当于卷积操作。
3. 参数共享
但其实这样的话参数仍然过多,那么就启动第二级神器,即权值共享(也就是参数共享)。
在上面的局部连接中,每个神经元都对应100个参数,一共1000000个神经元,如果这1000000个神经元的100个参数都是相等的,那么参数数目就变为100了。
怎么理解权值共享呢?我们可以这100个参数(也就是卷积操作)看成是提取特征的方式,该方式与位置无关。这其中隐含的原理则是:图像的一部分的统计特性与其他部分是一样的。这也意味着我们在这一部分学习的特征也能用在另一部分上,所以对于这个图像上的所有位置,我们都能使用同样的学习特征。
更直观一些,当从一个大尺寸图像中随机选取一小块,比如说 8×8 作为样本,并且从这个小块样本中学习到了一些特征,这时我们可以把从这个 8×8 样本中学习到的特征作为探测器,应用到这个图像的任意地方中去。特别是,我们可以用从 8×8 样本中所学习到的特征跟原本的大尺寸图像作卷积,从而对这个大尺寸图像上的任一位置获得一个不同特征的激活值。
4. 多卷积核
上面所述只有100个参数时,表明只有1个100*100的卷积核,显然,特征提取是不充分的,我们可以添加多个卷积核,比如32个卷积核,可以学习32种特征。在有多个卷积核时,如下图所示:
每个卷积核都会将图像生成为另一幅图像。比如两个卷积核就可以将生成两幅图像,这两幅图像可以看做是一张图像的不同的通道:比如,一张图像是经过边缘检测的结果,一张图像是锐化的结果。
5. 池化
在通过卷积获得了特征 (features) 之后,下一步我们希望利用这些特征去做分类。理论上讲,我们可以用所有提取得到的特征去训练分类器,例如 softmax 分类器,但这样做将面临计算量的挑战。
例如:对于一个 96X96 像素的图像,假设我们已经学习得到了400个定义在8X8输入上的特征,每一个特征和图像卷积都会得到一个 (96 − 8 + 1) × (96 − 8 + 1) = 7921 维的卷积特征,由于有 400 个特征,所以每个样例 (example) 都会得到一个 892 × 400 = 3,168,400 维的卷积特征向量。学习一个拥有超过 3 百万特征输入的分类器十分不便,并且容易出现过拟合 (over-fitting)。
为了解决这个问题,首先回忆一下,我们之所以决定使用卷积后的特征是因为图像具有一种“静态性”的属性,这也就意味着在一个图像区域有用的特征极有可能在另一个区域同样适用。因此,为了描述大的图像,一个很自然的想法就是对不同位置的特征进行聚合统计,例如,人们可以计算图像一个区域上的某个特定特征的平均值 (或最大值)。这些概要统计特征不仅具有低得多的维度 (相比使用所有提取得到的特征),同时还会改善结果(不容易过拟合)。这种聚合的操作就叫做池化 (pooling),有时也称为平均池化或者最大池化 (取决于计算池化的方法)。
平均池化:池化区域内所有值的平均值作为池化结果;
最大池化:池化区域内所有值的最大值作为池化结果。
举个例子:如有1000万人参加选举,每个家庭10个人,如果一个人一个人的去统计这1000万人的选票,太麻烦了,反正每个家庭投票意见基本一致,那么干脆就每个家庭一张选票,这样要统计的投票数量就缩小到了100万。
人民代表大会制度,是不是也是一种池化?
6. 多层卷积
在实际应用中,往往使用多层卷积,然后再使用全连接层进行训练,多层卷积的目的是一层卷积学到的特征往往是局部的,层数越高,学到的特征就越全局化。
那么,卷积神经网络为什么要设计多层的结构呢?
举个例子:我们要做一个区分猫和狗的图像应用,猫头和狗头是一个特征,但是对于全部都是由像素点组成的图像来说,用几个卷积核直接判断一个猫头实在是太困难了。那么,把猫头也作为一个识别目标,比如猫头应该具有更底层的一些特征,如猫眼睛、猫耳朵、猫鼻子等,但是这些特征还是太高级,必须继续往下寻找低级特征,一直找到最低级的像素点,这样就构成了多层神经网络。第一层卷积层也许是从原始像素点检测到一些边缘线条,然后根据边缘线条在第二层检测出一些简单的形状,后面的层根据这些形状检测出更高级的特征,比如脸部轮廓、眼睛轮廓、耳朵轮廓、鼻子轮廓等。最后一层是利用这些高级特征的一个分类器,也可以采用神经网络之外的分类器。
多层卷积神经网络也符合我们本身对图像的识别过程:图像本身是由像素组成,由像素构成线条,由线条构成基本形状,由基本形状构成物体,然后再由物体构成我们的概念。
是不是有点儿像画家在绘制一幅画?
卷积神经网络的Hello World-手写数字识别:
import tensorflow as tf
import numpy as np
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images, mnist.test.labels
# 把上述trX和teX的形状变为[-1,28,28,1],-1表示不考虑输入图片的数量,28×28是图片的长和宽的像素数,
# 1是通道(channel)数量,因为MNIST的图片是黑白的,所以通道是1,如果是RGB彩色图像,通道是3。
trX = trX.reshape(-1, 28, 28, 1) # 28x28x1 input img
teX = teX.reshape(-1, 28, 28, 1) # 28x28x1 input img
X = tf.placeholder("float", [None, 28, 28, 1])
Y = tf.placeholder("float", [None, 10])
# 初始化权重与定义网络结构。
# 这里,我们将要构建一个拥有3个卷积层和3个池化层,随后接1个全连接层和1个输出层的卷积神经网络
def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))
w = init_weights([3, 3, 1, 32]) # patch大小为3×3,输入维度为1,输出维度为32
w2 = init_weights([3, 3, 32, 64]) # patch大小为3×3,输入维度为32,输出维度为64
w3 = init_weights([3, 3, 64, 128]) # patch大小为3×3,输入维度为64,输出维度为128
w4 = init_weights([128 * 4 * 4, 625]) # 全连接层,输入维度为 128 × 4 × 4,是上一层的输出数据又三维的转变成一维, 输出维度为625
w_o = init_weights([625, 10]) # 输出层,输入维度为 625, 输出维度为10,代表10类(labels)
# 神经网络模型的构建函数,传入以下参数
# X:输入数据
# w:每一层的权重
# p_keep_conv,p_keep_hidden:dropout要保留的神经元比例
def model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden):
# 第一组卷积层及池化层,最后dropout一些神经元
l1a = tf.nn.relu(tf.nn.conv2d(X, w, strides=[1, 1, 1, 1], padding='SAME'))
# l1a shape=(?, 28, 28, 32)
l1 = tf.nn.max_pool(l1a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l1 shape=(?, 14, 14, 32)
l1 = tf.nn.dropout(l1, p_keep_conv)
# 第二组卷积层及池化层,最后dropout一些神经元
l2a = tf.nn.relu(tf.nn.conv2d(l1, w2, strides=[1, 1, 1, 1], padding='SAME'))
# l2a shape=(?, 14, 14, 64)
l2 = tf.nn.max_pool(l2a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l2 shape=(?, 7, 7, 64)
l2 = tf.nn.dropout(l2, p_keep_conv)
# 第三组卷积层及池化层,最后dropout一些神经元
l3a = tf.nn.relu(tf.nn.conv2d(l2, w3, strides=[1, 1, 1, 1], padding='SAME'))
# l3a shape=(?, 7, 7, 128)
l3 = tf.nn.max_pool(l3a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l3 shape=(?, 4, 4, 128)
l3 = tf.reshape(l3, [-1, w4.get_shape().as_list()[0]]) # reshape to (?, 2048)
l3 = tf.nn.dropout(l3, p_keep_conv)
# 全连接层,最后dropout一些神经元
l4 = tf.nn.relu(tf.matmul(l3, w4))
l4 = tf.nn.dropout(l4, p_keep_hidden)
# 输出层
pyx = tf.matmul(l4, w_o)
return pyx # 返回预测值
# 我们定义dropout的占位符——keep_conv,它表示在一层中有多少比例的神经元被保留下来。生成网络模型,得到预测值
p_keep_conv = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")
py_x = model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden) # 得到预测值
# 定义损失函数,这里我们仍然采用tf.nn.softmax_cross_entropy_with_logits来比较预测值和真实值的差异,并做均值处理;
# 定义训练的操作(train_op),采用实现RMSProp算法的优化器tf.train.RMSPropOptimizer,学习率为0.001,衰减值为0.9,使损失最小;
# 定义预测的操作(predict_op)
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=py_x, labels=Y))
train_op = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)
predict_op = tf.argmax(py_x, 1)
# 定义训练时的批次大小和评估时的批次大小
batch_size = 128
test_size = 256
# 在一个会话中启动图,开始训练和评估
# Launch the graph in a session
with tf.Session() as sess:
# you need to initialize all variables
tf.global_variables_initializer().run()
for i in range(100):
training_batch = zip(range(0, len(trX), batch_size),
range(batch_size, len(trX) + 1, batch_size))
for start, end in training_batch:
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_conv: 0.8, p_keep_hidden: 0.5})
test_indices = np.arange(len(teX)) # Get A Test Batch
np.random.shuffle(test_indices)
test_indices = test_indices[0:test_size]
print(i, np.mean(np.argmax(teY[test_indices], axis=1) ==
sess.run(predict_op, feed_dict={X: teX[test_indices],
p_keep_conv: 1.0,
p_keep_hidden: 1.0})))
Kevin,2018年8月4日,成都。