Lenet5实现及代码详解——以MINST数据集为例

看了卷积神经网络(CNN)的原理及介绍,想着自己动手解决一个案例,在网上也看了很多博客,这里整理一下,顺便记录一下自己解决一个完成的CNN实例的过程,以便以后方便看。
如果有不足之处,欢迎大家指正。

数据获取与可视化操作

1、下载minst数据集

在网上可以很容易找到minst数据集,这里为了方便大家,直接给出链接,大家直接点击下载即可.。注意下载后不要解压缩

2、数据的读取

利用python的pickle模块以及gzip库直接读取文件

import pickle
import gzip
# 数据的读取
f = gzip.open("D:\\wangyang\\图像识别\\程序\\CNN\\minst.gz", "rb")
train_set, valid_set, test_set = pickle.load(f, encoding="bytes")
#注意后面的编码参数encoding不能省略,否则会报错
f.close()
#记得及时关闭文件

到这里数据的读取工作就已经完成了,值得注意的是训练集train_set是一个元组(tuple,一共有两个元素),第一个元素是一个多维数组(np.ndarray),存储了50000个28*28的二维数组(可以看成是一个矩阵),也就是说mnist.pkl.gz库中图像的大小是28 * 28的。

3.数据的可视化(tile)

为了更清楚看到数据究竟是什么样子的,我们需要对数据进行可视化操作(当然这不是必须的)对训练集数据进行可视化操作,结果如下图所示
在这里插入图片描述
具体程序如下所示

class DataofVisualization(object):
    """对数据的可视化操作,将数据tile出来"""

    def normalize(self, data):
        """
        对数据进行归一化操作,这里采用线性归一化方法——Min-Max归一化
        X_norm=(X-min)/(X_max-X_min)
        """
        from sklearn import preprocessing
        X = preprocessing.MinMaxScaler()
        return X.fit_transform(data)

    def tile_raster_images(self, X, image_shape, tile_shape, tile_spacing=(0, 0), normalize_rows=True, output_pixel_vals=True):
        """
        把图片按贴砖的形式打印出来
        image_shape:每一块砖的高和宽;
        tile_shape:在横纵方向上分别有多少砖;
        tile_spacing:砖与砖之间的距离
        normalize_rows:是否对砖进行归一化;
        output_pixel_vals:是否对砖以图像的形式进行显示
        """
        # 对参数进行断言(asser),确保它们都是二维数组,否则报错
        assert len(image_shape) == 2
        assert len(tile_shape) == 2
        assert len(tile_spacing) == 2

        output_shape = [(ishp + tsp) * tshp - tsp for ishp, tshp, tsp in zip(image_shape,
                                                                             tile_shape, tile_spacing)]
        # 计算最后的"大砖"高和宽

        im_H, im_w = image_shape
        Hs, Ws = tile_spacing
        dt = "uint8" if output_pixel_vals else X.dtype  # 确定数据的编码格式
        output_array = np.zeros(output_shape, dtype=dt)

        # 开始贴砖
        for i in range(tile_shape[0]):
            for j in range(tile_shape[1]):
                if i * tile_shape[1] + j < X.shape[0]:
                    this_x = X[i * tile_shape[1] + j]
                    this_image = self.normalize(this_x.reshape(
                        image_shape)) if normalize_rows else this_x.reshape(image_shape)

                    c = 255 if output_pixel_vals else 1
                    output_array[i * (im_H + Hs):i * (im_H + Hs) + im_H, j * (im_w + Ws):
                                 j * (im_w + Ws) + im_w] = this_image * c
        return output_array
mydata = DataofVisualization()
X = train_set[0]
arr = mydata.tile_raster_images(X, image_shape=(28, 28),
                                tile_shape=(12, 12), tile_spacing=(5, 5))
img = Image.fromarray(arr)
img.show()

