TensorFlow学习笔记——MNIST手写数字识别的CNN代码实现(代码注释详细,方便小清新入门)

  • Post author:
  • Post category:其他




1. 全连接神经网络 vs. 卷积神经网络



1.1 全连接神经网络

先回顾多层神经网络

在这里插入图片描述

多层神经网络包括一个输入层和一个输出层,中间有多个隐藏层。每层有若干个神经元,相邻两层之间的后一层的每个神经元都分别与前一层的所有神经元连接。在识别问题中,输入层(即网络的第一层)代表特征向量,其每一个神经元代表一个特征值。

多层神经网络在图像识别问题中,输入层的每一个神经元可能代表一个像素的灰度值。但这种神经网络用于图像识别有几个问题,

一是没有考虑图像的空间结构,识别性能受到限制;二是每相邻两层的神经元都是全相连,参数太多,训练速度受到限制。



1.2 卷积神经网络结构

CNN就可解决上面传统神经网络的问题。CNN是在多层神经网络基础上发展起来的针对图像分类和识别而设计的一种深度学习方法。CNN使用了针对图像识别的特殊结构,可以快速训练。因为速度快,使得训练多层神经网络变得容易,而多层结构在识别准确率上又很大优势。

CNN有三个基本概念:

局部感知域(local receptive fields)

共享权重(shared weights)

池化(pooling

局部感知域: 在上图中的神经网络中输入层是用一列神经元来表示的,在CNN中,不妨将输入层当做二维矩阵排列的神经元。

与常规神经网络一样,输入层的神经元需要和隐藏层的神经元连接。但这里不是将每一个输入神经元都与每一个隐藏神经元连接,而是仅仅在一个图像的局部区域创建连接。以大小为28X28的图像为例,假如第一个隐藏层的神经元与输入层的一个5X5的区域连接,如下图所示:

在这里插入图片描述

这个5X5的区域就叫做

局部感知域

。该局部感知域的25个神经元与第一个隐藏层的同一个神经元连接,每个连接上有一个权重参数,因此局部感知域共有5X5个权重。如果将局部感知域沿着从左往右,从上往下的顺序滑动,就会得对应隐藏层中不同的神经元,如下图分别展示了第一个隐藏层的前两个神经元与输入层的连接情况。

在这里插入图片描述

在这里插入图片描述



2. 卷积神经网络核心函数介绍



2.1 卷积函数tf.nn.conv2d()


tf.nn.conv2d(input,filter,strides,padding,use_cudnn_on_gpu=None,name=None)


  • input:

    指定需要做卷积的输入图像,它要求是一个Tensor,具 [batch,in_height,in_width,in_channels]这样的形状(shape),具体含义是”训练时一个batch的图片数量,图片高度,图片宽度,图片通道数”,注意这是一个四维的Tensor,要求类型为float32或者float64.

  • filter:

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

  • strides

    :卷积时在图像每一维的步长,这是一个一维的向量,长度为4,与输入input对应,一般值为[1,x,x,1],x取步长。

  • padding

    :定义元素边框与元素内容之间的空间。string类型的量,只能是”SAME”和“VALID”其中之一,这个值决定了不同的卷积方式。

    SAME表示填充,VALID则表示不填充。

  • use_cudnn_on_gpu:

    bool类型,是否使用cudnn加速,默认是True.

  • name

    :指定名字

该函数返回一个Tensor,这个输出就是常说的

feature map。



2.2 池化函数 tf.nn.max_pool()和tf.nn.avg_pool()


tf.nn.max_pool(input,ksize,strides,padding,name=None)

tf.nn.avg_pooll(input,ksize,strides,padding,name=None)

这两个函数中的4个参数和卷积参数很相似,具体说明如下:


  • input

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

  • ksize:

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

  • strides:

    和卷积参数含义类似,窗口在每一个维度上滑动的步长,一般也是[1,stride,stride,1]。

  • padding

    :和卷积参数含义一样,也是”VALID”或者”SAME”。

该函数返回一个Tensor。类型不变,shape仍然是

[batch,height,width,channels]

这种形式。



3. 基于TensorFlow的mnist数字识别CNN代码实现



3.1 mnist的CNN程序主要包括以下几块内容

  1. 导入数据,即测试集和验证集
  2. 引入 tensorflow 启动InteractiveSession(比session更灵活)
  3. 定义两个初始化w和b的函数,方便后续操作
  4. 定义卷积和池化函数,这里卷积采用padding,使得输入输出图像一样大,池化采取2×2,那么就是4格变1格
  5. 分配输入x_和y_
  6. 修改x的shape
  7. 定义第一层卷积的w和b
  8. 把x_image和w进行卷积,加上b,然后应用ReLU激活函数,最后进行max-pooling
  9. 第二层卷积,和第一层卷积类似
  10. 全连接层
  11. 为了减少过拟合,可以在输出层之前加入dropout。(但是本例子比较简单,即使不加,影响也不大)
  12. 由一个softmax层来得到输出
  13. 定义代价函数,训练步骤,用Adam来进行优化
  14. 使用测试集样本进行测试



3.2 代码实现

# -*- coding: utf-8 -*-
"""
**使用CNN实现手写数字识别代码练习
@author: <Colynn Johnson>
@date: 2020-08-18
"""

import tensorflow as tf
import numpy as np

"""
一 导入数据
"""
from tensorflow.examples.tutorials.mnist import input_data

#mnist是一个轻量级的类,它以numpy数组的形式储存着训练,校验,测试数据集   one_hot表示输出二值后的10维
mnist = input_data.read_data_sets('MNIST-data',one_hot=True)

print(type(mnist))

print('Training data shape:', mnist.train.images.shape)
print('Test data shape:', mnist.test.images.shape)
print('Validation data shape:', mnist.validation.images.shape)
print('Training label shape:', mnist.train.labels.shape)

#设置tennsorflow对GPU使用按需分配
config = tf.ConfigProto()
config.gpu_options.allow_growth = True

sess = tf.InteractiveSession(config=config)

"""
二 构建网络
"""
"""
初始化权值和偏重
为了创建这个模型,我们需要创建大量的权重的话偏置项。这个模型中的权重在初始化时应该加入少量的噪声来打破对称性以及避免0梯度。
由于我们使用的是ReLU神经元,因此比较好的做法是用一个较小的正数来初始化偏置项,以避免神经元节点输出恒为0的问题(dead neurons).
为了不再建立模型时反复做初始化操作,我们定义两个函数用于初始化。
"""
def weight_variable(shape):
    #使用正态分布初始化权值
    initial = tf.truncated_normal(shape, stddev=0.1)  #标准差为0.1
    return tf.Variable(initial)

def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

"""
卷积层和池化层
TensorFlow在卷积和池化上有很强的灵活性。我们怎样处理边界?步长应该设置多大?在这个实例里,我们会一直使用vanilla版本。我们
的卷积使用1步长(stride size),0边距(padding size)的模板,保证输出和输入是同一个大小。我们的池化用简单传统的2x2大小的模板做
max_pooling。为了代码更简洁,我们把这部分抽象成一个函数。
"""
#定义卷积层
def conv2d(x, W):
    """
    默认 strides[0] = strides[3] = 1, strides[1]为x方向步长,strides[2]为y方向步长,
    """
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

#pooling层
def max_pooling(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')

#我们通过输入图像和目标输出类别创建节点,来开始构建计算题  None表示数值不固定,用来指定batch的大小
x_ = tf.placeholder(tf.float32, [None,784])
y_ = tf.placeholder(tf.float32, [None, 10])

#把x转换为卷积所需要的的形式  batch_size张手写数字,每张维度为 1x28x28
"""
为了用这一层,我们把 x 变成了一个4d向量,其第2、第3维对应图片的高、宽,最后一维代表图片的颜色通道数(因为是灰度图,所以这里
的通道数为1,如果是rgb彩色图,则为3)
"""
X = tf.reshape(x_, shape=[-1, 28, 28, 1])  # -1 表示reshape的行数在前不知道,直接reshape将 n*784 reshape为 n个28x28

"""
现在我们可以开始第一层。它由一个卷积接一个max pooling完成。卷积在每个 5x5 的patch中算出32个特征。卷积的权重张量形状是
[5,5,1,32],前两维是patch的大小,接着是输入的通道数,最后是输出的通道数。而对于每一个输出通道都有个对应的偏置量。
"""
#第一层卷积,32个过滤器,共享权重矩阵为 1*5*5   h_conv1.shape=[-1,28,28,32]
w_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32])
h_conv1 = tf.nn.relu(conv2d(X, w_conv1) + b_conv1)

