到目前为止,我们使用的数据集都能够全部加载到内存中。对于小数据集,我们可以加载全部图像数据到内存中,进行预处理,并进行前向传播处理。然而,对于大规模数据集(比如ImageNet),我们需要创建数据生成器,每次只访问一小部分数据集(比如mini-batch),然后对batch数据进行预处理和前向传播。

Keras模块很方便进行数据加载,可以使用磁盘上的原始文件路径作为训练过程的输入。你不需要将整个数据集存储在内存中——只需为Keras数据生成器提供图像路径,生成器会自动从路径中加载数据并进行前向传播。

然而,这种方法非常低效。读取磁盘上的每张图像都需要一个I/O操作,这样会造成一定的延迟。训练深度学习网络本身已经够慢了,所以我们应该尽可能避免I/O瓶颈。

一个比较合理的解决方案是将原始图像生成HDF5数据集,就像我们第3章中所做的那样,只是这一次我们存储的是原始图像,而不是提取的特征。HDF5不仅可以存储大量的数据集,而且还可以用于I/O操作,特别是用于从文件中提取batch(称为“片”)。我们将在磁盘上的原始图像保存到HDF5文件中,这可以让模型快速的遍历数据集并在其上训练深度学习网络。

在本章的其余部分,将演示如何为Kaggle上的猫狗比赛[3]构建一个HDF5数据集。然后,在下一章中,我们将使用这个HDF5数据集来训练具有开创性的AlexNet架构[6],并在排行榜中排名前25。

Kaggle: 猫狗比赛

首先,从kaggle官方网页地址上下载狗和猫的数据集.

下载完数据之后,按照下面目录结构解压数据:

1
2
3
kaggle_dogs_vs_cats/train/cat.11866.jpg
...
kaggle_dogs_vs_cats/train/dog.11046.jpg

这个项目中将使用以下数据结构:

1
2
3
4
5
6
7
8
|--- datasets
| |--- kaggle_dogs_vs_cats
| | |--- hdf5
| | |--- train
|--- dogs_vs_cats
| |--- config
| |--- build_dogs_vs_cats.py
| |--- ...

接下来,我们创建配置文件。

配置文件

现在我们开始构建更高级的项目和深度学习算法,为了好管理,将使用python为每个项目创建一个特殊的配置模块。例如,以下是kagge猫狗项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- dogs_vs_cats
| |--- config
| | |--- __init__.py
| | |--- dogs_vs_cats_config.py
| |--- build_dogs_vs_cats.py
| |--- crop_accuracy.py
| |--- extract_features.py
| |--- train_alexnet.py
| |--- train_model.py
| |--- output/
| | |--- __init__.py
| | |--- alexnet_dogs_vs_cats.model
| | |--- dogs_vs_cats_features.hdf5
| | |--- dogs_vs_cats_mean.json
| | |--- dogs_vs_cats.pickle

在config目录中新建一个名为dogs_vs_cats_config.py文件,该文件的主要目的是存储项目的所有相关配置,包括:

  • 1.输入图像的路径。

  • 2.类标签的总数。

  • 3.训练、验证和测试数据划分信息。

  • 4.HDF5数据集的路径。

  • 5.模型、图表和日志等保存路径。

配置文件使用Python格式而不是JSON格式,主要是为了使配置文件更有效地使用(可以使用os.path模块进行操作文件路径)。我建议您养成使用python脚本作为自己的深度学习项目的配置文件的习惯,因为它将极大地提高您的工作效率,并允许您通过单个文件控制项目中的大部分参数。

配置信息

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: utf-8 -*-

# 原始图像路径
IMAGES_PATH = "../datasets/kaggle_dogs_vs_cats/train"

#类别总数
NUM_CLASSES = 2
# 验证数据集大小
NUM_VAL_IMAGES = 1250 * NUM_CLASSES
# 测试数据集代销
NUM_TEST_IMAGES = 1250 * NUM_CLASSES

# hdf5数据保存路径
TRAIN_HDF5 = "../datasets/kaggle_dogs_vs_cats/hdf5/train.hdf5"
VAL_HDF5 = "../datasets/kaggle_dogs_vs_cats/hdf5/val.hdf5"
TEST_HDF5 = "../datasets/kaggle_dogs_vs_cats/hdf5/test.hdf5"

下面定义输出序列化权重的路径、数据集平均值和图、分类报告、日志等的输出保存路径:

