TensorFlow:实战Google深度学习框架(五)图像识别与卷积神经网络

第6章 图像识别与卷积神经网络

本章通过利用CNN实现图像识别的应用来说明如何使用TensorFlow实现卷积神经网络

6.1 图像识别问题简介及经典数据集

1. Cifar

Cifar-10:10种不同种类的60000张图像,像素大小为32*32的彩色图像

Cifar-100:20 个大类,大类又细分为 100 个小类别,每类包含 600 张图像。

  • Cifar相比MNIST的最大区别:彩色,且每张图包含一个种类的实体分类难度更高。
  • 无论Cifar还是MNIST,相比真实环境的图像识别,还有两个主要的问题:
    1. 现实生活的图片分辨率远远高于32*32,且分辨率不固定;
    2. 现实生活中物体类别很多,且每张图像中不会仅仅出现一种物体;

2. ImageNet

  • 由斯坦福教授李飞飞(Feifei Li)带头整理的数据库,更加贴近真实生活环境。
  • Imagenet数据集有1400多万幅图片,涵盖2万多个类别;其中有超过百万的图片有明确的类别标注和图像中物体位置的标注,
  • 相关信息:
    1)Total number of non-empty synsets: 21841

    2)Total number of images: 14,197,122

    3)Number of images with bounding box annotations: 1,034,908

    4)Number of synsets with SIFT features: 1000

    5)Number of images with SIFT features: 1.2 million
    这里写图片描述

6.2 卷积神经网络简介

前面所介绍的神经网络都为全连接神经网络,本节的卷积神经网络是非全连接的神经网络,全连接与卷积神经网络的结构对比如下:

这里写图片描述

两者之间最主要的区别就在于相邻两层之间的连接方式

1. 为什么全连接不能很好的处理图像数据

最大的问题在于全连接层参数太多,使得计算速度减慢,且容易导致过拟合问题。

2. 卷积神经网络的优势

这里写图片描述

卷积神经网络的前几层都被组织成一个三维矩阵,可以看出前几层的每个节点都是和上层中的部分节点相连,卷积神经网络由以下五部分构成:

1. 输入层

输入层是整个神经网络的输入,在图像处理中,输入一般代表一张图片的像素矩阵。在上图中,最左侧的三维矩阵就代表一张图片。三维矩阵的长和宽代表图像的大小,深度代表了图像的色彩通道。从输入层开始,卷积神经网络通过不同的神经网络结构将上一层的三维矩阵转化为下一层的三维矩阵,知道最后的全连接层。

2. 卷积层

卷积层是卷积神经网络中最重要的部分。卷积层中每一个节点的输入只是上一层神经网络的一小块,这个小块常用的大小有3∗3或5∗5,卷积层试图将神经网络中的每个小块进行更加深入的分析从而抽象得到更高层次的特征,一般来说经过卷积层处理过的节点矩阵会变得更深。

3. 池化层

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

4. 全连接层

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

5. Softmax层(pooling层)

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

6.3 卷积神经网络常用结构

6.3.1 卷积层

TensorFlow文档中将下图的部分被称为“过滤器”或“内核”,过滤器可以将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵(长和宽都为1,但深度不限的节点矩阵)。

这里写图片描述

  • 过滤器:

    1. 常用的过滤器的大小为 3*3 或 5*5 的,过滤器处理的矩阵深度和当前车网络节点矩阵的深度一致。
    2. 尺寸:过滤器输入节点矩阵的大小
    3. 深度:输出节点矩阵的深度
    4. 上图中,左侧小矩阵的尺寸为过滤器的尺寸,右侧单位矩阵的深度为过滤器的深度。
    5. 前向传播过程:通过左侧小矩阵的节点计算出右侧单位矩阵中节点的过程
  • 过滤器的前向传播
    卷积层结构的前向传播过程就是通过将一个过滤器从神经网络当前层的左上角移动到右下角,并且在移动过程中计算每一个对应的单位矩阵得到的。
    这里写图片描述
    传播过程:左上角 右上角 左下角 右下角

  • 全零填充:为了避免尺寸变化,可以使用“全零填充”,可以使得前向传播后两矩阵大小不变。
    这里写图片描述

  • 设置不同的步长:也可以调整卷积后矩阵的大小
    这里写图片描述

  • 参数共享:每一个卷积层中使用的滤波器中的参数都是一样的(很重要的性质)

    1. 使得图像上的内容不受位置的影响,因为一幅图上的滤波器是相同的,无论“1”出现在图中的哪个位置,滤波的结果都是一样的。

    2. 很大程度上减少神经网络的参数

  • 示例:全零填充、步长为2的卷积层前向传播过程

