卷积神经网络(CNN) 实现手写数字识别

原文链接:https://blog.csdn.net/polyhedronx/article/details/94476824

一、引言
前一篇博文使用单隐层的全连接神经网络,并结合一些神经网络的优化策略,如指数衰减学习率、正则化、Relu激活函数和Adam优化算法等,用包含100个隐层神经元的神经网络实现了MNIST数据集上手写数字识别98%的准确率。但是全连接神经网络也是有局限的,即使使用很深的网络、很多的隐层结点、很大的迭代轮数,也很难在MNIST数据集上得到99%以上的准确率。不过,卷积神经网络的出现解决了这一问题,最终可以达到99%以上的准确率,满足了一些高精度识别系统的需求。

二、卷积神经网络的基本原理
2.1 卷积操作
图像中的每一个像素点和周围的像素点是紧密联系的,但和太远的像素点就不一定有什么关联了,这就是人的视觉感受野的概念,每一个感受野只接受一小块区域的信号。这一小块区域的像素是互相关联的,每一个神经元不需要接收全部的像素点的信息,只需要接收局部的像素点作为输入,而后将这些神经元收到的局部信息综合起来就可以得到全局的信息。

图像的卷积操作就是指从图像的左上角开始,利用一个卷积模板在图像上滑动,在每一个位置将图像像素点上的像素灰度值与对应的卷积核上的数值相乘,并将所有相乘后的结果求和作为卷积核中心像素对应的卷积结果值,按照此步骤在图像的所有位置完成滑动得到卷积结果的过程。这个卷积模板在卷积神经网络中通常叫做卷积核,或滤波器,下图所示为一个图像卷积操作部分过程的示意图,图中采用33的卷积核对55大小的图片进行卷积操作。

图像卷积操作可以表示为,其中:

其中,为待卷积图像的矩阵,为卷积核函数,为图像卷积操作的输出图像。在深度学习中,输入的图像矩阵和输出的结果矩阵都称作特征图。

2.2 池化操作
在通过卷积层获得了二维特征图之后,通常情况下特征图的尺寸仍然很大,如果直接用这些特征送给分类器进行分类,那么在计算量上将会面临很大的考验,另外也有可能出现过拟合问题,因此不方便直接用这些特征图做分类。池化操作就是为了解决这样的问题而设计的技术,它在特征图矩阵上对不同位置的特征进行聚合统计,浓缩特征。

池化操作的示意图如上图所示。常用的池化操作有两种,一种是平均池化,平均池化操作的输出是池化核对应范围内输入特征图中特征的均值;另一种是最大池化,最大池化操作的输出是池化核对应范围内输入特征图中特征的最大值。可以看出池化操作是一种特殊的图像卷积操作。

进行池化操作能显著改善卷积神经网络的效果,这主要是由于特征浓缩,特征图维度降低,卷积神经网络经常出现的过拟合现象也会相应减轻的缘故。此于,由于浓缩了一定范围内特征信息,池化操作还有增强卷积神经网络中小范围内平移不变性的作用。

2.3 卷积层
一般的卷积神经网络由多个卷积层构成,每个卷积层中通常会进行如下几个操作。

图像通过多个不同的卷积核的滤波,并加偏置(bias),提取出局部特征,每一个卷积核会映射出一个新的2D图像。
将前面的卷积核的滤波输出结果进行非线性的激活函数的处理,目前一般用ReLU函数。
对激活函数的结果再进行池化操作(即降采样,比如22的图片降为11的图片),目前一般是使用最大池化,保留最显著的特征,并提升模型的畸变容忍能力。
注意,一个卷积层中一般有多个不同的卷积核,因为每个卷积核只能提取一种图片特征,可以增加卷积核的数量多提取一些特征。每一个卷积核对应一个滤波后映射出的新图像,同一个新图像中的每一个像素都来自完全相同的卷积核,这就是卷积核的权值共享。共享卷积核的权值参数的目的很简单,降低模型复杂度,减轻过拟合并降低计算量。

卷积层需要训练的权值数量只和卷积核的大小以及卷积核的数量有关,我们可以使用非常少的参数处理任意大小的图片。每一个卷积层提取的特征,在后面的层中都会抽象组合成更高阶的特征。