归一化方法有两种形式,一种是把数变为(0,1)之间的小数,一种是把有量纲表达式变为无量纲表达式。主要是为了数据处理方便提出来的,把数据映射到0~1范围之内处理,更加便捷快速,应该归到数字信号处理范畴之内

注意上面的利用到了归一化处理,为了简单起见只用了线性归一化方法,对其他归一化方法感兴趣的同学可以看这篇博客

卷积神经网络

关于卷积神经网络网上已经有很多优秀的博客了,这里就不再多说什么了,感兴趣的可以看看下面这篇博客
卷积神经网络详解 - 卷积层逻辑篇
接下来我想复现经典的卷积神经网络——Lenet-5模型
前期准备工作,请确保以下模块以及安装成功

基于theano实现Lenet 5

import os
import sys
import timeit
import theano
# theano与tensorfloe类似,都是基于建立神经网络的组件
import theano.tensor as T
from theano.tensor.signal import pool
from theano.tensor.nnet import conv
from logistic_sgd import LogisticRegression, load_data
from mlp import HidderLayer

期间会出现如下问题
Q:No module named ‘logistic_sgd’
A:对照着博客一步一步解决就好了(注意最好把文件放在你当前工程的目录下)
Q:出现下列问题在这里插入图片描述
将keras版本退化到1.2.2版本,win+r调出命令窗口,输入如下命令

pip install keras == 1.2.2

Q:缺少模块
A:根据错误信息,利用pip install下载缺少的模块即可

Q:出现下列问题
在这里插入图片描述
A:后来发现这个目前mlp类中已经不支持HidderLayer了,在网上找了很久都没有找到对应的解决办法,希望以后有大佬能够解决吧
到这里我实在是不知道怎么看下去了,本来就是初学者,哈哈哈哈,所以改用基于Tensorflow来实现

基于Tensorflow实现Lenet 5

简单回顾一下Lenet 5的基本结构
在这里插入图片描述
在此之前导入相关模块,以下的代码均基于此来运行

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

运行编译一下,观察是否出现错误,如果报错根据错误信息对应解决
(一般多是没有对应的模块,win+r调出命令窗口利用pip命令下载对应的模块即可)
因为笔者本身就是一个新手,所有下面的东西难免会出错,同时也会有些啰嗦,还请大家多多担待。

数据的读取

同样的,要实现一个卷积神经网络最开始的就是数据的读取工作,所以接下来实现数据的读取操作

mnist = input_data.read_data_sets(
    'D:\\wangyang\\图像识别\\程序\\CNN\\mnist.gz', one_hot=True)

sess = tf.InteractiveSession() #创建一个对话

运行上述代码之后会发现之前的文件夹中多了一个文件夹,里面是这样的
在这里插入图片描述
是不是很神奇?其实不是,秘密就在那一行命令中。
首先来说明一下input_data是手写数字识别的一个模块,其中read_data_sets方法的具体含义不再多说,相关参数如下

def read_data_sets(train_dir,
                   fake_data=False,
                   one_hot=False,
                   dtype=dtypes.float32,
                   reshape=True,
                   validation_size=5000,
                   seed=None,
                   source_url=DEFAULT_SOURCE_URL)

我们一般常用第一个参数(数据集的绝对路径,即在电脑中的位置)和第三个参数,直白点说就是用0或1去描述图片上的数字,只能有一个1,它在哪一位就代表数字几,其他位均为0。最后关于InteractiveSession就不再多说了,感兴趣的自己点击看看。(TensorFlow中的变量是惰性加载的,如果你想看到变量的值,必须建立一个对话(Session))

训练数据

# 训练数据
x = tf.placeholder("float", shape=[None, 784])  
# 训练标签数据
y_ = tf.placeholder("float", shape=[None, 10])  
# 把x更改为4维张量,第1维代表样本数量,第2维和第3维代表图像长宽, 第4维代表图像通道数, 1表示黑白
x_image = tf.reshape(x, [-1, 28, 28, 1])
#因为minst数据集中图像的大小都是28*28的,所以第二个参数和第三个参数均为28,第一个参数为-1表示大小不确定