这里写图片描述
左上角的计算方式:

ReLU(0×1+0×(1)+0×0+1×2+1)=ReLU(3)=3 R e L U ( 0 × 1 + 0 × ( − 1 ) + 0 × 0 + 1 × 2 + 1 ) = R e L U ( 3 ) = 3

  • TensorFlow实现卷积神经网络

1.创建滤波器的权值和偏置


filter_weight=tf.get_variable('weights',[5,5,3,16],
                              initializer=tf.truncated_initializer(stddev=0.1))
 #  t通过tf.get_variable的方式创建滤波器的权值和偏置
 #  声明一个4维矩阵,前面两个是滤波器尺寸、第三个表示当前层深度、第四个表示过滤器的深度(也就是卷积核个数)
biases=tf.get_variable('biases',[16],initializer=tf.constant_initializer(0.1))
 #  当前层矩阵上不同位置的偏置项也是共享的,所以偏置项个数=下一层深度,本例为16

2.实现卷积层的前向传播

conv=tf.nn.conv2d(input,filter_weight,strides=[1,1,1,1],padding='SAME')
 #  tf.nn.conv2提供了一个非常方便的函数来实现卷积层的前向传播
 #  第一个输入:当前层节点矩阵
 # (比如输入层,input[0,:,:,:]表示输入第一张图像,input[1,:,:,:]表示输入第二张图像
 #  第二个参数:卷积层的权重
 #  第三个参数不同维度上的步长(第一维和最后一维要求一定是1,因为步长只对矩阵的长和宽有效)
 #  第四个参数:填充的方法,可选'SAME'(全0填充)/'VALID'(不填充)

3.加上偏置项

bias=tf.nn.bias_add(conv,biases)
 #  tf.nn.bias_add提供了一个方便的函数给每个节点加上偏置项
 #  不直接使用加法:因为矩阵上不同位置上的节点都需要加上相同的偏置项

4.激活

actived_conv=tf.nn.relu(bias)
 #  将计算结果通过ReLU函数激活

6.3.2 池化层

  • 作用:

    1. 减少参数
    2. 防止过拟合
    3. 获得平移不变性
  • 常用池化类型:

    1. 最大池化
    2. 平均池化
  • 池化层的作用范围:

    1. 只影响一个深度上的节点
    2. 在长、宽、深这三个维度都要进行移动

这里写图片描述

  • TensorFlow实现最大池化层的前向传播
pool = tf.nn.max_pool(actived_conv,ksize[1,3,3,1],strides=[1,2,2,1],padding='SAME')

 # 第一个参数:当前层节点矩阵
 # 第二个参数:过滤器尺寸
 #             给出的是一个长度为4的一位数组,但数组的首位和末位必须为1
 #             意味着池化层的过滤器是不可以跨过不同样例或节点矩阵深度的
 # 第三个参数:步长,第一维和最后一维必须为1,即池化层不能减少节点矩阵的深度或者输入样例的个数
 # 第四个参数:填充方法,'SAME'表示全0填充,'VALID'表示不填充
  • TensorFlow实现平均池化层的前向传播
