前面几节内容中,我们都是对小数据集(相对于工业界而言)进行实验,使用CPU环境也可以完美地实现。接下来,我们将使用ImageNet数据集进行实验,该数据集比较大,需要在GPU环境下进行。在对ImageNet数据进行建模之前,我们首先来认识下ImageNet数据集以及对该数据集进行预处理。

ImageNet数据集介绍

ImageNet是一个计算机视觉系统识别项目,是目前世界上图像识别最大的数据库。是美国斯坦福的计算机科学家,模拟人类的识别系统建立的。能够从图片中识别物体。ImageNet是一个非常有前景的研究项目,未来用在机器人身上,就可以直接辨认物品和人了。超过1400万的图像URL被ImageNet手动注释,以指示图片中的对象;在至少一百万张图像中,还提供了边界框。ImageNet包含2万多个类别; 一个典型的类别,如“气球”或“草莓”,每个类包含数百张图像。

下载地址

ImageNet数据集可以直接从该地址中下载,当然你可以根据对应的任务选择相应的数据集下载即可。

备注:我花了好几天将官网的数据下载完,速度比较慢,如果你们有需要的,可以留言,我上传到百度网盘,分享给你们(很容易被和谐,所以单独发给你们)。

ImageNet数据说明

下载完ImageNet数据集之后,你会发现里面包含了很多文件,虽然磁盘上有将近2百万的图片数据,但是没有一个比较直观的名字,且没有一个很明显的方式能够知道每张图像对应的标签信息,当我们对这些数据集训练一个特定的卷积神经网络时,需要提前进行预处理。

在本节中,首先,我们将分析ImageNet数据文件结构,包括原始图像数据和开发工具包(“DevKit”)。之后,我们将编写一个Python脚本,解析ImageNet文件和对应标签,即将给定的输入文件名映射到相应的标签(每行一个文件名和标签)。

最后,我们利用Tensorflow对数据创建更高效的TFRecord (.tfrecords)文件。当数据大到无法放入内存时,我们可以使用这些文件数据进行训练深度学习模型。后面我们将看到,这种.tfrecords格式不仅比HDF5更紧凑,而且它的I/O效率也更高,使我们能够更快地训练网络。

当数据处理完之后,就方便我们后续对ImageNet数据集从头开始训练自定义的CNNs网络结构。

ImageNet文件结构

首先,我们先认识下ImageNet数据文件结构。假设你已经下载完了ILSVRC2016_CLS_LOC.tar.gz文件(数据本身比较大,约166G)。对文件进行解压:

1
$ tar -xvf ILSVRC2016_CLS-LOC.tar.gz

这个解压相对而言需要花点时间,因为本身数据文件比较多。

解压完成之后,将得到一个名为ILSVRC的目录:

进入ILSVRC文件夹,您将发现三个子目录:

1
2
3
$ cd ILSVRC
$ ls
Annotations Data ImageSets

其中:

  • Annotations:物体位置标注数据文件,一般是在物体检测任务中使用到,目前我们可以忽略这个数据集
  • Data:数据文件夹,这个是我们需要重点关注的,里面包含了train、val和test原始图像数据
  • ImageSets:图像对应的属性信息

进入Data数据目录,我们将看到一个名为CLS-LOC的子目录:

1
2
3
4
$ ls Data/
CLS-LOC
$ ls Data/CLS-LOC
test train val

而在子目录,我们将找到我们需要处理的train、val和test数据集。

接下来,我们将重点分析这三个目录文件。

test目录

test目录包含10万张图像(1000个类,每一类中都有100个数据),如:

1
2
3
4
5
6
7
8
9
10
$ ls -l Data/CLS-LOC/test/ | head -n 10
-rw-r--r-- 1 lone lone 33889 7月 1 2012 ILSVRC2012_test_00000001.JPEG
-rw-r--r-- 1 lone lone 122117 7月 1 2012 ILSVRC2012_test_00000002.JPEG
-rw-r--r-- 1 lone lone 26831 7月 1 2012 ILSVRC2012_test_00000003.JPEG
-rw-r--r-- 1 lone lone 124722 7月 1 2012 ILSVRC2012_test_00000004.JPEG
-rw-r--r-- 1 lone lone 98627 7月 1 2012 ILSVRC2012_test_00000005.JPEG
-rw-r--r-- 1 lone lone 211157 7月 1 2012 ILSVRC2012_test_00000006.JPEG
-rw-r--r-- 1 lone lone 219906 7月 1 2012 ILSVRC2012_test_00000007.JPEG
-rw-r--r-- 1 lone lone 181734 7月 1 2012 ILSVRC2012_test_00000008.JPEG
-rw-r--r-- 1 lone lone 10696 7月 1 2012 ILSVRC2012_test_00000009.JPEG

这些数据是没有对应的标签信息,因此,我们无法将这些图像数据直接用于我们的实验。实际上每年都会有一次ILSVRC比赛,使用的数据就是ImageNet数据,为了保证这个比赛公平(并确保没有人作弊),test数据集的标签是保密的。