其中tf.placeholder的用法如下,只有dtype参数是必选参数,其余均是可选参数。
在这里插入图片描述

卷积层C1


# 第一层:卷积层
# 过滤器大小为5*5, 当前层深度为1, 所以过滤器的深度为1,一共6个过滤器,即6个卷积核
conv1_weights = tf.get_variable("conv1_weights", [5, 5, 1, 6], initializer=tf.truncated_normal_initializer(stddev=0.1))#过滤器,即卷积核
#偏置项,因为有6个卷积核,所以需要6个偏置项
conv1_biases = tf.get_variable("conv1_biases", [6], initializer=tf.constant_initializer(0.0))
# 移动步长为1, 不使用全0填充
conv1 = tf.nn.conv2d(x_image, conv1_weights, strides=[1, 1, 1, 1], padding='VALID')
# 激活函数Relu去线性化
relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))
#relu1即为本层的输出,亦为下一层的输入

代码解释

注意:get_variable是创建一个变量并初始化,第一个参数的变量名,第二个参数是大小,第三个参数用于初始化,详细说明点击此处并且两个初始化方法
在这里插入图片描述
在这里插入图片描述

conv1 = tf.nn.conv2d(x_image, conv1_weights, strides=[1, 1, 1, 1], padding='VALID')

strides参数,第一个和第四个是1,不能改变,第二个和第三个分别指水平滑动和垂直滑动的步长值(一般来说是一样的,这里就是说步长为1),参数padding通俗的说就是决定是否补0,后面是padding='VALID'就是不进行补0操作的,详细解析点击此处
这句话的命令实际就是在进行卷积运算,tf.nn.conv2d的具体用法点击此处,这里就不再多说了,只要明白这一步是在进行卷积运算就好了。

relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

函数tf.nn.relu就是我们通常立即的Relu激活函数(相关用法),即
在这里插入图片描述
函数tf.nn.bias_add表示加上偏置项,用一维向量加到矩阵的每一行上去。
到这里就已经结束了第一层卷积。

逻辑分析

输入:28 * 28大小的矩阵,深度为1,数目未知
卷积核:5 * 5,深度为1,一共6个
偏置项:constant,一维,一共6个
步长S:S=1
padding(P):P=0,不填充0
输出:6个特征图,每一个大小24*24,深度为1(与输入矩阵的大小保持一致)

因为计算公式如下
在这里插入图片描述

池化层S2

# 第二层:最大池化层
# 池化层过滤器的大小为2*2, 移动步长为2,不使用全0填充
pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[
                       1, 2, 2, 1], padding='VALID')

代码解释

这里采用的最大池化,在池化层中一般使用非重叠池化,所以这里步长设置为了2,池化大小为(2,2),这里通过tf.nn.max_pool函数来实现了池化层的功能,相关用法请点击此处

逻辑分析

输入:24 * 24大小的矩阵,一个有6个(也就是特征图)
池化大小:2 * 2
padding:不补0,P=0
步长(S):S=2
输出:12 * 12大小的特征图,一共有6个(池化不会改变特征图的个数,只会改变每个特征图的大小)

卷积层C3

# 第三层:卷积层
conv2_weights = tf.get_variable("conv2_weights", [5, 5, 6, 16], initializer=tf.truncated_normal_initializer(
    stddev=0.1))  # 过滤器大小为5*5, 当前层有6个输入, 过滤器的个数为16
conv2_biases = tf.get_variable(
    "conv2_biases", [16], initializer=tf.constant_initializer(0.0))
conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[
                     1, 1, 1, 1], padding='VALID')  # 移动步长为1, 使用全0填充
relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

代码解释

相关命令与C1层类似(只是相关的参数不一样),这里就不再重复了。这里仅说明

