参考博客:https://blog.csdn.net/sinat_24143931/article/details/78958931
卷积神经网络是一种特殊的多层神经网络,像其它的神经网络一样,卷积神经网络也使用一种反向传播算法来进行训练,不同之处在于网络的结构。卷积神经网络的网络连接具有局部连接、参数共享的特点。局部连接是相对于普通神经网络的全连接而言的,是指这一层的某个节点只与上一层的部分节点相连。参数共享是指一层中多个节点的连接共享相同的一组参数。
本次实验采用LenNet-5模型。LenNet-5共有7层(不包括输入层),每层都包含不同数量的训练参数。
LeNet-5中主要的有卷积层、下抽样层、全连接层3中连接方式。
C1层是卷积层,形成6个特征图谱。特征图谱中的每个单元与输入层的一个5x5的相邻区域相连,即卷积的输入区域大小是5x5,每个特征图谱内参数共享,即每个特征图谱内只使用一个共同卷积核,卷积核有5x5个连接参数加上1个偏置共26个参数。卷积区域每次滑动一个像素,这样卷积层形成的特征图谱每个的大小是28x28。C1层共有26x6=156个训练参数,有(5x5+1)x28x28x6=122304个连接。
S2层是一个下抽样层。C1层的6个28x28的特征图谱分别进行以2x2为单位的下抽样得到6个14x14的图。每个特征图谱使用一个下抽样核,每个下抽象核有两个训练参数,所以共有2x6=12个训练参数,但是有5x14x14x6=5880个连接。Figure 9是S2层的网络连接的结构。
C3层是一个卷积层,卷积和和C1相同,不同的是C3的每个节点与S2中的多个图相连。C3层有16个10x10的图,每个图与S2层的连接的方式如Table1 所示。C3与S2中前3个图相连的卷积结构见Figure 10.这种不对称的组合连接的方式有利于提取多种组合特征。改成有(5x5x3+1)x6 + (5x5x4 + 1) x 3 + (5x5x4 +1)x6 + (5x5x6+1)x1 = 1516个训练参数,共有1516x10x10=151600个连接。
S4是一个下采样层。C3层的16个10x10的图分别进行以2x2为单位的下抽样得到16个5x5的图。这一层有2x16共32个训练参数,5x5x5x16=2000个连接。连接的方式与S2层类似。
C5层是一个卷积层。由于S4层的16个图的大小为5x5,与卷积核的大小相同,所以卷积后形成的图的大小为1x1。这里形成120个卷积结果。每个都与上一层的16个图相连。所以共有(5x5x16+1)x120 = 48120个参数,同样有48120个连接。
F6层是全连接层。F6层有84个节点,对应于一个7x12的比特图,-1表示白色,1表示黑色,这样每个符号的比特图的黑白色就对应于一个编码。该层的训练参数和连接数是(120 + 1)x84=10164
Output层也是全连接层,共有10个节点,分别代表数字0到9,且如果节点i的值为0,则网络识别的结果是数字i。采用的是径向基函数(RBF)的网络连接方式。
以上是LeNet-5的卷积神经网络的完整结构,共约有60,840个训练参数,340,908个连接。
首先介绍一下:mnist是一个非常有名的手写数字识别数据集,是nist数据集的一个子集,包含了60000张图片作为训练数据,10000张图片作为测试数据。在mnist数据集中的每一张图片都代表0~9中的一个数字。利用上面所说的lenet-5模型实现mnist手写数字识别可以达到大约99.2%的正确率。
我采用的是CPU版本的tensorflow,运行相对较慢,但是安装简单,只需要pip install tensorflow
即可。
整体来说,使用TensorFLow编程主要分为两个阶段,第一个阶段是构建模型,把网络模型用代码搭建起来。TensorFlow的本质是数据流图,因此这一阶段其实是在规定数据的流动方向。第二个阶段是开始训练,把数据输入到模型中,并通过梯度下降等方法优化变量的值。
import tensorflow as tf
import numpy as np # 习惯加上这句,但这边没有用到
from tensorflow.examples.tutorials.mnist import input_data
import matplotlib.pyplot as plt
mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
sess = tf.InteractiveSession()
# 1、权重初始化,偏置初始化
# 为了创建这个模型,我们需要创建大量的权重和偏置项
# 为了不在建立模型的时候反复操作,定义两个函数用于初始化
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)
# 2、卷积层和池化层也是接下来要重复使用的,因此也为它们定义创建函数
# tf.nn.conv2d是Tensorflow中的二维卷积函数,参数x是输入,w是卷积的参数
# strides代表卷积模块移动的步长,都是1代表会不遗漏地划过图片的每一个点,padding代表边界的处理方式
# padding = 'SAME',表示padding后卷积的图与原图尺寸一致,激活函数relu()
# tf.nn.max_pool是Tensorflow中的最大池化函数,这里使用2 * 2 的最大池化,即将2 * 2 的像素降为1 * 1的像素
# 最大池化会保留原像素块中灰度值最高的那一个像素,即保留最显著的特征,因为希望整体缩小图片尺寸
# ksize:池化窗口的大小,取一个四维向量,一般是[1,height,width,1]
# 因为我们不想再batch和channel上做池化,一般也是[1,stride,stride,1]
def conv2d(x, w):
return tf.nn.conv2d(x, w, strides=[1,1,1,1],padding='SAME') # 保证输出和输入是同样大小
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1],padding='SAME')
# 3、参数
# 这里的x,y_并不是特定的值,它们只是一个占位符,可以在TensorFlow运行某一计算时根据该占位符输入具体的值
# 输入图片x是一个2维的浮点数张量,这里分配给它的shape为[None, 784],784是一张展平的MNIST图片的维度
# None 表示其值的大小不定,在这里作为第1个维度值,用以指代batch的大小,means x 的数量不定
# 输出类别y_也是一个2维张量,其中每一行为一个10维的one_hot向量,用于代表某一MNIST图片的类别
x = tf.placeholder(tf.float32, [None,784], name="x-input")
y_ = tf.placeholder(tf.float32,[None,10]) # 10列
# 4、第一层卷积,它由一个卷积接一个max pooling完成
# 张量形状[5,5,1,32]代表卷积核尺寸为5 * 5,1个颜色通道,32个通道数目
w_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32]) # 每个输出通道都有一个对应的偏置量
# 我们把x变成一个4d 向量其第2、第3维对应图片的宽、高,最后一维代表图片的颜色通道数(灰度图的通道数为1,如果是RGB彩色图,则为3)
x_image = tf.reshape(x,[-1,28,28,1])
# 因为只有一个颜色通道,故最终尺寸为[-1,28,28,1],前面的-1代表样本数量不固定,最后的1代表颜色通道数量
h_conv1 = tf.nn.relu(conv2d(x_image, w_conv1) + b_conv1) # 使用conv2d函数进行卷积操作,非线性处理
h_pool1 = max_pool_2x2(h_conv1) # 对卷积的输出结果进行池化操作
# 5、第二个和第一个一样,是为了构建一个更深的网络,把几个类似的堆叠起来
# 第二层中,每个5 * 5 的卷积核会得到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)# 输入的是第一层池化的结果
h_pool2 = max_pool_2x2(h_conv2)
# 6、密集连接层
# 图片尺寸减小到7 * 7,加入一个有1024个神经元的全连接层,
# 把池化层输出的张量reshape(此函数可以重新调整矩阵的行、列、维数)成一些向量,加上偏置,然后对其使用Relu激活函数
w_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])
h_pool2_flat = tf.reshape(h_pool2, [-1,7 * 7 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, w_fc1) + b_fc1)
# 7、使用dropout,防止过度拟合
# dropout是在神经网络里面使用的方法,以此来防止过拟合
# 用一个placeholder来代表一个神经元的输出
# tf.nn.dropout操作除了可以屏蔽神经元的输出外,
# 还会自动处理神经元输出值的scale,所以用dropout的时候可以不用考虑scale
keep_prob = tf.placeholder(tf.float32, name="keep_prob")# placeholder是占位符
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
# 8、输出层,最后添加一个softmax层
w_fc2 = weight_variable([1024,10])
b_fc2 = bias_variable([10])
y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, w_fc2) + b_fc2, name="y-pred")
# 9、训练和评估模型
# 损失函数是目标类别和预测类别之间的交叉熵
# 参数keep_prob控制dropout比例,然后每100次迭代输出一次日志
cross_entropy = tf.reduce_sum(-tf.reduce_sum(y_ * tf.log(y_conv),reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
# 预测结果与真实值的一致性,这里产生的是一个bool型的向量
correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
# 将bool型转换成float型,然后求平均值,即正确的比例
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 初始化所有变量,在2017年3月2号以后,用 tf.global_variables_initializer()替代tf.initialize_all_variables()
sess.run(tf.initialize_all_variables())
# 保存最后一个模型
saver = tf.train.Saver(max_to_keep=1)
for i in range(1000):
batch = mnist.train.next_batch(64)
if i % 100 == 0:
train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1],keep_prob: 1.0})
print("Step %d ,training accuracy %g" % (i, train_accuracy))
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
print("test accuracy %f " % accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
# 保存模型于文件夹
saver.save(sess,"save/model")
可以从代码看出循环训练1000次,每次从训练集中随机取64个数据放入模型中进行训练。在训练的过程中,每隔100次对模型进行一次评估。评估使用测试集数据,统计正确预测的个数的百分比并输出。每100次会输出一次运行日志,可以观察到随着训练次数增加,预测准确率逐渐上升,运行结果都稳定在96%左右。(这个是用包来构建和评估模型的,至于那个用图片的实在跑太慢了就不展示了)
界面代码(感谢小学弟~)
import tensorflow as tf
import numpy as np
import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk
from tkinter import filedialog
import time
def creat_windows():
win = tk.Tk() # 创建窗口
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
ww, wh = 400, 450
x, y = (sw-ww)/2, (sh-wh)/2
win.geometry("%dx%d+%d+%d"%(ww, wh, x, y-40)) # 居中放置窗口
win.title('手写体识别') # 窗口命名
bg1_open = Image.open("timg.jpg").resize((300, 300))
bg1 = ImageTk.PhotoImage(bg1_open)
canvas = tk.Label(win, image=bg1)
canvas.pack()
var = tk.StringVar() # 创建变量文字
var.set('')
tk.Label(win, textvariable=var, bg='#C1FFC1', font=('宋体', 21), width=20, height=2).pack()
tk.Button(win, text='选择图片', width=20, height=2, bg='#FF8C00', command=lambda:main(var, canvas), font=('圆体', 10)).pack()
win.mainloop()
def main(var, canvas):
file_path = filedialog.askopenfilename()
bg1_open = Image.open(file_path).resize((28, 28))
pic = np.array(bg1_open).reshape(784,)
bg1_resize = bg1_open.resize((300, 300))
bg1 = ImageTk.PhotoImage(bg1_resize)
canvas.configure(image=bg1)
canvas.image = bg1
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
saver = tf.train.import_meta_graph('save/model.meta') # 载入模型结构
saver.restore(sess, 'save/model') # 载入模型参数
graph = tf.get_default_graph() # 加载计算图
x = graph.get_tensor_by_name("x-input:0") # 从模型中读取占位符变量
keep_prob = graph.get_tensor_by_name("keep_prob:0")
y_conv = graph.get_tensor_by_name("y-pred:0") # 关键的一句 从模型中读取占位符变量
prediction = tf.argmax(y_conv, 1)
predint = prediction.eval(feed_dict={x: [pic], keep_prob: 1.0}, session=sess) # feed_dict输入数据给placeholder占位符
answer = str(predint[0])
var.set("预测的结果是:" + answer)
if __name__ == "__main__":
creat_windows()
自己的手写体总是会出现奇怪的值问题:
好不容易有一张成功进入识别结果是这样的
于是开始找找测试集里面可能出现预测错误的图展示一下:
首先估计一下可能产生错误的数据:
当我看到这张图片的时候我以为会是6,结果并没有因为那个凸起的角识别错误
点了半天终于找到了一个识别成6的,但是个人认为这样并不像6,比这个像的却能识别正确
然后意外发现了一个因为一个墨点被识别成8的数据
1的数据集真是可怕。我以为1和7最容易出错,结果却意外测出一堆1识别成了8的数据。观察可以看出这些错误数据大多都是很粗的,在像素较小的情况下,1和8的内部特征差异并不大,如果1写得稍微粗一点,很有可能因为轮廓部分被识别为8。
还有一些奇怪的写法,被识别错误感觉真的是写的问题了
当然还是有些写法比较奇特但识别正确的例子,并没有发现1被识别成7的数据(随机挑了20多个吧)
3的话发现了一个被识别成5的,数据集里面没有发现上下闭口比较严重的写法,所以没发现被识别为8的数据
4的话很多比较粗的被识别成了6,由于上半部分没有闭口所以没有发现被识别成9的数据
6的话基本上没有出现识别错误,找到了小部分错误
7的话因为上半部分的横都比较长所以没发现被识别为1的数据。
9的话可能会被识别成8
还有一些写法比较奇怪的数字出现识别错误我觉得理所应当。
Mnist模型识别自己手写数字正确率低的原因主要是笔画的粗细还有形状导致的,粗细就像1和8,形状就像4和9,虽然找到了不少识别错误的数据,但是这些数据算是鸡蛋挑骨头,专门找到了一些感觉容易出错的数据来验证。总体来说只要手写体不太飘逸,基本都能识别正确。
至于一些其他的原因可以参考博客:
https://blog.csdn.net/xiqi4145/article/details/84970496