本篇文章在上篇TensorFlow-手写数字识别(二)的基础上,将全连接网络改为LeNet-5卷积神经网络,实现手写数字识别。
1 引言
全连接网络:每个神经元与前后相邻层的每一个神经元都有连接关系,输入是特征,输出为预测的结果。
参数个数:Σ(前层x后层+后层)
如之前用于手写识别的3层全连接网络,输入层784个节点,隐藏层500个节点,输出层10个节点。则:
-
隐藏层参数:748*500+500
-
输出层参数:500*10+10
- 总计:397510≈40万
注:这里说的某层的参数是指该层与前一层之间的参数,因而输入层没有参数。
所以,一张分辨率仅仅是28x28的黑白图像,就有近40万个待优化的参数。
现实生活中高分辨率的彩色图像,像素点更多,且为红绿蓝三通道信息。
待优化的参数过多, 容易导致模型过拟合。
为避免这种现象,实际应用中一般不会将原始图片直接喂入全连接网络。
而是先对原始图像进行特征提取,把提取到的特征喂给全连接网络,再让全连接网络计算出分类评估值。
2 CNN基础
2.1 卷积 Convolutional
卷积是一种有效提取图片特征的方法。
一般用一个正方形卷积核,遍历图片上的每一个像素点。
图片与卷积核重合区域内相对应的每一个像素值乘卷积核内相对应点的权重,然后求和,再加上偏置后,
最后得到输出图片中的一个像素值。
2.2 全零填充 Pdding
有时会在输入图片周围进行全零填充,这样可以保证输出图片的尺寸和输入图片一致。
输出数据的尺寸=(w+2*p-k)/s+1
-
w:输入尺寸
-
p:padding尺寸
-
k:卷积核大小(有时也用f表示)
- s:核滑动步长
如:输入量是32x32x3,核是5x5x3,不用全零填充,则输出为(32-5+1)/1=28。
如果要让输出量保持在32x32x3,可根据公式计算出需要填充几层零。
32=(32+2P-5)/1 +1,计算出P=2,即需填充2层(圈)零。
2.3 TensorFlow中卷积计算函数
tf.nn.conv2d(输入,卷积核,步长,padding='VALID')
-
输入:eg.[batch,5,5,1]
-
用 batch 给出一次喂入多少张图片
-
每张图片的分辨率大小,比如5行5列
-
这些图片包含几个通道的信息(单通道灰度图:1,红绿蓝三通道彩图:3)
-
卷积核:eg.[3,3,1,16]
-
卷积核行列分辨率分别为3行3列
-
卷积核是1通道的,通道数由输入图片的通道数决定,它等于输入图片的通道数,所以也是1。
-
一共有16个这样的卷积核,说明卷积操作后输出图片的深度是16,也就是输出为16通道。
-
步长:eg.[1,1,1,1]
-
第二个参数表示横向滑动步长
-
第三个参数表示纵向滑动步长。
-
第一个和最后一个1这里固定的,表示横向纵向都以1为步长。
- padding:是否使用padding,默认用的是VALID,注意这里是以字符串的形式给出VALID。
2.4 多通道图片求卷积
多数情况下,输入的图片是RGB三个颜色组成的彩色图,输入的图片包含了红、绿、蓝三层数据,
卷积核的深度应该等于输入图片的通道数,所以使用3x3x3的卷积核,
最后一个3表示匹配输入图像的3个通道,这样这个卷积核有三层,
每层会随机生成9个待优化的参数,一共有27个待优化参数w和一个偏置b。
卷积计算方法和单层卷积核相似,卷积核为了匹配RGB三个颜色,把三层的卷积核套在三层的彩色图片上,
重合的27个像素进行对应点的乘加运算,最后的结果再加上偏置项b,求得输出图片中的一个值。
如5x5x3的输入图片加了1圈全零填充,使用3x3x3的卷积核,所有27个点与
对应的待优化参数相乘,乘积求和再加上偏置b得到输出图片中的一个值。
2.5 池化 Polling
TensorFlow给出了计算池化的函数。
最大池化用tf.nn.max_pool函数,平均用池化用tf.nn.avg_pool函数。
pool=tf.nn.max_pool(输入,池化核,核步长,padding='SAME')
-
输入:eg.[batch,28,28,6],给出一次输入batch张图片、行列分辨率、输入通道的个数。
-
池化核:eg.[1,2,2,1],只描述行分辨率和列分辨率,第一个和最后一个参数固定是1
-
核步长:eg.[1,2,2,1],池化核滑动步长,只描述横向滑动步长和纵向滑动步长,第一个和最后一个参数固定是1
- padding :是否使用零填充padding,可以是使用SAME或不使用充VALID
2.6 舍弃 Dropout
在神经网络训练过程中,为了减少过多参数常使用 dropout 的方法,将一部分神经元按照一定概率从神经网络中舍弃。
这种舍弃是临时性的,仅在训练时舍弃一些神经元;
在使用神经网络时,会把所有的神经元恢复到神经网络中。
在实际应用中,常常在前向传播构建神经网络时使用dropout来减小过拟合以及加快模型训练速度,
dropout一般会放到全连接网络中。
TensorFlow提供的dropout函数:tf.nn.dropout(上层输出,暂时舍弃的神经元的概率)
如:在训练参数的过程中,输出=tf.nn.dropout(上层输出,舍弃概率),
这样就有指定概率的神经元被随机置零,置零的神经元不参加当前轮的参数优化。
2.7 卷积神经网络 CNN
卷积神经网络可以认为由两部分组成,一部分是对输入图片进行特征提取,另一部分就是全连接网络,
只不过喂入全连接网络的不再是原始图片,而是经过若干次卷积、激活和池化后的特征信息。
卷积神经网络从诞生到现在,已经出现了许多经典网络结构,比如Lenet-5、Alenet、VGGNet、GoogleNet和ResNet等。
每一种网络结构都是以卷积、激活、池化、全连接这四种操作为基础进行扩展。
LeNet-5是最早出现的卷积神经网络,它有效解决了手写数字的识别问题。
3 LeNet-5网络分析
LeNet-5神经网络是Yann LeCun等人在 1998 年提出的,该神经网络充分考虑图像的相关性。
3.1 LeNet-5神经网络结构
-
输入为32321的图片大小,为单通道的输入;
-
进行 卷积,卷积核大小为551,个数为6,步长为1,非全零填充模式;
-
将卷积结果通过非线性激活函数;
-
进行池化,池化大小为2*2,步长为1,全零填充模式;
-
进行卷积,卷积核大小为556,个数为16,步长为1,非全零填充模式;
-
将卷积结果通过非线性激活函数;
-
进行池化,池化大小为2*2,步长为1,全零填充模式;
- 全连接层进行10分类
分析:
LeNet-5神经网络的输入是32321,经过551的卷积核,卷积核个数6,非全零填充,步长1:
输出=(32+0-5)/1+1=28,故经过卷积后输出为28286。
经过第一层池化层,池化大小为2*2,全零填充,步长2:
输出=输入/步长=28/2=14,池化层不改变深度,深度仍为6。
用同样计算方法,得到第二层池化后的输出为5516。
将第二池化层后的输出拉直送入全连接层。
特点;
-
卷积(Conv)、池化(ave-pooling)、非线性激活函数(sigmoid)相互交替;
- 层与层之间稀疏连接,减少计算复杂度
3.2 微调LeNet-5结构,适应MNIST数据
由于MNIST数据集中图片大小为28281的灰度图片,而 LeNet-5神经网络的输入为32321,故需要对LeNet-5结构进行微调。
调整后结构:
-
输入为3232128281的图片大小,为单通道的输入;
-
进行卷积,卷积核大小为551,个数为632,步长为1,非全零全零填充模式;
-
将卷积结果通过非线性激活函数;
-
进行池化,池化大小为2*2,步长为12,全零填充模式;
-
进行卷积,卷积核大小为5565532,个数为1664,步长为1,非全零全零填充模式;
-
将卷积结果通过非线性激活函数;
-
进行池化,池化大小为2*2,步长为12,全零填充模式;
- 全连接层进行10分类
4 代码实现LeNet-5
LeNet-5神经网络在MNIST数据集上的实现,主要分为三个部分:
-
前向传播过程(mnist_ lenet5_forward.py)
-
反向传播过程(mnist_ lenet5_backword.py)
- 测试过程(mnist_ lenet5_test.py)
4.1 前向传播过程(mnist_lenet5_forward.py)
实现对网络中参数和偏置的初始化、定义卷积结构和池化结构、定义前向传播程。
具体代码如下所示:
- 定义前向传播过程中常用到的参数
import tensorflow as tf
#输入图片的尺寸和通道数
IMAGE_SIZE = 28
NUM_CHANNELS = 1
#第一层卷积核的大小和个数
CONV1_SIZE = 5
CONV1_KERNEL_NUM = 32
#第二层卷积核的大小和个数
CONV2_SIZE = 5
CONV2_KERNEL_NUM = 64
#第三层全连接层的神经元个数
FC_SIZE = 512
#第四层全连接层的神经元个数
OUTPUT_NODE = 10
权重w生成函数和偏置b生成函数与之前的定义相同
def get_weight(shape, regularizer): #生成张量的维度,正则化项的权重
# tf.truncated_normal:生成去掉过大偏离点的正态分布随机数的张量,stddev是指标准差
w = tf.Variable(tf.truncated_normal(shape,stddev=0.1))
# 为权重加入L2正则化
if regularizer != None:
tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(regularizer)(w))
return w
def get_bias(shape):
b = tf.Variable(tf.zeros(shape))
return b
卷积层与池化计算函数如下
def conv2d(x,w): #一个输入 batch,卷积层的权重 'SAME' 表示使用全 0 填充,而'VALID'
return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x): #ksize表示池化过滤器的边长为2 strides表示过滤器移动步长是2 'SAME'提供使用全0填充
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) #初始化卷积核
conv1_b = get_bias([CONV1_KERNEL_NUM]) #初始化偏置项
conv1 = conv2d(x, conv1_w)
relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_b)) #非线性激活,相比sigmoid和tanh函数,relu函数可快速收敛
pool1 = max_pool_2x2(relu1)
#【卷积池化】
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 = max_pool_2x2(relu2)
#将上一池化层的输出pool2(矩阵)转化为下一层全连接层的输入格式(向量)
pool_shape = pool2.get_shape().as_list() #得到pool2输出矩阵的维度,并存入list中,注意pool_shape[0]是一个batch的值
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3] #从list中依次取出矩阵的长宽及深度,并求三者的乘积就得到矩阵被拉长后的长度
reshaped = tf.reshape(pool2, [pool_shape[0], nodes]) #将pool2转换为一个batch的向量再传入后续的全连接
#【全连接】
fc1_w = get_weight([nodes, FC_SIZE], regularizer)
fc1_b = get_bias([FC_SIZE])
fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_w) + fc1_b)
if train:
fc1 = tf.nn.dropout(fc1, 0.5)#如果是训练阶段,则对该层输出使用dropout,随机将该层输出中的一半神经元置为无效
##全连接
fc2_w = get_weight([FC_SIZE, OUTPUT_NODE], regularizer)
fc2_b = get_bias([OUTPUT_NODE])
y = tf.matmul(fc1, fc2_w) + fc2_b
return y
4.2 反向传播过程(mnist_lenet5_backward.py)
用于完成神经网络参数的训练
- 定义训练过程中的超参数
#coding:utf-8
BATCH_SIZE = 50#100 #batch
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" #模型命名
* 完成反向传播过程
* 给x, y_ 是占位
* 调用前向传播过程
* 求含有正则化的损失值
* 实现指数衰减学习率
* 实现滑动平均模型
* 将train_step和ema_op两个训练操作绑定到train_op上
* 实例化一个保存和恢复变量的saver,并创建一个会话
def backward(mnist):
#x,y_占位
x = tf.placeholder(tf.float32,[
BATCH_SIZE,
mnist_lenet5_forward.IMAGE_SIZE,
mnist_lenet5_forward.IMAGE_SIZE,
mnist_lenet5_forward.NUMCHANNELS])
y = tf.placeholder(tf.float32, [None, mnist_lenet5_forward.OUTPUT_NODE])
#前向传播
y = mnist_lenet5_forward.forward(x,True, REGULARIZER)
#声明一个全局计数器,并输出化为0
global_step = tf.Variable(0, trainable=False)
#先是对网络最后一层的输出y做softmax,再将此向量和实际标签值做交叉熵
ce = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
#再对得到的向量求均值就得到 loss
cem = tf.reduce_mean(ce)
#添加正则化中的 losses
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)
#传入学习率,构造一个实现梯度下降算法的优化器,再通过使用minimize更新存储要训练的变量的列表来减小loss
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
#实现滑动平均模型,参数MOVING_AVERAGE_DECAY用于控制模型更新的速度
ema = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
ema_op = ema.apply(tf.trainable_variables())
#将train_step和ema_op两个训练操作绑定到train_op
with tf.control_dependencies([train_step, ema_op]):
train_op = tf.no_op(name='train')
#实例化一个保存和恢复变量的saver
saver = tf.train.Saver()
#创建一个会话,并通过python中的上下文管理器来管理这个会话
with tf.Session() as sess:
init_op = tf.global_variables_initializer()
sess.run(init_op)
# 通过checkpoint文件定位到最新保存的模型
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):
#读取一个batch的数据
xs, ys = mnist.train.next_batch(BATCH_SIZE)
#将输入数据xs转换成与网络输入相同形状的矩阵
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)
运行结果
RESTART: G:...\lenet5\mnist_lenet5_backward.py
Extracting ./data/train-images-idx3-ubyte.gz
Extracting ./data/train-labels-idx1-ubyte.gz
Extracting ./data/t10k-images-idx3-ubyte.gz
Extracting ./data/t10k-labels-idx1-ubyte.gz
After 19312 training step(s), loss on training batch is 0.650531.
After 19412 training step(s), loss on training batch is 0.699633.
After 19512 training step(s), loss on training batch is 0.686086.
After 19612 training step(s), loss on training batch is 0.725393.
After 19712 training step(s), loss on training batch is 0.788735.
After 19812 training step(s), loss on training batch is 0.697031.
After 19912 training step(s), loss on training batch is 0.712534.
After 20012 training step(s), loss on training batch is 0.746723.
After 20112 training step(s), loss on training batch is 0.776782.
After 20212 training step(s), loss on training batch is 0.791459.
After 20312 training step(s), loss on training batch is 0.731853.
After 20412 training step(s), loss on training batch is 0.666092.
损失函数值在0.7左右徘徊,继续调节训练参数应该可以得到更好的结果。
**4.3 测试过程(mnist_lenet5_test.py)**
对MNIST数据集中的测试数据进行预测,测试模型准确率。
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
#+++++++++++++++++++++++++++++++修改读入的大小
BATCH_SIZE = 500#0 #batch
STEPS = 2
def test(mnist):
with tf.Graph().as_default() as g:
x = tf.placeholder(tf.float32,[
BATCH_SIZE,#mnist.test.num_examples,
mnist_lenet5_forward.IMAGE_SIZE,
mnist_lenet5_forward.IMAGE_SIZE,
mnist_lenet5_forward.NUMCHANNELS])
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]
for i in range(STEPS):
#读取一个batch的数据
xs, ys = mnist.test.next_batch(BATCH_SIZE)
reshaped_x = np.reshape(xs,(
BATCH_SIZE,#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_:ys})
print("After %s training step(s), test accuracy = %g" % (global_step, accuracy_score))
else:
print('No checkpoint file found')
return
#每隔5秒寻找一次是否有最新的模型
time.sleep(TEST_INTERVAL_SECS)
def main():
mnist = input_data.read_data_sets("./data/", one_hot=True)
test(mnist)
if name == 'main':
main()
运行结果:
RESTART: G:\TestProject\python\tensorflow\peking_caojian\7CNNbase\lenet5\mnist_lenet5_test2.py
Extracting ./data/train-images-idx3-ubyte.gz
Extracting ./data/train-labels-idx1-ubyte.gz
Extracting ./data/t10k-images-idx3-ubyte.gz
Extracting ./data/t10k-labels-idx1-ubyte.gz
After 21512 training step(s), test accuracy = 0.9842
After 21512 training step(s), test accuracy = 0.9802
测试集上的准确率在98%左右。
**4.4 测试真实图片数据**
修改之前的mnist_app.py文件,主要有两点改变:
* 读取图片的格式:原来是[1,784]这种形式,现在使用的是[1,28,28,1]。
* 读取图片的方式:原来是手动输入文件名,现在修改为自动读取整个文件夹里的图片,
图片按自定义的格式命名,还可以直接判断出知否预测准确,并给出总的准确率。
import tensorflow as tf
import numpy as np
from PIL import Image
import mnist_lenet5_backward as mnist_backward
import mnist_lenet5_forward as mnist_forward
import os
filedir = os.getcwd()+ '\pic'
m = 0
n = 0
def restore_model(testPicArr):
with tf.Graph().as_default() as tg:
x = tf.placeholder(tf.float32,[
1,
mnist_forward.IMAGE_SIZE,
mnist_forward.IMAGE_SIZE,
mnist_forward.NUM_CHANNELS])
y = mnist_forward.forward(x,False,None)
preValue = tf.argmax(y, 1)
variable_averages = tf.train.ExponentialMovingAverage(mnist_backward.MOVING_AVERAGE_DECAY)
variables_to_restore = variable_averages.variables_to_restore()
saver = tf.train.Saver(variables_to_restore)
with tf.Session() as sess:
ckpt = tf.train.get_checkpoint_state(mnist_backward.MODEL_SAVE_PATH)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess, ckpt.model_checkpoint_path)
preValue = sess.run(preValue, feed_dict={x:testPicArr})
return preValue
else:
print("No checkpoint file found")
return -1
def pre_pic(picName):
img = Image.open(picName) #加载待测试图片(白底)
reIm = img.resize((28,28), Image.ANTIALIAS) #调整大小到28x28
im_arr = np.array(reIm.convert('L'))
threshold = 50 #二进制阈值
for i in range(28):
for j in range(28):
im_arr[i][j] = 255 - im_arr[i][j] #反色(黑底)
if (im_arr[i][j] < threshold): #黑底白字
im_arr[i][j] = 0
else:
im_arr[i][j] = 255
#nm_arr = im_arr.reshape([1, 784]) #图片转成1行
nm_arr = np.reshape(im_arr,(
1,#mnist.test.num_examples,
mnist_forward.IMAGE_SIZE,
mnist_forward.IMAGE_SIZE,
mnist_forward.NUM_CHANNELS))
nm_arr = nm_arr.astype(np.float32)
img_ready = np.multiply(nm_arr, 1.0/255.0) #取值范围限制在0~1之间
return img_ready
'''
def application():
testNum = input("input the number of test pictures:")
for i in range(int(testNum)):
testPic = input("the path of test picture:")
testPicArr = pre_pic(testPic)
preValue = restore_model(testPicArr)
print ("The prediction number is:", preValue)
'''
def application():
global m,n
for root, dirs, files in os.walk(filedir):
for file in files:
if os.path.splitext(file)[1] == '.png':
n = n+1
imagename = os.path.splitext(file)[0]+'.png'
testPic = os.path.join(root, file)
testPicArr = pre_pic(testPic)
preValue = restore_model(testPicArr)
print ("The %d image name is %s:" % (n,imagename))
print ("The prediction number is:", preValue)
if int(imagename[0])== preValue:
m = m+1
print("TRUE")
else:
print("FALSE!!!!!!!!!!!!!!!!!!!!")
print("m = %d,n = %d" % (m,n))
print("test accuracy = %d%%" % (m/n*100))
def main():
application()
if name == 'main':
main()
运行结果:
RESTART: G:...\lenet5\mnist_app.py
The 1 image name is 0.png:
The prediction number is: [0]
TRUE
The 2 image name is 1.png:
The prediction number is: [1]
TRUE
The 3 image name is 2.png:
The prediction number is: [2]
TRUE
The 4 image name is 3.png:
The prediction number is: [3]
TRUE
The 5 image name is 4.png:
The prediction number is: [4]
TRUE
The 6 image name is 5.png:
The prediction number is: [5]
TRUE
The 7 image name is 6.png:
The prediction number is: [6]
TRUE
The 8 image name is 7.png:
The prediction number is: [7]
TRUE
The 9 image name is 8.png:
The prediction number is: [8]
TRUE
The 10 image name is 9.png:
The prediction number is: [9]
TRUE
m = 10,n = 10
test accuracy = 100%
该测试结果用的是下面教程链接中的图片(下图第一排),换成自己手写的数字(下图第二排),准确率为80%(上篇文章使用全连接网络的准确率只有50%)。
![](http://www.icode9.com/i/li/?n=4&i=images/blog/202102/27/6c58937b434838954fb797686cc22603.png?,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
参考:人工智能实践:Tensorflow笔记