目标分类网络的介绍及应用
摘要:之前介绍了基于前向反馈的BP神经网络,了解了神经网络模型的工作原理,以及损失函数、分类器、优化方法等基本概念。由于BP神经网络具有其局限性,如泛化能力弱,难以处理数据量大的数据。本文旨在介绍基于卷积神经网络的分类器,目标分类的任务是识别图片中是什么物体,并给出其对应的置信度。卷积神经网络与传统的BP神经网络最大的不同是其采用卷积核来做特征提取,这能极大的减少计算量,从而有效提高运算效率。此外,卷积目标分类网络是我们后面将介绍的目标检测网络的基础,而卷积神经网络的出现才真正将神经网络技术应用于人们生活的方方面面。
一、卷积神经网络简介
传统的神经网络算法以及卷积神经网络算法都是针对图像分类而提出的解决方案,目标检测算法我们后面会提到,先从目标分类网络讲起。所谓目标分类,就是对给定一张图片,通过特定的算法得出该图片的类别,常用的方法有利用图像的HoG、SIFT特征结合SVM分类器等算法,传统的BP神经网络算法以及我们现在提到的卷积神经网络算法等。从2007年Alex大神首次将卷积神经网络用于ImageNet分类比赛以来,卷积神经网络就替代了经典的基于图像特征的分类方法,大大提高了图像分类的正确率,2016年的GoogleNet神经网络模型可以在ImageNet数据集上达到6.7%的误分类率,在2017年,38个竞争团队中有29个错误率低于5%,可以说正是卷积神经网络的出现,将人工智能带出了寒冬。
1.1 卷积神经网络原理
卷积神经网络(Convolution Neural Network,CNN)是近年来应用最为广泛的深度学习的神经网络结构,它与传统的BP神经网络相比,最大的一个显著特征就是能有效的降低网络结构的参数量,同时能够更有效地提取图像特征。早在20世纪60年代,就有学者将卷积模型应用到工业电路的模型预测中,80年代末反向传播的神经网络模型开始变得流行,2006年Hinton和Salakhutdinov将卷积算法应用于深度学习,但是受限于计算机硬件的发展,直到2012年Alex将卷积神经网络用于分类检测,卷积神经网络算法成为了神经网络算法中的主流。
1.1.1 深度学习中的卷积操作
在学习卷积神经网络的原理之前,我们先了解什么是卷积。相信上过控制工程的同学们都知道卷积是信号处理的一种手段,应用傅立叶变换我们可以将时域与频域中的信号相互转换,刚入门深度学习的朋友很难将图像处理中的卷积操作和我们信号处理中的卷积操作联系起来,是因为我们在学习信号处理时接触的大多是一维的信号,而我们的图像是包含了二维的信息的。在数学上,对于二维离散的模型,它具有如下的表现形式:
有了上面的式子,我们看看深度学习中的卷积操作是怎么实现的。其实卷积操作非常简单,我们刚接触数字图像处理时所用的高斯滤波、均值滤波、膨胀、腐蚀等操作均可以看作是卷积的应用。要想更深地了解卷积与深度学习之间的联系,可以看这里。进行卷积操作我们需要一个卷积核,卷积核就相当于一个滤波器,卷积核的参数有尺寸和权值,卷积核与输入信号做加权叠加得到输出。假设我们有一副5*5的图像和一个3*3的卷积核,它的权值如图所示:
卷积操作的示意如图:
在图像处理中,所谓卷积就是将图像中对应的位置的像素值和卷积核的对应位置的权值相乘再求和的过程。需要特别指出的是,单个卷积核也是有通道数的,且其应该与对输入信号的通道数相等,注意这与卷积核的个数是不同的概念,通常我们写程序时只指定卷积核的个数,卷积核的通道数默认与输入数据相匹配。如我们有一个3通道的RGB图像,那么我们一个3*3卷积核的参数量应该是 个。多通道卷积如下图所示:
如果我们将卷积核的中心点视为初始点,那么它四邻域的点可以表示为:
原始输入图像的像素点可以表示为:
那么我们之前提到的卷积操作就可以表示为:
这和我们定义的二维离散的卷积操作的数学表达式相符,这也是为什么我们把上述操作称为卷积的原因。使用卷积操作能够极大地减少数据的运算量。举个例子[1],如果我们处理Cifar-10数据集,每张图片的大小是32*32*3,其中32*32表示图像的尺寸,*3代表图像的通道数,即为RGB图像。若我们采用全连接层网络(BP神经网络),设置第一层输入层的节点数为500个,那么仅包含一个输入层的全连接网络的参数量为:
随着网络结构的加深,数据量则会更大!无疑这样的数据量处理是很难满足处理要求的。而当我们使用卷积神经网络时,比如当我们同样使用500个3*3的卷积核作为第一个输入卷积层,其参数量应为: ,数据量大大降低。当然我们一般不会使用这么多卷积核来做特征提取,而是增加卷积网络的深度,使用卷积网络的网络结构可能达到152层甚至更深。全连接网络与卷积神经网络的对比如下图所示:
1.1.2 卷积神经网络结构
介绍完深度学习中的卷积操作,接下来正式提出卷积神经网络。一般而言,卷积神经网络主要由下面五个部分组成:
-
输入层(INPUT LAYER)
-
卷积层(CONVOLUTION LAYER)
-
池化层(POOLING LAYER)
-
全连接层(FULLCONECT LAYER)
-
输出层(OUTPUT LAYER)
结合一个实际的卷积分类神经网络,其结构如下图所示:
一般而言,我们的输入层是一个代表图像的三维像素矩阵,即像素*像素*通道数。卷积层所做的就是我们上述提到卷积操作,用于提取图像特征,一般卷积层的参数包括卷积核尺寸(size),卷积核个数(channels),步长(stride),填充参数(padding)等。在卷积操作之后我们通常还要进行类似全连接神经网络中的激活函数的操作,常用的激活函数有Relu,Sigmoid,tanh激活函数等,具体实现方式可以参考我上一篇blog。池化层对每一个经卷积激活后的数据(activation
map)进行操作,能够进一步提取特征并降低参数的数量,最常用的池化策略是最大池化(Max Pooling)。全连接层即是将经卷积池化的特征连接起来,重新组合成一个n*1的数据,再连接一个普通的神经网络结构,目的是为了方便后续的分类任务。输出层采用某一的分类器(通常是softmax分类器)输出种类的概率并给出输出结果。输入层,全连接层以及输出层在之前的文章中已经有过介绍了,这里就不重复介绍了,下面具体看一下卷积层和池化层。
1.1.2.1 卷积层
卷积层是卷积神经网络中最重要的部分,它的参数直接决定了下一层的输入的数据。下面给出一个例子来讲解单个卷积层输出尺寸的计算。
卷积层的输出数据尺寸主要和下面这几个参数有关:输入尺寸N,卷积核尺寸F,步长stride,填充参数padding。其中步长stride表示每次卷积核移动的间隔距离;填充参数表示给图像周围填充0,由我们之前的例子可以知道,只要我们卷积核尺寸不是1*1,那么输出尺寸必定会小于输入尺寸大小,所以有时候为了保证输入与输出尺寸相同,我们就会添加这个填充参数。输出数据尺寸可以由如下公式确定:
若干个卷积层的堆叠就组成了卷积神经网络,每个卷积层的输入通常会经过卷积,添加偏置,激活函数激活操作。如下图:
之前提过我们通常忽略卷积核的通道数,而只指明卷积核的个数。上图所示的卷积层的维数实际上分别是5*5(*3)*6,5*5(*6)*10。还有,当不采用padding策略时,可以看到维度的缩减得非常快,每经过一个卷积层都会减少。此外,有时候我们会采用1*1的卷积核来升/降特征的维度,而不改变图片的宽和高。
卷积层就是卷积神经网络与全连接神经网络最大的区别,它极大地降低了计算量,使得深层次的深度神经网络成为可能。卷积层具有如下特点:
- 权值共享
图片中不同感受野(receptive field)卷积操作所使用的卷积核的数据是相同的,即每一层输出的特征图(feature map)上的像素点在输入图片上映射的区域共用同一个卷积核的参数。正式权值共享这一特点大大减少了网络的参数,这不仅可以降低计算的复杂度,而且还能减少因为连接过多导致的严重过拟合,从而提高了模型的泛化能力。
- 局部感知
即网络部分连通,每个卷积核只与上一层的部分数据相连,只感知局部,而不是整幅图像。一般认为图像的空间联系是局部的像素联系比较密切,而距离较远的像素相关性较弱,因此,每个卷积核没必要对全局图像进行感知,只要对局部进行感知,然后在更高层将局部的信息综合起来得到全局信息。
1.1.2.2 池化层
通常我们的卷积层后面会跟一个池化层,当然也有其他的策略如先经过局部响应归一化层(LRN)或Batch Normalization层。池化层能够进一步提取特征并继续降低数据量起到降采样的目的。常见的池化操作有最大池化,平均池化以及随机池化等。池化层也采用和卷积层类似的操作,也使用一个滤波器来采样数据,只不过这个池化层里的滤波器没有需要训练权值,它只存储池化策略。如下图所示:
上图表示了最大池化的过程,我们用一个2*2的核来对数据采样,步长设置为2,每次只取采样数据中的最大值,得到的数据如上图右所示。通过池化这一操作,这样我们即能进一步进行特征提取,还能将4*4的数据量降维成2*2的数据量,更有利于后续的卷积操作。通常进行池化层后数据维度会变为输入的一半,即步长stride一般取2。其他池化策略的操作也是一样的,大家有兴趣自行google或百度。
到这里,卷积神经网络的基本结构及基本计算方法就已经差不多介绍清楚了,下面我们来谈谈卷积神经网络的训练。
1.2 卷积神经网络反向传输过程
如下图,我们用一个2*2没有填充参数padding的卷积核来对一个3*3的输入数据进行卷积操作,并设步长为1:
在前向传输的过程中,我们会缓存变量X和卷积核W以便后续的反向传输计算。记:
反向传输即利用梯度下降法求损失函数对变量的梯度,进而不断更新权值的过程,具体的实现步骤可以参考上一篇博文。由于卷积核的每个权值都参与了输出向量 的计算,所以任何卷积核权值的改变都会引起输出向量的改变,并且这些改变最终都会累加到损失函数。根据偏微分的基本规则,记 ,有:
根据上式,我们只需要计算卷积层的输出的梯度即可以求得卷积核权值的梯度,如下图所示:
同理随着卷积神经网络的加深,我们利用链式求导法则求得不同层的梯度,有兴趣的同学可以自己试试推导一下。
1.3 基于卷积神经网络实现数字识别
设计一个卷积神经网络,实现MNIST手写数据集的识别。网络结构如下:
Type | Filters | Size | Output |
---|---|---|---|
Convolutional | 32 | 5*5 | 28*28*32 |
Maxpool | 2*2 | 14*14*32 | |
Convolutional | 64 | 5*5 | 14*14*64 |
Maxpool | 2*2 | 7*7*64 | |
Connected | 1024 | 1024 | |
Connected | 10 | 10 |
代码实现如下:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data',one_hot=True)
#每个批次的大小
batch_size = 100
#计算一共有多少个批次
n_batch = mnist.train.num_examples // batch_size
#参数概要
def variable_summaries(var):
with tf.name_scope('summaries'):
mean = tf.reduce_mean(var)
tf.summary.scalar('mean', mean)#平均值
with tf.name_scope('stddev'):
stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
tf.summary.scalar('stddev', stddev)#标准差
tf.summary.scalar('max', tf.reduce_max(var))#最大值
tf.summary.scalar('min', tf.reduce_min(var))#最小值
tf.summary.histogram('histogram', var)#直方图
#初始化权值
def weight_variable(shape,name):
initial = tf.truncated_normal(shape,stddev=0.1)#生成一个截断的正态分布
return tf.Variable(initial,name=name)
#初始化偏置
def bias_variable(shape,name):
initial = tf.constant(0.1,shape=shape)
return tf.Variable(initial,name=name)
#卷积层
def conv2d(x,W):
#x input tensor of shape `[batch, in_height, in_width, in_channels]`
#W filter / kernel tensor of shape [filter_height, filter_width, in_channels, out_channels]
#`strides[0] = strides[3] = 1`. strides[1]代表x方向的步长,strides[2]代表y方向的步长
#padding: A `string` from: `"SAME", "VALID"`
return tf.nn.conv2d(x,W,strides=[1,1,1,1],padding='SAME')
#池化层
def max_pool_2x2(x):
#ksize [1,x,y,1]
return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
#命名空间
with tf.name_scope('input'):
#定义两个placeholder
x = tf.placeholder(tf.float32,[None,784],name='x-input')
y = tf.placeholder(tf.float32,[None,10],name='y-input')
with tf.name_scope('x_image'):
#改变x的格式转为4D的向量[batch, in_height, in_width, in_channels]`
x_image = tf.reshape(x,[-1,28,28,1],name='x_image')
with tf.name_scope('Conv1'):
#初始化第一个卷积层的权值和偏置
with tf.name_scope('W_conv1'):
W_conv1 = weight_variable([5,5,1,32],name='W_conv1')#5*5的采样窗口,32个卷积核从1个平面抽取特征
with tf.name_scope('b_conv1'):
b_conv1 = bias_variable([32],name='b_conv1')#每一个卷积核一个偏置值
#把x_image和权值向量进行卷积,再加上偏置值,然后应用于relu激活函数
with tf.name_scope('conv2d_1'):
conv2d_1 = conv2d(x_image,W_conv1) + b_conv1
with tf.name_scope('relu'):
h_conv1 = tf.nn.relu(conv2d_1)
with tf.name_scope('h_pool1'):
h_pool1 = max_pool_2x2(h_conv1)#进行max-pooling
with tf.name_scope('Conv2'):
#初始化第二个卷积层的权值和偏置
with tf.name_scope('W_conv2'):
W_conv2 = weight_variable([5,5,32,64],name='W_conv2')#5*5的采样窗口,64个卷积核从32个平面抽取特征
with tf.name_scope('b_conv2'):
b_conv2 = bias_variable([64],name='b_conv2')#每一个卷积核一个偏置值
#把h_pool1和权值向量进行卷积,再加上偏置值,然后应用于relu激活函数
with tf.name_scope('conv2d_2'):
conv2d_2 = conv2d(h_pool1,W_conv2) + b_conv2
with tf.name_scope('relu'):
h_conv2 = tf.nn.relu(conv2d_2)
with tf.name_scope('h_pool2'):
h_pool2 = max_pool_2x2(h_conv2)#进行max-pooling
#28*28的图片第一次卷积后还是28*28,第一次池化后变为14*14
#第二次卷积后为14*14,第二次池化后变为了7*7
#进过上面操作后得到64张7*7的平面
with tf.name_scope('fc1'):
#初始化第一个全连接层的权值
with tf.name_scope('W_fc1'):
W_fc1 = weight_variable([7*7*64,1024],name='W_fc1')#上一场有7*7*64个神经元,全连接层有1024个神经元
with tf.name_scope('b_fc1'):
b_fc1 = bias_variable([1024],name='b_fc1')#1024个节点
#把池化层2的输出扁平化为1维
with tf.name_scope('h_pool2_flat'):
h_pool2_flat = tf.reshape(h_pool2,[-1,7*7*64],name='h_pool2_flat')
#求第一个全连接层的输出
with tf.name_scope('wx_plus_b1'):
wx_plus_b1 = tf.matmul(h_pool2_flat,W_fc1) + b_fc1
with tf.name_scope('relu'):
h_fc1 = tf.nn.relu(wx_plus_b1)
#keep_prob用来表示神经元的输出概率
with tf.name_scope('keep_prob'):
keep_prob = tf.placeholder(tf.float32,name='keep_prob')
with tf.name_scope('h_fc1_drop'):
h_fc1_drop = tf.nn.dropout(h_fc1,keep_prob,name='h_fc1_drop')
with tf.name_scope('fc2'):
#初始化第二个全连接层
with tf.name_scope('W_fc2'):
W_fc2 = weight_variable([1024,10],name='W_fc2')
with tf.name_scope('b_fc2'):
b_fc2 = bias_variable([10],name='b_fc2')
with tf.name_scope('wx_plus_b2'):
wx_plus_b2 = tf.matmul(h_fc1_drop,W_fc2) + b_fc2
with tf.name_scope('softmax'):
#计算输出
prediction = tf.nn.softmax(wx_plus_b2)
#交叉熵代价函数
with tf.name_scope('cross_entropy'):
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=prediction),name='cross_entropy')
tf.summary.scalar('cross_entropy',cross_entropy)
#使用AdamOptimizer进行优化
with tf.name_scope('train'):
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
#求准确率
with tf.name_scope('accuracy'):
with tf.name_scope('correct_prediction'):
#结果存放在一个布尔列表中
correct_prediction = tf.equal(tf.argmax(prediction,1),tf.argmax(y,1))#argmax返回一维张量中最大的值所在的位置
with tf.name_scope('accuracy'):
#求准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
tf.summary.scalar('accuracy',accuracy)
#合并所有的summary
merged = tf.summary.merge_all()
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
train_writer = tf.summary.FileWriter('logs/train',sess.graph)
test_writer = tf.summary.FileWriter('logs/test',sess.graph)
for i in range(1001):
#训练模型
batch_xs,batch_ys = mnist.train.next_batch(batch_size)
sess.run(train_step,feed_dict={x:batch_xs,y:batch_ys,keep_prob:0.5})
#记录训练集计算的参数
summary = sess.run(merged,feed_dict={x:batch_xs,y:batch_ys,keep_prob:1.0})
train_writer.add_summary(summary,i)
#记录测试集计算的参数
batch_xs,batch_ys = mnist.test.next_batch(batch_size)
summary = sess.run(merged,feed_dict={x:batch_xs,y:batch_ys,keep_prob:1.0})
test_writer.add_summary(summary,i)
if i%100==0:
test_acc = sess.run(accuracy,feed_dict={x:mnist.test.images,y:mnist.test.labels,keep_prob:1.0})
train_acc = sess.run(accuracy,feed_dict={x:mnist.train.images[:10000],y:mnist.train.labels[:10000],keep_prob:1.0})
print ("Iter " + str(i) + ", Testing Accuracy= " + str(test_acc) + ", Training Accuracy= " + str(train_acc))