卷积神经网络

卷积神经网络

全连接神经网络的缺点

主要体现在以下几个方面:

  • 参数太多,在cifar-10的数据集中,只有32*32*3,就会有这么多权重,如果说更大的图片,比如200*200*3就要120000多个,这完全是浪费。

  • 没有利用像素之间位置信息,对于图像识别任务来说,每个像素与周围的像素都是联系比较紧密的。

  • 层数限制。

卷积神经网络简介

卷积神经网络的发展历史

卷积神经网络的发展历史如下图所示。卷积神经网络(Convolutional Neural Network,简写CNN)是由Yann LeCun发明的,Yann LeCun是Geoffrey Everest Hinton的学生,于1989年发明卷积神经网络。

后来,出现了AlexNet,一个历史性的突破,它内部集成了一系列的卷积、激活和池化,我们可以直接调用该模型进行训练。后来演变产生了VGG16、NIN、R-CNN、Inception V2等分支,其中VGG16和NIN属于Google研发的,后来合并为了ResNet(残差网络),这个网络模型是进行图像识别的非常重要的一个网络模型。

卷积神经网络_第1张图片
image.png

下图是几个神经网络的错误率比较图:

卷积神经网络_第2张图片
image.png

通过对比,我们发现,GoogleNet、ResNet能大幅度减少错误率。

常见的卷积模型

LeNet

诞生于1986年,模型结构如下所示:

卷积神经网络_第3张图片
image.png

AlexNet

诞生于2012年,模型结构如下所示:

卷积神经网络_第4张图片
image.png

该模型有60M以上的参数总量,也就是60*10000个参数,一般的计算机很难运行起来。

GoogleNet

GoogleNet的模型结构算是比较好的,模型过程结构如下图所示:

卷积神经网络_第5张图片
image.png

该模型结构中的一些卷积、激活和池化操作如下:

卷积神经网络_第6张图片
image.png

卷积神经网络的结构分析

神经网络(neural networks)的基本组成包括输入层、隐藏层、输出层。而卷积神经网络的特点在于隐藏层分为卷积层和池化层(pooling layer,又叫下采样层)。

  • 卷积层:通过在原始图像上平移来提取特征,每一个特征就是一个特征映射。
  • 池化层:通过特征后稀疏参数来减少学习的参数,降低网络的复杂度(最大池化和平均池化)

如图是一个使用卷积神经网络进行图片对象识别的案例

卷积神经网络_第7张图片
image.png

如下图所示,在整个卷积网络中,我们有卷积(CONV)、激活(RELU)、池化(POOL)等步骤,每一层神经网络都必须包含CONV、RELU和POOL三个部分。最后是一个全连接层(FC)输出类别,这个和目标值的类别个数有关。

如下图是数据在卷积过程中的体量变化,原始数据在卷积过程中数据的体量会变大;然后经过RELU激活函数,激活函数不改变数据的形状;最后我们进行池化操作,将相邻的特征进行特征提取,以代表性的特征来代替这一块区域的特征,减少数据的体量。

卷积神经网络_第8张图片
image.png

如下图所示是一个池化操作的过程分析,池化前的数据体量为[224,224,64],池化后为[112,112,64],我们提取相邻的代表性特征。如[[1,1],[5,6]],6最大是这一块的代表性特征。

卷积神经网络_第9张图片
image.png

卷积计算公式和过程分析

在卷积计算过程中,我们会涉及到一些参数,如下图所示,我们可以根据卷积的一些参数来计算一个卷积计算的输出数据体积。核心参数为:

  • 过滤器的个数。
  • 过滤器的大小,也就是过滤器的形状或者观察窗口的大小。如3x3,5x5,如果图片比较大,我们就选用比较大的观察窗口;反之,选择较小的观察窗口。
  • 卷积的步长。
  • 零填充,是否零填充。
卷积神经网络_第10张图片
image.png

我们以一个示例进行说明:

现在假设有一个输入为1000张彩色图片,形状为230x240,卷积的观察窗口大小为5x5,padding="1", 步长为1,请计算卷积的输出体积。

输入体积:[1000, 230, 240, 3]

计算过程:H2 = (H1 - F1 + 2xP)/S + 1 =(230 -5 + 2x1)/1 + 1 = 228,W2 = (240 -5 + 2 x1)/1 + 1 = 238