首先,每个参赛者/组织使用train和val数据集来训练和评估他们的算法。一旦他们训练好模型,然后就会在test数据集上进行预测。随后将预测结果上传到ImageNet服务器,并与真实标签进行比较(任何情况下,任何参赛者都不能使用test数据集的真实标签)。最后,ImageNet服务器将返回它们的总体精度。

所以,我们将忽略test数据集目录,而是从train数据集中提取一部分子集作为test数据集。这样,我们可以在本地评估整个模型的性能。

train目录

train目录中由一系列子目录组成,如下所示:

1
2
3
4
5
6
7
8
9
10
$ ls -l Data/CLS-LOC/train/ | head -n 10
drwxr-xr-x 2 lone lone 57344 9月 29 2014 n01440764
drwxr-xr-x 2 lone lone 65536 9月 29 2014 n01443537
drwxr-xr-x 2 lone lone 57344 9月 29 2014 n01484850
drwxr-xr-x 2 lone lone 65536 9月 29 2014 n01491361
drwxr-xr-x 2 lone lone 61440 9月 29 2014 n01494475
drwxr-xr-x 2 lone lone 61440 9月 29 2014 n01496331
drwxr-xr-x 2 lone lone 49152 9月 29 2014 n01498041
drwxr-xr-x 2 lone lone 65536 9月 29 2014 n01514668
drwxr-xr-x 2 lone lone 61440 9月 29 2014 n01514859

这些目录名称看样子好像没有包含任何图像信息。实际上,ImageNet数据集是根据WordNet IDs映射的,称为同义词集或简称为“syn sets”。每个ID映射到特定的数据标签,如金鱼、秃鹰、飞机或吉他。因此,这些文件名实际上是每一类对应的WordNet ID,并且在这些标签子目录中,每个类大约有732到1300张图像。

例如,WordNet ID为n01440764的子目录包含了1300张 “tench”图像,“tench”是一种欧洲淡水鱼,与minnow类很相似(如图13.1所示):