1
2
3
4
5
6
7
8
# 模型保存路径
MODEL_PATH = "output/alexnet_dogs_vs_cats.model"

# 数据均值保存路径
DATASET_MEAN = "output/dogs_vs_cats_mean.json"

# 其余输出保存路径
OUTPUT_PATH = "output"

DATASET_MEAN文件主要是存储整个(training)数据集中三个颜色通道(红、绿、蓝)的像素平均值。在训练网络之前,我们需要将图像中的每个像素值减去平均值(验证集和测试集也需要进行操作处理)。这种方法称为零均值化,是一种数据归一化技术,它将像素强度归一化到[0,1]范围中,这对大型数据集和更深层次的神经网络的训练很有效。

保存数据

下面,构建HDF5数据集。打开一个新文件,命名为build_dogs_vs_cats.py,写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-

# 加载模块
from config import dogs_vs_cats_config as config
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from pyimagesearch.preprocessing import AspectAwarePreprocessor as AAP
from pyimagesearch.io import hdf5datasetwriter as HDF
from imutils import paths
import numpy as np
import progressbar
import json
import cv2
import os

获取Kaggle猫狗数据集中的图像路径:

1
2
3
4
5
6
7
# 图像路径
trainPaths = list(paths.list_images(config.IMAGES_PATH))
# 获取标签
trainLabels = [p.split(os.path.sep)[2].split(".")[0] for p in trainPaths]
# 标签编码化
le = LabelEncoder()
trainLabels = le.fit_transform(trainLabels)

原始的猫狗数据集的目录结构如下:

1
2
3
kaggle_dogs_vs_cats/train/cat.11866.jpg
...
kaggle_dogs_vs_cats/train/dog.11046.jpg

注意:类的名称是从实际的文件名中获取到的。

接下来,我们将原始数据分成三部分:训练集、验证集和测试集。

1
2
3
4
5
6
# 将原始的train分割成train和test两份
split = train_test_split(trainPaths, trainLabels,test_size=config.NUM_TEST_IMAGES, stratify=trainLabels,random_state=42)
(trainPaths, testPaths, trainLabels, testLabels) = split
# 将新的train分割成train和val两份
split = train_test_split(trainPaths, trainLabels,test_size=config.NUM_VAL_IMAGES, stratify=trainLabels,random_state=42)
(trainPaths, valPaths, trainLabels, valLabels) = split

划分完数据集之后,我们将数据集写入HDF5文件中:

1
2
3
4
5
6
7
8
9
# 将数据构建一个list,方便写入HDF5文件中
datasets = [
("train", trainPaths, trainLabels, config.TRAIN_HDF5),
("val", valPaths, valLabels, config.VAL_HDF5),
("test", testPaths, testLabels, config.TEST_HDF5)]

# 数据预处理
aap = AAP.AspectAwarePreprocessor(256, 256)
(R, G, B) = ([], [], [])

首先,我们定义一个数据集列表,其中包括训练数据集、验证数据集和测试数据集以及各自相关变量。列表中的每个元素都是一个4元组,包括:

  • 1.数据类型名称。

  • 2.数据路径。

  • 3.数据标签。

  • 4.保存HDF5数据的路径。

然后,我们初始化AspectAwarePreprocessor,在写入HDF5之前将图像大小调整为256x256像素(注意,图像的纵横比)。最后,初始化三个列表——R、G和B(图像的三个颜色通道),用于存储每个通道的平均像素值。

最后,我们准备构建HDF5数据集:

1
2
3
4
5
6
7
8
9
10
11
# 遍历数据集
for (dType, paths, labels, outputPath) in datasets:
# HDF5 writer
print("[INFO] building {}...".format(outputPath))
writer = HDF.HDF5DatasetWriter((len(paths), 256, 256, 3), outputPath)

# 初始化进度条
widgets = ["Building Dataset: ", progressbar.Percentage(), " ",
progressbar.Bar(), " ", progressbar.ETA()]
pbar = progressbar.ProgressBar(maxval=len(paths),
widgets=widgets).start()

其中HDF5DatasetWriter的输出数据集的shape是(len(paths),256,256,3),这意味着有len(paths)张图像且每一个shape都是(256,256,3)

然后初始化进度条模块,这样我们就可以很容易地监视数据集生成的过程。