conv2_weights = tf.get_variable("conv2_weights", [5, 5, 6, 16], initializer=tf.truncated_normal_initializer(
    stddev=0.1)) 

中的第二个参数中的第三个为什么是6,因为该层的输入就是池化层S2的输出,而S2的输出一共有6个特征图,所以这里为6

逻辑分析

输入:12 * 12的特征图,一共6个,每个深度为1
卷积核:5 * 5的矩阵,一共16个,每个深度也为1
步长(S):S=1
padding:P=0,不填充0
输出:8 * 8的特征图,得到16个特征图(一般是几个卷积核就会得到几个特征图)
值得注意的是:
卷积层S3与S1是有不同的,最主要的区别就是连接方式
在这里插入图片描述
输入的6个特征图与卷积层中16个卷积核的连接方式如下图所示
在这里插入图片描述
简单的就是
C2得到了6个特征图,S3中有16个卷积核,最后经过卷积层S3,得到了16个特征图

1、前6个特征图是以C2中的三个特征图作为输入,作用到6个卷积核中,得到了6个
eg:用特征图0,1,2作为输入,作用到卷积核0中,得到了一个特征图;用特征图1,2,3作用到卷积核1中又得到一个特征图(也就是第一个红色的方框里的形式),以此类推可以得到6个特征图

2、接下来的6个特征图是用连续4个特征图作为输入,作用到另外6个卷积核,又可以得到6个特征图
eg:用特征图0,1,2,3作为输入,作用到卷积核6中,得到一个特征图;用特征图1,2,3,4作为输入,作用到卷积核7中又可以得到一个特征图(即第二个蓝色方框中的形式),以此类推可以得到6个特征图

3、接下来的3个特征图是用间隔的4个特征图作用到3个卷积核中得到的
eg:用特征图0,1,3,4作为输入,作用到卷积核12中,得到一个特征图;用特征图1,2,4,5作为输入,又可以得到一个特征图(即第三个红色方框中的形式),以此类推得到3个特征图

4、最后一个特征图是用6个特征图作为输入,作用到最后一个卷积核得到的
eg:用特征图0,1,2,3,4,5作为输入,作用到卷积核15中得到第16个特征图

池化层C4

# 第四层:最大池化层
# 池化层过滤器的大小为2*2, 移动步长为2,使用全0填充
pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[
                       1, 2, 2, 1], padding='VALID')

代码解释

这一层代码池化层C2类似,这里就不在重复了

逻辑分析

输入:8*8的特征图,一共有16个

池化器:2 * 2

步长:S=2

padding:P=0,不填充

输出:4 * 4的特征图,一共16个

全连接层C5

# 第五层:全连接层
fc1_weights = tf.get_variable("fc1_weights", [
                              4 * 4 * 16, 120], initializer=tf.truncated_normal_initializer(stddev=0.1))  # 7*7*64=3136把前一层的输出变成特征向量
fc1_baises = tf.get_variable(
    "fc1_baises", [120], initializer=tf.constant_initializer(0.1))
pool2_vector = tf.reshape(pool2, [-1, 4 *4* 16])
fc1 = tf.nn.relu(tf.matmul(pool2_vector, fc1_weights) + fc1_baises)

# 为了减少过拟合,加入Dropout层
keep_prob = tf.placeholder(tf.float32)
fc1_dropout = tf.nn.dropout(fc1, keep_prob)

代码解释

这一层在Lenet论文中也是卷积层,只不过在经过卷积核作用以后它变成了一个1*1的,起到了一个类似全连接的作用

fc1_weights = tf.get_variable("fc1_weights", [
                              4 * 4 * 16, 120], initializer=tf.truncated_normal_initializer(stddev=0.1)) 

创建一个张量,因为池化层的输入是4 * 4的,一共有16个,经过这一层后要得到120个1 * 1的特征图,所以要创建一个[4*4*16,120]的张量