pool = tf.nn.avg_pool(actived_conv,ksize[1,3,3,1],strides=[1,2,2,1],padding='SAME')

 # 第一个参数:当前层节点矩阵
 # 第二个参数:过滤器尺寸
 #             给出的是一个长度为4的一维数组,但数组的首位和末位必须为1
 #             意味着池化层的过滤器是不可以跨过不同样例或节点矩阵深度的
 # 第三个参数:步长,第一维和最后一维必须为1,即池化层不能减少节点矩阵的深度或者输入样例的个数
 # 第四个参数:填充方法,'SAME'表示全0填充,'VALID'表示不填充

卷积层、池化层样例

# 《TensorFlow实战Google深度学习框架》06 图像识别与卷积神经网络
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:ts06.01.py # 卷积层、池化层样例

import tensorflow as tf
import numpy as np

# 1. 输入矩阵
M = np.array([
        [[1],[-1],[0]],
        [[-1],[2],[1]],
        [[0],[2],[-2]]
    ])
print("Matrix shape is: ",M.shape)
# Matrix shape is:  (3, 3, 1)

# 2. 定义卷积过滤器, 深度为1
filter_weight = tf.get_variable('weights', [2, 2, 1, 1], initializer = tf.constant_initializer([[1, -1],[0, 2]]))
biases = tf.get_variable('biases', [1], initializer = tf.constant_initializer(1))

# 3. 调整输入的格式符合TensorFlow的要求
M = np.asarray(M, dtype='float32')
M = M.reshape(1, 3, 3, 1)

# 4. 计算矩阵通过卷积层过滤器和池化层过滤器计算后的结果
x = tf.placeholder('float32', [1, None, None, 1])
conv = tf.nn.conv2d(x, filter_weight, strides=[1, 2, 2, 1], padding='SAME')
bias = tf.nn.bias_add(conv, biases)
pool = tf.nn.avg_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
with tf.Session() as sess:
    tf.global_variables_initializer().run()
    convoluted_M = sess.run(bias, feed_dict={x: M})
    pooled_M = sess.run(pool, feed_dict={x: M})
    print("convoluted_M: \n", convoluted_M)
    print("pooled_M: \n", pooled_M)

输出:

Matrix shape is:  (3, 3, 1)
convoluted_M: 
 [[[[ 7.],[ 1.]]
  [[-1.],[-1.]]]]

pooled_M: 
 [[[[ 0.25],[ 0.5 ]]
  [[ 1.  ],[-2.  ]]]]

6.4 经典卷积神经网络模型

6.4.1 LeNet-5模型

Yann LeCun 教授于1998年提出,是第一个成功用于数字识别的卷积神经网络,在mnist数据集上,可以达到99.2%的效果,共有7层,如下图所示。
这里写图片描述
输入原始图像的大小是32×32

1. 卷积层

输入:原始图像的像素(32*32*1)

过滤器:尺寸为5*5,深度为6,不使用全0填充,步长为1

输出:尺寸为 32-5+1=28,深度为6

参数个数:5*5*1*6+6=156,

下一层节点矩阵的节点:28*28*6=4704,每个节点和5*5=25 个当前层节点相连

本层卷积层共有连接个数:4704*(25+1)=122304

2. 池化层

输入:第一层的输出,是一个28*28*6的节点矩阵

过滤器:大小为2*2,长、宽、步长都为2

输出:14*14*6

3. 卷积层

输入:14*14*6

过滤器:大小为5*5,深度为16,不使用0填充,步长为1

输出:10*10*16,按标准的卷积层,本层应该有5*5*6*16+16=2416个参数

共有:10*10*16*(25+1)=41600个连接

4. 池化层

输入:10*10*16

过滤器:大小为2*2,步长为2

输出:矩阵大小为5*5*16

5. 全连接层

输入:5*5*16,本来论文中称本层为卷积层,但是因为滤波器大小为5*5,所以和全连接层没有区别,之后就将其看成全连接层。如果将矩阵5*5*16拉成一个向量,则和第四章的无区别

输出:节点个数为120

总共参数:5*5*16*120+120个参数。

6. 全连接层

输入:节点个数为120个

输出:节点个数为84个

总共参数:120*84+84=10164个

7. 全连接层

输入:84个节点

输出:10个节点

