参考    基于卷积神经网络的图像识别 - 云+社区 - 腾讯云

一、图像识别问题简介与经典数据集

视觉是人类认识世界非常重要的一种知觉。对于人类来说,通过识别手写体数字、识别图片中的物体或者是找出图片中人脸的轮廓都是非常简单的任务。然而对于计算机而言,让计算机识别图片中的内容就不是一件容易的事情了。图像识别问题希望借助计算机程序来处理、分析和理解图片中的内容,使得计算机可以从图片中自动识别各种不同模式的目标和对象。图像识别作为人工智能的一个重要领域,在最近几年已经取得了很多突破性的进展,而神经网络就是这些突破性进展背后的主要技术支持。

MNIST手写体识别数据集是一个相对简单的数据集,在其他更加复杂的图像识别数据集上,卷积神经网络有更加突出的表现。CIFAR就是一个影响力很大的图像分类数据集。CIFAR数据分为了CIFAR-10和CIFAR-100两个问题,它们都是图像词典项目(Visual Dictionary)中800万张图片的一个子集。CIFAR数据集中的图片为32*32的彩色图片,这些图片是由Alex Krizhevsky教授、Vinod Nair博士和Geoffrey Hinton教授整理的。

CIFAR-10问题收集了来自10个不同种类的60000张图片,CIFAR官网:Home - CIFAR提供了不同格式的CIFAR数据集下载。具体的数据格式这里不再赘述。

和MNIST数据集类似,CIFAR-10中的图片都是固定的且每一张图片中仅包含一个种类的实体,但和MNIST相比,CIFAR数据集最大的区别在于图片由黑白变成彩色,且分类的难度也相对较高。在CIFAR-10数据集上,人工标注的正确率大概为94%,这比MNIST数据集上的人工表现要低很多。无论是MNIST数据集还是CIFAR数据集,相比真是环境下的图像识别问题,有两个最大问题。第一,现实生活中的图片分辨率要远高于32*32,而且图像的分辨率也不会是固定的。第二,现实生活中的物体类别很多,无论是10种还是1000中都远远不够,而且一张图片中不会只出现一个种类的物体。为了更佳贴近真实环境下的图像识别问题,由斯坦福大学(Stanford University)的李飞飞(Feifei Li)教授带头整理的ImageNet很大程度地解决了这两个问题。

ImageNet是一个基于WordNet的大型图像数据库。在ImageNet中,将近1500万图片被关联到了WordNet的大约20000个名词同义词集上。目前每一个与ImageNet相关的WordNet同义词集都代表了现实世界中的一个实体,可以被认为是分类问题中的一个类别。ImageNet中的图片都是从互联网上爬去下来的,并且通过亚马逊的人工标注服务(Amozon Mechanical Turk)将图像分类到WordNet的同义词集上。在ImageNet的图片中,一张图片中可能出现多个同义词集所代表的实体。

ImageNet每年都举办图像识别相关的竞赛(ImageNet Large VIsual Recognition Challenge, ILSVRC),而且每年的竞赛都会有一些不同的问题,这些问题基本涵盖了图像识别的主要研究方向。ImageNet的官网ImageNet列出了历届ILSVRC竞赛的题目和数据集。不同年份的ImageNet比赛提供了不同的数据集,下面介绍使用得最多的ILSVRC2012图像分类数据集。

ILSVRC2012图像分类数据集的任务和CIFAR数据集时基本一致的,也是识别图像中的主要物体。ILSVRC2012图像分类数据集包含了来自1000个类别的120万张图片,其中每张图片属于且只属于一个类别。因为ILSVRC图像分类数据集中的图片是直接从互联网上爬取得到的,所以图片的大小从几千字到几百万字节不等。

二、卷积神经网络简介

为了将只包含全连接神经网络与卷积神经网络、循环神经网络区分开,将只包含全连接神经网路称之为全连接神经网络。使用全连接神经网络处理图像的最大问题在于全连接层的参数太多。对于MNIST数据,每一张图片的大小是28*28*1,其中28*28为图片的大小,*1表示图像是黑白的,只有一个彩色通道。假设第一层隐藏层的节点数为500个,那么一个全链接层的神经网络将有28*28*500 + 500 = 392500个参数。当图片更大时,比如在CIFAR-10数据集中,图片的大小为32*32*3,其中32*32表示图片的大小,*3表示图片是通过红绿蓝三个色彩通道(channel)表示的。这样输入层就有3072个节点,如果第一层全连接层仍然是500个节点,那么这一层全链接神将网络将有3072*500+500=150万参数。参数增多除了导致计算速度减慢,还很容易导致过拟合问题。所以需要一个更合理的神经网络结构来有效地减少神经网络中参数个数,卷积神将网络就可以达到这个目的。

在卷积神经网络的前几层中,每一层的节点都被组织成一个三维矩阵。比如处理CIFAR-10数据集中的图片时,可以将输入层组织成一个32*32*3的三维矩阵。一个卷积神经网络主要由以下5种结构组成:

1.输入层。输入层是整个神经网络的输入,在处理图像的卷积神经网络中,它一般代表了一张图片的像素矩阵。从输入层开始卷积神经网络通过不同的神经网络结构将上一层的三维矩阵转化为下一层的三维矩阵,直到最后的全连接层。

2.卷积层。从名字就可以看出,卷积层是一个卷积神经网络中最重要的部分。和传统全连接层不同,卷积层中每一个节点的输入只是上一层神经网络的一小块,这个小块常用的大小有3*3或者5*5。卷积层试图将神经网络中的每一小块进行更加深入地分析从而得到抽象程序更高的特征。一般来说,通过卷积层处理过的节点矩阵会变得更深,所以经过卷积层之后的节点矩阵的深度会增加。

3.池化层(Pooling)。池化层神经网络不会改变三维矩阵的深度,但是它可以缩小矩阵的大小。池化层操作可以认为是将一张分辨率较高的图片转化为分辨率较低的图片。通过池化层,可以进一步缩小最后全连接层中节点的个数。从而达到减少整个神经网络中参数的目的。

