6.1 MNIST手写数字识别数据解读

> MNIST手写数字识别:分类问题

上图中有许多手写的阿拉伯数字(0-9),我们要做的事情就是把这些手写的数字图像作为样本输入,让机器学习生成的模型能够自动地去判别当前这幅图像写的是数字几。

> MNIST手写数字识别数据集

MNIST手写数字识别数据集来自美国国家标准与技术研究所(National Institute of Standards and Technology,NIST)。

这个数据集由250个不同人手写的数字构成, 其中50%来自高中生, 50%来自美国人口普查局(the Census Bureau) 的工作人员。整个数据集由三部分构成,训练集有55000条数据,验证集有5000条数据,测试集有10000条数据。

虽然这个数据集听起来还是挺多的,但实际上在机器学习里,这个只是一个入门级的案例,所以它的数据集谈不上非常大,但是通过这样量级的数据集,可以给我们做到一个比较好的模型训练。

> 数据集下载

MNIST 数据集可在 http://yann.lecun.com/exdb/mnist/直接获取。

当然,TensorFlow也提供了数据集读取方法:

首先引入TensorFlow,在TensorFlow.examples.tutorials这个包中提供了input_data的方法,可以通过这个包来进行数据的读取。

读取的方法如下图所示:

input_data.read_data_sets()函数里面有两个参数:第一个为数据集所放的文件的目录,即它的路径,本案例中取的是MNIST_data;第二个参数是one_hot,设置为True,当然也可以把它设置为False,这两种情况下读进来的标签的数据格式是不一样的,在后面会详细介绍。

当这个数据集被读取出来后,会在指定目录里形成四个文件,总共大概10M左右。

如果是第一次读取MNIST数据集,在指定目录下不存在这个文件,那么它会自动去网上下载对应的文件,不过需要一定的等待时间;如果数据集已经存在了,那么它就会直接读取数据,速度会比较快。

> 了解MNIST手写数字识别数据集

通过print,分别输出了训练集、验证集以及测试集的数据的数量。

读取的样本数据包含两部分:一部分是images,即图像的数据;还有一部分是labels,即标签的数据。

通过输出图像的形(shape)和标签的形(shape),可以看到图像的形是(55000,784),55000代表55000条数据,784是因为图像的大小为28×28。标签的形是(55000,10),10是因为阿拉伯数字从0到9,总共是10个。这是一个十分类的问题,这个十分类用了独热编码(one hot编码)的模式,因此每一个标签的长度都是10。

还可以来看一幅具体的图像,比如images[0](第一幅图像),可以用len()获取它的长度:

也可以用shape来获取它的形,这是一个一维数组:

如果查看它具体的值,那么它里面总共有 784 个单元,前面大部分都是0,但中间其实有一些非0的数字,这跟图像的特征是相关的。因为一幅图像周围大多数都是白的,有颜色的地方是数字的部分,它只占了图像的一小部分。

真实的一幅图像实际上是28×28的像素点。如果想要把图像显示出来,需要用到reshape()把它变成一个28×28的二维数组。

这样就已经把图像按照行和列的模式,对数据做了重新整理。

如果想要把这幅图像显示出来,需要用到一个可视化图像的函数,下面先对其进行定义:

图像的可视化通过matplotlib来进行。

首先,定义了一个显示的函数:plot_image()。传入的参数就是image这幅图像的数值。在这里直接调用plt.imshow(),它的第一个参数是这个图像的数值内容,第二个参数是这个图像的模式参数。然后通过plt.show()把它显示出来。

定义好了以后,可以查看下标为1的图像,显示出来的图像是一个“3”:

也可以查看下标为20000的图像,在这里是一个不同字体的“3”:

> 进一步了解reshape()

下面来看一个例子:

在这个例子中,用了列表递推式,生成64个整数,形成一个numpy的数组。把它print后的结果就是0、1、2、3 ……63。