2.4 卷积神经网络
卷积神经网络和多层感知器网络的不同之处在于,卷积神经网络包含了若干个卷积层和池化层构成的特征抽取器,可以有效地减少参数数量,大大简化模型的复杂度,从而减少过拟合的风险。同时赋予了卷积神经网络对平移和轻度变形的容忍性,提高了模型的泛化能力。

著名的LeNet5的结构如下图所示,包含了三个卷积层,一个全连接层和一个高斯连接层。一般来说,针对不同数据集、不同大小的输入图片可以根据需要合理地设计不同的卷积神经网络,来应对不同的实际问题。

三、用于手写数字识别问题的卷积神经网络的设计
手写数字识别问题较简单,因此采用两个卷积层和一个全连接层构建一个简单的卷积神经网络。

3.1 主要TensorFlow函数释义
(1)tf.nn.conv2d

给定4维的输入和滤波器Tensor,计算2维卷积。

tf.nn.conv2d(
    input,
    filter=None,
    strides=None,
    padding=None,
    use_cudnn_on_gpu=True,
    data_format='NHWC',
    dilations=[1, 1, 1, 1],
    name=None,
    filters=None
)

input:
指需要做卷积的输入图像,是一个4维的Tensor,类型为half,bfloat16,float32和float64其中之一,维度顺序根据data_format设定,默认为 [batch, in_height, in_width, in_channels] 这样的shape,具体含义是[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数]。

filter:
相当于CNN中的卷积核,它要求是一个Tensor,必须和input类型相同。具有 [filter_height, filter_width, in_channels, out_channels] 这样的shape,具体含义是[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数]。注意,第三维in_channels,就是参数input的第四维。

strides:
卷积时在图像每一维的滑动窗口步长。这是一个一维的向量,维度顺序根据data_format设定,默认为 [NHWC],类型为int或int的list,长度为1、2或4。N和C默认设置为1,一般格式为[1, stride[1], stride[2], 1]。在大部分情况下,因为在height和width上的步进设为一样,因此通常为[1, stride, stride, 1]。

padding:
string类型的量,只能是”SAME”或”VALID”其中之一,指示是否需要填充。因为卷积完之后输出尺寸一般小于输入,这时候可以利用填充获得和输入尺寸相同的输出。

 strides=[1, 1, 1, 1], padding="VALID"          strides=[1, 1, 1, 1], padding="SAME"

use_cudnn_on_gpu:
bool类型,是否使用cudnn加速,默认为true。

data_format
指定输入和输出数据的格式。string类型的可选项,为"NHWC"或 “NCHW"其中之一,默认为"NHWC”。使用默认格式“NHWC”时,数据按以下顺序存储:[batch, height, width, channels]。

dilations
每个输入维度的扩张因子。这是一个一维的向量,维度顺序根据data_format设定,默认为 [NHWC],类型为int或int的list,长度为1、2或4,取值默认设置为全1。如果给出单个值,则将其复制到H和W。

给定一个input的张量[batch, in_height, in_width, in_channels]和一个过滤器 / 内核张量 [filter_height, filter_width, in_channels, out_channels]后,执行以下操作:

展平filter为一个shape为[filter_height * filter_width * in_channels, output_channels]的二维矩阵。
从input中按照filter大小提取图片块形成一个大小为[batch, out_height, out_width, filter_height * filter_width * in_channels]的虚拟张量。
对每个图像块,右乘filter矩阵。
计算公式为:

一些例子:

输入[1,3,3,1],过滤器是[2,2,1,1],padding=‘SAME’,填充方式如图:

输入[1,2,2,1],过滤器是[3,3,1,1],padding=‘SAME’,填充方式如图:

对于多通道来说,输入[1x3x3x2]是3x3图像有2个通道,过滤器是[2x2x2x1],步长是1,padding=VALID,输出是[1x2x2x1],如图:

(2)tf.nn.max_pool

在Input上执行最大池化操作,可以看作是一种特殊的卷积操作。

tf.nn.max_pool(
    value,
    ksize,
    strides,
    padding,
    data_format='NHWC',
    name=None,
    input=None
)

value
需要池化的输入,一般池化层接在卷积层后面,所以输入通常是feature map,依然是[batch, height, width, channels]这样的shape。

ksize
池化窗口的大小,取一个四维向量,一般是[1, height, width, 1],因为我们不想在batch和channels上做池化,所以这两个维度设为了1。

一个例子:

