2.1 CIFAR-10数据集
2.1.1 CIFAR-10数据集简介
CIFAR-10是由Hinton的学生Alex Krizhevsky和Ilya Sutskever整理的一个用于识别普适物体的小型数据集。它一共包含10个类别的RGB彩色图片:飞机(airplane)、汽车(automobile)、鸟类(bird)、猫(cat)、鹿(deer)、狗(dog)、蛙类(frog)、马(horse)、船(ship)和卡车(truck)。图片的尺寸为32×32,数据集中一共有50000张训练图片和10000张测试图片。CIFAR-10的图片样例如图2-1所示。
图2-1 CIFAR-10数据集的图片样例
与MNIST数据集相比,CIFAR-10具有以下不同点:
· CIFAR-10是3通道的彩色RGB图像,而MNIST是灰度图像。
· CIFAR-10的图片尺寸为32×32,而MNIST的图片尺寸为28×28,比MNIST稍大。
· 相比于手写字符,CIFAR-10含有的是现实世界中真实的物体,不仅噪声很大,而且物体的比例、特征都不尽相同,这为识别带来很大困难。直接的线性模型如Softmax在CIFAR-10上表现得很差。
本章将以CIFAR-10为例,介绍深度图像识别的基本方法。本章代码中的一部分来自于TensorFlow的官方示例,如表2-1所示。
表2-1 TensorFlow官方示例的CIFAR-10代码文件
除了以上官方提供的文件外,本书还额外编写了三个文件,如表2-2所示。
表2-2 本书中额外提供的三个文件
2.1.2 下载CIFAR-10数据
运行cifar10_download.py程序就可以下载CIFAR-10数据集的全部数据:
# 引入当前目录中已经编写好的cifar10模块 import cifar10 # 引入tensorFlow import tensorflow as tf # tf.app.flags.FLAGS是TensorFlow内部的一个全局变量存储器,同时可以用于命令行参数 的处理 FLAGS=tf.app.flags.FLAGS # 在cifar10模块中预先定义了f.app.flags.FLAGS.data_dir为CIFAR-10的数据路径 # 把这个路径改为cifar10_data FLAGS.data_dir='cifar10_data/' # 如果不存在数据文件,就会执行下载 cifar10.maybe_download_and_extract()
这段程序会把CIFAR-10数据集下载到目录cifar10_data/中。在cifar10模块中,预先使用语句tf.app.flags.DEFINE_string('data_dir', '/tmp/cifar10_data', """Path to the CIFAR-10 data directory.""")定义了默认的data_dir是/tmp/cifar10_data,也就是把数据集下载到目录/tmp/cifar10_data中。本书做了简单修改,即使用FLAGS=tf.app.flags.FLAGS、FLAGS.data_dir='cifar10_data/’两条语句把待下载目录修改为cifar10_data/。
cifar10.maybe_download_and_extract()函数会自动检测数据集有没有下载,如果下载过则直接跳过,不做操作,如果之前没有下载就会自动下载数据集并解压缩。
等待一段时间后,系统会输出:“Successfully downloaded cifar-10-binary.tar.gz 170052171 bytes.”。这表明下载成功了。此时打开文件夹cifar10_data/,会看到一个cifar-10-binary.tar.gz文件,是数据集原始的压缩包;还有一个cifar-10-batches-bin文件夹,是压缩包解压后的结果。
打开cifar10_data/cifar-10-batches-bin/文件夹,一共有8个文件,是CIFAR-10的全部数据,文件名及用途如表2-3所示。
表2-3 CIFAR-10数据集的数据文件名及用途
2.1.3 TensorFlow的数据读取机制
在讲解如何用TensorFlow读取CIFAR-10数据之前,作为基础,先来简单介绍TensorFlow中数据读取的基本机制。
首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用图2-2来表示。
图2-2 图像的数据读取过程
假设硬盘中有一个图片数据集0001.jpg、0002.jpg、0003.jpg……只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有那么简单。事实上,必须先读入数据后才能进行计算,假设读入用时0.1s,计算用时0.9s,那么就意味着每过1s, GPU都会有0.1s无事可做,这大大降低了运算的效率。
如何解决这个问题?方法就是将读入数据和计算分别放在两个线程中,将数据读入内存的一个队列,如图2-3所示。
图2-3 改进的读取方式:先将图片读取到内存队列中
读取线程源源不断地将文件系统中的图片读入一个内存的队列中,而负责计算的是另一个线程,计算需要数据时,直接从内存队列中取就可以了。这样可以解决GPU因为I/O而空闲的问题!
而在TensorFlow中,为了方便管理,在内存队列前又添加了一层所谓的“文件名队列”。
为什么要添加这一层文件名队列呢?首先需要了解机器学习中的一个概念:epoch。对于一个数据集来讲,运行一个epoch就是将这个数据集中的图片全部计算一遍。如果一个数据集中有三张图片A.jpg、B.jpg、C.jpg,那么运行一个epoch就是指对A、B、C三张图片都计算一遍。两个epoch就是指先对A、B、C各计算一遍,然后再全部计算一遍,也就是说每张图片都计算了两遍。
TensorFlow使用“文件名队列+内存队列”双队列的形式读入文件,可以很好地管理epoch。下面用图片的形式来说明这个机制的运行方式。如图2-4所示,还是以数据集A.jpg、B.jpg、C.jpg为例,假定要运行一个epoch,那么就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束。
图2-4 TensorFlow中的文件名队列和内存队列
程序运行后,内存队列首先读入A(此时A从文件名队列中出队),如图2-5所示。
图2-5 读入A
再依次读入B和C,如图2-6所示。
图2-6 读入B和C
此时,如果再尝试读入,由于系统检测到了“结束”,就会自动抛出一个异常(OutOfRange)。外部捕捉到这个异常后就可以结束程序了。这就是TensorFlow中读取数据的基本机制。如果要运行2个epoch而不是1个epoch,则只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。
如何在TensorFlow中创建上述的两个队列呢?
对于文件名队列,使用tf.train.string_input_producer函数。这个函数需要传入一个文件名list,系统会自动将它转为一个文件名队列。
此外,tf.train.string_input_producer还有两个重要的参数:一个是num_epochs,它就是上文中提到的epoch数;另外一个是shuffle, shuffle是指在一个epoch内文件的顺序是否被打乱。若设置shuffle=False,如图2-7所示,每个epoch内,数据仍然按照A、B、C的顺序进入文件名队列,这个顺序不会改变。
图2-7 shuffle=False时的数据读取顺序
如果设置shuffle=True,那么在一个epoch内,数据的前后顺序就会被打乱,如图2-8所示。
图2-8 shuffle=True时的数据读取顺序
在TensorFlow中,内存队列不需要自己建立,只需要使用reader对象从文件名队列中读取数据就可以了,具体实现可以参考下面的实战代码。
除了tf.train.string_input_producer外,还要额外介绍一个函数:tf.train.start_queue_runners。初学者会经常在代码中看到这个函数,但往往很难理解它的用处。有了上面的铺垫后,就可以解释这个函数的作用了。
在使用tf.train.string_input_producer创建文件名队列后,整个系统其实还处于“停滞状态”,也就是说,文件名并没有真正被加入队列中,如图2-9所示。如果此时开始计算,因为内存队列中什么也没有,计算单元就会一直等待,导致整个系统被阻塞。
图2-9 未调用tf.train.start_queue_runners时队列处于停滞状态
而使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不再“停滞”。此后,计算单元就可以拿到数据并进行计算,整个程序也就运行起来了,这就是函数tf.train.start_queue_runners的用处。
下面用一个具体的例子体会TensorFlow中的数据读取(对应的程序为test.py)。如图2-10所示,假设在当前文件夹中已经有A.jpg、B.jpg、C.jpg三张图片,希望读取这三张图片的5个epoch并且把读取的结果重新存到read文件夹中。
图2-10 当前文件夹中包含的文件
代码如下(对应的文件为test.py):
# 导入TensorFlow import tensorflow as tf # 新建一个Session with tf.Session() as sess: # 要读3张图片A.jpg, B.jpg, C.jpg filename=['A.jpg', 'B.jpg', 'C.jpg'] # string_input_producer会产生一个文件名队列 filename_queue=tf.train.string_input_producer(filename, shuffle=False, num_epochs=5) # reader从文件名队列中读数据。对应的方法是reader.read reader=tf.WholeFileReader() key, value=reader.read(filename_queue) # tf.train.string_input_producer定义了一个epoch变量,要对它进行初始化 tf.local_variables_initializer().run() # 使用start_queue_runners之后,才会开始填充队列 threads=tf.train.start_queue_runners(sess=sess) i=0 while True: i+=1 # 获取图片数据并保存 image_data=sess.run(value) with open('read/test_%d.jpg' % i, 'wb') as f: f.write(image_data)
这里使用filename_queue=tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)建立了一个会运行5个epoch的文件名队列。并使用reader读取,reader每次读取一张图片并保存。
运行代码后(程序最后会抛出一个OutOfRangeError异常,不必担心,这就是epoch跑完,队列关闭的标志),得到read文件夹中的图片,正好是按顺序的5个epoch,如图2-11所示。
图2-11 shuffle=False时读取出的5个epoch
如果设置filename_queue=tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)中的shuffle=True,那么在每个epoch内图像会被打乱,如图2-12所示。
图2-12 shuffle=True时读取出的5个epoch
这里只是用三张图片举例,实际应用中一个数据集肯定不止3张图片,不过涉及的原理都是共通的。
2.1.4 实验:将CIFAR-10数据集保存为图片形式
介绍了TensorFlow的数据读取的基本原理,再来看如何读取CIFAR-10数据。在CIFAR-10数据集中,文件data_batch_1.bin、data_batch_2.bin、……、data_batch_5.bin和test_batch.bin中各有10000个样本。一个样本由3073个字节组成,第一个字节为标签(label),剩下3072个字节为图像数据。如下所示:
<1 x label><3072 x pixel> ... <1 x label><3072 x pixel>
样本和样本之间没有多余的字节分割,因此这几个二进制文件的大小都是30730000字节。
如何用TensorFlow读取CIFAR-10数据呢?步骤与第2.1.3节类似:
· 第一步,用tf.train.string_input_producer建立队列。
· 第二步,通过reader.read读数据。在第2.1.3节中,一个文件就是一张图片,因此用的reader是tf.WholeFileReader()。CIFAR-10数据是以固定字节存在文件中的,一个文件中含有多个样本,因此不能使用tf.WholeFileReader(),而是用tf.FixedLengthRecordReader()。
· 第三步,调用tf.train.start_queue_runners。
· 最后,通过sess.run()取出图片结果。
遵循上面的步骤,本节会做一个实验:将CIFAR-10数据集中的图片读取出来,并保存为.jpg格式。对应的程序为cifar10_extract.py。读者现在就可以在这个代码里查找,看步骤中的tf.train.string_input_producer、tf.FixedLengthRecordReader()、tf.train.start_queue_runners、sess.run()都在什么地方。
按照程序的执行顺序来看:
if__name__=='__main__': # 创建一个会话sess with tf.Session() as sess: # 调用inputs_origin。cifar10_data/cifar-10-batches-bin是下载数据的 文件夹位置 reshaped_image=inputs_origin('cifar10_data/cifar-10-batches-bin') # 这一步start_queue_runner很重要 # 之前有filename_queue=tf.train.string_input_producer (filenames) # 这个queue必须通过start_queue_runners才能启动 # 若缺少start_queue_runners,程序将不能执行 threads=tf.train.start_queue_runners(sess=sess) # 对变量初始化 sess.run(tf.global_variables_initializer()) # 创建文件夹cifar10_data/raw/ if not os.path.exists('cifar10_data/raw/'): os.makedirs('cifar10_data/raw/') # 保存30张图片 for i in range(30): # 每次sess.run(reshaped_image),都会取出一张图片 image_array=sess.run(reshaped_image) # 将图片保存 scipy.misc.toimage(image_array).save('cifar10_data/raw/%d.jpg' % i)
inputs_origin是一个函数。这个函数中包含了前两个步骤,tf.train.string_input_producer和使用reader。函数的返回值reshaped_image是一个Tensor,对应一张训练图像。下面要做的并不是直接运行sess.run(reshaped_image),而是使用threads=tf.train.start_queue_runners(sess=sess)。只有调用过tf.train.start_queue_runners后,才会让系统中的所有队列真正地“运行”,开始从文件中读数据。如果不调用这条语句,系统将会一直等待。
最后用sess.run(reshaped_image)取出训练图片并保存。此程序一共在文件夹cifar10_data/raw/中保存了30张图片。读者可以打开该文件夹,看到原始的CIFAR-10训练图片,如图2-13所示。
图2-13 cifar10_data/raw/目录下保存的CIFAR-10训练图片
再回过头来看inputs_origin函数:
def inputs_origin(data_dir): # filenames一共5个,从data_batch_1.bin到data_batch_5.bin # 读入的都是训练图像 filenames=[os.path.join(data_dir, 'data_batch_%d.bin' % i) for i in xrange(1, 6)] # 判断文件是否存在 for f in filenames: if not tf.gfile.Exists(f): raise ValueError('Failed to find file: '+f) # 将文件名的list包装成TensorFlow中queue的形式 filename_queue=tf.train.string_input_producer(filenames) # cifar10_input.read_cifar10是事先写好的从queue中读取文件的函数 # 返回的结果read_input的属性uint8image就是图像的Tensor read_input=cifar10_input.read_cifar10(filename_queue) # 将图片转换为实数形式 reshaped_image=tf.cast(read_input.uint8image, tf.float32) # 返回的reshaped_image是一张图片的tensor # 应当这样理解reshaped_image:每次使用sess.run(reshaped_image),就会取出一张图 片 return reshaped_image
tf.train.string_input_producer(filenames)创建了一个文件名队列,其中filenames是一个列表,包含从data_batch_1.bin到data_batch_5.bin一共5个文件名。这正好对应了CIFAR-10的训练集。cifar10_input.read_cifar10 (filename_queue)对应“使用reader”的步骤。为此需要查看cifar10_input.py中的read_cifar10函数,其中关键的代码如下:
# Dimensions of the images in the CIFAR-10 dataset. # See http://www.cs.toronto.edu/~kriz/cifar.html for a description of the # input format. label_bytes=1 # 2 for CIFAR-100 result.height=32 result.width=32 result.depth=3 image_bytes=result.height * result.width * result.depth # Every record consists of a label followed by the image, with a # fixed number of bytes for each. record_bytes=label_bytes+image_bytes # Read a record, getting filenames from the filename_queue. No # header or footer in the CIFAR-10 format, so we leave header_bytes # and footer_bytes at their default of 0. reader=tf.FixedLengthRecordReader(record_bytes=record_bytes) result.key, value=reader.read(filename_queue)
语句tf.FixedLengthRecordReader(record_bytes=record_bytes)创建了一个reader,它每次在文件中读取record_bytes字节的数据,直到文件结束。结合代码,record_bytes就等于1+32*32*3,即3073,正好对应CIFAR-10中一个样本的字节长度。使用reader.read(filename_queue)后,reader从之前建立好的文件名队列中读取数据(以Tensor的形式)。简单处理结果后由函数返回。至此,读者应当对CIFAR-10数据的读取流程及TensorFlow的读取机制相当熟悉了。