#第一个pooling层  最大池化层2x2 [-1,28,28,1]->[-1,14,14,32]
h_pool1 = max_pooling(h_conv1)



#第二层卷积,64个过滤器,共享权重矩阵为32*5*5  h_conv2.shape=[-1,14,14,64]
w_conv2 = weight_variable([5,5,32,64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, w_conv2) + b_conv2)

#第二个pooling层  最大值池化2x2 [-1,14,14,64]->[-1,7,7,64]
h_pool2 = max_pooling(h_conv2)

"""
全连接层
现在,图片尺寸减小到7*7,我们加入一个有1024个神经元的全连接层,用于处理整个图片。我们把池化层输出的张量reshape成一些向量,
乘上权重矩阵,加上偏置,然后对其使用ReLU
"""
h_pool2_falt = tf.reshape(h_pool2, [-1, 7*7*64])
#隐藏层
w_h = weight_variable([7*7*64, 1024])
b_h = bias_variable([1024])
hidden = tf.nn.relu(tf.matmul(h_pool2_falt, w_h) + b_h)



"""
加入弃权,把部分神经元输出置为0
为了减少过拟合,我们在输出层之间加入dropout。我们用一个placeholder来代表一个神经元的输出在dropout中保持不变的概率。这样
我们可以在训练过程中启用dropout,在测试过程中关闭dropout。TensorFlow的tf.nn.dropout操作除了可以屏蔽神经元的输出外,还会
自动处理神经元输出值的scale。所以用dropout的时候可以不用考虑scale。
"""
keep_prob = tf.placeholder(tf.float32)  #弃权概率 0.0-1.0    1.0表示不使用弃权
hidden_drop = tf.nn.dropout(hidden, keep_prob)