如果reshape一下,把它变成一个8×8的二维数组,就是下图的样子:

它总共有八行,每一行八个元素,按行优先顺序排列。

注意:reshape有一个前提是reshape前的总数和reshape后的总数要保持一致。

如果把它reshape成4×16之后,就会变成这个样子:

它总共有四行,每一行十六个元素,也是按行优先顺序排列的。

因此,通过这两个例子可以看出,reshape的做法是行优先,逐行排列。

下面结合刚才所讲的图像输出的例子,来思考一个问题:如果输出的代码换成下图的样子,把下标为20000的这张图片的数据reshape成为14×56的模式,即行减少了一半,列增加了一倍,再把它当做图像输出,会得到什么样的结果呢?

其实最终输出的图像将会是这个样子:

行减少了一半,列增加了一倍,输出了两个“3”。

为什么会这样呢?首先把图像重新进行了行的切割,变成了下图的样子:

一开始,图像是28×28的,一行有28个像素点。reshape后,一行有56个像素点,按照行优先的原则,会把原来的第二行往前挪,放在第一行的后面,于是第三行就到了原来第二行的位置,然后第四行又挪到了第三行的后面,即后面每一个偶数行的切片会被抽取出来,放在前面的奇数行后面,于是整个图片被拉长,最终变成图6-18的样子。

> 标签数据与独热编码

> 认识标签label

上图输出了训练集中下标为1的样本标签,输出的结果是一个一维数组,里面总共有10个元素。也许大家会奇怪,为什么这里并不是直接输出3呢?因为刚才看到下标为1的图片对应的是3,但实际上它输出了有 10 个元素的一维数组,在下标为3的地方取值为 1,其他的取值全为0。这就是独热编码的形式。

在打开数据集的时候,把one_hot的值设为True,这个时候,会把读取的数据格式化为标签,作为独热编码的形式。

> 独热编码(one hot encoding)

独热编码是一组稀疏向量,其中只有一个元素为1,其他元素均为0。

通常,独热编码被用于表示拥有有限个可能值的字符串或标识符。

比如刚刚手写数字识别的例子,总共有10个数字,图像类别是有限种可能的取值,就可以采用独热编码。

再比如:假设某个植物学数据集记录了15000 个不同的物种,其中每个物种都用独一无二的字符串标识符来表示。在特征工程过程中,可能需要将这些字符串标识符编码为独热向量,向量的大小为15000,其中只有一个元素为1,其它元素全为0,即所谓的稀疏向量。目前做分类应用一般都建议用独热编码。

> 为什么采用独热编码

这是一个分类任务,每一个类别都是一个离散的值或者说离散的特征,对于这些离散特征的取值扩展到了欧式空间,离散特征的某个取值就对应欧式空间的某个点。

而且在机器学习算法中,比如计算损失的时候,都会用到特征之间距离的计算或相似度的计算,这些常用计算方法都是基于欧式空间的。把离散值转变到欧氏空间,有利于做模型训练的损失计算。

将离散型特征使用one-hot编码,会让特征之间的距离计算更加合理。比如这个0-9的手写数字识别,如果不用one-hot编码,而直接使用数字本身的值,也是可以完全区分的。但是就需要考虑这样一个问题:能不能说1比8更相似于3呢?如果计算这两个数字之间的距离,1和3相差了2,而3和8相差了5。看起来5比2大,但是实际上,我们会认为3和8长得跟接近。如果采用one-hot编码,就能够有效地避免这个问题。对于这10个数进行one-hot编码,编出的结果如下图所示:

如果这个数字为3,就让下标为3的地方值为1,其他都为0。如果是8,就让下标为8的地方值为1,其他都为0。

如果已经完成了独热编码,怎样去获取它的值呢?

可以通过argmax()函数做计算。在numpy里面提供了argmax()方法,它能把传入的参数数组中最大数的索引值提取出来。根据它的含义,如果传入的参数是如下图所示的数组,它最大值的下标就等于3,所以输出结果是3,就可以从独热编码中取出它真正表达的值。