fc1 = tf.nn.relu(tf.matmul(pool2_vector, fc1_weights) + fc1_baises)

函数tf.matmul表示两个矩阵相乘,具体用法点击此处

fc1_dropout = tf.nn.dropout(fc1, keep_prob)

函数tf.nn.dropout()是tensorflow里面为了防止或减轻过拟合而使用的函数,它一般用在全连接层,具体用法点击此处

逻辑分析

输入:4 * 4的特征图,一共16个
输出:1 * 1的特征图,一个120个

全连接层F6

# 第六层:全连接层
fc2_weights = tf.get_variable("fc2_weights", [
                              120, 10], initializer=tf.truncated_normal_initializer(stddev=0.1))  # 神经元节点数120, 分类节点10,也就是输入为120,输出为10
fc2_biases = tf.get_variable(
    "fc2_biases", [10], initializer=tf.constant_initializer(0.1))
fc2 = tf.matmul(fc1_dropout, fc2_weights) + fc2_biases

代码解释

与上一层大致一样,最大的区别是上一层虽然叫做全连接层,但是本质还是一个卷积层,所以需要通过激活函数,这一层就是完完全全的全连接层,所以不需要激活函数

逻辑分析

输入:神经节点120个

输出:分类节点10个

输出层Output

# 第七层:输出层
# softmax
y_conv = tf.nn.softmax(fc2)
# 定义交叉熵损失函数
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)

# tf.argmax()返回的是某一维度上其数据最大所在的索引值,在这里即代表预测值和真实值
# 判断预测值y和真实值y_中最大数的索引是否一致,y_conv的值为1-10概率
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))

# 用平均值来统计测试准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

代码解释

函数tf.nn.softmax主要是将N * 1的向量转成N * 1的向量(每个元素的大小在0-1之间,也就是属于某一类的概率),具体用法点击此处

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ *
                                              tf.log(y_conv), reduction_indices=[1]))

函数tf.log用来求自然对数(底数为e),具体用法点击此处
函数tf.reduce_sum是求和,该命令的意思是对矩阵y*tf.log(y_cov)横向求和,具体用法点击此处
函数tf.reduce_mean是用来求平均值,具体用法点击此处
另外交叉熵损失函数如下
在这里插入图片描述

train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

选择Adam优化器学习率设置为1e-4),优化最小化损失函数,找到最优的矩阵W,其实反向传播的目的就是不断更新矩阵W,找到最优的W,是的W*x后的概率矩阵的损失函数最小。

correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))

函数tf.argmax表示返回某一维度上最大值所在的索引值(该命令是返回某一行最大值所在的位置,因为y_conv和y_是一个行向量),详细用法点击此处
函数tf.equal用来判断两个张量是否相等(逐个元素进行比较),具体用法点击此处

# 用平均值来统计测试准确率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

函数tf.cast进行张量的数据类型转换,将布尔值转成浮点数(ture变成1,false变成0),具体用法点击此处

逻辑分析

输入:1*10的向量

输出:1*10的向量,但是向量中的值均在0-1之间

训练数据

# 开始训练
sess.run(tf.global_variables_initializer())
for i in range(5000):#这里仅训练五千次
    batch = mnist.train.next_batch(100)#批量读取数据
    if i % 100 == 0:
        train_accuracy = accuracy.eval(
            feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})  # 评估阶段不使用Dropout
        print("step %d, training accuracy %g" % (i, train_accuracy))
    # 训练阶段使用50%的Dropout
    train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
# 在测试数据上测试准确率
print("test accuracy %g" % accuracy.eval(
    feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

代码解释

函数feed_dict是用来给之前占位符赋值,即把batch[0]给x等。

运行结果

在这里插入图片描述

完整代码

完整的代码请点击此处下载

参考博客

数据集可视化——tile(“贴砖”)
深度学习 CNN卷积神经网络 LeNet-5详解

  • 6
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值