4.全连接层。在经过多轮卷积层和池化层的处理之后,在卷积神经网络的最后一般会是由1到2个全连接层来给出最后的分类结果。经过几轮卷积层和池化层之后,可以认为图像中的信息已经被抽象成了信息含量更高的特征。可以将卷积层和池化层看成自动图像特征提取的过程。在特征提取完成之后,仍然需要使用全连接层来完成分类任务。

5.Softmax层。Softmax层主要用于分类问题。通过Softmax层可以得到当前样例属于不同种类的概率分布情况。

三、卷积神经网络常用结构

1.卷积层

滤波器(filter)可以将当前神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位点矩阵。每个节点矩阵指的是一个长和宽都为1,但深度不限的节点矩阵。

在一个卷积层中,滤波器所处理的节点矩阵的长和宽都是由人工指定的,这个节点矩阵的尺寸也被称之为滤波器的尺寸。常用的滤波器尺寸有3*3或5*5。因为滤波器处理的矩阵深度和当前神经网络节点矩阵的深度是一致的,所以节点矩阵是三维的,但滤波器的尺寸只需要指定两个维度。滤波器中另外一个需要人工指定的设置是三维的,但滤波器的尺寸只需要指定两个维度。滤波器中另外一个需要人工指定的设置是处理得到的单位节点矩阵的深度,这个设置称为滤波器的深度。注意滤波器的尺寸指的是一个滤波器输入节点矩阵的深度,这个设置称为滤波器的深度。注意滤波器的尺寸指的是一个滤波器输入节点矩阵的大小,而深度指的是输出单位节点矩阵的深度。

假设通过滤波器将一个2*2*3的节点矩阵变化为一个1*1*5的单位节点矩阵。一个滤波器的前向传播过程和全连接层相似,总共需要2*2*3*5 + 5 = 65个参数,其中最后的+5为偏置项参数的个数。使用来表示对于输出节点矩阵中的第i个节点,滤波器输入节点(x, y, z)的权重,使用表示第i个输出节点对应的偏置项参数,那么单位矩阵中的第i个节点的取值g(i)为:

其中为滤波器中节点(x,y,z)的取值,f为激活函数。下图展示了在给定a,的情况下,使用ReLU作为激活函数时g(0)的计算过程,左侧给出了a和的取值,这里通过3个二维矩阵来表示一个三维矩阵的取值,其中每一个二维矩阵表示三维矩阵在某一深度上的取值。符号表示点积,也就是矩阵中对应元素乘积的和。右侧显示了g(0)的计算过程。如果给出,那么也可以类似地计算出g(1)到g(4)的取值。如果将a和组织成两个向量,那么一个滤波器的计算过程完全可以通过向量乘法来完成。

以上样例已经介绍了在卷积层中计算一个滤波器的前向传播过程。卷积层结构的前向传播过程就是通过将一个滤波器从神经网络当前层的左上角移到右下角,并且在移动中计算每一个对应的单位矩阵得到的。 卷积层的结构的前向传播过程就是通过将一个滤波器从神经网络当前层的左上角移动到右下角,并且在移动中计算每一个对应的单位矩阵得到的。

在卷积神经网络中,每一个卷积层中使用的滤波器中的参数都是一样的,这是卷积神经网络一个重要的性质。从直观上解释,共享滤波器的参数可以使得图像上的内容不受位置的影响。以MNIST手写体数字识别为例,无论数字“1”出现在左上角还是右下角,图片的种类都是不变的。因为在左上角和右下角使用的过滤器参数相同,所以通过卷积层之后无论数字在图像上的哪个位置,得到的结果都是一样的。

共享每一个卷积层中滤波器中的参数可以巨幅减少神经网络上的参数。以CIFAR-10问题为例,输入层矩阵的维度是32*32*3。假设第一层卷积层使用尺寸为5*5。深度为16的滤波器,那么这个卷积层的参数个数为5*5*3*16 + 16 = 1216个,使用500个隐藏节点的全连接层将有1.5百万个参数。相比之下,卷积层的参数个数要远远小于全连接层。而且卷积层的参数个数和图片的大小无关,它只和滤波器的尺寸、深度以及当前层节点矩阵的深度有关。这使得卷积神经网络可以很好地扩展更大的图像数据。以下程序实现了一个卷积层的前向传播过程。从以下代码可以看出,通过tensoflow实现卷积层是非常方便的。

# 通过tf.get_variable的方式创建滤波器的权重变量和偏置项变量。上面介绍了卷积层
# 的参数个数只和滤波器的尺寸、深度以及当前层节点矩阵的深度有关,所以这里声明的参数变
# 量是一个四维矩阵,前面两个维度代表了滤波器的尺寸,第三个维度表示当前层的深度,第四
# 个维度表示滤波器的深度。filter_weight = tf.get_variable('weights', [5, 5, 3, 16],             initializer = tf.truncated_normal_initializer(stddev=0.1))# 和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共享的,所以总共有下一层深度个不
# 同的偏置项。
biases = tf.get_variable('biases', [16], initializer = tf.constant_initializer(0.1))# tf.nn.conv2d提供了一个非常方便的函数来实现卷积层前向传播的算法。这个函数的第一个输入为
# 当前层的节点矩阵。注意这个矩阵是一个四维矩阵,后面三个维度对应一个节点矩阵,第一
# 维对应一个输入batch。比如在输入层,input[0,:,:,:]表示第一张图片,input[1,:,:,:]
# 表示第二张图片,以此类推。tf.nn.conv2d第二个参数提供了卷积层的权重,第三个参数为不同
# 维度上的步长。虽然第三个参数提供的是一个长度为4的数组,但是第一维和最后一维的数字
# 要求一定是1。这是因为卷积层的步长只对矩阵的长和宽有效。最后一个参数是填充(padding)
# 的方法,tensorflow中提供SAME或是VALID两种选择。其中SAME表示添加全0填充,'VALID'表示
# 不添加。conv = tf.nn.conv2d(input, filter_weight, strides=[1,1,1,1], padding= 'SAME')# tf.nn.bias_add提供了一个方便的函数给每一个节点加上偏置项。注意这里不能直接使用加
# 法,因为矩阵上不同位置上的节点都需要加上同样的偏置项。虽然下一层神经网络的大小为2*2
# 但是偏置项只有一个数(因为深度为1),而2*2矩阵中的每一个值都需要加上这个偏置项。
bias = tf.nn.bias_add(conv, biases)# 将计算结果通过ReLU激活函数完成去线性化。
actived_conv = tf.nn.relu(bias)