> 非独热编码的标签值

如果在最开始时,设置one_hot=True,读出来的训练集的标签就是用独热编码所表达的。如果把这个参数设为False,那么读出来的标签就是用真实的数值来表示的。根据下图,可以看到这个训练集的前面10个样本的标签值为7、3、4、6、1、8、1、0、9、8。

> 训练集的划分

我们构建和训练机器学习模型,是希望训练出来的模型可以对新的数据做出良好预测。那么如何去保证训练的效果,可以用来应对以前未见过的数据呢?如果它只对训练的这批数据有效,而当遇到了新的数据它就未必那么有效,在机器学习中,这种现象叫过拟合,或者叫泛化能力不强。为了保证训练的效果,需要针对数据集做出合理的划分。

第一种方法就是把数据集分成两个子集:一个是训练集,它是专门用来训练模型的子集; 另一个是测试集,是用来测试模型的子集。在上一讲波士顿房价预测问题的时候,我们把506条数据全部都用于训练,这就导致后面没有真实的数据可以用于测试了,所以这个模型训练的效果到底好不好是不能下结论的。即使是针对训练集的效果非常好,也不能下结论。就像我们平时的学习,做练习题时,是不能把考试时的题目拿来反复练习,然后考试的时候又用同样的题目,这样就不能反映真实水平了。所以我们通常会藏起一部分数据来,不用于训练,而是专门用来做测试。因为测试集是以前没有见过的,如果模型在测试集上也能表现的非常好,那就能推断这个模型在遇到新的数据时,可能也会表现得比较好。

当然,这里有两个前提条件:第一是测试集要足够大。如果正好一两个题目恰巧对了也不能说明问题;第二是不能反复使用相同的测试集,因为这有可能会带来作假的嫌疑。就像考试,如果同样的题目反复考,其实这个考试本身已经变成了练习,在考试的时候,做以前做过的题目,自然会得到比较好的结果,但这样的考试已经不能反映真实水平了。

> 拆分数据

通常,会将单个数据集拆分为一个训练集和一个测试集:

一般来说,训练集占的比例会更大,而测试集只需要一小部分,但它也需要满足前面提到的规模足够大,以产生统计学意义上的结果。此外,这个测试集不能比训练集更简单或更难,而是应该和训练集具有相同的特征分布的数据。

在数据拆分以后,整个机器学习的训练过程,就可以用下图来表示:

一开始,使用训练集去训练模型,训练完之后,使用测试集去评估这个模型,根据它在测试集上的表现来调整整个模型的超参数,比如说是否需要继续训练,训练的轮数要增加还是减少,学习率应该怎么变化等等。通过调整超参数以获得模型的最佳效果,最终,选择在测试集上获得最佳效果的模型。

但是,如果仅仅是把数据分解为训练集和测试集,这种做法有没有什么问题呢?因为测试集和训练集都会用于整个模型的迭代开发流程,而且在每一次迭代过程中,都会用到测试集和训练集,基于测试集的评估结果来指导更改模型的超参数。问题就出在这里,多次重复执行该流程有可能会导致模型不知不觉拟合了特定测试集的特点。就像考试的题目翻来覆去就是这些,在考过多轮以后,你会选择考试结果比较好的模型,从而使模型本身迎合了测试集的特点。

为了避免这个问题,需要对数据集做进一步的划分。

在这个新的数据划分中,增加了一个验证集:

通过验证集去测试训练的效果,而真正的测试集在一开始是不会露面的,只有当你认为该模型在验证集上验证效果达到最好时,才可以用测试集去测试其效果,如果这时候还能通过测试集的测试,就认为这个训练出来的模型是符合预期的。所以就相当于用验证集代替了前面所说的测试集,而保留了一份新的测试集,这个测试集从未参加任何训练和验证过程。

