看了卷积神经网络(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等。
运行结果
完整代码
完整的代码请点击此处下载