基于卷积神经网络的图像识别

2022-09-04 22:17:19 浏览数 (1)

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

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

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

CIFAR-10问题收集了来自10个不同种类的60000张图片,CIFAR官网:https://www.cifar.ca/提供了不同格式的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的官网http://www.image-net.org/列出了历届ILSVRC竞赛的题目和数据集。不同年份的ImageNet比赛提供了不同的数据集,下面介绍使用得最多的ILSVRC2012图像分类数据集。

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

二、卷积神经网络简介

为了将只包含全连接神经网络与卷积神经网络、循环神经网络区分开,将只包含全连接神经网路称之为全连接神经网络。使用全连接神经网络处理图像的最大问题在于全连接层的参数太多。对于MNIST数据,每一张图片的大小是28*28*1,其中28*28为图片的大小,*1表示图像是黑白的,只有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为偏置项参数的个数。使用w_{x, y, z} 来表示对于输出节点矩阵中的第i个节点,过滤器输入节点(x, y, z)的权重,使用b^i 表示第i个输出节点对应的偏置项参数,那么单位矩阵中的第i个节点的取值g(i)为:

g(i)=fleft(sum_{x=1}^{2} sum_{y=1}^{2} sum_{y=1}^{2} a_{x, y, z} times w_{x, y, z}^{j} b^{i}right)

其中a_{x,y,z} 为过滤器中节点(x,y,z)的取值,f为激活函数。下图展示了在给定a,w^0b^0 的情况下,使用ReLU作为激活函数时g(0)的计算过程,左侧给出了a和w^0 的取值,这里通过3个二维矩阵来表示一个三维矩阵的取值,其中每一个二维矩阵表示三维矩阵在某一深度上的取值。

cdot 符号表示点积,也就是矩阵中对应元素乘积的和。右侧显示了g(0)的计算过程。如果给出w^0w^4b^1b^4 ,那么也可以类似地计算出g(1)到g(4)的取值。如果将a和w^i 组织成两个向量,那么一个滤波器的计算过程完全可以通过向量乘法来完成。

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

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

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

以下程序实现了一个卷积层的前向传播过程。从以下代码可以看出,通过tensoflow实现卷积层是非常方便的。

代码语言:javascript复制
# 通过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.conv2第二个参数提供了卷积层的权重,第三个参数为不同
# 维度上的步长。虽然第三个参数提供的是一个长度为4的数组,但是第一维和最后一维的数字
# 要求一定是1.这是因为卷积层的步长只对矩阵的长和宽有效。最后一个参数是填充(padding)
# 的方法,tensorflow中提供SAME或是VALID两种选择。其中SAME表示添加全0填充,'VALID'表示
# 不添加。

conv = tf.nn.conv2(
       input, filter_weight, strides=[1,1,1,1], padding='SAME')


# tf.nn.bisa_add提供了一个方便的函数给每一个节点加上偏置项。注意这里不能直接使用加
# 法,因为矩阵上不同位置上的节点都需要加上同样的偏置项。虽然下一层神经网络的大小为2*2
# 但是偏置项只有一个数(因为深度为1),而2*2矩阵中的每一个值都需要加上这个偏置项。
bias = tf.nn.bias_add(conv, biases)
# 将计算结果通过ReLU激活函数完成去线性化。
activated_conv = tf.nn.relu(bias)

2.池化层

在卷积层之间往往会加上一个池化层(pooling layer)。池化层可以非常有效地缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层可以加快计算速度也有防止过拟合问题的作用。

池化层滤波器中的计算不是节点的加权和,而是采用更加简单的最大值或者平均值运算。使用最大值操作的池化层被称之为最大值池化层(max pooling),这是被使用得最多的池化层结构。使用平均值操作的池化层被称之为平均池化层(average pooling)。其他池化层在实践中使用的比较少,本书不做过的介绍。

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

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

代码语言:javascript复制
# 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表示全零填充,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个,输出节点个数为84个,总共参数为120*84 84=10164个。

第七层,全连接层

本层的输入节点个数为84个,输出节点个数为10个,总共参数为84*10 10=850个。

上面介绍了LeNet-5模型每一层结构和设置,下面给出了一个tensorflow的程序来实现一个类似LeNet-5模型的卷积神经网络来解决MNIST数字识别问题。

代码语言:javascript复制
# 调整输入数据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.(xs,  (BATCH_SIZE,
                        mnist_inference.IMAGE_SIZE,
                        mnist_inference.IMAGE_SIZE,
                        mnist_inference.NUM_CHANNELS))

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

代码语言:javascript复制
# -*- coding: utf-8 -*-

import tensorflow as tf


# 配置神经网络的参数。
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_SZIE = 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.trancated_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], pedding='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, kszie=[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_SZIE, CONV1_DEEP, CONV2_DEEP],  
       initializer=tf.truncated_normal_initializer(stddev=0.1))
   conv2_baises = 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的向量。
reshape = 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,可以得到以下输出:

代码语言:javascript复制
~/mnist$ python mnist_train.py
Extracting /tmp/data/train-images-idx3-ubyte.gz
Extracting /tmp/data/train-images-idx1-ubyte.gz
Extracting /tmp/data/t10k-images-idx3-ubyte.gz
Extracting /tmp/data/t10k-images-idx1-ubyte.gz
After 1 training step(s), loss on training betch is 6.45373.
After 1001 training step(s), loss on training betch is 0.824825.
After 2001 training step(s), loss on training betch is 0.646993.
After 3001 training step(s), loss on training betch is 0.75997.
After 4001 training step(s), loss on training betch is 0.68468.
After 5001 training step(s), loss on training betch is 0.630368.

在MNIST测试数据集上,上面给出的卷积神经网络可以达到大约99.4%的正确率。然而一种卷积神经网络架构不能解决所有问题。比如LeNet-5模型就无法很好地处理类似ImageNet这样比较大的图像数据集。那么如何设计卷积神经网络的架构呢?以下正则表达式总结了一些经典的用于图片分类问题的卷积神经网络架构:

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

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

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

除了LeNet-5模型,2012年ImageNet ILSVRC图像分类挑战的第一名AlexNet模型、2013年ILSVRC第一名ZF Net模型以及2014年第二名VGGNet模型的架构都满足上面介绍的正则表达式。

2.Inception-v3模型

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

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

代码语言:javascript复制
# 直接使用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模块的代码实现。

代码语言:javascript复制
# 加载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('Branch_0'):
       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.varibale_scope('Branch_2'):
           branch_2 = slim.conv2d(
              net, 448, [1,1], scope='Conv2d_0a_3x3')
           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])

0 人点赞