1
2
3
4
5
6
7
8
$ ls -l Data/CLS-LOC/train/n01440764/*.JPEG | wc -l
1300
$ ls -l Data/CLS-LOC/train/n01440764/*.JPEG | head -n 5
-rw-r--r-- 1 lone lone 13697 6月 11 2012 Data/CLS-LOC/train/n01440764/n01440764_10026.JPEG
-rw-r--r-- 1 lone lone 9673 6月 11 2012 Data/CLS-LOC/train/n01440764/n01440764_10027.JPEG
-rw-r--r-- 1 lone lone 67029 6月 11 2012 Data/CLS-LOC/train/n01440764/n01440764_10029.JPEG
-rw-r--r-- 1 lone lone 146489 6月 11 2012 Data/CLS-LOC/train/n01440764/n01440764_10040.JPEG
-rw-r--r-- 1 lone lone 6350 6月 11 2012 Data/CLS-LOC/train/n01440764/n01440764_10042.JPEG

图13.1
因此,对于这些由WordNet IDs命名的train数据集,我们将直接与train_cls.txt进行匹配,可以直接得到数据对应的标签名称。

val目录

与test数据集目录类似,val数据集目录包含50000张图像(1000个类中每类有50张图像):

1
2
$ ls -l Data/CLS-LOC/val/*.JPEG | wc -l
50000

这50000张图像直接存放在一个目录文件夹中,这意味着我们无法使用目录名直接映射得到数据对应的标签名称。

1
2
3
4
5
6
7
8
9
10
11
$ ls -l Data/CLS-LOC/val/ | head -n 10
total 6648996
-rw-r--r-- 1 lone lone 109527 6月 13 2012 ILSVRC2012_val_00000001.JPEG
-rw-r--r-- 1 lone lone 140296 6月 13 2012 ILSVRC2012_val_00000002.JPEG
-rw-r--r-- 1 lone lone 122660 6月 13 2012 ILSVRC2012_val_00000003.JPEG
-rw-r--r-- 1 lone lone 84885 6月 13 2012 ILSVRC2012_val_00000004.JPEG
-rw-r--r-- 1 lone lone 130340 6月 13 2012 ILSVRC2012_val_00000005.JPEG
-rw-r--r-- 1 lone lone 151397 6月 13 2012 ILSVRC2012_val_00000006.JPEG
-rw-r--r-- 1 lone lone 165863 6月 13 2012 ILSVRC2012_val_00000007.JPEG
-rw-r--r-- 1 lone lone 107423 6月 13 2012 ILSVRC2012_val_00000008.JPEG
-rw-r--r-- 1 lone lone 114708 6月 13 2012 ILSVRC2012_val_00000009.JPEG

另外,通过检查文件名,我们没有看到类似于WordNet IDs中ID命名格式。但是,在val.txt文件中,提供了val数据集文件名到类标签的映射关系。

ImageSets目录

ImageSets目录中主要存放的是标签的映射关系数据。如下所示:

1
2
3
4
5
6
$ cd ImageSets/
$ ls
CLS-LOC
$ cd CLS-LOC
$ ls
test.txt train_cls.txt train_loc.txt val.txt

目录中包含了四个txt文件,我们可以忽略test.txt文件,因为我们将从train数据集中构建自己的test数据集。train_cls.txt(其中“cls”代表“分类”)包含了train数据集(1281167张图像)的文件名映射到标签数据,val.txt包含了val数据集(50,000)的标签数据(直接对应index)。即:

1
2
3
4
$ wc -l train_cls.txt val.txt
1281167 train_cls.txt
50000 val.txt
1331167 total

两个文件总共包含了1331167张需要处理的图像。首先,我们可以看到train_cls.txt文件内容,每行都是由一个基本的图像文件名(没有文件扩展名)和一个唯一的整数ID组成,即:

1
2
3
4
5
6
7
8
9
10
11
$ head -n 10 train_cls.txt
n01440764/n01440764_10026 1
n01440764/n01440764_10027 2
n01440764/n01440764_10029 3
n01440764/n01440764_10040 4
n01440764/n01440764_10042 5
n01440764/n01440764_10043 6
n01440764/n01440764_10048 7
n01440764/n01440764_10066 8
n01440764/n01440764_10074 9
n01440764/n01440764_10095 10

该整数只是一个递增的计数器,没有特殊含义。val.txt也类似,即:

1
2
3
4
5
6
7
8
9
10
11
$ head -n 10 val.txt
ILSVRC2012_val_00000001 1
ILSVRC2012_val_00000002 2
ILSVRC2012_val_00000003 3
ILSVRC2012_val_00000004 4
ILSVRC2012_val_00000005 5
ILSVRC2012_val_00000006 6
ILSVRC2012_val_00000007 7
ILSVRC2012_val_00000008 8
ILSVRC2012_val_00000009 9
ILSVRC2012_val_00000010 10

备注:这些整数ID并不太有用,除非我们需要确定“黑名单”图像——由ImageNet数据集管理员标记为“黑名单”的图像,由于该图像的类标签过于模糊,因此在评估过程中我们不考虑这些图像。在后部分内容中,我们将遍历所有被列入黑名单的图像,并通过该整数ID映射从数据集中删除。

使用train_cls.txt和val.txt文件,有一个好处就是我们不必使用额外的路径类似paths.list_images将train和val数据列举出。相反,我们只需要遍历这两个文件,并合理拼接图像信息,就可以构建TFRecord文件。

DevKit目录

除了下载原始图像本身外,还需要下载ILSVRC2016_devkit.tar.gz。这个文件包含实际的索引文件、validation数据中黑名单图像id和图像文件名映射到相应的实际类标签等数据。将ILSVRC2016_devkit.tar.gz文件放到与ILSVRC同目录并进行解压:

1
$ tar -xvf ILSVRC2016_devkit.tar.gz

在ILSVRC目录中,你将会发现新增了一个devkit文件目录:

1
2
3
$ cd ILSVRC/
$ ls
Annotations Data devkit ImageSets

进入devkit目录:

1
2
3
$ cd devkit/
$ ls
COPYING data evaluation readme.txt

里面包含了四个文件,这里我们只需要关心data目录即可。在data目录中,你会发现很多文件,都是MATLAB和纯文本(.txt)格式,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cd data
$ ls -l
total 6052
-rw-r--r-- 1 lone lone 10216 6月 1 2016 ILSVRC2015_clsloc_validation_blacklist.txt
-rw-r--r-- 1 lone lone 1167074 6月 1 2016 ILSVRC2015_clsloc_validation_ground_truth.mat
-rw-r--r-- 1 lone lone 194650 6月 1 2016 ILSVRC2015_clsloc_validation_ground_truth.txt
-rw-r--r-- 1 lone lone 6455 6月 1 2016 ILSVRC2015_det_validation_blacklist.txt
-rw-r--r-- 1 lone lone 635183 6月 1 2016 ILSVRC2015_det_validation_ground_truth.mat
-rw-r--r-- 1 lone lone 2219755 9月 3 2016 ILSVRC2015_vid_validation_ground_truth.mat
-rw-r--r-- 1 lone lone 1812198 9月 3 2016 ILSVRC2015_vid_validation_track_ground_truth.mat
-rw-r--r-- 1 lone lone 24366 6月 1 2016 map_clsloc.txt
-rw-r--r-- 1 lone lone 4479 6月 1 2016 map_det.txt
-rw-r--r-- 1 lone lone 598 6月 1 2016 map_vid.txt
-rw-r--r-- 1 lone lone 83277 6月 1 2016 meta_clsloc.mat
-rw-r--r-- 1 lone lone 10355 6月 1 2016 meta_det.mat
-rw-r--r-- 1 lone lone 1685 6月 1 2016 meta_vid.mat

在这个目录中,我们最关心的是以下三个文件:

  • map_clsloc.txt
  • ILSVRC2015_clsloc_validation_ground_truth.txt
  • ILSVRC2015_clsloc_validation_blacklist.txt

map_clsloc.txt文件主要包含WordNet ID映射到图像真实的类标签数据,比如:

1
2
3
4
5
6
7
8
9
10
11
$ head -n 10 map_clsloc.txt
n02119789 1 kit_fox
n02100735 2 English_setter
n02110185 3 Siberian_husky
n02096294 4 Australian_terrier
n02102040 5 English_springer
n02066245 6 grey_whale
n02509815 7 lesser_panda
n02124075 8 Egyptian_cat
n02417914 9 ibex
n02123394 10 Persian_cat

我们可以看到
n02119789对应到kit_fox类标签。n02096294对应到Australian_terrier(是狗的一种品种)。val目录中的图像不包含任何WordNet ID信息,但是在ImageSets中包含一个val.txt文件,该文件包含了val数据集的文件名(没有图像扩展名),而在ILSVRC2015_clsloc_validation_ground_truth.txt中包含了val数据集的标签,即:

1
2
3
4
5
6
7
8
9
10
11
$ head -n 10 ILSVRC2015_clsloc_validation_ground_truth.txt
490
361
171
822
297
482
13
704
599
164

每一行都只有一个整数。取val.txt的第一行和ILSVRC2015_clsloc_validation_ground_truth的第一行。最后我们得到:

1
(ILSVRC2015_val_0000000001,490)

图13.2
如果我们打开ILSVRC2012_val_00000001.JPEG。我们将看到图13.2中的图像。很明显,这是一种蛇——但是哪种蛇呢?如果我们检查map_clsloc.txt,我们可以看到带有490的类标签ID是WordNet ID=n01751748,它是sea_snake:
1
2
3
$ grep '490' map_clsloc.txt

n01751748 490 sea_snake

因此,我们需要同时使用val.txt和ILSVRC2015_clsloc_validation_ground_truth.txt来构建我们的val数据集。

接下来,查看ILSVRC2015_clsloc_validation_blacklist.txt,即:

1
2
3
4
5
6
7
8
9
10
11
$ head -n 10 ILSVRC2015_clsloc_validation_blacklist.txt
36
50
56
103
127
195
199
226
230
235

正如我前面提到的,val数据集中包含一些类标签中过于模糊的图像数据。因此,ILSVRC组织者将这些图像数据标记为“黑名单”,这意味着这些数据不应该包含在val数据集中。在构建val数据集时,我们也需要排除这些黑名单id对应的图像数据。

可以看到,构建ImageNet数据集需要很多文件。我们不仅需要原始图像本身,还需要一些.txt文件,用于从原始train和val数据集中提取相应类标签。接下来,我们将对ImageNet数据进行预处理并保存到TFRecord文件中。

构建ImageNet数据集

对原始的ImageNet数据进行处理,主要是便于后续我们训练一个自定义的CNN网络结构,我们会将原始图像信息保存到TFRecord文件中。另外,我们还需要计算train数据集的RGB通道的均值,并保存磁盘中。

配置文件

我们将按照以下目录结构进行开发流程:

1
2
3
4
5
6
7
8
9
10
--- tf_imagenet_alexnet
| |--- pyimagesearch
| |--- config
| | |--- __init__.py
| | |--- imagnet_alexnet_config.py
| |--- imagenet
| |--- outputs/
| |--- build_imagenet.py
| |--- test_alexnet.py
| |--- train_alexnet.py

正如目录和文件名所示,这个配置文件是为AlexNet网络准备的。在config目录中,包含了两个文件:

  • init.py
  • imagenet_alexnet_config.py

init.py文件将config转换成Python包,实际上可以通过import语句导入到我们自己的脚本中—这个文件使我们能够在实际配置中使用Python语法/库,从而构建网络时更加方便。imagenet_alexnet_config.py包含我们整个项目的配置信息。

在imagenet目录中新建一个tfrecords目录,主要是存放我们保存的TFRecord文件数据。

1
$ mkdir imagenet/tfrecords

build_imagenet.py脚本主要是构建从输入图像文件到输出类标签的映射以及将数据保存到TFRecord文件中。train_alexnet.py脚本主要是在ImageNet数据集上从头开始训练AlexNet网络。最后,test_alexnet.py脚本主要是利用测试集验证AlexNet模型的性能。

本节我们主要是对ImageNet数据进行预处理。主要是完成build_imagenet.py脚本的内容。首先,我们先配置整个项目的配置信息。

打开imagenet_alexnet_config.py并写入以下代码:

1
2
3
4
5
#encoding:utf-8
from os import path

# 定义Imagenet数据集路径
BASE_PATH = 'yourPath/ILSVRC'

首先加载所需模块,这里path是os的一个子模块。path模块包含一个名为path的特殊变量。这是操作系统的路径分隔符。在Unix机器上,路径分隔符是/ -一个示例文件路径可能看起来像path/to/your/file.txt。然而,在Windows上,路径分隔符是\,比如示例文件路径path\your\file.txt。我们希望配置与操作系统无关,因此我们将使用path的sep变量。

然后我们定义个一个base_path,该路径下包含所有原始图像数据信息。

从BASE_PATH中,我们可以拼接出三个更重要的路径:

1
2
3
4
# 基于base path 定义原始图像和工具路径
IMAGES_PATH = path.sep.join([BASE_PATH,'Data/CLS-LOC'])
IMAGE_SETS_PATH = path.sep.join([BASE_PATH,'ImageSets/CLS-LOC/'])
DEVKIT_PATH = path.sep.join([BASE_PATH,'devkit/data'])

其中:

  • IMAGES_PATH:包含train、val和test数据集。
  • IMAGE_SETS_PATH:包含重要train_cls.txt和val.txt。
  • DEVKIT_PATH:是DevKit所在位置的基本路径。

接下来,定义WORD_IDS,即map_clsloc.txt文件的路径,它将1000个WordNet ID映射为:

  • 唯一的标识整数
  • 实际可读标签。
1
2
# 定义WordNet IDs文件路径
WORD_IDS = path.sep.join([DEVKIT_PATH,'map_clsloc.txt'])

为了构建train数据集,我们需要定义TRAIN_LIST,这个路径包含了120万个(部分)train数据的图像文件名:

1
2
# 定义training文件路径
TRAIN_LIST = path.sep.join([IMAGE_SETS_PATH,'train_cls.txt'])

接下来,我们需要定义一些val数据集相关配置:

1
2
3
4
5
# 定义验证集数据路径以及对应的标签文件路径
VAL_LIST = path.sep.join([IMAGE_SETS_PATH,'val.txt'])
VAL_LABELS = path.sep.join([DEVKIT_PATH,'ILSVRC2015_clsloc_validation_ground_truth.txt'])
# 定义val blacklisted 文件路径
VAL_BLACKLIST = path.sep.join([DEVKIT_PATH,'ILSVRC2015_clsloc_validation_blacklist.txt'])

VAL_LIST:ImageSets目录中的val.txt文件。注意,val.txt列出了50,000个(部分)图像文件名。为了获得val数据的真实标签,我们需要定义val_tags路径—这样我们就可以将文件名和标签组合一起。最后,VAL_BLACKLIST文件包含已被列入黑名单的val数据中的唯一整数id。在构建ImageNet数据集时,我们需要注意确保这些图像不包含在验证数据中。

定义额外一些变量:

1
2
3
4
# 定义类别个数
# 定义我们需要从train数据集中划分一个子集作为test数据集
NUM_CLASSES = 1000
NUM_TEST_IMAGES = 50 * NUM_CLASSES

ImageNet数据集中包含1000个类别图像,因此,NUM_CLASSES等于1000。为了得到我们的test数据集,我们需要从train数据集中提取一些子集作为test数据集。我们将NUM_TEST_IMAGES设置为50 * 1000 = 50000张图像。

接下来,我们定义保存TFRecord文件路径:

1
2
3
4
5
# 定义tfrecord文件的输出路径
TF_OUTPUT = 'imagenet'
TRAIN_TFRECORD = path.sep.join([TF_OUTPUT,'tfrecords/train.tfrecords'])
VAL_TFRECORD = path.sep.join([TF_OUTPUT,'tfrecords/val.tfrecords'])
TEST_TFRECORD = path.sep.join([TF_OUTPUT,'tfrecords/test.tfrecords'])

这里稍微提下,在TFRecord文件里保存的是二进制文件信息,而不是图像的numpy数组信息(HDF5保存方式),这样我们能够获得比之前使用的HDF5数据集更好的性能以及更小的磁盘使用量。

在构建数据集时,我们还需要对train数据集计算RGB三个颜色通道的平均值,并保存到磁盘中:

1
2
# 定义均值文件路径
DATASET_MEAN = 'outputs/imagenet_mean.json'

预处理

上面,我们完成了配置文件的创建,接下来,我们将处理图像数据,比如获取对应的标签信息,处理黑名单数据等等。在utils子目录中新建一个名为imagenethelper.py的文件,如下目录结构:

1
2
3
4
5
6
7
8
9
10
--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- io
| |--- nn
| |--- preprocessing
| |--- utils
| | |--- __init__.py
| | |--- imagenettfrecord.py
| | |--- imagenethelper.py

打开imagenethelper.py,并写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
#encoding:utf-8
import numpy as np
import progressbar
import os

class ImageNetHelper(object):
def __init__(self,config):
# 配置
self.config = config
# 标签映射
self.labelMappings = self.buildClassLabels()
self.valBlacklist = self.buildBlacklist()

上面我们定义了一个ImageNetHelper类。并且只需要传一个config的参数。所有的信息我们将从config变量中提取。

接下来,我们定义一个_pbar函数,该函数主要功能是显示进度条,方便我们实时监控脚本运行情况。

1
2
3
4
5
6
def _pbar(self,maxval,name):
widgets = [name, progressbar.Percentage(), ' ',
progressbar.Bar(), ' ', progressbar.ETA()]
pbar = progressbar.ProgressBar(maxval=maxval,
widgets=widgets).start()
return pbar

其中:

  • maxval: 最大数据量
  • name:进度条显示名称

接着,我们构建一个类标签映射字典:

1
2
3
4
5
def buildClassLabels(self):
# 文件名映射类标签
# n02110185 3 Siberian_husky
rows = open(self.config.WORD_IDS).read().strip().split('\n')
labelMappings = {}

读取WORD_IDs文件的全部内容,并构建一个labelMappings字典,其中key为WordNet ID,值为整数类标签。

接下来,遍历整个文件内容,对于每一行,我们分解为一个3元祖,即:

  • WordNet ID (wordID)。
  • 唯一整数类标签ID(标签)。
  • 实际可读标签。
1
2
3
4
for row in rows:
(wordId,label,hrLabel) = row.split(" ")
labelMappings[wordId] = int(label) - 1
return labelMappings

这里,我们对整数标签值减1,为什么要减1呢?ILSVRC提供的ImageNet工具是使用MATLAB构建的。MATLAB编程语言是单索引的(即从1开始计数),而Python编程语言是零索引的(我们从0开始计数)。

接下来,处理黑名单id数据:

1
2
3
4
5
def buildBlacklist(self):
# 验证集
rows = open(self.config.VAL_BLACKLIST).read()
rows = set(rows.strip().split("\n"))
return rows

处理train数据集:

1
2
3
4
5
6
7
8
def buildTrainingSet(self):
# 训练数据集
# n01440764/n01440764_12131 189
rows = open(self.config.TRAIN_LIST).read().strip()
rows = rows.split('\n')
paths = []
labels = []
probar = self._pbar(name='building training set: ',maxval=len(rows))

TRAIN_LIST文件部分内容如下所示,我们将根据该结构提取数据:

1
2
3
4
5
6
7
8
9
10
n01440764/n01440764_10026 1
n01440764/n01440764_10027 2
n01440764/n01440764_10029 3
n01440764/n01440764_10040 4
n01440764/n01440764_10042 5
n01440764/n01440764_10043 6
n01440764/n01440764_10048 7
n01440764/n01440764_10066 8
n01440764/n01440764_10074 9
n01440764/n01440764_10095 10

我们需要完成:

  • 通过字符拼接,获取完成的图像路径
  • 提取图像对应的类标签

对原始数据进行遍历:

1
2
3
4
5
6
7
8
9
for i,row in enumerate(rows):
(partialPath,imageNum) = row.strip().split(" ")
# 原始图像数据路径
path = os.path.sep.join([self.config.IMAGES_PATH,
'train','{}.JPEG'.format(partialPath)])
# wordId
wordId = partialPath.split("/")[0]
label = self.labelMappings[wordId]

其中:

  • partialPath对应图像的文件名,比如:n01440764/n01440764_10026。
  • imageNum变量只是一个计数器——它在构建train数据集时没有任何用途,可以忽略。

一个完整的图像路径主要由:

  • IMAGES_PATH:该路径包含了train、val和test数据目录
  • train字符串表示我们处理的train数据集
  • partialPath:图像的子目录以及文件名

比如:

1
yourPath/ILSVRC/Data/CLS-LOC/train/n15075141/n15075141_999.JPEG

提取完完整图像路径和标签之后,我们对列表进行更新:

1
2
3
4
5
    paths.append(path)
labels.append(label)
probar.update(i)
probar.finish()
return (np.array(paths),np.array(labels))

val数据处理步骤类似于train数据,即:

1
2
3
4
5
6
7
8
9
10
11
12
def buildValidationSet(self):
paths = []
labels = []
#验证数据
# ILSVRC2012_val_00000001 1
valFilenames = open(self.config.VAL_LIST).read()
valFilenames = valFilenames.strip().split('\n')

# 验证集对应的标签
# 490
valLabels = open(self.config.VAL_LABELS).read()
valLabels = valLabels.strip().split("\n")

构建完整的图像路径和对应的标签信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
probar = self._pbar(name='building validation set: ',maxval=len(valFilenames))
for i,(row,label) in enumerate(zip(valFilenames,valLabels)):
(partialPath,imageNum) = row.strip().split(" ")

if imageNum in self.valBlacklist:
continue
#val数据集真实图片数据
path = os.path.sep.join([self.config.IMAGES_PATH,'val',
"{}.JPEG".format(partialPath)])
paths.append(path)
labels.append(int(label) - 1)
probar.update(i)
probar.finish()
return (np.array(paths),np.array(labels))

其中,我们增加一个黑名单id判断逻辑,如果该图像对应的id落在黑名单列表中,则直接过滤掉该图像信息。

以上我们完成图像数据的处理,接下来,我们将构建一个ImageNetTfrecord类,主要负责将图像数据写入TFRecord文件中。

TFRecord

前面我们提到对于小数据,我们使用的是HDF5进行保存数据(我们直接将图像的numpy数组信息存入HDF5中),对于大数据集,我们建议使用TFRecord文件进行存储(当然,这里我们是使用Tensorflow框架为前提条件)。在该部分,我们将学习如何将我们的数据转换为Tensorflow标准格式。对于如何将原始数据转化为tfrecords文件,可以参考该篇文章,这里就不做详细描述。

总的来说,在我们将数据存储到TFRecord文件之前,我们应该将它填入名为Example的协议缓冲区中。

然后,我们将协议缓冲区序列化为字符串并将其写入TFRecord文件,协议缓冲区示例包含功能。

Feature是一种描述数据的协议,可以有三种类型:bytes,float和int64。 总之,要存储数据,您需要按照以下步骤操作:

  • 使用tf.python_io.TFRecordWriter打开TFRecord文件
  • 使用tf.train.Int64List,tf.train.BytesList或tf.train.FloatList将数据转换为该功能的正确数据类型
  • 使用tf.train.Feature创建一个功能,并将转换后的数据传递给它
  • 使用tf.train.Example创建一个示例协议缓冲区,并将该功能传递给它
  • 使用example.SerializeToString()将Example序列化为字符串
  • 使用writer.write将序列化示例写入TFRecord文件

接下来,我们将按照上述步骤实现将数据保存到TFRecord文件的功能,在pyimagesearch的子目录utils中新建一个名为imagenettfrecord.py的文件,并写入以下代码:

1
2
3
4
5
6
7
8
#encoding:utf-8
from os import path
import tensorflow as tf

class ImageNetTfrecord(object):
def __init__(self,tfrecord_name):
self.tfrecord_name = tfrecord_name
self.tfwriter = tf.python_io.TFRecordWriter(self.tfrecord_name)

首先,我们创建了一个ImageNetTfrecord类,该类只有一个参数,即:

  • tfrecord_name: 保存数据的TFRecord文件路径

并且,我们初始化了一个TFRecord文件写入器—tfwriter

接下来,我们定义数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def _int64_feature(self,value):
"""Wrapper for inserting int64 features into Example proto."""
if not isinstance(value, list):
value = [value]
return tf.train.Feature(int64_list=tf.train.Int64List(value=value))

def _float_feature(self,value):
"""Wrapper for inserting float features into Example proto."""
if not isinstance(value, list):
value = [value]
return tf.train.Feature(float_list=tf.train.FloatList(value=value))

def _bytes_feature(self,value):
"""Wrapper for inserting bytes features into Example proto."""
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

注意:在类型转换中,每一个value都是使用list进行包装的,即value=[value]

1
2
3
4
5
def _process_image(self,filename):
"""Process a single image file."""
with tf.gfile.FastGFile(filename, 'rb') as f:
image_data = f.read()
return image_data

这里我们使用tf.gfile.FastGFile该函数获取图像数据,该image_data不是图像的numpy数组数据,而是一个二进制信息。

接下来,将image_data保存到TFRecord文件中:

1
2
3
4
5
6
7
8
9
10
def _save_one(self,label,filename,isTrain=True):
image_data = self._process_image(filename)
name = path.split(filename)[-1]
if isTrain:
example = tf.train.Example(features=tf.train.Features(feature={
'image': self._bytes_feature(tf.compat.as_bytes(image_data)),
'label': self._int64_feature(label),
'name': self._bytes_feature(tf.compat.as_bytes(name))
}))
self.tfwriter.write(example.SerializeToString())

如果数据类型为test,由于test数据集没有对应标签信息,因此这里我们默认使用-1代替。即:

1
2
3
4
5
6
7
8
else:
label = int(-1)
example = tf.train.Example(features=tf.train.Features(feature={
'image': self._bytes_feature(tf.compat.as_bytes(image_data)),
'label': self._int64_feature(label),
'name': self._bytes_feature(tf.compat.as_bytes(name))
}))
self.tfwriter.write(example.SerializeToString())

构建TFRecord和均值文件

像前几节的build_*.py脚本一样,build_imagenet.py主要实现:

  • 建立training数据集
  • 建立validation数据集
  • 从training数据集中提取一小部分子集作为testing数据集
  • 将数据集写入TFRecord文件,并保存到磁盘中
  • 计算train数据集的RGB通道均值数据,并保存到磁盘中

在pyimagesearch根目录下,新建一个名为build_imagenet.py文件,并写入以下代码:

1
2
3
4
5
6
7
8
#encoding:utf-8
import cv2
import json
import numpy as np
from sklearn.model_selection import train_test_split
from config import imagenet_alexnet_config as config
from pyimagesearch.utils.imagenethelper import ImageNetHelper
from pyimagesearch.utils.imagenettfrecord import ImageNetTfrecord

这里加载了我们之前构建好的配置模块imagenet_alexnet_config,以及数据预处理模块ImageNetHelper和写入TFRecord文件模块ImageNetTfrecord。

接下来,构建training数据集和validation数据集以及对应的标签数据:

1
2
3
4
print('[INFO] loading image paths...')
inh = ImageNetHelper(config)
(trainPaths,trainLabels) = inh.buildTrainingSet()
(valPaths,valLabels) = inh.buildValidationSet()

然后,我们从trainPaths和trainlabel提取出NUM_TEST_IMAGES大小数据量,作为我们的testing数据,即:

1
2
3
4
5
print('[INFO] constructing splits...')
split = train_test_split(trainPaths,trainLabels,
test_size = config.NUM_TEST_IMAGES,stratify=trainLabels,
random_state=42)
trainPaths,testPaths,trainLabels,testLabels = split

接着,我们将三类数据集合并到一个datasets列表中:

1
2
3
4
5
6
datasets = [
('train',trainPaths,trainLabels,config.TRAIN_TFRECORD),
('val',valPaths,valLabels,config.VAL_TFRECORD),
('test',testPaths,testLabels,config.TEST_TFRECORD)
]
(R,G,B) = [],[],[]

其中,列表中的每一个value由一个4元组组成,即:

  • 数据的类型
  • 图像数据路径
  • 图像数据对应标签
  • TFRecord文件名

接下来,遍历datasets数据列表:

1
2
3
4
5
for (dType,paths,labels,outputPath) in datasets:
print('[INFO] building {}...'.format(outputPath))
inr = ImageNetTfrecord(outputPath)

probar = inh._pbar(name='Building %s List: '%dType,maxval=len(paths))

首先,初始化ImageNetTfrecord类,用来将数据写入TFRecord文件中,其次,调用inh类中的进度条函数_pbar,该函数有助于我们实时掌握数据处理过程。

对每一类型数据集进行遍历:

1
2
3
4
5
6
7
8
9
10
11
12
for (i,(path,label)) in enumerate(zip(paths,labels)):
inr._save_one(label=label, filename=path, isTrain=True)
# 如果是train数据,则计算RGB均值信息
if dType == 'train':
image = cv2.imread(path)
(b,g,r) = cv2.mean(image)[:3]
R.append(r)
G.append(g)
B.append(b)
probar.update(i)
probar.finish()
inr.tfwriter.close()

将每条数据写入TFRecord文件中,如果数据类型为train,我们还需计算该数据集的RGB颜色通道均值,并更新相应的通道列表数据。

最后,将均值文件写入磁盘中:

1
2
3
4
print('[INFO] serializing means...')
D = {'R':np.mean(R),'G':np.mean(G),'B':np.mean(B)}
with open(config.DATASET_MEAN,'w') as f:
f.write(json.dumps(D))

以上,我们完成了整个执行脚本的构建,接着执行build_imagenet.py脚本,完成整个数据处理工作:

1
2
3
4
5
6
7
$ python build_imagenet.py
[INFO] loading image paths...
building training set: 100% |########################################################| Time: 0:00:02
building validation set: 100% |########################################################| Time: 0:00:00
[INFO] constructing splits...
[INFO] building imagenet/tfrecords/train.tfrecords...
Building train List: 0% |

由于train数据集,我们需要用cv2模块读取图像的numpy数组数据,并计算RGB通道均值数据,因此相对于val和test数据,我们需要更多的时间处理train数据集。并且由于我们所需要做的只是将图像路径和标签写入文件(不需要额外的I/O),所以val数据和test数据会很快的写入TFRecord文件中并保存到磁盘中。

当执行完之后,你将在imagenet目录中tfrecords子目录中看到三个文件;

1
2
3
4
$ ls -lht
5.4G test.tfrecords
6.1G val.tfrecords
132G train.tfrecords

我们可以看到train.tfrecords文件是最大的,达到了132G。val.tfrecords文件为6.1G和test.tfrecords文件大小为5.4G。这样做的好处是,我们可以将整个ImageNet数据集有效地压缩到压缩记录文件中。压缩不仅节省了我们的磁盘空间,而且由于需要执行的I/O操作更少,它还将大大加快训练过程。

这种方法与HDF5方法不同,HDF5方法需要存储每个图像的原始数字数组信息。如果我们使用HDF5(256x256x3图像)存储ImageNet,结果文件将超过1.9TB,所以对于大数据集,使用TFRecord文件进行存储比较合理。

总结

在本章中,我们学习了如何预处理ImageNet数据集。首先,我们介绍了ImageNet数据文件结构以及数据说明,之后,我们构建了整个项目的配置文件,一般而言,配置文件不会怎么变化,接着,我们创建了两个py脚本,一个是关于ImageNet数据处理脚本,另一个是将数据写入TFRecord文件脚本。最后,脚本build_imagenet.py完成了整个数据集构造过程。后面,我们将直接从TFRecord文件中读取数据,并训练深度学习模型。