总共参数:84*10+10=850个

代码示例:

  • LeNet_inference.py
# 《TensorFlow实战Google深度学习框架》06 图像识别与卷积神经网络
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:LeNet5_infernece.py # LeNet5前向传播

import tensorflow as tf

# 1. 设定神经网络的参数
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

# 2. 定义前向传播的过程
def inference(input_tensor, train, regularizer):
    with tf.variable_scope('layer1-conv1'):
        conv1_weights = tf.get_variable(
            "weight", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
            initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv1_biases = tf.get_variable("bias", [CONV1_DEEP],
                                       initializer=tf.constant_initializer(0.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))

    with tf.name_scope("layer2-pool1"):
        pool1 = tf.nn.max_pool(relu1, ksize = [1,2,2,1],strides=[1,2,2,1],padding="SAME")

    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))
        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))

    with tf.name_scope("layer4-pool2"):
        pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1],
                               padding='SAME')
        pool_shape = pool2.get_shape().as_list()
        nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
        reshaped = tf.reshape(pool2, [pool_shape[0], nodes])

    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)

    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
  • LeNet_train.py
# 《TensorFlow实战Google深度学习框架》06 图像识别与卷积神经网络
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:LeNet5_train.py # LeNet5训练

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import LeNet_inference
import os
import numpy as np

# 1. 定义神经网络相关的参数
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 55000
MOVING_AVERAGE_DECAY = 0.99
MODEL_SAVE_PATH = "LeNet5_model/" # 在当前目录下存在LeNet5_model子文件夹
MODEL_NAME = "LeNet5_model"

# 2. 定义训练过程
def train(mnist):
    # 定义输出为4维矩阵的placeholder
    x = tf.placeholder(tf.float32, [
        BATCH_SIZE,
        LeNet_inference.IMAGE_SIZE,
        LeNet_inference.IMAGE_SIZE,
        LeNet_inference.NUM_CHANNELS],
                       name='x-input')
    y_ = tf.placeholder(tf.float32, [None, LeNet_inference.OUTPUT_NODE], name='y-input')

    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    y = LeNet_inference.inference(x, True, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定义损失函数、学习率、滑动平均操作以及训练过程。
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE,
        global_step,
        mnist.train.num_examples / BATCH_SIZE, LEARNING_RATE_DECAY,
        staircase=True)

    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variables_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化类。
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)

            reshaped_xs = np.reshape(xs, (
                BATCH_SIZE,
                LeNet_inference.IMAGE_SIZE,
                LeNet_inference.IMAGE_SIZE,
                LeNet_inference.NUM_CHANNELS))
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys})

            if i % 1000 == 0:
                print("After %d training step(s), loss on training batch is %g." % (step, loss_value))
                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=global_step)

# 3. 主程序入口
def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    train(mnist)

if __name__ == '__main__':
    tf.app.run()

输出:

Extracting MNIST_data\train-images-idx3-ubyte.gz
Extracting MNIST_data\train-labels-idx1-ubyte.gz
Extracting MNIST_data\t10k-images-idx3-ubyte.gz
Extracting MNIST_data\t10k-labels-idx1-ubyte.gz

After 1 training step(s), loss on training batch is 6.10953.
After 1001 training step(s), loss on training batch is 0.801586.
After 2001 training step(s), loss on training batch is 0.829287.
After 3001 training step(s), loss on training batch is 0.655056.
After 4001 training step(s), loss on training batch is 0.698159.
After 5001 training step(s), loss on training batch is 0.744295.
After 6001 training step(s), loss on training batch is 0.657604.
After 7001 training step(s), loss on training batch is 0.697003.
After 8001 training step(s), loss on training batch is 0.685206.
After 9001 training step(s), loss on training batch is 0.651352.
After 10001 training step(s), loss on training batch is 0.729663.
After 11001 training step(s), loss on training batch is 0.666927.
After 12001 training step(s), loss on training batch is 0.65114.
After 13001 training step(s), loss on training batch is 0.648548.
...
  • LeNet_eval.py(测试该网络在mnist的正确率,达到99.4%,巨幅高于第五章的98.4%)