假设输入为一个双通道的图,即 value=[1,4,4,2];池化窗口大小为 ksize=[1,2,2,1],结果如下。

测试程序:

import tensorflow as tf
 
a = tf.constant([
    [[1.0, 2.0, 3.0, 4.0],
     [5.0, 6.0, 7.0, 8.0],
     [8.0, 7.0, 6.0, 5.0],
     [4.0, 3.0, 2.0, 1.0]],
    [[4.0, 3.0, 2.0, 1.0],
     [8.0, 7.0, 6.0, 5.0],
     [1.0, 2.0, 3.0, 4.0],
     [5.0, 6.0, 7.0, 8.0]]
])
 
a = tf.reshape(a, [1, 4, 4, 2])
 
pooling = tf.nn.max_pool(value=a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
with tf.Session() as sess:
    print("image:")
    image = sess.run(a)
    print(image)
    print("reslut:")
    result = sess.run(pooling)
    print(result)

(3)tf.nn.dropout

随机地将一部分节点置为0。在训练时,我们随机丢弃一部分节点的数据来减轻过拟合,预测时则保留全部数据来获得更好的预测性能。一般用在全连接层。

在TensorFlow中,其参数定义如下:

tf.nn.dropout(
    x,
    keep_prob=None,
    noise_shape=None,
    seed=None,
    name=None,
    rate=None
)

其中,参数x表示输入,为一浮点张量(tensor)。keep_prob为神经元被选中的概率,rate为x中元素被丢弃的概率,显然有keep_prob=1-rate(官网上说keep_prob将被弃用,建议用rate代替)。在初始化时keep_prob是一个占位符, keep_prob = tf.placeholder(tf.float32) ,tensorflow在run时设置keep_prob具体的值,例如keep_prob: 0.5。noise_shape是一个一维的int32类型的张量,表示随机产生的“保留/丢弃”标志的shape。seed是随机数种子,为一整数。

对于输入x中的每个元素,以概率rate输出0,否则将输入按比例放大 1/(1-rate) 倍,目的是使输出总和的期望保持不变。

默认情况下,每个元素保留或删除都是独立的。如果指定了noise_shape,则只有 noise_shape[i] == shape(x)[i] 的维度才是独立的(noise_shape里边的元素,只能是1或者是x.shape里边对应的元素)。例如,若 shape(x) = [k, l, m, n] ,noise_shape = [k, 1, 1, n],则每个batch和channel将保持独立,每一行或列要么全部保留,要么全部置零。

下面给出一些例子:

假设输入的一个batch里有两张图片,每张图片为3×3大小的双通道图,即 x=[2,3,3,2];keep_prob=0.5,结果如下。

测试程序:

import tensorflow as tf
 
b = tf.constant([
    [[1.0, 2.0, 3.0],
     [4.0, 5.0, 6.0],
     [7.0, 8.0, 9.0]],
    [[9.0, 8.0, 7.0],
     [6.0, 5.0, 4.0],
     [3.0, 2.0, 1.0]],
    [[1.0, 2.0, 3.0],
     [4.0, 5.0, 6.0],
     [7.0, 8.0, 9.0]],
    [[9.0, 8.0, 7.0],
     [6.0, 5.0, 4.0],
     [3.0, 2.0, 1.0]]
])
 
b = tf.reshape(b, [2, 3, 3, 2])
 
drop = tf.nn.dropout(x=b, keep_prob=0.5, noise_shape=[2, 3, 3, 2])
with tf.Session() as sess:
    print("image:")
    image = sess.run(b)
    print(image)
    print("result:")
    result = sess.run(drop)
    print(result)

noise_shape=[2,3,3,2]或noise_shape=None,输出随机置零,并将其它数放大 1/(1-0.5)=2倍:

noise_shape=[1,3,3,2],输出batch中的不同图片之间置零图案相同:

noise_shape=[2,1,3,2],输出不同行之间的置零图案相同:

noise_shape=[2,3,1,2],输出不同列之间的置零图案相同:

noise_shape=[2,3,3,1],输出不同channel之间的置零图案相同:

3.2 程序和结果
程序运行版本为:python–>3.7.3,tensorflow–>1.13.1

weight初始化为服从标准差为0.1的截断正态分布的随机数。因为使用ReLU函数,bias初始化为常数0.1,用来避免死亡节点。

输入图像为28×28的灰度图。第一个卷积层卷积核的尺寸设为5×5,1个颜色通道,卷积核的数量(输出channel)设为32(也就是这个卷积层会提取多少类的特征),宽和高的移动步长均为1,并进行填充(padding=“SAME”,输出图像大小与输入图像相同);激活函数为ReLU函数;池化操作的窗口大小为2×2,宽和高的移动步长均为2。

第二个卷积层卷积核的尺寸设为5×5,输入channel为32(即上一层的输出channel),卷积核的数量(输出channel)设为64,其它参数与第一卷积层相同。

经过前面两次步长为2×2的最大池化,边长变为原来的1/4,即图片尺寸由28×28变成了7×7。而第二个卷积层的卷积核数量为64,其输出的tensor的尺寸即为7×7×64。将其转化为一维向量,然后连接一个全连接层,隐含节点为1024,并使用ReLU激活函数。

为了减轻过拟合,下面使用一个Dropout层,训练时keep_prob设为0.5,测试时设为1。Dropout层的输出连接一个softmax层,得到最后的概率输出。

损失函数定义为交叉熵,优化器使用Adam,学习率设为1e-4。batch的大小为50,训练次数为20000,程序如下。

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import matplotlib.pyplot as plt
 
# move warning
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
old_v = tf.logging.get_verbosity()
tf.logging.set_verbosity(tf.logging.ERROR)
 
 
# weight initialization
def weight_variable(shape):
    return tf.Variable(tf.truncated_normal(shape, stddev=0.1))
 
 
# bias initialization
def bias_variable(shape):
    return tf.Variable(tf.constant(0.1, shape=shape))
 
 
# convolution operation
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding="SAME")
 
 
# pooling operation
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")
 
 
# Convolutional Neural Network
def cnn2(x):
    x_image = tf.reshape(x, [-1, 28, 28, 1])
 
    # Layer 1: convolutional layer
    W_conv1 = weight_variable([5, 5, 1, 32])
    b_conv1 = bias_variable([32])
    h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
    h_pool1 = max_pool_2x2(h_conv1)
 
    # Layer 2: convolutional layer
    W_conv2 = weight_variable([5, 5, 32, 64])
    b_conv2 = weight_variable([64])
    h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
    h_pool2 = max_pool_2x2(h_conv2)
 
    # Layer 3: full connection layer
    W_fc1 = weight_variable([7 * 7 * 64, 1024])
    b_fc1 = bias_variable([1024])
    h_pool2_flat = tf.reshape(h_pool2, [-1, 7 * 7 * 64])
    h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
 
    # dropout layer
    keep_prob = tf.placeholder("float")
    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
 
    # output layer
    W_fc2 = weight_variable([1024, 10])
    b_fc2 = bias_variable([10])
    y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)
 
    return y_conv, keep_prob
 
 
