21个项目玩转深度学习:基于TensorFlow的实践详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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个字节为图像数据格式介绍参考官方文档,地址为http://www.cs.toronto.edu/~kriz/cifar.html。。如下所示:

    <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的读取机制相当熟悉了。