MNIST手写数字识别(Softmax回归)
一 前言
MNIST手写数字识别在TensorFlow中文社区中分为MNIST机器学习入门和深入两个部分,不管哪一部分,在机器学习领域中都只是“hello world”级别的存在,主要是让大家了解TensoFlow和CNN的一些基本概念,谁都想学好,但饭总要一口一口吃,先从最基本的入手,由浅到深才是正确的学习方式。
本博客主要是记录学习MNIST过程中,小编的一些想法,可能会有错误,只是想做一个总结记录。
二 实际操作
1. 下载MNIST训练和测试数据集
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
使用from tensorflow.examples.tutorials.mnist import input_data
语句可以引入input_data,input_data.read_data_sets("MNIST_data/", one_hot=True)
首先会检测本地是否存在包含数据集的MNIST_data文件夹,若存在直接读取数据,若不存在,程序将会下载数据集,需要注意的是:因为“科学上网”的缘故,代码下载数据集将会失败,所以大家还是需要手动下载,创建一个名为MNIST_data的文件夹(名字随便取,记得把代码中修改成同样的文件夹就行),把下载好的数据集放进去就行。
下载完成后,我们打印输出,看一下数据集的中训练集和测试集长什么样?
print("训练集图片的shape: ", mnist.train.images.shape)
print("训练集label的shape: ", mnist.train.labels.shape)
print("测试集图片的shape: ", mnist.test.images.shape)
print("测试集label的shape: ", mnist.test.labels.shape)
# 还可以打印其中一张图片,图片打印结果小编就不输出了,大家运行代码就可以看到
print("输出一张图片:")
im = mnist.train.images[1,:] # 索引从零开始的
im = im.reshape((-1,28)) #-1是不管多少,就先不管行,但是保证列必须是28
pylab.imshow(im)
pylab.show()
'''
对应输出如下:
训练集图片的shape: (55000, 784)
训练集label的shape: (55000, 10)
测试集图片的shape: (10000, 784)
测试集label的shape: (10000, 10)
'''
由输出结果可知:训练集中有55000张像素为784(28 * 28)的图片以及55000张one_hot类型的标签,测试集中有10000张图片以及10000张图片对应的标签。
2. 实现原理
本博客记录的MNIST不包含卷积池化操作,就是只是实现一个Softmax回归,所以比较简单。
此外,关于基本内容的讲解,小编这里就不详细说了,大家可以去 TensorFlow中文社区 MNIST机器学习入门 仔细阅读,肯定比小编说的更详细,小编这里只是说一个梗概。
主要流程可以归纳如下(不只是MNIST手写数字识别):
所有的图片用[55000, 784]这样的一个2D矩阵表示,第一个数字表示图片的张数,第二个数字表示某一张图片的784个像素点。所有图片的标签用[55000, 10]这样的一个2D矩阵表示,第一个数字同样表示图片的张数,第二个数字表示某个图片对应哪个数字,采用一个one-hot向量实现,例如 [0, 0, 0, 0, 0, 1, 0 ,0, 0, 0]第5号位置为1,剩余位置为0,说明该图片上写的数字就是5。
我们现在要做的就是:实现从图片到数字的一个映射,了解这个转换过程是如何完成的?
从[55000, 784]矩阵变化到[55000, 10]这样的一个矩阵,根据矩阵乘法可知,需要乘以一个[784, 10]的矩阵,这个矩阵就是上面的W,同时为了更好的变换,我们还引入一个偏移量b,size为[10]。最终的形式为: y = softmax(Wx + b)。现在我们要做的就是不断的训练,改进W和b矩阵的内容,从而当我拿到一张图片后,带入上面的表达式,就可以得到该图片上写的数字是什么!
那么如何训练呢? 不着急,我们先粘一下代码,对照着代码讲解。
3. 实现代码
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# 导入数据,若数据不存在,将下载数据,但是由于科学上网的缘故,程序下载不了
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
# x 表示这些图片,每一张图片就是拉伸成一个1D的向量,图片数量不限
x = tf.placeholder(dtype=tf.float32, shape=[None, 784])
# 权值
weights = tf.Variable(tf.zeros(shape=[784, 10], dtype=tf.float32))
# 偏移量
bias = tf.Variable(tf.zeros(shape=[10], dtype=tf.float32))
# 得到的值 y = softmax(Wx+ b)
# softmax实现概率分配,可能是某个数字的概率,概率为正且和为1
y = tf.nn.softmax(tf.matmul(x, weights) + bias)
# 真实的数字
y_ = tf.placeholder(dtype=tf.float32, shape=[None, 10])
# 定义损失函数
loss = -tf.reduce_sum(y_ * tf.log(y))
# 梯度下降对应的步长
learning_rate = 0.01
# 采用梯度下降法,最小化损失,同时反转传播实现参数调整
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
# 检验预测和真实值是否相同,返回一组bool值
correct_prediction = tf.equal(tf.argmax(y, axis=1), tf.argmax(y_, axis=1))
# 得到测试的准取率
accuracy = tf.reduce_mean(tf.cast(correct_prediction, dtype=tf.float32))
with tf.Session() as sess:
# 初始化所有变量
sess.run(tf.initialize_all_variables())
# 循环训练模型1000次,每次随机抓取训练数据中的100个数据点
for i in range(1000):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(optimizer, feed_dict={x: batch_xs, y_: batch_ys})
if i % 200 == 0:
print("第%d阶段训练结束" % (i / 200 + 1))
# 评估模型,得到最终的准确率
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))
'''
输出结果如下:
第1阶段训练结束
第2阶段训练结束
第3阶段训练结束
第4阶段训练结束
第5阶段训练结束
0.9126
'''
我们首先初始化W和b矩阵的值,然后带入图片和标签,公式最终可以得到图片中写的数字,但由于W和b矩阵的值和最终训练好的值一开始肯定是偏差很大的,所以得到的数字和图片上真实反映出来的数字不一样。我们规定真实数字的概率分布用y_来表示,由公式计算得到的数字分布概率用y表示,采用两者的交叉熵作为损失函数。
显然若y和y_两者相差越大,损失函数的值就越大,若两者相差越小,交叉熵的值也就越小。我们要做的是调整W和b矩阵的值,从而使y和y_两者之间的差距很小或没有,换句话说,就是调整W和b矩阵的值,使得损失函数的值越小。
为此我们对损失函数进行求导,采用梯度下降法降低损失函数的值,同时程序通过反向传播算法自动更新W和b矩阵的值。
随着训练的不断进行,W和b矩阵会不断的优化,并最终实现手写数字识别的能力。
三 后记
采用测试集对训练好的模型进行测试,最终的正确率只有91%多,这个正确率显然是很低的,这主要是因为咱们使用的模型太过于简单,只是一个Softmax回归模型,连最基本的卷积、池化、激活操作都没有,所以若想要提高测试的正确率,我们需要采用更加复杂的模型。
在了解损失函数,反向传播法和这样的一个基本的程序流程之后,大家可以尝试着进行一些练习,例如:对于函数 y = f(x1, x2, x3, …, x10),给定该函数图像经过的一些散点,将这些散点坐标作为数据集,让我们实现函数曲线的一个拟合,也就是找到每一个自变量X前的参数是多少。