一、LeNet-5 简介
LeNet-5 是 Yann Lecun 于1998提出的神经网络架构,更是卷积神经网络的开山鼻祖,虽然该网络模型仅有 7 层神经网络结构,但在 MNIST 数据集上的识别精度高达 99.2%,是卷积神经网络首次在数字图像识别领域的成功运用。
但是需要说明的有几点:
(1)LeNet-5 主要采用 tanh 和 sigmoid 作为非线性激活函数,但是目前 relu 对卷积神经网络更有效
(2)LeNet-5 采用平均池化作为下采样操作,但是目前最大池化操作被更广泛地采用
(3)LeNet-5 网络最后一层采用 Gaussian 连接层,用于输出 0~9 这 10 个类别中的一类,但是目前分类器操作已经被 softmax 层取代
二、LeNet-5 网络结构
第一层:卷积层
卷积核尺寸:(长, 宽) = (5, 5)
卷积核深度:6(卷积核种类,可以简单理解经过卷积操作变换后得到的特征图 feature maps 的通道数)
卷积操作的步长:(长, 宽) = (1, 1)
边缘填充方式:padding = 'VALID'(不使用0填充边缘,得到的特征图 feature maps 的尺寸小于输入图像的尺寸)
输入数据的尺寸:(长, 宽) = (32, 32)
输出特征图 feature maps 的尺寸:(长, 宽) = (28, 28),不使用 0 填充边缘时输出特征图的尺寸计算方式为:(输入数据的长或宽 - 卷积核的长或宽 + 1) / 步长 = (32 - 5 + 1) / 1 = 28, 取上整为 28
输出节点的数量:28 * 28 * 6 = 4704(即下一层网络输入层的节点个数,对应输出特征图 feature maps 的尺度)
训练参数的数量:(5 * 5 * 1 + 1) * 6 = 156(每个卷积核有 5 * 5 个权重和 1 个偏置,共 6 种卷积核)
卷积层连接的数量:(28 * 28 * 6) * (5 * 5 + 1) = 122304
第二层:平均池化层
池化操作的区域尺寸:(长, 宽) = (2, 2)
池化操作的深度:6(池化操作不改变深度 / 通道数)
池化操作的步长:(长, 宽) = (2, 2)
边缘填充方式:padding = 'VALID'
输入数据的尺寸:(长, 宽) = (28, 28)
输出特征图 feature maps 的尺寸:(长, 宽) = (14, 14),(输入数据的长或宽 - 池化区域的长或宽 + 1) / 步长 = (28 - 2 + 1) / 2 = 13.5,取上整为 14
输出节点的数量:14 * 14 * 6
第三层:卷积层
卷积核尺寸:(长, 宽) = (5, 5)
卷积核深度:16
卷积操作的步长:(长, 宽) = (1, 1)
边缘填充方式:padding = 'VALID'
输入数据的尺寸:(长, 宽) = (14, 14)
输出特征图 feature maps 的尺寸:(长, 宽) = (28, 28),不使用 0 填充边缘时输出特征图的尺寸计算方式为:(输入数据的长或宽 - 卷积核的长或宽 + 1) / 步长 = (14 - 5 + 1) / 1 = 10,取上整为10
输出节点的数量:10* 10 * 16 = 1600
训练参数的数量:(5 * 5 * 6 + 1) * 16 = 2416(每个卷积核有 5 * 5 个权重和 1 个偏置,共 6 种卷积核)
卷积层连接的数量:(10 * 10 * 16) * (5 * 5 + 1) = 41600
第四层:平均池化层
池化操作的区域尺寸:(长, 宽) = (2, 2)
池化操作的深度:16
池化操作的步长:(长, 宽) = (2, 2)
边缘填充方式:padding = 'VALID'
输入数据的尺寸:(长, 宽) = (10, 10)
输出特征图 feature maps 的尺寸:(长, 宽) = (5, 5),(输入数据的长或宽 - 池化区域的长或宽 + 1) / 步长 = (10 - 2 + 1) / 2 = 4.5,取上整为 5
输出节点的数量:5 * 5 * 16
第五层:全连接层
输入数据的尺度:(长, 宽, 通道数) = (5, 5, 16)
输出节点的数量:120
训练参数的数量:(5 * 5 * 16) * 120 + 120 = 48120 (连接权重个数 (5 * 5 * 16) * 120, 偏置个数 120)
第六层:全连接层
输入节点个数:120
输出节点的数量:84
训练参数的数量:120 * 84 + 84 = 10164 (连接权重个数 120 * 84, 偏置个数 84)
第七层:高斯连接层(Gaussian Connections)(最后一层是分类器,目前主要通过 softmax 分类层代替)
输入节点个数:84
输出节点的数量:10
训练参数的数量:84* 10 + 10 = 850 (连接权重个数 84 * 10, 偏置个数 10)
三、LeNet-5 网络实现代码
1、环境
TensorFlow API r1.12
CUDA 9.2 V9.2.148
cudnn64_7.dll
Python 3.6.3
Windows 10、Mac
##################################################################################################
# 原始的 LeNet 网络复现 Tensorflow代码
# 尽量接近最原始的 LeNet 网络,但也有些许不同之处
# 1998 [LeNet-5] Gradient-Based Learning Applied to Document Recognition(Proceedings of the IEEE)
# LeNet-5 共 7 层
# 第一层:卷积层
# 第二层:平均池化层
# 第三层:卷积层
# 第四层:平均池化层
# 第五层:全连接层
# 第六层:全连接层
# 第七层:Gaussian连接层(以下代码通过全连接层 + softmax 代替实现)
# 卷积核在 TensorFlow 称为过滤器(filter)
# 卷积核的维度:卷积核的长、卷积核的宽、输入通道数、卷积后的输出通道数
# 池化核的维度:批量、高度、宽度、深度 / 通道
# Tensorboard 保存在当前路径下的 summary文件夹,命令行中运行:tensorboard --logdir=="./summary/"
# 浏览器中查看相关变量:http://localhost:6006
##################################################################################################import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_dataclass LeNet5_Origin(object):def __init__(self):self.batch_size = 100 # 每个批次的数据量self.image_height = 32 # 输入图片的高度self.image_width = 32 # 输入图片的宽度self.image_channel = 1 # 输入图片的深度/通道数self.epoch_num = 1000 # 训练的轮次self.lr = 0.001 # 优化器的学习率def build_model(self, images):# 第一层:卷积层with tf.variable_scope("conv1"):conv1_weights = tf.get_variable(name="conv1_w", shape=[5, 5, 1, 6], initializer=tf.truncated_normal_initializer(mean=0.0, stddev=1.0))conv1_bias = tf.get_variable(name="conv1_b", shape=[6], initializer=tf.constant_initializer(value=0.1))conv1 = tf.nn.conv2d(input=images, filter=conv1_weights, strides=[1, 1, 1, 1], padding="VALID")conv1 = tf.nn.bias_add(value=conv1,bias=conv1_bias)relu1 = tf.nn.relu(features=conv1)# 第二层:平均池化层with tf.variable_scope("pool2"):pool2 = tf.nn.avg_pool(value=relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="VALID")# 第三层:卷积层with tf.variable_scope("conv3"):conv3_weights = tf.get_variable(name="conv3_w", shape=[5, 5, 6, 16], initializer=tf.truncated_normal_initializer(mean=0.0, stddev=1.0))conv3_bias = tf.get_variable(name="conv3_b", shape=[16], initializer=tf.constant_initializer(value=0.1))conv3 = tf.nn.conv2d(input=pool2, filter=conv3_weights, strides=[1, 1, 1, 1], padding="VALID")conv3 = tf.nn.bias_add(value=conv3, bias=conv3_bias)relu3 = tf.nn.relu(features=conv3)# 第四层:平均池化层with tf.variable_scope("pool4"):pool4 = tf.nn.avg_pool(value=relu3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="VALID")pool4_shapes = pool4.get_shape().as_list()batch_size = pool4_shapes[0]fc5_nodes_num = pool4_shapes[1]*pool4_shapes[2]*pool4_shapes[3]reshape_pool4 = tf.reshape(tensor=pool4,shape=[batch_size, fc5_nodes_num])# 第五层:全连接层with tf.variable_scope("fc5"):fc5_weights = tf.get_variable(name="w5", shape=[fc5_nodes_num, 120], initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))fc5_bias = tf.get_variable(name="b5", shape=[120], initializer=tf.constant_initializer(value=0.1))fc5 = tf.nn.relu(features=(tf.matmul(a=reshape_pool4, b=fc5_weights) + fc5_bias))# 第六层:全连接层with tf.variable_scope("fc6"):fc6_weights = tf.get_variable(name="w6", shape=[120, 84], initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))fc6_bias = tf.get_variable(name="b6", shape=[84], initializer=tf.constant_initializer(value=0.1))fc6 = tf.nn.relu(features=(tf.matmul(a=fc5, b=fc6_weights) + fc6_bias))# 第七层:Gaussian Connection输出层(输出0~9这10个类别中的一个,目前已有 softmax 层实现)with tf.variable_scope("softmax"):fc7_weights = tf.get_variable(name="w7", shape=[84, 10], initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))fc7_bias = tf.get_variable(name="b7", shape=[10], initializer=tf.constant_initializer(value=0.1))fc7 = tf.matmul(a=fc6, b=fc7_weights) + fc7_biasfc7 = tf.nn.softmax(logits=fc7)return fc7def train(self):mnist = input_data.read_data_sets('./mnist_data/', one_hot=True)images_holder = tf.placeholder(dtype=tf.float32, shape=[self.batch_size, self.image_height, self.image_width, self.image_channel], name="x")labels_holder = tf.placeholder(dtype=tf.float32, shape=[self.batch_size, 10], name="y")label_predict = self.build_model(images_holder)loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=tf.argmax(labels_holder,1), logits=label_predict)# loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=tf.argmax(labels_holder,1), logits=label_predict)correct_predict = tf.equal(tf.argmax(label_predict, 1), tf.argmax(labels_holder, 1))accuracy = tf.reduce_mean(input_tensor=tf.cast(x=correct_predict, dtype=tf.float32))train_op = tf.train.GradientDescentOptimizer(learning_rate=self.lr).minimize(loss=loss)tf.summary.histogram(name='loss', values=loss)tf.summary.scalar(name="accuracy", tensor=accuracy)merged = tf.summary.merge_all()init_op = tf.global_variables_initializer()saver = tf.train.Saver(max_to_keep=1)with tf.Session() as sess:sess.run(init_op)writer = tf.summary.FileWriter(logdir="./summary/", graph=sess.graph)for i in range(self.epoch_num):batch_images, batch_labels = mnist.train.next_batch(self.batch_size)batch_images = tf.reshape(tensor=batch_images, shape=[self.batch_size, 28, 28, 1])batch_images = tf.image.resize_images(images=batch_images,size=(32,32))sess.run(train_op, feed_dict={images_holder:batch_images.eval(), labels_holder:batch_labels})accuracy_result = sess.run(accuracy, feed_dict={images_holder: batch_images.eval(), labels_holder: batch_labels})summary_result = sess.run(fetches=merged, feed_dict={images_holder: batch_images.eval(), labels_holder: batch_labels})writer.add_summary(summary=summary_result, global_step=i)saver.save(sess=sess, save_path="./models/lenet-5.ckpt", global_step=i+1)# print(accuracy_result)if __name__ == "__main__":model = LeNet5_Origin()model.train()
Tensorboard 图
Tensorboard Scalar 观察 accuracy
Tensorboard Histgram 观察 loss
参考文献:
[1] LeCun Y, Bottou L, Bengio Y, et al. Gradient-based learning applied to document recognition[J]. Proceedings of the IEEE, 1998, 86(11): 2278-2324.
[2] 《TensorFlow 实战 Google 深度学习框架》第 6 章 图像识别与卷积神经网络