输出体积: [1000, 228, 238, 3]

如下图所示是当图片通道为1,过滤器为1,形状为3x3,步长为1的卷积计算过程。

卷积神经网络_第11张图片
image.png

如下图所示是当图片通道为1,过滤器为1,形状为3x3,步长为2的卷积计算过程。

卷积神经网络_第12张图片
image.png

我们再来看一下三通道图片的卷积计算过程动态图,如下所示:

[图片上传失败...(image-30da08-1560495989377)]

卷积层的零填充

在进行卷积计算过程分析的时候,我们有一个超参数为零填充,它也是影响卷积计算的核心参数。

卷积核在提取特征映射时的动作称之为padding(零填充),由于移动步长不一定能整出整张图的像素宽度。所以需要在图片外围进行零填充,其中有两种方式,SAME和VALID。

  • SAME:越过边缘取样,取样的面积和输入图像的像素宽度一致。
  • VALID:不越过边缘取样,取样的面积小于输入人的图像的像素宽度

如下图所示,在零填充之前,我们的形状为[32, 32, 3],零填充之后的形状为[36, 36, 3]。

卷积神经网络_第13张图片
image.png

激活函数

激活函数有以下几种,我们可以在tensorflow神经网络演示页面看到:

  • Linear:表示没有激活函数
  • sigmoid:使用的最多的激活函数
  • Relu:在卷积神经网络中使用最多的激活函数,2018年发明。
  • Tanh:在Relu没有出来之前的替代产品,现在基本不使用。

早期,我们的激活函数只有一个,也就是sigmoid函数,公式如下:

image.png

在计算卷积神经网络的时候,我们发现使用sigmoid激活函数的计算工作量非常大,从公式我们也可以看出,我们需要进行大量的指数运算。后来,出现了一个激活函数叫做Tanh,可以大量减少计算工作量。

直到2018年,出现了一个新的激活函数,完全替代了Tanh,它就是Relu,公式如下:

image.png

它的取值只有两个,当x <=0时,y=0;当x>0时,y=x,这个计算方式非常简单,大幅度提高了计算效率,图像如下所示:

卷积神经网络_第14张图片
image.png

卷积网络API介绍

一个卷积网络过程通常包含卷积层、激活层、池化层三个部分,下面分别说明这三个部分的API。

卷积层

# 计算给定4-D input和filter张量的2维卷积
tf.nn.conv2d(input, filter, strides=, padding=, name=None)

参数说明:

  • input:给定的输入张量,具有[batch,heigth,width,channel],类型为float32,64
  • filter:指定过滤器的大小或者观察窗口的大小,[filter_height, filter_width, in_channels, out_channels]
  • strides:strides = [1, stride, stride, 1],步长
  • padding:“SAME”, “VALID”,使用的填充算法的类型。其中”VALID”表示滑动超出部分舍弃,“SAME”表示填充,使得变化后height,width一样大。基本都是使用“SAME”,保证能完整扫描整张图片的所有特征,一个不漏。

激活层

现在使用的激活函数主要有2个,sigmoid和relu,但是sigmoid不适合卷积神经网络,我们使用Relu激活函数。

主要有两点原因:

  • 第一,采用sigmoid等函数,反向传播求误差梯度时,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。
  • 第二,对于深层网络,sigmoid函数反向传播时,很容易就会出现梯度消失的情况(求不出权重和偏置)。

tensorflow提供的Relu激活函数API如下

tf.nn.relu(features, name=None)

参数说明:

  • features:卷积后加上偏置的输出结果,作为激活的输入。

说明:激活函数的输入和输出结果形状是一样的,也就是说激活层不改变数据的形状。

池化层

Pooling层主要的作用是特征提取,通过去掉Feature Map中不重要的样本,进一步减少参数数量。Pooling的方法很多,最常用的是Max Pooling。

卷积神经网络_第15张图片
image.png

API如下:

池化函数我们一般选max_pool

# 执行最大池数
tf.nn.max_pool(value, ksize=, strides=, padding=,name=None)