"""
输出层
最后,我们添加一个softmax层,就像前面的单层softmax regression一样
"""
w_o = weight_variable([1024,10])
b_o = bias_variable([10])
output = tf.nn.softmax(tf.matmul(hidden_drop, w_o) + b_o)

"""
三 设置对数似然损失函数
"""
#代价函数 J =-(∑y.logaL)/n     .表示逐元素乘
cost = tf.reduce_mean(-tf.reduce_sum(y_*tf.log(output), axis=1))

"""
四 求解
"""
train = tf.train.AdamOptimizer(0.0001).minimize(cost)

#预测结果评估
#tf.argmax(output,1)  按行统计最大的值的索引
correct = tf.equal(tf.argmax(output,1), tf.argmax(y_,1))   #返回一个数组   表示统计预测正确或者错误
accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))    #tf.cast(correct, tf.float32)表示将correct的bool类型转化成0、1数组

#创建list保存每一迭代的结果
training_accuracy_list = []
test_accuracy_list = []
training_cost_list = []
test_cost_list = []

#使用会话执行图
sess.run(tf.global_variables_initializer())   #初始化变量



#开始迭代  使用Adam优化的随机梯度下降法
for i in range(5000):   #一个epoch需要迭代次数计算公式:测试集长度/batch_size
    x_batch, y_batch = mnist.train.next_batch(batch_size=64)
    #开始训练
    train.run(feed_dict={x_:x_batch,y_:y_batch,keep_prob:1.0})
    if (i+1)%200 ==0:
        #输出训练集准确率
        #training_accuracy = accuracy.eval(feed_dict={x_:mnist.train.images, y_:mnist.trainn.labels})
        training_accuracy, training_cost = sess.run([accuracy,cost], feed_dict={x_:x_batch, y_:y_batch, keep_prob:1.0})
        training_accuracy_list.append(training_accuracy)
        training_cost_list.append(training_cost)
        print('Step {0}:Training set accuracy {1},cost {2}'.format(i+1, training_accuracy, training_cost))

#全部测试完成做测试  分为200次,一次测试50个样本
#输出测试集准确率   如果一次性全部做测试,内容不够会出现OOM错误。所以测试时选取比较小的mini_batch来测试
#test_accuracy = accuracy.eval(feed_dict={x_:mnist.test.images, y_:mnist.test.labels})
for i in range(200):
    x_batch, y_batch = mnist.test.next_batch(batch_size=50)
    test_accuracy, test_cost = sess.run([accuracy,cost], feed_dict={x_:x_batch, y_:y_batch, keep_prob:1.0})
    test_accuracy_list.append(test_accuracy)
    test_cost_list.append(test_cost)
    if (i+1)%20 ==0:
        print('Step {0}: Test set accuracy {1}, cost {2}'.format(i + 1, test_accuracy, test_cost))
print('Test accuracy: ', np.mean(test_cost_list))



"""
图像操作
"""
import matplotlib.pyplot as plt
#随便取一张图像
img = mnist.train.images[2]
label = mnist.train.labels[2]

#print('图像像素值: {0},对应的标签{1}'.format(img.reshape(28,28), np.argmax(label)))
print('图像对应的标签{0}'.format(np.argmax(label)))

plt.figure()

#子图1
plt.subplot(1,2,1)
plt.imshow(img.reshape(28,28))  #显示的是热度图片
plt.axis('off')

#子图2
plt.subplot(1,2,2)
plt.imshow(img.reshape(28,28), cmap='gray')
plt.axis('off')

plt.show()



###############################
##   显示卷积和池化层结果    ##
###############################

plt.figure(figsize=(1.0*8,1.6*4))
plt.subplots_adjust(bottom=0, left=.01, right=.99, top=.90, hspace=.35)
#显示第一个卷积层之后的结果 (1,28,28,32)
conv1 = h_conv1.eval(feed_dict={x_:img.reshape([-1,784]), y_:label.reshape([-1,10]), keep_prob:1.0})
print('conv1 shape:', conv1.shape)

for i in range(32):
    show_image = conv1[:,:,:,1]
    show_image.shape = [28,28]
    plt.subplot(4,8,i+1)
    plt.imshow(show_image, cmap='gray')
    plt.axis('off')
plt.show()

plt.figure(figsize=(1.2*8, 2.0*4))
plt.subplots_adjust(bottom=0, left=.01, right=.99, top=.90, hspace=.35)
#显示第一个池化层以后的结果
pool1 = h_pool1.eval(feed_dict={x_:img.reshape([-1,784]), y_:label.reshape([-1,10]), keep_prob:1.0})
print('pool1 shape:', pool1.shape)

for i in range(32):
    show_image = pool1[:,:,:,1]
    show_image.shape = [14,14]
    plt.subplot(4,8,i+1)
    plt.imshow(show_image, cmap='gray')
    plt.axis('off')
plt.show()


代码已验证无误!关键位置亦完整注释,一来方便自己复习,二来方便小清新阅读。



版权声明:本文为weixin_40730615原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。