2.池化层

在卷积层之间往往会加上一个池化层(pooling layer)。池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层可以加快计算速度也有防止过拟合问题的作用。池化层的前向传播过程也是通过移动一个类似滤波器的结构完成的。

不过池化层滤波器中的计算不是节点的加权和,而是采用更加简单的最大值或者平均值运算。使用最大值操作的池化层被称之为最大值池化层(max pooling),这是被使用得最多的池化层结构。使用平均值操作的池化层被称之为平均池化层(average pooling)。

与卷积层的滤波器类似,池化层的滤波器也需要人工设定滤波器的尺寸、是否使用0填充以及滤波移动的步长等设置,而且这些设置的意义也是一样的。卷积层和池化层中滤波器移动的方式是类似的,唯一的区别在于卷积层使用的滤波器是横跨整个深度的,而池化层使用的滤波器只影响一个深度上的节点。所以池化层的滤波器除了在长和宽两个维度移动,它还需在深度这个维度移动。

以下tensorflow程序实现了最大池化层的前向传播算法。

# tf.nn.max_pool实现了最大池化层的前向传播算法,它的参数和tf.nn,conv2d函数类似。
# ksize提供了滤波器的尺寸,strides提供了步长信息,padding提供了是否使用全0填充。
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1],strides = [1, 2, 2, 1], padding='SAME')

对比池化层和卷积层前向传播在tensorflow中的实现,可以发现函数的参数形式是相似的。在tf.nn.max_pool函数中,首先需要传入当前层的节点矩阵,这个矩阵是一个四维矩阵,格式和tf.nn.conv_2d函数中的第一个参数一致。第二个参数为滤波器的尺寸。虽然给出的是一个长度为4的一维数组,但是这个数组的第一个和最后一个数必须为1。这意味着池化层的滤波器是不可以跨不同输入样例或者节点矩阵深度的。在实际应用中使用得最多的池化层滤波器尺寸为[1, 2, 2, 1]或者[1, 3, 3, 1]。

tf.nn.max_pool函数的第三个参数为步长,它和tf.nn.conv2d函数中步长的意义是一样的,而且第一维和最后一维也只能为1。这意味着在tensorflow中,池化层不能减少节点矩阵的深度或者输入样例的个数。tf.nn.max_pool函数的最后一个参数指定了是否使用全0填充。这个参数也只有两种取值------VALID或者SAME,其中VALID表示全0填充,SAME表示使用全0填充。tensorflow还提供了tf.nn.avg_pool来实现平均池化层。tf.nn.avg_pool函数的调用格式和tf.nn.max_pool函数是一致的。

四、经典卷积网络模型

通过卷积层和池化层这些网络结构任意组合得到的神经网络有无限多种,怎样的神经网络更有可能解决真实的图像处理呢?下面介绍LeNet-5模型,并给出一个完整的tensorflow程序来实现LeNet-5模型。通过这个模型,将给出卷积神经网络结构设计的一个通用模式,接下来介绍卷积神经网络结构的另外一种思路------Inception模型。

1.LeNet-5模型

LeNet-5模型时Yann LeCun教授于1998年在论文Gradient-based learning applied to document recognition中提出的,它是第一个成功应用于数字识别问题的卷积神经网络。在MNIST数据集上,LeNet-5模型可以达到大约99.2%的正确率。LeNet-5模型总共有7层。

第一层,卷积层

这一层输入就是原始的图像像素,LeNet-5模型接受的输入层大小为32*32*1。第一个卷积层滤波器的尺寸为5*5,深度为6。不使用全0填充,步长为1。因为没有使用全0填充,所以这一层的输出的尺寸为32-5+1=28,深度为6。这一个卷积层总共有5*5*1*6 + 6=156个参数,其中6个为偏置项参数。因为下一层的节点矩阵有28*28*5=4704个节点,每个节点和5*5=25个当前层节点相连,所以本层卷积层总共有4704*(25+1)=122304个连接。

第二层,池化层

这一层的输入为第一层的输出,是一个28*28*6的节点矩阵。本层采用的滤波器大小为2*2,长和宽的步长均为2,所以本层的输出矩阵大小为14*14*6。

第三层,卷积层

本层的输入矩阵大小为14*14*6,使用的滤波器大小为5*5,深度为16。本层不使用全0填充,步长为1。本层的输出矩阵大小为10*10*16。按照标准的卷积层,本层应该有5*5*6*16=2416个参数,10*10*16*(25+1)=41600个连接。

第四层,池化层

本层的输入矩阵大小为10*10*16,采用的过滤器大小为2*2,步长为2。本层的输出矩阵大小为5*5*16。

第五层,全连接层

本层的输入矩阵大小为5*5*16,在LeNet-5模型的论文中将这一层称为卷积层,但是因为滤波器的大小就是5*5,所以全连接层没有区别,在之后的tensorflow程序实现中也会将这一层看成全连接层。本层的输出节点个数为为120,总共有5*5*16*120 + 120 =48120个参数。

第六层,全连接层

本层的输入节点个数为120个,输出节点个数为84个,总共参数为120*84+84=10164个。

第七层,全连接层

本层的输入节点个数为84个,输出节点个数为10个,总共参数为84*10+10=850个。上面介绍了LeNet-5模型每一层结构和设置,下面给出了一个tensorflow的程序来实现一个类似LeNet-5模型的卷积神经网络来解决MNIST数字识别问题。