# read data
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
 
# input layer
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
 
# cnn
y_conv, keep_prob = cnn2(x)
 
# loss function & optimization algorithm
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_conv), reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
 
# new session
sess = tf.Session()
sess.run(tf.global_variables_initializer())
 
# train
losss = []
accurs = []
steps = []
correct_predict = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_predict, "float"))
for i in range(20000):
    batch = mnist.train.next_batch(50)
    sess.run(train_step, feed_dict={
    
    x: batch[0], y_: batch[1], keep_prob: 0.5})
 
    if i % 100 == 0:
        loss = sess.run(cross_entropy, {
    
    x: batch[0], y_: batch[1], keep_prob: 1.0})
        accur = sess.run(accuracy, feed_dict={
    
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0})
        steps.append(i)
        losss.append(loss)
        accurs.append(accur)
        print('Steps: {} loss: {}'.format(i, loss))
        print('Steps: {} accuracy: {}'.format(i, accur))
 
# plot loss
plt.figure()
plt.plot(steps, losss)
plt.xlabel('Number of steps')
plt.ylabel('Loss')
 
plt.figure()
plt.plot(steps, accurs)
plt.hlines(1, 0, max(steps), colors='r', linestyles='dashed')
plt.xlabel('Number of steps')
plt.ylabel('Accuracy')
plt.show()
 
tf.logging.set_verbosity(old_v)

Loss和测试集准确度随训练次数变化的曲线如下图所示,最终准确度约为99.2%。
在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42293496/article/details/110005450