参数说明:

  • value:4-D Tensor形状[batch, height, width, channels]
  • ksize:池化窗口大小,[1, ksize, ksize, 1]
  • strides:步长大小,[1,strides,strides,1]
  • padding:“SAME”, “VALID”,使用的填充算法的类型,使用“SAME”

一般我们选择ksize=2, strides=2,padding="SAME",相当于就是进行数据体量维度减半的操作,例如:

池化前的数据:[10000, 32, 32, 1],池化后[10000, 16, 16, 1]

全连接层

前面的卷积和池化相当于做特征工程,后面的全连接相当于做特征加权。

最后的全连接层在整个卷积神经网络中起到“分类器”的作用。

卷积神经网络实现手写数字识别项目源代码

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_string("data_dir", "/Users/zhusheng/WorkSpace/Tmp/dataset/Mnist", "mnist数据集")
tf.app.flags.DEFINE_integer("is_train", 1, "指定程序是预测还是训练")

# 定义一个初始化权重的函数
def weight_variables(shape):
    w = tf.Variable(tf.random_normal(shape=shape, mean=0.0, stddev=1.0))
    return w

# 定义一个初始化偏置的函数
def bias_variables(shape):
    b = tf.Variable(tf.constant(0.0, shape=shape))
    return b

def model():
    """
    自定义的卷积模型
    :return:
    """
    # 1、准备数据的占位符 x [None, 784]  y_true [None, 10]
    with tf.variable_scope("data"):
        x = tf.placeholder(tf.float32, [None, 784])
        y_true = tf.placeholder(tf.int32, [None, 10])

    # 2、一卷积层 卷积: 5*5*1,32个,strides=1 激活: tf.nn.relu ,池化
    with tf.variable_scope("conv1"):
        # 随机初始化权重, 偏置[32]
        w_conv1 = weight_variables([5, 5, 1, 32])
        b_conv1 = bias_variables([32])

        # 对x进行形状的改变[None, 784]  [None, 28, 28, 1],如果形状是None,需要写成-1
        x_reshape = tf.reshape(x, [-1, 28, 28, 1])

        # [None, 28, 28, 1]-----> [None, 28, 28, 32]
        x_relu1 = tf.nn.relu(tf.nn.conv2d(x_reshape, w_conv1, strides=[1, 1, 1, 1], padding="SAME") + b_conv1)

        # 池化 2*2 ,strides2 [None, 28, 28, 32]---->[None, 14, 14, 32]
        x_pool1 = tf.nn.max_pool(x_relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    # 3、二卷积层卷积: 5*5*32,64个filter,strides=1 激活: tf.nn.relu 池化:
    with tf.variable_scope("conv2"):
        # 随机初始化权重,  权重:[5, 5, 32, 64]  偏置[64]
        w_conv2 = weight_variables([5, 5, 32, 64])
        b_conv2 = bias_variables([64])

        # 卷积,激活,池化计算
        # [None, 14, 14, 32]-----> [None, 14, 14, 64]
        x_relu2 = tf.nn.relu(tf.nn.conv2d(x_pool1, w_conv2, strides=[1, 1, 1, 1], padding="SAME") + b_conv2)

        # 池化 2*2, strides 2, [None, 14, 14, 64]---->[None, 7, 7, 64]
        x_pool2 = tf.nn.max_pool(x_relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    # 4、全连接层 [None, 7, 7, 64]--->[None, 7*7*64]*[7*7*64, 10]+ [10] =[None, 10]
    with tf.variable_scope("conv2"):

        # 随机初始化权重和偏置
        w_fc = weight_variables([7 * 7 * 64, 10])
        b_fc = bias_variables([10])

        # 修改形状 [None, 7, 7, 64] --->None, 7*7*64]
        x_fc_reshape = tf.reshape(x_pool2, [-1, 7 * 7 * 64])

        # 进行矩阵运算得出每个样本的10个结果
        y_predict = tf.matmul(x_fc_reshape, w_fc) + b_fc

        # 收集高维度变量
        tf.summary.histogram("w_fc", w_fc)
        tf.summary.histogram("b_fc", b_fc)

    return x, y_true, y_predict


def conv_fc():
    # 获取真实的数据
    mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)

    # 定义模型,得出输出
    x, y_true, y_predict = model()

    # 进行交叉熵损失计算
    # 3、求出所有样本的损失,然后求平均值
    with tf.variable_scope("soft_cross"):
        # 求平均交叉熵损失
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_predict))

    # 4、梯度下降求出损失
    with tf.variable_scope("optimizer"):
        train_op = tf.train.GradientDescentOptimizer(0.0001).minimize(loss)

    # 5、计算准确率
    with tf.variable_scope("acc"):
        equal_list = tf.equal(tf.argmax(y_true, 1), tf.argmax(y_predict, 1))

        # equal_list  None个样本   [1, 0, 1, 0, 1, 1,..........]
        accuracy = tf.reduce_mean(tf.cast(equal_list, tf.float32))


    # 收集单个数字值变量
    tf.summary.scalar("loss", loss)
    tf.summary.scalar("accuracy", accuracy)

    # 定义一个合并变量de op
    merged = tf.summary.merge_all()

    # 创建一个saver
    saver = tf.train.Saver()

    # 定义一个初始化变量的op
    init_op = tf.global_variables_initializer()

    # 开启回话运行
    with tf.Session() as sess:
        sess.run(init_op)

        # 建立events文件,然后写入
        filewriter = tf.summary.FileWriter("./tmp/summary/test/", graph=sess.graph)

        # 训练
        if FLAGS.is_train == 1:
            # 循环去训练
            for i in range(1000):

                # 取出真实存在的特征值和目标值
                mnist_x, mnist_y = mnist.train.next_batch(50)

                # 运行train_op训练
                sess.run(train_op, feed_dict={x: mnist_x, y_true: mnist_y})
                print("训练第%d步,准确率为:%f" % (i, sess.run(accuracy, feed_dict={x: mnist_x, y_true: mnist_y})))

                # 写入每步训练的值
                summary = sess.run(merged, feed_dict={x: mnist_x, y_true: mnist_y})
                filewriter.add_summary(summary, i)

            # 保存模型
            saver.save(sess, "./tmp/ckpt/fc_model")

        else:
            # 加载模型
            saver.restore(sess, "./tmp/ckpt/fc_model")

            # 如果是0,做出预测
            for i in range(100):
                # 每次测试一张图片 [0,0,0,0,0,1,0,0,0,0]
                x_test, y_test = mnist.test.next_batch(1)

                print("第%d张图片,手写数字图片目标是:%d, 预测结果是:%d" % (
                    i,
                    tf.argmax(y_test, 1).eval(),
                    tf.argmax(sess.run(y_predict, feed_dict={x: x_test, y_true: y_test}), 1).eval()
                ))
    return None