# 调整输入数据placeholder的格式,输入为一个四维矩阵。
x = tf.placeholder(tf.float32,BTACH_SIZE,                      # 第一维表示一个batch中样例的个数。mnsit_inference.IMAGE_SIZE,      # 第二维和第三维表示图片的尺寸。     mnist_inference.IMAGE_SIZE,      mnist_inference.NUM_CHANNELS]    # 第四维表示图片的深度,对于RBG格# 式的图片,深度为3。        name='x-input')
# 类似将输入的训练数据格式调整为一个四维矩阵,并将这个调整后的数据传入sess.run过程。
reshaped_xs = np.reshape(xs,  (BATCH_SIZE,mnist_inference.IMAGE_SIZE,mnist_inference.IMAGE_SIZE,mnist_inference.NUM_CHANNELS))

在调整完输入格式之后,只需要在程序mnist_inference.py中实现类似LeNet-5模型结构的前向传播过程即可。下面给出了修改后的mnist_inference.py程序。

# -*- coding: utf-8 -*-
import tensorflow as tf# # 定义神经网络结构相关的参数。
# INPUT_NODE = 784
# OUTPUT_NODE = 10
# LAYER1_NODE = 500
# 通过tf.get_variable函数来获取变量。在训练神经网络时会创建这些变量:在测试时会通过
# 保存的模型加载这些变量的取值。而且更加方便的是,因为可以在变量加载时将滑动平均变量
# 重命名,所以可以直接通过同样的名字在训练时使用变量自身,而在测试时使用变量的滑动平
# 均值。在这个函数中也会将变量的正则化损失加入损失集合。
# 配置神经网络的参数。
INPUT_NODE  = 784
OUTPUT_NODE = 10
IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10# 第一层卷积层的尺寸和深度。
CONV1_DEEP = 32
CONV1_SIZE = 5
# 第二层卷积层的尺寸和深度。
CONV2_DEEP = 64
CONV2_SIZE = 5
# 全连接层的节点个数。
FC_SIZE = 512# 定义卷积神经网络的前向传播过程。这里添加了一个新的参数train,用于区分训练过程和测试
# 过程。在这个程序中将其用到dropout方法,dropout可以进一步提升模型可靠性并防止过拟合,
# dropout过程只在训练时使用。
def inference(input_tensor, train, regularizer):# 声明第一层卷积层的变量并实现前向传播过程。# 通过使用不同的命名空间来隔离不同层的变量,这可以让每一层中的变量名只需要# 考虑在当前层的作用,而不需要担心命名的问题。和标准LeNet-5模型不大一样,这里# 定义的卷积层输入为28*28*1的原始MNIST图片像素,因为卷积层中使用了全0填充,# 所以输出为28*28*32的矩阵。with tf.variable_scope('layer1-conv1'):conv1_weights = tf.get_variable("weights", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],initializer=tf.truncated_normal_initializer(stddev=0.1))conv1_biases = tf.get_variable("biases", [CONV1_DEEP], initializer = tf.constant_initializer(0.0))# 使用变长为5,深度为32的滤波器,滤波器移动的步长为1,且使用全0填充。conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))# 实现第二层池化层的前向传播过程。这里选用最大池化,池化层滤波器的边长为2,# 使用全0填充且移动步长为2.这一层的输入是上一层的输出,也就是28*28*32的# 矩阵。输出为14*14*32的矩阵。with tf.name_scope('layer2-pool1'):pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')# 声明第三层卷积层的变量并实现前向传播过程。这一层的输入为14*14*32的矩阵。# 输出为14*14*64的矩阵。with tf.variable_scope('layer3-conv2'):conv2_weights = tf.get_variable("weight", [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],initializer=tf.truncated_normal_initializer(stddev=0.1))conv2_biases = tf.get_variable("bias", [CONV2_DEEP],initializer=tf.constant_initializer(0.0))# 使用边长为5,深度为64的滤波器,滤波器移动的步长为1,且使用全0填充。conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))# 实现第四层池化层的前向传播过程。这一层和第二层的结构是一样的。这一层的输入为# 14*14*64的矩阵,输出为7*7*54的矩阵。with tf.name_scope('layer4-pool2'):pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')# 将第四层池化层的输出转化为第五层的输入格式。第四层的输出为7*7*64的矩阵,# 然而第五层全连接层需要的输入格式为向量,所以在这里需要将这个7*7*64的矩阵拉直成# 一个向量。pool2.get_shape函数可以得到第四层输出矩阵的维度而不需要手工计算。注意# 因为每一层神经网络的输入输出都为一个batch的矩阵,所以这里得到的维度也包含了一个# batch中数据的个数。pool_shape = pool2.get_shape().as_list()# 计算将矩阵拉直成向量之后的长度。这个长度就是矩阵长度及深度的乘积。注意这里# pool_shape[0]为一个batch中的数据。nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]# 通过tf.reshape函数将第四层的输出变成一个batch的向量。reshaped = tf.reshape(pool2, [pool_shape[0], nodes])# 声明第五层全连接层的变量并实现前向传播过程。这一层的输入时拉直之后的一组向量,# 向量长度为3136,输出是一组长度为512的向量。这一层和之前介绍的基本一致,唯一的区别# 就是引入了droopout的概念。dropout在训练时会随机将部分节点的# 输出改为0。dropout可以避免过拟合问题,从而得到在测试数据上的效果更好。# dropout一般只在全连接层而不是卷积层或者池化层使用。with tf.variable_scope('layer5-fc1'):fc1_weights = tf.get_variable('weight', [nodes, FC_SIZE],initializer=tf.truncated_normal_initializer(stddev=0.1))# 只有全连接层的权重需要加入正则化。if regularizer != None:tf.add_to_collection('losses', regularizer(fc1_weights))fc1_biases = tf.get_variable("bias", [FC_SIZE],initializer=tf.constant_initializer(0.1))fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)if train: fc1 = tf.nn.dropout(fc1, 0.5)# 声明第六层全连接层的变量并实现前向传播过程。这一层的输入为一组长度为512的向量,# 输出为一组长度为10的向量。这一层的输出通过Softmax之后就得到了最后的分类结果。with tf.variable_scope('layer6-fc2'):fc2_weights = tf.get_variable("weight", [FC_SIZE, NUM_LABELS],initializer=tf.truncated_normal_initializer(stddev=0.1))if regularizer != None:tf.add_to_collection('losses', regularizer(fc2_weights))fc2_biases = tf.get_variable("bias", [NUM_LABELS],initializer=tf.constant_initializer(0.1))logit = tf.matmul(fc1, fc2_weights) + fc2_biases# 返回第六层的输出return logit

运行修改后的mnist_train.py,可以得到以下输出:

在MNIST测试数据集上,上面给出的卷积神经网络可以达到大约99.4%的正确率。然而一种卷积神经网络架构不能解决所有问题。比如LeNet-5模型就无法很好地处理类似ImageNet这样比较大的图像数据集。

那么如何设计卷积神经网络的架构呢?

以下正则表达式总结了一些经典的用于图片分类问题的卷积神经网络架构:

输入层    --->    (卷积层    +--->    池化层?)    +--->    全连接层+

在以上公式中," 卷积层  + “ 表示一层或者多层卷积层,大部分卷积神经网络中一般最多连读使用三层卷积层。”池化层?“表示没有或者一层卷积层。池化层虽然可以起到减少参数防止过拟合问题,但是在部分论文中也发现可以直接通过调整卷积层步长来完成。所以有些卷积神经网络中没有池化层。在多轮卷积层和池化层之后,卷积神经网路在输出之前一般会经过1~2个全连接层。比如LeNet-5模型就可以表示为以下结构。

输入层---卷积层---池化层---卷积层---池化层---全连接层---全连接层---输出层

除了LeNet-5模型,2012年ImageNet ILSVRC图像分类挑战的第一名AlexNet模型、2013年ILSVRC第一名ZF Net模型以及2014年第二名VGGNet模型的架构都满足上面介绍的正则表达式。下表给出了VGGNet论文Very Deep Convolutional Networks for Large-Scale Image Recognition中作者尝试试过的不同卷积神经网络架构。从下表中可以看出这些卷积神经网络架构都满足介绍的正则表达式。

有了卷积神经网络的架构,那么每一层卷积层或者池化层中的配置需要如何设置呢?上表也提供了很多线索。在上表中,convX-Y表示过滤器的变长为X,深度为Y。比如conv3-64表示过滤器的长和宽都为3,深度为64。从上表中可以看出,VGG Net中的滤波器变长一般为3或者1。在LeNet-5模型中,也使用了边长为5的滤波器。一般卷积层的滤波器边长不会超过5,但有些卷积神经网络结构中,处理输入的卷积层中使用了边长为7甚至是11的滤波器。

在滤波器的深度上,大部分卷积神经网络都采用逐层递增的方式。比如在上表中可以看到,每经过一次池化之后,卷积层滤波器的深度会乘以2。虽然不同的模型会选择使用不同的具体数字,但是逐层递增是比较普遍的方式。卷积层的步长一般为1,但是有些模型中也会使用2,或者3作为步长。池化层的配置相对简单,用得最多的是最大池化层。池化层的滤波器边长一般为2或者3,步长一般为2或3。

2.Inception-v3模型

Inception结构是一种和LeNet-5结构完全不同的卷积神经网络结构。在LeNet-5模型中,不同卷积层通过串联的方式连接在一起,而Inception-v3模型中的Inception结构是不同的卷积层通过并联的方式结合在一起。在下面的篇幅中将具体介绍Inception结构是将不同的卷积层通过并联的方式结合在一起。

Inception-v3模型总共有46层,由11个Inception模块组成。为了更好地实现类似Inception-v3模型这样的复杂卷积神经网络,在下面将先借号tensorflow-slim工具来更加简洁地实现一个卷积层,以下代码对比了直接使用tensorflow实现一个卷积层和使用tensorflow实现同样结构的神经网络的代码。

# 直接使用tensorflow原始API实现卷积层。
with tf.variable_scope(scope_name):weights = tf.get_variable("weight", ...)biases = tf.get_variable("biases", ...)conv = tf.nn.conv2d(...)relu = tf.nn.relu(tf.nn.bias_add(conv, biases))# 使用tensorflow-slim实现卷积层。通过tensorflow-slim可以在一行中实现一个卷积层
# 的前向传播算法。slim.conv2d函数的有3个参数是必填的。第一个参数为输入节点矩阵,
# 第二参数是当前卷积层滤波器的深度,第三个参数是滤波器的尺寸。可选的参数有滤波器
# 移动的步长,是否使用全0填充,激活函数的选择以及变量的命名空间等。
net = slim.conv2d(input, 32, [3, 3])

因为完整的Inception-v3模型比较长,所以下面进介绍一个相对复杂的Inception模块的代码实现。


# 加载slim库。
slim = tf.contrib.slim# slim.arg_scope函数可以用于设置默认参数值。slim.arg_scope函数的第一个参数是
# 一个参数列表,在这个列表中的函数将使用默认的参数取值。比如通过下面的定义,调用
# slim.conv2d(net, 320, [1, 1])函数时会自动加上stride=1和padding='SAME'的参
# 数。如果在函数调用时指定了stride,那么这里设置的默认值就不会再使用。通过这种方式
# 可以进一步减少冗余的代码。
with slim.arg_scope([slim.conv2d, slim.max_pool2d, slim.avg_pool2d], stride = 1, padding='VALID'):...# 此处省略了Inception-v3模型中其他的网络结构而直接实现最后面的# Inception结构。假设输入图像讲过之前的神经网络前向传播的结果保存在变量net# 中。# net = 上一层的输出节点矩阵# 为一个Inception模块声明一个统一的变量命名空间。with tf.variable_scope('Mixed_7c'):# 给Inception模块中声明一个统一的变量命名空间。with tf.variable_scope('Branch_0'):# 实现一个滤波器边长为1,深度为320的卷积层。branch_0 = slim.conv2d( net, 320, [1 ,1], scope = 'Conv2d_0a_1x1' )with tf.variable_scope('Branch_1'):branch_1 = slim.conv2d(net, 320, [1 ,1], scope='Conv2d_0a_1x1')# tf.concat函数可以将多个矩阵拼接起来。tf.concat函数的第一个参数指定# 了拼接的维度,这里给出了"3"代表了矩阵是在深度这个维度上进行拼接。branch_1 = tf.concat(3 ,[slim.conv2d(branch_1, 384, [1 ,3], scope='Conv2d_0b_1x3'),slim.conv2d(branch_1, 384, [3 ,1], scope='Conv2d_0c_3x1')])# Inception模块中第三条路径。此计算路径也是一个Inception结构。with tf.variable_scope('Branch_2'):branch_2 = slim.conv2d(net, 448, [1, 1], scope= 'Conv2d_0a_1x1')branch_2 = slim.conv2d(branch_2, 384, [3, 3], scope='Conv2d_0b_3x3')branch_2 = tf.concat(3, [slim.conv2d(branch_2, 384, [1, 3], scope='Conv2d_0c_1x3'),slim.conv2d(branch_2, 384, [3, 1], scope='Conv2d_0d_3x1')])# Inception模块中的第四条路径。with tf.variable_scope('Branch_3'):branch_3 = slim.avg_pool2d(net, [3 ,3], scope='AvgPool_0a_3x3')branch_3 = slim.con2d(branch_3, 192, [1, 1], scope='Conv2d_0b_1x1')# 当前Inception模块的最后输出是由上面4个计算结果拼接得到的。net = tf.concat(3, [branch_0, branch_1, branch_2, branch_3])

五、卷积神经网络迁移学习

下表给出了从2012年到2015年ILSVRC(Large Scale Visual Recognition Challenge)第一名模型的层数以及前五个答案的错误率。

年份 模型名称 层数 Top5错误率
2012 AlexNet 8 15.3%
2013 ZF Net 8 14.8%
2014 GoogleNet 22 6.67%
2015 ResNet 152 3.57%

从上表可以看出,随着模型层数及复杂度的增加,模型在ImageNet上的错误率也随之降低。然而,训练复杂的卷积神经网络需要非常多的标注数据。ImageNet图像分类数据集中有120万标注图片,所以才能将152层的ResNet模型训练到96.5%的正确率。在真实的应用中,很难收集到如此多的标注数据。即使可以收集到,也需要花费大量人力物力,而且即使有海量的训练数据,要训练一个复杂的卷积神经网络也需要花费几天甚至几周的时间。为了解决标注数据和训练时间的问题,可以使用迁移学习。

所谓迁移学习,就是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。下面介绍如何使用ImageNet数据集上训练好的Inception-v3模型来解决一个新的图像分类问题。根据论文DeCAF: A Deep Convolutional Activation Feature for Generic Visual Recognition中的结论,可以保留训练好的Inception-v3模型中的所有卷积层的参数,只是替换最后一层的全连接层。在最后这一层全连接层之前的网络层称之为瓶颈层(bottleneck)。

将新的图像通过训练好的卷积神将网络直到瓶颈层的过程可以看成是对图像进行特征提取的过程。在训练好的Inception-v3模型中,因为将瓶颈层的输出再通过一个单层的全连接层神经网络可以很好地区分1000种类别的图像,所以有理由认为瓶颈层输出的节点向量可以被作为任何图像的一个更加精简且表达能力更强的特征向量。于是,在新数据集上,可以直接利用这个训练好的神经网络对图像进行特征提取,然后再将提取到的特征向量作为输入来训练一个新的单层全连接神将网络处理新的分类问题。

一般来说,在数据集足够的情况下,迁移学习的效果远不如重新训练。但是迁移学习所需要的训练时间和训练样本数要远远小于训练完整的模型。

下面将给出一个完整的tensorflow程序来介绍如何通过tensorflow实现迁移学习。以下代码给出了如何下载这一节中将要用到的数据集。

wget http://download.tensorflow.org/example_images/flower_photos.tgztar xzf flower_photos.tgz

解压之后的文件夹包含了5个子文件夹,每一个子文件夹的名称为一种花的名称,代表了不同的类别。平均每一种花有734张图片,每一张图片都是RGB色彩模式的,大小也不相同。和之前的样例不同,在这一节中给出的程序将直接处理没有整理过的图像数据。以下代码给出了如何将原始的图像数据整理成模型需要的输入数据。

import glob
import os.path
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile# 原始输入数据的目录,这个目录下有5个子目录,每个子目录底下保存属于该类别的所有
# 图片。
INPUT_DATA = '/home/user8/PycharmProjects/test/flower_photos'
# 输出文件地址。将整理后的图片数据通过numpy的格式保存。
OUTPUT_FILE = '/home/user8/PycharmProjects/test/flower_processed_data.npy'# 测试数据个验证数据比例
VALIDATION_PERCENTAGE = 10
TEST_PERCENTAGE = 10# 读取数据并将数据分割成训练数据、验证数据和测试数据。
def create_image_lists(sess, testing_percentage, validation_percentage):sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]is_root_dir = True# 初始化各个数据集。training_images = []training_labels = []testing_images = []testing_labels = []validation_images = []validation_labels = []current_label = 0# 读取所有的子目录。for sub_dir in sub_dirs:if is_root_dir:is_root_dir = Falsecontinue# 获取一个子目录中所有的图片文件。extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']file_list = []dir_name = os.path.basename(sub_dir)for extension in extensions:file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)file_list.extend(glob.glob(file_glob))if not file_list: continue# 处理图片数据。for file_name in file_list:# 读取并解析图片,将图片转化为299*299以便inception-v3模型来处理。image_raw_data = gfile.FastGFile(file_name, 'rb').read()image = tf.image.decode_jpeg(image_raw_data)if image.dtype != tf.float32:image = tf.image.convert_image_dtype(image, dtype=tf.float32)image = tf.image.resize_images(image, [299, 299])image_value = sess.run(image)# 随机划分数据集。chance = np.random.randint(100)if chance < validation_percentage:validation_images.append(image_value)validation_labels.append(current_label)elif chance < (testing_percentage + validation_percentage):testing_images.append(image_value)testing_labels.append(current_label)else:training_images.append(image_value)training_labels.append(current_label)current_label += 1# 将训练数据随机打乱以获取更好的训练效果。state = np.random.get_state()np.random.shuffle(training_images)np.random.set_state(state)np.random.shuffle(training_labels)return np.asarray([training_images, training_labels,validation_images, validation_labels,testing_images, testing_labels])# 数据整理主函数。
def main():with tf.Session() as sess:processed_data = create_image_lists(sess, TEST_PERCENTAGE, VALIDATION_PERCENTAGE)# 通过numpy格式保存处理后的数据。np.save(OUTPUT_FILE, processed_data)if __name__ == '__main__':main()