有了这样一个新的划分,那么整个新的工作流程就变成下图的样子:

还是使用训练集去训练模型,训练完后用验证集评估模型的效果,根据在验证集上获得的效果去调整模型超参数,直到得到在验证集上效果最好的那个模型,最后再用测试集确认这个训练出来的模型效果是否像预期一样,如果通过测试,那么训练过程就算结束了。

实际上,MNIST数据集已经把数据划分成了训练集、验证集和测试集。

下图是读取验证集的数据信息:

minist.valication.images是验证集的图片数据,minist.valication.labels是验证集的标签数据。输出它的形,可以看到验证集有5000条数据。

读取测试集是用minist.test来标识的:

整个测试集有10000条数据。所以在整个MNIST数据库里,它的训练集有55000条,验证集有5000条,测试集有10000条,所以这个数据量相对来说还是可以的,整个MNIST数据集是70000条数据。

> 数据的批量读取

训练模型输入的样本可以一次只有一条,也可以是批量的,比如十条甚至一百条的小批量,作为一次优化的样本输入。

如何去读取多条数据呢?python中可以通过切片的方法来读取多条数据:

比如在训练集的标签中读取前十条数据,可以把数组的下标写成“0:10”表示读取第0到9共十条数据。

在MNIST的包中,提供了一个批量读取样本的方法叫“next_batch”

它有一个参数batch_size,这个参数的值可以根据需要去设定,比如这里为10。返回结果包含两部分:一部分是图像数据,另一部分是标签数据。

返回后把标签数据输出,就形成了下图中二维数组的数据:

此外,它是针对专门的模型训练所提供的方法,所以在next_batch 内部实现时会对数据集先做一个shuffle, 即洗牌,把数据都打乱。

示例代码

下载并读取数据.py
import tensorflow as tf
import tensorflow.examples.tutorials.mnist.input_data as input_data

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

print('训练集 train 数量:',mnist.train.num_examples,
      ',验证集 validation 数量:',mnist.validation.num_examples,
      ',测试集 test 数量:',mnist.test.num_examples)
查看train Data.py
print('train images shape:', mnist.train.images.shape,
      'labels shaple:', mnist.train.labels.shape)

len(mnist.train.images[0])

mnist.train.images[0].shape

mnist.train.images[0]

mnist.train.images[0].reshape(28,28)
显示图像.py
import matplotlib.pyplot as plt

def plot_image(image):
    plt.imshow(image.reshape(28,28),cmap='binary')
    plt.show()
    
plot_image(mnist.train.images[0])
进一步了解 reshape().py
import numpy as np
int_array = np.array([i for i in range(64)])
print (int_array)

int_array.reshape(8,8)

int_array.reshape(4,16)

plt.imshow(mnist.train.images[20000].reshape(14,56),cmap='binary')
plt.show()

plt.imshow(mnist.train.images[20000].reshape(7,112),cmap='binary')
plt.show()
认识标签.py
mnist.train.labels[1]

import numpy as np
np.argmax(mnist.train.labels[1])
one hot 独热编码.py
mnist_no_one_hot = input_data.read_data_sets("MNIST_data/", one_hot=False)

print(mnist_no_one_hot.train.labels[0:10])
批量读取数据.py
print(mnist.train.images[0:10])

print(mnist.train.labels[0:10])

batch_images_xs, batch_labels_ys = \
     mnist.train.next_batch(batch_size=10)
     
print(batch_images_xs.shape,batch_labels_ys.shape)

print(batch_images_xs)
print(batch_labels_ys)

batch_images_xs, batch_labels_ys = \
     mnist.train.next_batch(batch_size=10)
print(batch_labels_ys)
读取验证数据.py
print('validation images:', mnist.validation.images.shape,
      'labels:', mnist.validation.labels.shape)
读取测试数据.py
print('test images:', mnist.test.images.shape,
      'labels:', mnist.test.labels.shape)

Last updated