# 《TensorFlow实战Google深度学习框架》06 图像识别与卷积神经网络
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:LeNet5_eval.py # 测试

import time
import math
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
import LeNet5_infernece
import LeNet5_train

def evaluate(mnist):
    with tf.Graph().as_default() as g:
        # 定义输出为4维矩阵的placeholder
        x = tf.placeholder(tf.float32, [
            mnist.test.num_examples,
            #LeNet5_train.BATCH_SIZE,
            LeNet5_infernece.IMAGE_SIZE,
            LeNet5_infernece.IMAGE_SIZE,
            LeNet5_infernece.NUM_CHANNELS],
                           name='x-input')
        y_ = tf.placeholder(tf.float32, [None, LeNet5_infernece.OUTPUT_NODE], name='y-input')
        validate_feed = {x: mnist.test.images, y_: mnist.test.labels}
        global_step = tf.Variable(0, trainable=False)

        regularizer = tf.contrib.layers.l2_regularizer(LeNet5_train.REGULARIZATION_RATE)
        y = LeNet5_infernece.inference(x, False, regularizer)
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        variable_averages = tf.train.ExponentialMovingAverage(LeNet5_train.MOVING_AVERAGE_DECAY)
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)

        #n = math.ceil(mnist.test.num_examples / LeNet5_train.BATCH_SIZE)
        n = math.ceil(mnist.test.num_examples / mnist.test.num_examples)
        for i in range(n):
            with tf.Session() as sess:
                ckpt = tf.train.get_checkpoint_state(LeNet5_train.MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
                    xs, ys = mnist.test.next_batch(mnist.test.num_examples)
                    #xs, ys = mnist.test.next_batch(LeNet5_train.BATCH_SIZE)
                    reshaped_xs = np.reshape(xs, (
                        mnist.test.num_examples,
                        #LeNet5_train.BATCH_SIZE,
                        LeNet5_infernece.IMAGE_SIZE,
                        LeNet5_infernece.IMAGE_SIZE,
                        LeNet5_infernece.NUM_CHANNELS))
                    accuracy_score = sess.run(accuracy, feed_dict={x:reshaped_xs, y_:ys})
                    print("After %s training step(s), test accuracy = %g" % (global_step, accuracy_score))
                else:
                    print('No checkpoint file found')
                    return

# 主程序
def main(argv=None):
    mnist = input_data.read_data_sets("../../../datasets/MNIST_data", one_hot=True)
    evaluate(mnist)

if __name__ == '__main__':
    tf.app.run()

LeNet-5模型的缺点
不能很好的解决所有问题,比如类似ImageNet的复杂数据集

如何设计卷积神经网络架构
该正则表达公式总结了经典的用于图像分类问题的卷积神经网络架构

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

其中:

  1. +表示一层或多层
  2. “池化层?”表示没有或一层池化层,因为有的网络是通过直接调整卷积层步长来完成的参数减少
  3. 多伦卷积核池化之后,输出层之前会有1~2个全连接层

LeNet-5的正则表达:

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

从VGGNet观察正则表达式的特点:
这里写图片描述

  1. convX-Y:滤波器的边长为X,深度为Y

  2. VGGNet中的滤波器的边长基本都为3或1

  3. LeNet-5中使用了大小为5*5的滤波器,一般都不会超过5,但也有的设置为7*7,甚至11*11的

  4. 在滤波器的深度选择上,大部分都采用逐层递增的方式,VGG中,每经过一个池化层,滤波器的深度*2,

  5. 卷积层的步长一般为2,但也有例外(使用2或3)

  6. 池化层配置相对简单,用的最多的是最大池化层,边长一般为2或3,步长一般为2或3

6.4.2 Inception模型

已知滤波器的大小可选,但是没有先验知识帮助我们去选择其大小,所以Inception模型将不同尺寸的滤波器的滤波结果通过并联的方式结合在一起,即将得到的矩阵拼接起来。

这里写图片描述

Inception模型会使用不同尺寸的滤波器处理矩阵,使用全0填充和步长为1的方法获得的特征图谱大小是相同的,不会影响矩阵的拼接。
这里写图片描述

Inception-v3模型共有46层,由11个Inception模块构成,上图中方框标注的就是一个模块,Inception-v3模型有96个卷积层,直接使用上文的TensorFlow模型会非常冗长,所以此处介绍TensorFlow-Slim工具来更加简洁的实现同样结构的神经网络代码量。

1.直接使用TensorFlow原始API实现卷积层

with tf.variable_scope(scope_name):
    weights=tf.get_variable("weight",...)
    biases=tf.get_variable("bias",...)
    conv=tf.nn.conv2d(...)
 relu=tf.nn.relu(tf.nn.bias_add(conv,biases))

2.使用TensorFlow-Slim实现卷积层

tf.contrib.slim.conv2d (inputs,
            num_outputs,#[卷积核个数]
            kernel_size,#[高度,宽度]
            stride=1,#步长
            padding='SAME',#VALID

存在于tensorflow.contrib的库中,导入方法:import tensorflow.contrib.slim as slim

net=slim.conv2d(input,32,[3,3])
# 可以在一行中实现一个卷积层的前向传播算法
# 第一个参数:输入节点矩阵
# 第二个参数:当前卷积层过滤器的深度
# 第三个参数:过滤器的尺寸
# 可选步长、填充、激活函数、变量命名空间等

6.5 卷积神经网络实现迁移学习

6.5.1 迁移学习的介绍

这里写图片描述

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

迁移学习:将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。比如可以保留训练好的Inception-v3模型中所有卷积层的参数,只是替换最后一层全连接层,可以解决一个新的图像分类问题。

瓶颈层:在最后这一层全连接层之前的网络层称之为瓶颈层。

  • 将新的图像通过训练好的卷积神经网络直到瓶颈层的过程可以看成是对图像进行特征提取的过程。
  • 可以认为瓶颈层的输出节点向量可以被作为任何图像的一个更加精简且表达能力更强的特征向量,于是可以用该训练好的神经网络在新的数据集上完成对图像特征的提取。
  • 再将提取到的特征作为输入来训练一个新的单层全连接神经网络处理新的分类问题

迁移学习相比重新训练的优缺点:

  • 缺点:效果不如完全重新训练
  • 优点:需要的训练时间和训练样本数要远远小于训练完整的模型

代码实现:

# 《TensorFlow实战Google深度学习框架》06 图像识别与卷积神经网络
# win10 Tensorflow1.0.1 python3.5.3
# CUDA v8.0 cudnn-8.0-windows10-x64-v5.1
# filename:ts06.03.py # 迁移学习

# 以下实验需要如下资源
# 源码及资源位置:git clone https://github.com/caicloud/tensorflow-tutorial.git
# 需要tensorflow-tutorial.git库中的flower_photos和inception_dec_2015
# tensorflow-tutorial\Deep_Learning_with_TensorFlow\datasets\flower_photos
# tensorflow-tutorial\Deep_Learning_with_TensorFlow\datasets\inception_dec_2015

import glob
import os.path
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile

# 1. 模型和样本路径的设置
# inception-v3模型瓶颈层的节点数
BOTTLENECK_TENSOR_SIZE = 2048

# inception-v3模型中代表瓶颈层结果的张量名称,谷歌提供的inception-v3模型中,这个张量名称就是'pool_3/_reshape:0'
# 训练过程中,可以利用tensor.name来获取张量的名称
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'

# 图像输入张量所对应的名称
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'

# 保存的模型文件目录
MODEL_DIR = '../../datasets/inception_dec_2015'
# 下载的谷歌训练好的inception-v3模型文件名
MODEL_FILE= 'tensorflow_inception_graph.pb'

# 因为一个训练数据会被使用多次,所以可以将原始图像通过inception-v3模型计算得到的特征向量保存在文件中,
# 以免重复计算,下面是这些文件的存放地址。
CACHE_DIR = '../../datasets/bottleneck'

# 图片数据文件夹
# 该文件夹中每一个子文件夹代表一个需要区分的类别,每个子文件夹中存放了对应类别的图片
INPUT_DATA = '../../datasets/flower_photos'

# 验证集百分比
VALIDATION_PERCENTAGE = 10
# 测试集百分比
TEST_PERCENTAGE = 10

# 2. 神经网络参数的设置
LEARNING_RATE = 0.01
STEPS = 4000
BATCH = 100

# 3. 把样本中所有的图片列表并按训练、验证、测试数据分开
# testing_percentage:测试数据集的大小
# validation_percentage:验证数据集的大小
def create_image_lists(testing_percentage, validation_percentage):
    # 定义一个result字典,key存储类别名称, 与key对应的value也是一个字典,存储所有该类的图片名称
    result = {}
    # 获取当前目录下所有的子目录
    sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
    is_root_dir = True
    for sub_dir in sub_dirs:
        if is_root_dir:
            is_root_dir = False
            continue

        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

        label_name = dir_name.lower()

        # 初始化
        training_images = []
        testing_images = []
        validation_images = []
        for file_name in file_list:
            base_name = os.path.basename(file_name)

            # 随机划分数据
            chance = np.random.randint(100)
            if chance < validation_percentage:
                validation_images.append(base_name)
            elif chance < (testing_percentage + validation_percentage):
                testing_images.append(base_name)
            else:
                training_images.append(base_name)

        result[label_name] = {
            'dir': dir_name,
            'training': training_images,
            'testing': testing_images,
            'validation': validation_images,
        }
    return result

# 4. 定义函数通过类别名称、所属数据集和图片编号获取一张图片的地址
def get_image_path(image_lists, image_dir, label_name, index, category):
    label_lists = image_lists[label_name]
    category_list = label_lists[category]
    mod_index = index % len(category_list)
    base_name = category_list[mod_index]
    sub_dir = label_lists['dir']
    full_path = os.path.join(image_dir, sub_dir, base_name)
    return full_path

# 5. 定义函数获取Inception-v3模型处理之后的特征向量的文件地址
def get_bottleneck_path(image_lists, label_name, index, category):
    return get_image_path(image_lists, CACHE_DIR, label_name, index, category) + '.txt'

# 6. 定义函数使用加载的训练好的Inception-v3模型处理一张图片,得到这个图片的特征向量
# 该过程实际上就是将当前图片作为输入九三瓶颈张量的值,这个瓶颈张量的值就是该图像的新的特征向量
def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):

    bottleneck_values = sess.run(bottleneck_tensor, {image_data_tensor: image_data})
    # 将四维数组压缩为一个特征向量
    bottleneck_values = np.squeeze(bottleneck_values)
    return bottleneck_values

# 7. 定义函数会先试图寻找已经计算且保存下来的特征向量,如果找不到则先计算这个特征向量,然后保存到文件
def get_or_create_bottleneck(sess, image_lists, label_name, index, category, jpeg_data_tensor, bottleneck_tensor):
    label_lists = image_lists[label_name]
    sub_dir = label_lists['dir']
    sub_dir_path = os.path.join(CACHE_DIR, sub_dir)
    if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path)
    bottleneck_path = get_bottleneck_path(image_lists, label_name, index, category)
    if not os.path.exists(bottleneck_path):

        image_path = get_image_path(image_lists, INPUT_DATA, label_name, index, category)

        image_data = gfile.FastGFile(image_path, 'rb').read()

        bottleneck_values = run_bottleneck_on_image(sess, image_data, jpeg_data_tensor, bottleneck_tensor)

        bottleneck_string = ','.join(str(x) for x in bottleneck_values)
        with open(bottleneck_path, 'w') as bottleneck_file:
            bottleneck_file.write(bottleneck_string)
    else:

        with open(bottleneck_path, 'r') as bottleneck_file:
            bottleneck_string = bottleneck_file.read()
        bottleneck_values = [float(x) for x in bottleneck_string.split(',')]

    return bottleneck_values