运行以上代码可以将所有的图片划分为训练、验证和测试3个数据集,并且将图片从原始的ipg格式转化为inception-v3模型需要的299*299*3的数字数据。在数据处理完毕之后,通过以下命名可以下载谷歌提供的训练好的Inception-v3模型。

wget http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz# 解析之后可以得到训练好的模型文件inception_v3.ckpt
tar xzf inception_v3_2016_08_28.tar.gz

当新的数据集和已经训练好的模型都准备之后,可以通过以下代码来完成迁移学习的过程。

# -*- coding: utf-8 -*-import glob
import os.path
import numpy as np
import tensorflow as tf
from tesorflow.python.platform import gfile
import tensorflow.contrib.slim as slim# 加载通过tesnorflow-slim定义好的inception_v3模型。
import tesnorflow.contrib.slim.python.slim.nets.inception_v3 as\inception_v3# 处理好之后的数据文件。
INPUT_DATA = '/path/to/flower_processed_data.npy'
# 保存训练好的模型和路径。这里可以将使用新数据训练得到的完整训练模型保存
# 下来,如果计算资源充足,还可以在训练完最后的全连接层之后再训练所有
# 网络层,这样可以使得新模型更加贴近新数据。
TRAIN_FILE = '/path/to/save_model'
# 谷歌提供的训练好的模型文件地址。
CKPT_FULE = '/path/to/inception_v3.ckpt'# 定义训练中使用的参数。
LEARNING_RATE = 0.0001
STEPS = 300
BATCH = 32
N_CLASSES = 5# 不需要从谷歌训练好的模型中加载的参数。这里就是最后的全连接层,因为在
# 新的问题中要重新训练这一层中参数。这里给出的是参数的前缀。
CHECKPOINT_EXCLUDE_SCOPES = 'InceptionV3/Logits, InceptionV3/AuxLogits'
# 需要训练的网络层的参数名称,在fine-tuning的过程中就是最后的全连接层。
# 这里给出的是参数的前缀。
TRAINABLE_SCOPES='InceptionV3/Logits, InceptionV3/AuxLogits'# 获取所有需要从谷歌训练好的模型中加载的参数。
def get_tuned_variables():exclusions = [scopes.strip() for scope in\CHECKPOINT_EXCLUDE_SCOPES.split(',')]variables_to_restore = []# 枚举inception-v3模型中所有的参数,然后判断是否需要从加载列表中移除。for var in slim.get_model_variables():excluded = Falsefor exclusion in exclusions:if var.op.name.startswith(exclusion):exculded = Truebreakif not excluded:variables_to_restore.append(var)return varibales_to_restore# 获取所有需要训练的变量列表。
def get_trainable_varibales():scopes = [scope.strip() for scope in TRAINABLE_SCOPES.split(',')]varibales_to_train = []# 枚举所有需要训练的参数前缀,并通过这些前缀找到所有的参数。for scope in scopes:variables = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope)variables_to_train.extend(variables)return variables_to_traindef main():# 加载预处理好的数据。processed_data = np.load(INPUT_DATA)training_images = processed_data[0]n_training_example = len(training_images)training_labels = processed_data[1]validation_images = processed_data[2]validation_labels = processed_data[3]testing_images = processed_data[4]testing_labels = processed_data[5]print("%d training examples, %d validation examples and %d""testing examples."% (n_training_examples, len(validation_labels), len(testing_labels)))# 定义inception-v3的输入,images为输入图片,labels为每一张图片对应的标签。images = tf.placeholder(tf.float32, [None, 299, 299, 3], name='input_images')labels = tf.placeholder(tf.int64, [None], name='labels')# 定义inception-v3模型。因为谷歌给出的只有模型参数取值,所以这里# 需要在这个代码中定义inception-v3的模型结构。虽然理论上需要区分训练和# 测试中使用的模型,也就是说在测试时应该使用is_training=Fales,但是# 因为预先训练好的inception-v3模型中使用的batch normalization参数与# 新的数据会有差异,导致结果很差,所以这里直接使用同一个模型来进行测试。with slim.arg_scope(inception_v3.inception_v3_arg_scope()):logits, _ = inception_v3.inception_v3(images, num_classes=N_CLASSES)# 获取需要训练的变量。trainable_variables = get_trainable_variables()# 定义交叉熵损失。注意在模型定义的时候已经将正则化损失加入损失集合了。tf.losses.softmax_cross_entropy(tf.one_hot(labels, N_CLASSES), logits, weights=1.0)# 定义训练过程。这里minimize的过程中指定了需要优化的变量集合。train_step = tf.train.RMSPropOptimizer(LEARNING_RATE).minimize(tf.losses.get_total_loss())# 计算正确率。with tf.name_scope('evaluation'):correct_prediction = tf.equal(tf.argmax(logits, 1), lables)evalution_step = tf.reduce.mean(tf.cast(correct_prediction, tf.float32))# 定义加载模型的函数。load_fn = slim.assign_from_checkpoint_fn(CKPT_FILE,get_tuned_variables(),ignore_missing_vars=True)# 定义保存新的训练好的模型的函数。saver = tf.train.Saver()with tf.Session() as sess:# 初始化没有加载进来的变量。注意这个过程一定要在模型加载之前,否则初始化过程# 会将已经加载好的变量重新赋值。init = tf.global_variables_initializer()sess.run(init)# 加载谷歌已经训练好的模型。print('Loading tuned variables from %s' % CKPT_FILE)load_fn(sess)start = 0end = BATCHfor i in range(STEPS):# 运行训练过程,这里不会更新全部参数,只会更新指定的部分参数。sess.run(train_step, feed_dict={images: training_images[strat:end],labels: training_labels[start:end]})# 输出日志if i % 30 == 0 or i + 1 == STEPS:saver.save(sess, TRAIN_FILE, global_step = i)validation_accuracy = sess.run(evaluation_step, feed_dict={images: validation_images, labels: validation_labels})print('Step %d: Validation accuracy = %.1f%%' % (i, validation_accuracy * 100.0))# 因为在数据预处理的时候已经做过了打乱数据的操作,所以这里只需要顺序使用# 训练数据就好。start = end if start == n_training_example:start = 0end = start + BATCHif end > n_training_examples:end = n_training_example# 在最后的测试数据上测试正确率。test_accuracy = sess.run(evaluation_step, feed_dict={images: testing_images, lables: testing_labels})print('Final test accuracy = %.1f%%' % (test_accuracy * 1000))if __name__ == '__main__'tf.app.run()