if __name__ == "__main__":
    conv_fc()

模型训练

首先执行训练,默认FLAGS.is_train == 1也就是进行训练

python cnn.py

输出结果如下:

训练第0步,准确率为:0.060000
训练第1步,准确率为:0.020000
训练第2步,准确率为:0.060000
...
训练第997步,准确率为:0.840000
训练第998步,准确率为:0.800000
训练第999步,准确率为:0.880000

训练过程中,我们添加了收集变量的代码,以及保存和加载模型的代码。

我们可以在tensorboard中查看变量的变化趋势

tensorboard --logdir="tmp/summary/test"

查看网址:http://127.0.0.1:6006/

模型预测

在代码中,我们已经编写了对100张图片进行预测的代码,我们如下执行上面的代码

python cnn.py --is_train="0"

输出结果如下:

第0张图片,手写数字图片目标是:7, 预测结果是:8
第1张图片,手写数字图片目标是:9, 预测结果是:9
第2张图片,手写数字图片目标是:0, 预测结果是:0
...
第93张图片,手写数字图片目标是:9, 预测结果是:9
第94张图片,手写数字图片目标是:4, 预测结果是:6
第95张图片,手写数字图片目标是:6, 预测结果是:6
第96张图片,手写数字图片目标是:6, 预测结果是:6
第97张图片,手写数字图片目标是:6, 预测结果是:6
第98张图片,手写数字图片目标是:8, 预测结果是:8
第99张图片,手写数字图片目标是:5, 预测结果是:5

通过预测结果,我们看到大部分结果预测都是正确的,只有少数预测的结果存在一些问题。

你可能感兴趣的:(卷积神经网络)