# 8. 这个函数随机获取一个batch的图片作为训练数据
def get_random_cached_bottlenecks(sess, n_classes, image_lists, how_many, category, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    for _ in range(how_many):
        label_index = random.randrange(n_classes)
        label_name = list(image_lists.keys())[label_index]
        image_index = random.randrange(65536)
        bottleneck = get_or_create_bottleneck(
            sess, image_lists, label_name, image_index, category, jpeg_data_tensor, bottleneck_tensor)
        ground_truth = np.zeros(n_classes, dtype=np.float32)
        ground_truth[label_index] = 1.0
        bottlenecks.append(bottleneck)
        ground_truths.append(ground_truth)

    return bottlenecks, ground_truths

# 9. 这个函数获取全部的测试数据,并计算正确率
def get_test_bottlenecks(sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    label_name_list = list(image_lists.keys())
    for label_index, label_name in enumerate(label_name_list):
        category = 'testing'
        for index, unused_base_name in enumerate(image_lists[label_name][category]):
            bottleneck = get_or_create_bottleneck(sess, image_lists, label_name, index, category,jpeg_data_tensor, bottleneck_tensor)
            ground_truth = np.zeros(n_classes, dtype=np.float32)
            ground_truth[label_index] = 1.0
            bottlenecks.append(bottleneck)
            ground_truths.append(ground_truth)
    return bottlenecks, ground_truths

# 10. 定义主函数
def main():
    image_lists = create_image_lists(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
    n_classes = len(image_lists.keys())

    # 读取已经训练好的Inception-v3模型。
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(
        graph_def, return_elements=[BOTTLENECK_TENSOR_NAME, JPEG_DATA_TENSOR_NAME])

    # 定义新的神经网络输入
    bottleneck_input = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
    ground_truth_input = tf.placeholder(tf.float32, [None, n_classes], name='GroundTruthInput')

    # 定义一层全链接层
    with tf.name_scope('final_training_ops'):
        weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.001))
        biases = tf.Variable(tf.zeros([n_classes]))
        logits = tf.matmul(bottleneck_input, weights) + biases
        final_tensor = tf.nn.softmax(logits)

    # 定义交叉熵损失函数。
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy_mean)

    # 计算正确率。
    with tf.name_scope('evaluation'):
        correct_prediction = tf.equal(tf.argmax(final_tensor, 1), tf.argmax(ground_truth_input, 1))
        evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    with tf.Session() as sess:
        init = tf.global_variables_initializer()
        sess.run(init)
        # 训练过程。
        for i in range(STEPS):

            train_bottlenecks, train_ground_truth = get_random_cached_bottlenecks(
                sess, n_classes, image_lists, BATCH, 'training', jpeg_data_tensor, bottleneck_tensor)
            sess.run(train_step,
                     feed_dict={bottleneck_input: train_bottlenecks, ground_truth_input: train_ground_truth})

            if i % 100 == 0 or i + 1 == STEPS:
                validation_bottlenecks, validation_ground_truth = get_random_cached_bottlenecks(
                    sess, n_classes, image_lists, BATCH, 'validation', jpeg_data_tensor, bottleneck_tensor)
                validation_accuracy = sess.run(evaluation_step, feed_dict={
                    bottleneck_input: validation_bottlenecks, ground_truth_input: validation_ground_truth})
                print('Step %d: Validation accuracy on random sampled %d examples = %.1f%%' %
                      (i, BATCH, validation_accuracy * 100))

        # 在最后的测试数据上测试正确率。
        test_bottlenecks, test_ground_truth = get_test_bottlenecks(
            sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor)
        test_accuracy = sess.run(evaluation_step, feed_dict={
            bottleneck_input: test_bottlenecks, ground_truth_input: test_ground_truth})
        print('Final test accuracy = %.1f%%' % (test_accuracy * 100))

if __name__ == '__main__':
    tf.app.run()
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值