运行以上程序需要大约3小时,可以得到类似下面的结果:

Step 0: Validation accuracy = 22.8%
Step 30: Validation accuracy = 29.6%
Step 60: Validation accuracy = 63.2%
Step 90: Validation accuracy = 81.2%
Step 120: Validation accuracy = 88.6%
...
Step 299: Validation accuracy = 22.8%
Final test accuracy = 91.9%

从以上结果可以看到,模型在新的数据集上很快能收敛,并达到还不错的分类效果。

基于tensorflow的卷积神经网络图像识别相关推荐

  1. 基于tensorflow和卷积神经网络的电影推荐系统的实现

    基于tensorflow和卷积神经网络的电影推荐系统的实现 一. 数据处理 1. MovieLens数据集 2. 处理后的数据 二. 建模&训练 1. 嵌入层 2. 文本卷积层 3. 全连接层 ...

  2. python神经网络库识别验证码_基于TensorFlow 使用卷积神经网络识别字符型图片验证码...

    本项目使用卷积神经网络识别字符型图片验证码,其基于TensorFlow 框架.它封装了非常通用的校验.训练.验证.识别和调用 API,极大地减低了识别字符型验证码花费的时间和精力. 项目地址:http ...

  3. 【深度学习】我的第一个基于TensorFlow的卷积神经网络

    基于MNIST数据集实现简单的卷积神经网络,熟悉基于TensorFlow的CNN的流程和框架. #1.导入相关库 import numpy as np import tensorflow as tf ...

  4. 毕业设计 - 题目:基于深度学习卷积神经网络的花卉识别 - 深度学习 机器视觉

    文章目录 0 前言 1 项目背景 2 花卉识别的基本原理 3 算法实现 3.1 预处理 3.2 特征提取和选择 3.3 分类器设计和决策 3.4 卷积神经网络基本原理 4 算法实现 4.1 花卉图像数 ...

  5. 从零开始用TensorFlow搭建卷积神经网络

     https://www.jiqizhixin.com/articles/2017-08-29-14 机器之心GitHub项目:从零开始用TensorFlow搭建卷积神经网络 By 蒋思源2017 ...

  6. 商品识别系统Python,基于深度学习卷积神经网络

    介绍 商品识别系统采用了Python.TensorFlow.ResNet50算法以及Django等技术栈.其中,Python作为主要的编程语言,它的清晰简洁的语法使得代码易于阅读和编写.TensorF ...

  7. TensorFlow CNN卷积神经网络实现工况图分类识别(一)

    1. Tensorflow知识点 1.1. 张量 在Tensorflow程序中,所有的数据都是通过张量的形式来表示.从功能的角度上看,张量可以简单的理解为多维数组. (1)占位符Placeholder ...

  8. 【深度学习】基于Pytorch的卷积神经网络概念解析和API妙用(一)

    [深度学习]基于Pytorch的卷积神经网络API妙用(一) 文章目录 1 不变性 2 卷积的数学分析 3 通道 4 互相关运算 5 图像中目标的边缘检测 6 基于Pytorch的卷积核 7 特征映射 ...

  9. 【深度学习】基于Pytorch的卷积神经网络概念解析和API妙用(二)

    [深度学习]基于Pytorch的卷积神经网络API妙用(二) 文章目录1 Padding和Stride 2 多输入多输出Channel 3 1*1 Conv(笔者在看教程时,理解为降维和升维) 4 池 ...

