本节目标:学会使用 CNN 实现对手写数字的识别
上几节重点讲述的 NN :每个神经元与前后相邻层的每一个神经元都有连接关系,输入是特征,输出为预测的结果
参数个数:∑ (前层× 后层+ 后层)
从上面可以看出,一张分辨率仅为28*28像素的黑白图像,在神经网络运算时就有近40万个参数待优化;而现实生活中高分辨率的彩色图像,不但像素点增多,而且有红绿蓝三通道信息,这样直接喂入(全连接)神经网络时,待优化参数将无限增大。
待优化参数的增多,容易导致模型的过拟合,为了避免该现象,在实际应用中一般不会将原始图片直接未入全连接网络。
实际操作方案:先对原始图像进行特征提取,然后将特征喂入全连接网络,之后再然全连接网络计算分类评估值。
1 卷积计算
1.1 无padding的卷积计算
卷积(也称为滤波器)是一种有效提取图像特征的方法。
一般用一个正方形卷积核,遍历图像上的每一个点。图像区域内的每一个像素值乘以卷积核内相对应点的权重,求和,再加上偏置。
输出图像值边长 = (输入图像边长 - 卷积核长 + 1 )/ 步长
备注:
5*5*1的灰度图释义为 5 行 5 列的灰度值:1表示单通道,5*5表示分辨率,在矩阵中意味 5 行 5 列。
3*3*1的卷积核对灰度图进行卷积计算,对应值乘积求和,再加上偏置项 b = 1 ;让卷积核中的 [1, 1] 值遍历 5*5 中的每一个灰度值 。
输出图片边长: ( 输入图像边长 5 - 卷积核边长 3 + 偏置项 1 )/ 步长 1 = 3
由于仅用了一个3*3*1卷积核,所以输出的深度是1
1.2 含padding的卷积计算
不含padding的卷积计算图像矩阵长度会不一致;有时为了让图像的输出、输出尺寸一致,会再输入图像像素值周边补充0。
在前面 5x5x1 的图片周围进行全零填充,可使输出图片仍保持 5x5x1 的维度。这个全零填充的过程叫做 padding。
输出数据体的长度为:
( W − F + 2P ) / S + 1
- W:输入数据体尺寸
- F:卷积层中神经元感知域
- S:步长
- P:零填充的数量。
在 Tensorflow 框架中,用参数 padding = ‘ SAME ’(全部 0 填充) 或 padding = ‘VALID ’(不用 0 填充) 表示 。
两者的结果若非整数,都是采用 “ 向上取整 ” 的方式获得最终值。
示例:
输入图像为32*32*3,卷积核为5*5*3。
不用 0 填充时,输出(32 - 5 + 1)/ 1 = 28
若让输出结果长度与输入的一致,则需要填充几层 0 ?
32 = ( 32 - 5 + 2P) / 1 + 1,计算得 P = 2,故需要填充 2 层零。
2 Tensorflow中的卷积计算(单通道)
tensorflow 中计算卷积的函数为 tf.nn.conv2d()
tf.nn.conv2d(
输入描述
卷积核描述
核滑动步长
padding
)
注解:
1)输入描述
[ batch, 5 , 5, 1 ]
- batch - 一次喂入图片数量
- 5, 5 - 两个数组一同描述每张图片的分辨率,5行5列
- 1 - 图片的通道数,若为灰度图,则为1,若为RGB的彩色,则为3
2)卷积核描述
[ 3, 3, 1, 16 ]
- 3, 3 - 卷积核的行列分辨率(3行3列)
- 1 - 通道数,卷积核的通道数是由输入图片的通道数决定的,其必须与输入图片的通道数一致
- 16 - 该卷积核数量为16个,共计进行16次卷积运算,则输入图片的深度为16,也就是说输出是16通道
3)核滑动步长
[ 1, 1, 1, 1 ]
- 第一个 1 - 固定的数值
- 第二个 1 - 行步长
- 第三个 1 - 列步长
- 第四个 1 - 固定的数值
4)padding
该处以字符串形式给出,例如本处 “ VALID “ 表示不用 零 来填充。
3 Tensorflow中的卷积计算(多通道)
上面是对单通道的灰度图进行阐述,而实际多是RGB三个颜色组成彩色图片,这也意味这图片对应有三个通道,每一种颜色对应一个通道。
由上面的规则可知,卷积核的深度 = 输入图片的通道数,所以卷积核也必须有 3 个通道。如下图所示
注解:
1)输入图片为RGB彩色图片,将其在R、G、B 的三色建立分量数据,5*5*3 即为每个分量为 5*5,3个通道。
2)卷积核也建立 3*3 个数据,为了匹配和输入数据通道,也建立 3 个通道的卷积核。
3)多通道与单通道的计算方法相似,每一个通道对应乘积和,再将每个通道和相加,最后加上偏置项 b = 1 ,最后得到一个输出值。
4)将滑动卷积核逐一求算出输出值,由于一共用了16个卷积核,所以最后输出图片是 5*5*16。
在Tensorflow中的代码形式与单通道一致,仅将通道数值进行修改即可,具体如下:
4 池化 Pooling
池化的目的减少特征数据量,但是不改变图片的深度,因为尽管经过卷积核计算,但是数据量还是非常大的。
Tensorflow 给出了计算池化的函数
- 最大池化 tf.nn.max_pool 函数,提取图片纹理。
- 平均池化 tf.nn.avg_pool 函数,保留背景特征。
函数的实际应用:
备注:
- 1)对输入的描述:给出一次输入 batch 张图片、行列分辨率、输入通道的个数。
- 2)对池化核的描述:只描述行分辨率和列分辨率,第一个和最后一个参数固定是 1。
- 3)对池化核滑动步长的描述:只描述横向滑动步长和纵向滑动步长,第一个和最后一个参数固定是 1。
- 4)是否使用 padding:padding 可以是使用零填充 SAME 或者不使用零填充VALID。
5 舍弃 dropout
在全连接神经网络训练过程中,为了减少过多参数,常使用舍弃dropout的方法。该方法可以有效防止过拟合。
实际上dropout时,是随机将神经元中的某些值置为 0 ,使其不参与参数优化。
将一部分神将元按照一定概率从神经网络中暂时舍弃,在使用神经网络时,会把所有的神经元恢复到神经网络中。
如下图,右图中带有 × 号标记的圆圈表示 dropout 掉的,
在 tensorflow 中的计算方法为
tf.nn.dropout(上层输出,暂时舍弃的概率)
在实际应用中,常在前向传播过程构建神经网络中使用dropout,减少过拟合、加快模型的训练速度。
if train: 输出 = tf.nn.dropout(上层输出,暂时舍弃的概率)
6 CNN 模块
卷积NN :借助卷积核(Kernel)提取特征后,送入全连接网络。
卷积神经网络可分为两部分:
- 首先是通过卷积、激活、池化对输入图片特征进行提取主要特征信息
- 再次将提取的特征信息喂入全连接网络中
卷积神经网络从诞生到现在,已经出现了许多经典网络结构,比如 Lenet-5、Alenet、VGGNet、GoogleNet 和 ResNet 等。每一种网络结构都是以卷积、激活、池化、全连接这四种操作为基础进行扩展。
Lenet-5 是最早出现的卷积神经网络,由 Lecun 团队首先提出,Lenet-5 有效解决了手写数字的识别问题。
7 lenet-5 代码讲解
该代码来源于文章:
Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner. Grandient-based learning applied to document recognition. Proceedings of the IEEE, 86(11):2278-2324, 1998.
该篇文章的代码如下所示。
依据上述代码是依据文章中梳理出来的,而mnist数据集训练是28*28,其计算过程如下:
7.1 前向传播过程 mnist_lenet5_forward
# coding:utf-8 import tensorflow as tf # 每个图片的像素点为28 IMAGE_SIZE = 28 # 由于是灰度图,每个图片的通道为1 NUM_CHANNELS = 1 # 第一层卷积核的大小为5 CONV1_SIZE = 5 # 第一层使用了32个卷积核 CONV1_KERNEL_NUM = 32 # 第二层卷积核大小为5 CONV2_SIZE = 5 # 第二层使用了64个卷积核 CONV2_KERNEL_NUM = 64 # 第一层神经网络有512个神经元 FC_SIZE = 512 # 第二层神经网络有10个神经元 OUTPUT_NODE = 10 # 权重w的生成函数, # shape表示生成张量的维度 # regularizer表示正则化权重 def get_weight(shape, regularizer): w = tf.Variable(tf.truncated_normal(shape, stddev=0.1)) if regularizer != None: tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(regularizer)(w)) return w # 偏置b生成函数 def get_bias(shape): b = tf.Variable(tf.zeros(shape)) return b # 卷积生成函数 def conv2d(x, w): # x输入图片描述[batch, 行分辨率,列分辨率,通道数] # w卷积核描述[行分辨率,列分辨率,通道数,卷积核个数] # strides 核滑动步长[1,行步长,列步长,1] # padding 填充模式 SAME零填充 return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME') # 最大池化计算函数 def max_pool_2x2(x): # x 输入描述[batch,行分辨率,列分辨率,通道数] # ksize 池化核描述[1,行分辨率,列分辨率,1] # strides 池化核滑动步长[1,行步长,列步长,1] # padding 填充模式 return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') # 前向传播的网络结构 def forward(x, train, regularizer): # 第一层卷积核 conv1_w = get_weight([CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_KERNEL_NUM], regularizer) # 初始化第一层偏置项b conv1_b = get_bias([CONV1_KERNEL_NUM]) # 执行卷积计算 conv1 = conv2d(x, conv1_w) # 对conv1添加偏置,并通过relu激活函数进行激活输出 relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_b)) # 最大池化,第一层最终输出pool1 pool1 = max_pool_2x2(relu1) # 第二层卷积核的深度CONV1_KERNEL_NUM等于上层卷积核的个数 conv2_w = get_weight([CONV2_SIZE, CONV2_SIZE, CONV1_KERNEL_NUM, CONV2_KERNEL_NUM],regularizer) conv2_b = get_bias([CONV2_KERNEL_NUM]) # 第二层卷积核的输入是第一层的输出 conv2 = conv2d(pool1, conv2_w) relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_b)) # pool2是第二层的最后输出 pool2 = max_pool_2x2(relu2) # 将pool2从三维张量变成二维张量 # 获得pool2的数据维度,并存入list中 pool_shape = pool2.get_shape().as_list() # pool_shape[0] 的值是一个batch值 # pool_shape[1] 特征的长度 # pool_shape[2] 特征的宽度 # pool_shape[3] 特征的深度 # nodes=长度*宽度*深度,所有数据点的个数 nodes = pool_shape[1] * pool_shape[2] * pool_shape[3] # 将pool2表示成以batch作为行,所有特征点为列的二维形状, # 把reshaped 喂入全连接网络中 reshaped = tf.reshape(pool2, [pool_shape[0], nodes]) fc1_w = get_weight([nodes, FC_SIZE], regularizer) fc1_b = get_bias([FC_SIZE]) # 把上层的输出reshaped 乘以 本层线上的权重fc1_w,再加上偏置fc1_b fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_w) + fc1_b) # 如果是训练阶段,则对fc1的50%进行dropout if train: fc1 = tf.nn.dropout(fc1, 0.5) # 通过第二层全连接网络,初始化第二层W和b fc2_w = get_weight([FC_SIZE, OUTPUT_NODE], regularizer) fc2_b = get_bias([OUTPUT_NODE]) # 上层的输出fc1和本层fc2_w相乘,再加上fc2_b y = tf.matmul(fc1, fc2_w) + fc2_b return y
7.2 反向传播过程 mnist_lenet5_backward
#coding:utf-8 import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data import mnist_lenet5_forward import os import numpy as np BATCH_SIZE = 100 LEARNING_RATE_BASE = 0.005 LEARNING_RATE_DECAY = 0.99 REGULARIZER = 0.0001 STEPS = 50000 MOVING_AVERAGE_DECAY = 0.99 MODEL_SAVE_PATH="./model/" MODEL_NAME="mnist_model" def backward(mnist): x = tf.placeholder(tf.float32,[ # 每轮喂入的图片数量 BATCH_SIZE, # 行分辨率 mnist_lenet5_forward.IMAGE_SIZE, # 列分辨率 mnist_lenet5_forward.IMAGE_SIZE, # 输入的通道数 mnist_lenet5_forward.NUM_CHANNELS]) y_ = tf.placeholder(tf.float32, [None, mnist_lenet5_forward.OUTPUT_NODE]) # True 意味使用dropout操作 y = mnist_lenet5_forward.forward(x,True, REGULARIZER) global_step = tf.Variable(0, trainable=False) ce = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1)) cem = tf.reduce_mean(ce) loss = cem + 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) ema = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step) ema_op = ema.apply(tf.trainable_variables()) with tf.control_dependencies([train_step, ema_op]): train_op = tf.no_op(name='train') saver = tf.train.Saver() with tf.Session() as sess: init_op = tf.global_variables_initializer() sess.run(init_op) ckpt = tf.train.get_checkpoint_state(MODEL_SAVE_PATH) if ckpt and ckpt.model_checkpoint_path: saver.restore(sess, ckpt.model_checkpoint_path) for i in range(STEPS): xs, ys = mnist.train.next_batch(BATCH_SIZE) reshaped_xs = np.reshape(xs,( BATCH_SIZE, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.NUM_CHANNELS)) _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys}) if i % 100 == 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) def main(): mnist = input_data.read_data_sets("./data/", one_hot=True) backward(mnist) if __name__ == '__main__': main()
7.3 测试 mnist_lenet5_test
#coding:utf-8 import time import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data import mnist_lenet5_forward import mnist_lenet5_backward import numpy as np TEST_INTERVAL_SECS = 5 def test(mnist): with tf.Graph().as_default() as g: x = tf.placeholder(tf.float32,[ mnist.test.num_examples, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.NUM_CHANNELS]) y_ = tf.placeholder(tf.float32, [None, mnist_lenet5_forward.OUTPUT_NODE]) y = mnist_lenet5_forward.forward(x,False,None) ema = tf.train.ExponentialMovingAverage(mnist_lenet5_backward.MOVING_AVERAGE_DECAY) ema_restore = ema.variables_to_restore() saver = tf.train.Saver(ema_restore) correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) while True: with tf.Session() as sess: ckpt = tf.train.get_checkpoint_state(mnist_lenet5_backward.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] reshaped_x = np.reshape(mnist.test.images,( mnist.test.num_examples, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.IMAGE_SIZE, mnist_lenet5_forward.NUM_CHANNELS)) accuracy_score = sess.run(accuracy, feed_dict={x:reshaped_x,y_:mnist.test.labels}) print("After %s training step(s), test accuracy = %g" % (global_step, accuracy_score)) else: print('No checkpoint file found') return time.sleep(TEST_INTERVAL_SECS) def main(): mnist = input_data.read_data_sets("./data/", one_hot=True) test(mnist) if __name__ == '__main__': main()