接下来,写入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (i, (path, label)) in enumerate(zip(paths, labels)):
# 读取数据并预处理
image = cv2.imread(path)
image = aap.preprocess(image)
# 如果是train,则计算RGB均值
if dType == "train":
(b, g, r) = cv2.mean(image)[:3]
R.append(r)
G.append(g)
B.append(b)
# 写入数据
writer.add([image], [label])
pbar.update(i)

pbar.finish()
writer.close()

首先,对图像路径数据进行遍历,读取每一张图像,并进行预处理,将图像的大小调整为256x256。

然后,如果是train数据集,则我们计算RGB的平均值(需要注意的是:我们只在train数据中计算RGB平均值,而验证集和测试集直接减去平均值)。

最后,我们将经过处理的图像写入HDF5文件中。

将RGB平均值序列化到磁盘:

1
2
3
4
5
6
# 保存成json文件
print("[INFO] serializing means...")
D = {"R": np.mean(R), "G": np.mean(G), "B": np.mean(B)}
f = open(config.DATASET_MEAN, "w")
f.write(json.dumps(D))
f.close()

打开终端,直接以下命令:

1
2
3
4
5
6
7
$ python build_dogs_vs_cats.py
[INFO] building kaggle_dogs_vs_cats/hdf5/train.hdf5...
Building Dataset: 100% |####################################| Time: 0:02:39
[INFO] building kaggle_dogs_vs_cats/hdf5/val.hdf5...
Building Dataset: 100% |####################################| Time: 0:00:20
[INFO] building kaggle_dogs_vs_cats/hdf5/test.hdf5...
Building Dataset: 100% |####################################| Time: 0:00:19

从输出结果中可以看到,训练数据集、测试数据集和验证数据集分别创建了一个HDF5文件。

查看这些文件大小

1
2
3
4
5
$ ls -l ../datasets/kaggle_dogs_vs_cats/hdf5/
total 38400220
-rw-rw-r-- 1 adrian adrian 3932182144 Apr 7 18:00 test.hdf5
-rw-rw-r-- 1 adrian adrian 31457442144 Apr 7 17:59 train.hdf5
-rw-rw-r-- 1 adrian adrian 3932182144 Apr 7 18:00 val.hdf5

磁盘上的猫狗数据集的大小只有595MB,为什么.hdf5文件这么大?train.hdf5文件是31.45GB,val.hdf5和test.hdf5文件几乎是4GB的.

原始的图像文件格式如JPEG和PNG应用了数据压缩算法来压缩了图像文件的大小。但是,实际上我们读取图像数据时,没有对数据进行任何压缩,保持了原本图像的大小,并将图像存储为原始的NumPy数组(比如,bitmaps),极大地增加了我们的存储成本,但也将加快我们的训练速度,因为我们可以直接从HDF5数据集读取图像,并进行预处理和前向传播。

查看RGB均值文件:

1
2
$ cat output/dogs_vs_cats_mean.json
{"B": 106.13178224639893, "R": 124.96761639328003, "G": 115.97504255599975}

其中,红色通道在数据集中的所有图像中平均像素强度为124.96。蓝色通道的平均值是106.13,绿色通道的平均值是115.97。下一章中,我们将会对输入图像的像素值减去这些RGB平均值,即对图像数据做归一化。均值归一化有助于将数据集中在0均值附近。通常,这种规范化使我们的网络能够更快地学习,这也是为什么我们在更大、更有挑战性的数据集中使用这种类型的规范化(而不是[0,1]缩放)。

总结

在本章中,我们学习了如何将原始图像序列化为适合训练深度神经网络的HDF5数据集。我们将原始图像序列化为HDF5文件,而不是简单地访问磁盘上的一小批图像路径,由于会造成I/O延迟——对于磁盘上的每张图像,我们必须执行一次I/O操作来读取图像。在深度训练工程中,I/O延迟是一个大问题——训练过程已经足够缓慢,如果我们的网络很难访问数据,那么就是搬起石头砸自己的脚。

相反,如果我们将所有图像序列化到一个高效的HDF5文件中,我们可以利用非常快速的数组切片来提取我们的mini-batch数据,从而显著减少I/O延迟,并且加快训练过程。无论何时,当您使用Keras库并处理一个太大而无法装入内存的数据集时,您首先应该考虑将数据集序列化为HDF5格式——我们将在下一章中发现,它使训练您的网络成为一项更容易(也更有效)的任务。

本章代码下载地址:github