最新文章

  1. HTML的标签描述6
  2. 节约能源,做个合格的环保主义者,不要做网络灾民
  3. MongoDB或CouchDB - 适合生产? [关闭]
  4. java 问题排查_JAVA问题排查笔记
  5. AC自动机(写的很乱,仅记录留作自己复习)
  6. ffmpeg for android shared library
  7. 在农村有100万存款,算有钱人吗?
  8. 【theano-windows】学习笔记七——logistic回归
  9. JavaScript中的一些特殊用法(一)
  10. opengl模板缓冲区
  11. 初探内核之《Linux内核设计与实现》笔记下
  12. 常用股票软件linux,在 Linux 下看股票?
  13. 耳机降噪功能(ANC、ENC、CVC、DSP)
  14. 计算机想ping一下网络,Ping命令以检测Windows7下的计算机网络速度
  15. 计算机桌面下方叫什么,电脑最下面一排桌面的小图标不见了
  16. 纯css制作遮罩层特效
  17. pandas中使用fillna函数填充NaN值
  18. python--打字练习的成绩判定
  19. BZOJ4811: [Ynoi2017]由乃的OJ 重链剖分
  20. 秋风无情 吹落叶飘满地 流水无心 像东去的涟漪

热门文章

  1. mysql 5.7.9源码安装教程_mysql5.7.9 源码安装 (转)
  2. oracle重复值排序问题
  3. 【Java UI】智能手表如何实现负一屏功能
  4. linux下samba服务器防病毒
  5. 荒野行动计算机中丢失api,荒野行动PC版提示没有Normaliz.dll怎么办 没有Normaliz.dll解决方法...
  6. 下载优酷腾讯等视频快捷网站
  7. 获取异常信息里再出异常就找不到日志了,我TM人傻了
  8. spring aop 动态代理模拟
  9. 基于CH375的智能数据采集卡设计分析
  10. lisp地物代码_一种用AutoLISP程序绘制独立地物的方法