4.2 数据准备

🏷sec0402_Datasets_DataPreparation

4.2.1 数据预处理概述

数据对于机器学习的重要性就像水之于鱼,空气之于人。数据的质量直接关系到模型的有效性和可用性。一种普遍的观点认为,神经网络模型是用数据喂出来的,也就是说神经网络是一种数据驱动型的模型。然而在深度学习的工程实践中,我们得到的数据通常是不能直接使用的。一方面,我们我们需要对它进行索引和标注;另一方面,可能还需要对它进行一定的预处理。大多数情况下,我们所得到的数据都是脏数据,会存在缺失、重复、噪声、不一致、不可用等问题。同时,也可能会存在数据量小、多样性不足等问题。数据预处理(Data Preprocessing) 是指在使用数据进行训练或推理前对数据进行的一些基本处理,具体包括数据清洗、数据标注、数据集成、数据列表生成、数据规约、数据增广六个部分,它们的主要功能如下:

  1. 数据清洗(Data Cleaning) 通过对样本中的噪声数据进行识别,实现对坏数据(数据集中无法访问的数据)和脏数据(与任务无关的数据)的删除或标记,或者对重复数据进行去除冗余操作等。
  2. 数据标注(Data Annotations) 根据任务的不同,对数据进行类别、区域或像素的认知与识别,并生成标注数据列表。
  3. 数据集成(Data Intergration) 指的是当需要同时使用多个来源的数据集进行合并训练时,在训练前所进行数据预处理操作。数据集成涉及标签的兼容与覆盖、数据的去重、数据的二次标注等问题。
  4. 数据列表生成(List Generation) 是指按照任务的要求,将数据集进行划分并生成包含样本数据路径和类别标签的文件,用于训练、验证和测试。例如分类任务列表的每一条数据都是[路径,标签]的二元组合,而检测任务常见的列表形式包括VOC格式(xml文件)和coco格式(json文件)。
  5. 数据规约(Data Reduction) 又称为基本数据预处理,主要包括像素归一化、均值消除、维度规约、数量规约、通道转换、数据类型变换等。像素归一化实现将像素值从[0,255]归一化到[0,1]。均值消除包括均值减除和方差归一化两部分。维度规约通过消除代表性不强的特征来实现特征数量的减少,例如主成分分析PCA。数量规约主要实现不同特征的量纲统一,在传统机器学习的特征处理中尤为重要。通道转换实现将Numpy数据类型转换为Paddle等工具包规定的Tensor类型,同时也将图片原始的HWC形态转换为CHW形态。数据类型转换实现将任意类型的输入值转化为特定的数据类型,例如通用的32位浮点型(float32)和低功耗的8位整型(Int8)。
  6. 数据增广(Data Augumentation) 技术是深度学习任务中最重要的数据预处理步骤,它的主要作用是缓解过拟合问题,并增强模型的泛化能力。常见的数据增广方法包括水平和垂直翻转、2D旋转、缩放、随机裁剪、平移、对比度调整、色彩扰动、增加噪声、添加遮挡以及融合多种变换的混合变换等。

在本书中,我们将数据清洗、数据标注、数据集成和数据列表生成统称为数据准备(Data Preparation),它们一般在模型训练之前执行。数据归约和数据增广通常可以与模型训练同步完成。数据准备是深度学习训练、测试等工作的基础,特别是对于自建数据集来说,数据清洗、数据标注、数据列表生成都是必不可少的工作。数据准备通常是一项工程性非常强的工作,每个数据集都需要针对性地撰写代码。在本小节中,我们将主要聚焦于介绍一些典型的数据清洗和数据列表生成方法,用来快速生成可以直接用于训练的数据。此外,我们将分别在 第4.3节 数据读取第4.4节 数据增广 中介绍有关数据规约和数据增广的相关知识和实现方法。

4.2.2 数据清洗

数据清洗,顾名思义就是将原始数据集中的脏数据给去除掉。深度学习兴起的二十一世纪是一个大数据的时代,我们在获取海量数据的同时,总是会伴随着大量脏数据的产生。一般来说,数据集的来源主要有三种。第一种是在互联网公开发布的数据集,这类数据集通常都是由作者进行整理过后进行发布的。这些数据有些是经过数据清洗,并且包含数据列表的,例如ImageNet数据集;当然,也有一些数据集依然是未处理状态,例如Clickture数据集。第二种是使用网络爬虫程序从互联网上进行批量采集获得,这类数据集通常噪声数据较多,且可能存在大量的冗余数据。第三种是由开发人员自行拍摄或者整理获得,这类数据集相对干净,但由于来源多样化,也可能存在损坏的数据。因此,在我们进行模型训练前,很有必要对数据进行清洗操作,以排除那些可能会影响模型精度或者训练进程正常执行的样本。数据样本常见的问题主要包括以下几种:

  1. 样本无法读取。无法读取的样本通常都是损坏的样本,它们可能是因为网络下载过程中导致的损坏,也可能是原始数据本身就是损坏的。这种样本会直接导致训练程序的崩溃,所以必须事先将这类样本从数据集中找出来并进行删除或索引。
  2. 样本通道不一致。由于大多数数据集都是从互联网收集或整理获得,所以一个数据集中同时存在彩色图像(depth=3)和灰度图像(depth=1)是非常常见的现象。对于通道不统一的数据集,我们首先需要确定任务和计划训练的模型是针对灰度图的还是针对彩色图,然后再进行统一的维度规约处理。在进行维度规约处理的时候,我们可以在训练前对样本进行重置,也可以直接在训练过程中通过图像读取函数来实现色彩通道的统一。例如OpenCV库中的 cv2.imread(src, cv2.IMREAD_COLOR) 就可以直接实现按照彩色模式或者灰度模式进行样本读取。
  3. 样本命名不规范。样本命名不规范主要包括两种情况,一种是文件名过长(例如超过255个字符),另一种是文件名中存在无法处理的特殊符号。一般来说,现代的操作系统都能很好地处理这两种问题,但是并不排除一些特殊的系统或者软件工具包无法很好地适配。此时,可以考虑对原始样本进行重命名操作来修正这种潜在的隐患。
  4. 样本存在冗余。冗余样本是指样本的名称不同,但是内容、尺度、质量完全一致。冗余数据会妨碍数据库中数据的完整性,也会造成存贮空间的浪费,特别是当冗余样本较多时,还会影响训练的平衡性和特殊性。
  5. 命名和内容不一致。样本的名称、描述和样本的实际内容不一致,通常指样本标注错误。此类错误,需要进行重新标注或直接将样本删除,否则将会影响模型训练的正确性。

在了解了数据样本常见的问题之后,我们再来看看数据清洗常用方法。在传统机器学习中,数据大多数是简单的文本、表格数字或者简单的图片数据。因此,我们常用的处理方法包括直接删除错误数据,为缺失数据赋常量、均值或中位数,利用插值法或者建模法补充或修复数据等。然而对于现在复杂度较高的图像和视频等视觉数据来说,补充数据、修复数据是比较困难的。因此,常见的数据清洗一般有以下三种方法:

  1. 修复数据。将无法使用的数据进行还原和修复或者对标注错误的样本进行重新标注。
  2. 删除数据。直接将坏数据或脏数据从数据集中删除。该方法比较简单,但是会对原始数据集的统计信息造成改变。所以,一般适用于自建数据集创建的时候使用。
  3. 索引数据。对非法文件和坏文件进行索引和标注。利用索引文件可以在保留原始数据集不变的情况下,实现非法样本和坏样本的自动过滤和排除。该方法稍微复杂一些,并且需要事先通过数据清洗生成非法文件和坏文件列表。

下面我们以 十二生肖分类数据集Zodiac 为例,对该数据集进行数据清洗。图4-5 给出了该数据集一些示例图片,不难看出该数据集是有一定难度的,各种类型的样本都有,包括真实照片、漫画、动漫、雕塑等。此外,该数据集中存在几张无法访问的坏数据。因此,我们需要通过数据清洗操作来将这些损坏的图片给找出来,并将它们的名称存入到一个坏文件列表,以方便在后续的数据列表生成时将它们进行排除。在下一小节中,我们还将依托该数据集来实现数据集的划分,并生成用来训练和评估模型时读取数据所使用的数据列表。十二生肖数据集Zodiac可以通过百度AIStudio的数据仓库获得,下载地址为:
https://aistudio.baidu.com/aistudio/datasetdetail/71363

图4-5 十二生肖数据集Zodiac部分样例图

图4-5 十二生肖数据集Zodiac部分样例图

程序清单4-1 是进行数据清洗的示例程序,主要包括参数初始化、定义并检查坏文件列表、执行数据清洗以及输出统计结果四个部分。该代码清单的主要功能是找到并索引无法读取的坏样本,并将索引结果保存到bad.txt文件中。程序设置了一个 exclusion 变量来保存扫描文件夹时需要自动跳过的文件或文件夹,默认情况下,我们会自动跳过.DS_Store.ipynb_checkpoints。大家可以根据实际需求来进行设置。在对文件进行是否损坏判断时,我们采用试错法来完成。简单说就是使用OpenCV库的cv2.imread()函数来尝试读取图片并输出图片形态。如果该操作可以正常运行,则证明图片是完好无损的;如果无法操作,则说明样本是损坏的。该方法实现简单,但比较有效。只是因为需要对每个样本图片都进行IO操作,因此需要耗费一定的时间。十二生肖数据集大约需要5-6分钟来完成(Intel i7-7700K@4.2G)数据清洗。有兴趣的读者也可以尝试调用CPU多线程并行来对下面的代码进行加速。值得注意的是,对于一个数据集来说,数据清洗通常只需要执行一次。

程序清单4-1 数据清洗
# codes04001_data_cleaning
import os
import cv2
import codecs

# 1. 参数初始化
dataset_name = 'Zodiac'                                          # 定义数据集名称
dataset_path = 'D:\\Workspace\\ExpDatasets\\'                    # 定义数据集保存的根路径
dataset_root_path = os.path.join(dataset_path, dataset_name)     # 生成本项目数据集路径
exclusion = ['.DS_Store', '.ipynb_checkpoints']                  # 定义被排除的文件
subPrefix = ['train', 'valid', 'test']                           # 定义数据集路径中已经事先分割好的文件夹名称,用于后续遍历
num_bad = 0                                                      # 定义统计数据:坏样本
num_good = 0                                                     # 定义统计数据:好样本
num_folder = 0

# 2. 定义并检查坏文件列表
bad_list = os.path.join(dataset_root_path, 'bad.txt')
if os.path.exists(bad_list):                                     # 检测坏文件列表是否存在,如果存在则先删除
    os.remove(bad_list)

# 3. 执行数据清洗**
with codecs.open(bad_list, 'a', 'utf-8') as f_bad:
    for prefix in subPrefix:                                                                    # 遍历类别前缀
        class_name_list = os.listdir(os.path.join(dataset_root_path, prefix))                   # 生成实际类别路径
        for class_name in class_name_list:                                                      # 遍历类别路径
            if class_name not in exclusion:                                                     # 跳过排除文件夹                        
                images = os.listdir(os.path.join(dataset_root_path, prefix, class_name))        # 生成图片路径列表
                for image in images:                                                            # 遍历图片路径列表
                    if image not in exclusion:                                                  # 跳过排除文件
                        img_path = os.path.join(dataset_root_path, prefix, class_name, image)   # 生成图片路径
                        # 通过尝试读取并显示图像维度来判断样本是否损坏
                        try:                                                                    # 正常图像直接跳过                                                                 
                            img = cv2.imread(img_path, 1)
                            x = img.shape
                            num_good += 1
                            pass
                        except:                                                                 # 异常图像,将文件名写入bad_file中
                            bad_file = os.path.join(prefix, class_name, image)
                            f_bad.write("{}\n".format(bad_file))
                            num_bad += 1
            num_folder += 1
            print('\r 当前清洗进度:{}/{}'.format(num_folder, 3*len(class_name_list)), end='')    # 输出进度信息

# 4. 输出执行结果
print('数据集清洗完成, 损坏文件{}个, 正常文件{}.'.format(num_bad, num_good))
 当前清洗进度:36/36
 数据集清洗完成, 损坏文件9, 正常文件8500.

在上面的代码中,我们通过试错法找到了9个无法正常读取的图片,并通过索引法将这些图片的信息写入到了一个固定的文件中。利用该文件,我们就可以在生成数据集列表的时候实现有针对性地排除,以防止这些无法读取的坏数据影响模型的训练。试错法仅仅只是一种检测文件是否损坏的方法,并非固定模式。处理损坏文件基本上也是所有数据集预处理时必须的操作之一。

4.2.3 数据列表生成

在对数据集进行清洗之后,就可以正式使用数据集了。根据前面的介绍,我们知道模型应当在训练集上进行训练,然后在测试集上进行评估。然而深度学习的模型训练是一个非常主观的过程。换句话说,在训练过程中存在大量的超参数需要选择,如何选择这些超参数并对这些超参数进行合理的组合是非常经验化的。更多的时候只能需要通过反复的实验来搜索和验证。因此我们需要一个与训练集和测试集完全不相交的数据集合来实现这些超参数的选择。这个数据集合就是验证集。

在深度学习的工程任务中,通常会将数据集划分为四个子集,分别是训练集、验证集、测试集和训练验证集,其中训练验证集是训练集和验证集的组合。下面,我们先来看看这四个数据子集是怎么进行划分的,并且它们之间有什么的关联和区别。

根据数据集的划分原则,我们可以得到这四个数据集的划分方法和使用时机。具体而言,这也是深度学习的基本训练过程,流程如下:

深度学习训练过程

  1. 将原始数据集按照一定的比例和规则划分为训练集(train)、验证集(val)和测试集(test),并将训练集和验证集进行组合,生成训练验证集(trainval)。
  2. 使用训练集和验证集对模型进行初步的迭代训练,其中训练集用于实现模型的迭代训练,验证集用于在训练过程中进行定期评估。迭代训练的次数可以根据验证集的结果反复执行多次,以获得最优的超参数设置。
  3. 使用第2步中所获得超参数,在训练验证集上进行迭代训练,训练结束时将获得最终模型(Final Model)。
  4. 使用测试集在最终模型上进行评估,并输出测试集上的评估结果。

对于深度学习中数据集的使用,还有几点需要特别注意。
(1)训练集和验证集都是模型初步训练时所使用的数据。其中训练集用于模型训练,验证集用于超参数的评估。在初步训练的迭代过程中,验证集不应该出现在模型的训练数据中,只能用来进行模型的验证。
(2)测试集在训练过程中是不可见的。在训练过程中能评估模型好坏的只能是验证集,而不应该在训练过程中直接使用测试集来评估模型。并且在任何时候都不能将测试集加入到训练集中参与模型的训练。测试集只能在最终模型训练好后,进行最终的性能评估时使用。
(3)为了更好地完成超参数选择,通常会将原始的训练集(即训练验证集)分割成新的训练集和验证集,然后在完成初步迭代训练的超参数选择之后,再将划分出来的新训练集和验证集合并在一起,当做训练数据进行统一训练。换句话说训练集和训练验证集都是拿来训练模型的,所不同的是前者用于初步训练并实现超参数的选择,而后者是在确定超参数之后用来完成最终模型的训练。一般来说,在相同的超参数设置下,在数据量更大的训练验证集上训练获得的性能要由于在划分出来的训练集上训练获得的性能。
(4)对于四个不同的数据子集,需要根据其功能和实际的应用场景选择不同的数据预处理和数据增广方法。

在机器学习时代,我们常用的数据划分方法有留出法(Hold-Out)、K折交叉验证法(K-fold Corss Validation)、自助法(Bootstrap)等。但在深度学习中,由于数据量巨大,这些方法通常都不太实用。更多的时候,还是直接采取手动划分的方式进行数据集划分。下面我们还是以 十二生肖分类数据集Zodiac 为例,将该数据集划分成四个数据子集,并生成对应的数据列表文件。这个数据集包含12个类,在 [图4-5] 中已经分别给出了这12个分类的样例图。数据集总共有8509张图片,并且已经由官方按照 [85:7.5:7.5] 的比例划分成了训练、验证和测试子集,并保存在train, valid和test三个文件夹中。因此,我们在进行数据列表生成的时候,不需要再进行分割,只需要利用训练图片和验证图片再合并出一个训练验证子集就可以了。值得再次提醒的是,在本小节中,无论是数据清洗还是数据列表生成中的设定都是工程性极强的设定。因此对于实际应用所面对的不同的数据集来说,在进行数据准备前,都需要事先对数据集进行分析后再确定操作方法。本小节给出的例子仅仅只是一个特殊的案例。不过,无论是何种数据集,我们最终的目标都是生成四个训练子集的样本列表文件以及数据集统计信息字典。

在对数据集进行数据列表生成的时候,需要根据任务的不同,采取不同的索引方式来保存GroundTruth标签信息。对于分类任务一般使用文本文件(.txt文件)来保存索引信息,因为它只需要对每个样本保存1个类别标签就可以了。对于目标检测任务,由于需要同时生成每个目标对象的边界框信息和其对应的类别标签,所以单单一个类别标签是无法完成目标的。此时使用结构化文件来保存目标检测任务的标签信息会更方便一些。其中最常用的是VOC格式(.xml文件)和coco格式(.json文件)。针对图像分割任务,由于目标是生成图像每个像素的标签,因此我们通常使用png图来保存样本的标签信息。在本小节中,我们主要介绍面向分类任务的数据列表生成,有关目标检测及图像分割任务的标签信息我们将在 第12.2节 目标检测第12.3节 图像分割 中进行介绍。在分类数据集中,要标识一个样本,通常只需要一条包含两个参数的数据列表就可以了,它们分别是数据的路径和数据的类别标签,其形式如下:

D:\Workspace\ExpDatasets\Gestures\Data\0\IMG_5991.JPG 0
D:\Workspace\ExpDatasets\Gestures\Data\1\IMG_1129.JPG 1
D:\Workspace\ExpDatasets\Gestures\Data\1\IMG_1139.JPG 1

在上面的数据列表中,第一项为图片的保存路径,第二项为该图片的分类标签。在两者中间,使用一个Tab(或者1个空格)进行分隔。值得注意的是,图片数据保存的路径可以使用绝对路径也可以使用相对路径,但为了避免不必要的麻烦和错误,建议使用绝对路径进行索引。在本小节中,我们将给出一种模板化的代码结构,来生成数据列表文件。一般来说,模板化的代码流程有利于理清代码结构,便于代码的编写和检查错误。但值得注意的是,该流程也并非是唯一的方法。下面的代码用于实现对十二生肖数据集Zodiac进行四个数据子集的列表文件生成,主要包括参数初始化、数据集相关路径定义、数据划分以及输出统计结果四个部分。

程序清单4-2 数据列表生成(十二生肖数据集Zodiac)
# codes04002_generate_annotation_Zodiac
import os
import json
import codecs
  1. 参数初始化
    参数初始化包括定义程序执行所需要的flag参数以及待输出参数的定义,具体包括数据集名称、四个数据子集样本的数量、类别数量以及类别标签字典。以上几个信息将组合成一个数据字典,并通过json方式进行输出。
num_trainval = 0              # 定义数据统计变量:训练验证集样本数
num_train = 0                 # 定义数据统计变量:训练集样本数
num_val = 0                   # 定义数据统计变量:验证集样本数
num_test = 0                  # 定义数据统计变量:测试集样本数
class_dim = 0                 # 定义数据统计变量:类别数量
dataset_info = {              # 定义数据集基本信息字典,并输出到dataset_info.json
    'dataset_name': '',       # 数据集名称
    'num_trainval': -1,       # 训练验证集样本数
    'num_train': -1,          # 训练集样本数
    'num_val': -1,            # 验证集样本数
    'num_test': -1,           # 测试集样本数
    'class_dim': -1,          # 类别数
    'label_dict': {}          # 类别标签字典
}
  1. 数据集相关路径的定义
    路径定义一般包括设置图像的保存位置,标签文件及相关说明文件的输出路径。同时还会包含一些其他功能的设置,例如排除文件设置、数据集类别设置等等。
# 2.1 数据集路径定义
dataset_name = 'Zodiac'                                                    # 数据集名称(文件夹)
dataset_path = 'D:\\Workspace\\ExpDatasets\\'                              # 数据集根路径
dataset_root_path = os.path.join(dataset_path, dataset_name)
exclusion = ['.DS_Store', '.ipynb_checkpoints']                            # 定义被排除的文件

# 2.2 数据集列表路径定义
subPrefix = ['train', 'valid', 'test']                                     # 定义数据集路径中的子文件夹,用于后续遍历,常见的分割方法如[train|val|test],如[Data]
trainval_list = os.path.join(dataset_root_path, 'trainval.txt')            # 训练验证集列表路径
train_list = os.path.join(dataset_root_path, 'train.txt')                  # 训练集列表路径
val_list = os.path.join(dataset_root_path, 'val.txt')                      # 验证集列表路径
test_list = os.path.join(dataset_root_path, 'test.txt')                    # 测试集列表路径
dataset_info_list = os.path.join(dataset_root_path, 'dataset_info.json')   # 数据集信息字典路径

# 2.3 检测数据集列表是否存在,如果存在则先删除。
if os.path.exists(trainval_list):
    os.remove(trainval_list)
if os.path.exists(train_list):
    os.remove(train_list)
if os.path.exists(val_list):
    os.remove(val_list)
if os.path.exists(test_list):
    os.remove(test_list)

# 2.4 读取数据清洗获得的坏样本列表,并将坏文件写入列表以备后续排除
bad_list = os.path.join(dataset_root_path, 'bad.txt')                       # 设置坏文件列表路径
with codecs.open(bad_list, 'r', 'utf-8') as f_bad:                          # 从坏文件中读取损坏的文件,并写入列表中
    bad_file = f_bad.read().splitlines()
exclusion = exclusion + bad_file                                            # 将排除文件与坏文件合并成一个文件

# 2.5 获取类别的名称。在本数据集中,train, valid, test的类别是相同的,因此只需要从train中获取即可
class_name_list = os.listdir(os.path.join(dataset_root_path, subPrefix[0])) # 从训练集文件夹中获取类别名称标签
  1. 执行数据划分
    数据划分有两种模式,一种是训练集+测试集,另一种是训练集+验证集+测试集+训练验证集。在很多示例代码中,我们都会看到第一种划分方式;但是,在实际应用中第二种方式才更为规范。在本书中,我们将统一使用第二种方法对数据集进行划分。在进行训练列表输出的时候,我们可以分别定义训练、验证和测试三个条件分支,并将每一个样本都根据一定的条件,按照三种不同的设置进行划分。注意训练验证列表将同时属于训练和验证两个分支。在数据划分代码部分,将同时输出四个子集列表文件。
# 遍历子文件夹,并分别输出四个不同的数据子集列表
with codecs.open(trainval_list, 'a', 'utf-8') as f_trainval, codecs.open(train_list, 'a', 'utf-8') as f_train, codecs.open(val_list, 'a', 'utf-8') as f_val, codecs.open(test_list, 'a', 'utf-8') as f_test:
    for prefix in subPrefix:                                                                 # 遍历子文件夹 
        subDataset_dir = os.listdir(os.path.join(dataset_root_path, prefix))                 # 获取子文件夹中的所有文件/文件夹列表
        for class_name_id in range(len(class_name_list)):                                    # 遍历类别列表
            class_name = class_name_list[class_name_id]                                      # 从类别名称列表中获取类别名称
            dataset_info['label_dict'][class_name_id] = class_name                           # 将类别名称和对应的ID写入数据集字典的类别标签字典中
            images = os.listdir(os.path.join(dataset_root_path, prefix, class_name))         # 获取某个分类文件夹中的所有图片
            for image in images:
                if image not in exclusion:                                                   # 判断图像文件是否是被排除的文件,若是则跳过    
                    image_path = os.path.join(dataset_root_path, prefix, class_name, image)  # 获取图像的绝对路径
                    lable_id = class_name_id                                                 # 获取图像的label_id
                    if os.path.join(prefix, class_name, image) not in exclusion:             # 判断文件是否是坏样本 
                        if prefix == 'train':                                                # 如果文件是否在train文件夹中,若是则写入trainval和train文件列表中
                            f_train.write('{}\t{}\n'.format(image_path, lable_id))
                            f_trainval.write('{}\t{}\n'.format(image_path, lable_id))
                            num_train += 1
                            num_trainval += 1
                        if prefix == 'valid':                                                # 如果文件是否在valid文件夹中,若是则写入trainval和val文件列表中
                            f_val.write('{}\t{}\n'.format(image_path, lable_id))
                            f_trainval.write('{}\t{}\n'.format(image_path, lable_id))
                            num_val += 1
                            num_trainval += 1
                        if prefix == 'test':                                                 # 如果文件是否在test文件夹中,若是则写入test文件列表中
                            f_test.write('{}\t{}\n'.format(image_path, lable_id))
                            num_test += 1
  1. 输出统计结果
    统计结果的输出包括两类,一方面是保存需要在后续的训练或者推理中应用的文件,例如前面所定义的字典文件;另一方面是输出程序运行过程中的一些统计信息,用来帮助我们判断程序运行的是否正确。在这里,我们使用 codecs.open() 函数将数据的基本信息保存成一个json文件,同时使用 print() 函数在屏幕打印运行过程中的统计信息。
# 4.1 将数据集信息保存到json文件中供训练时使用
dataset_info['dataset_name'] = dataset_name
dataset_info['num_trainval'] = num_trainval
dataset_info['num_train'] = num_train
dataset_info['num_val'] = num_val
dataset_info['num_test'] = num_test
dataset_info['class_dim'] = len(class_name_list)

# 4.2 输出数据集信息到json文件
with codecs.open(dataset_info_list, 'w', encoding='utf-8') as f_dataset_info:  
    json.dump(dataset_info, f_dataset_info, ensure_ascii=False, indent=4, separators=(',', ':')) # 格式化字典格式的参数列表

# 4.3 打印数据集信息。注意为方便可视化,我们使用display()替代print()对字典进行输出,但display()只支持notebook接口。
print('图像列表已生成, 其中训练验证集样本{},训练集样本{}个, 验证集样本{}个, 测试集样本{}个, 共计{}个。'.format(num_trainval, num_train, num_val, num_test, num_train+num_val+num_test))
display(dataset_info)   
图像列表已生成, 其中训练验证集样本7840,训练集样本7190, 验证集样本650, 测试集样本660, 共计8500个。
{'dataset_name': 'Zodiac',
 'num_trainval': 7840,
 'num_train': 7190,
 'num_val': 650,
 'num_test': 660,
 'class_dim': 12,
 'label_dict': {0: 'dog',
  1: 'dragon',
  2: 'goat',
  3: 'horse',
  4: 'monkey',
  5: 'ox',
  6: 'pig',
  7: 'rabbit',
  8: 'ratt',
  9: 'rooster',
  10: 'snake',
  11: 'tiger'}}

4.2.4 小节

数据准备是数据预处理和模型训练必不可少的环节。正如本小节中所介绍的内容,这个过程是一个非常工程化的过程,它需要根据数据集的实际情况来选择合适的处理方法。本小节中给出的代码也只是一种具有针对性的典型设置。不同的数据集可能会存在不同的设置方法,在一些数据集中,样本数据可能会被统一放到一个文件夹中,并且没有事先进行划分。此时,就是需要我们手工编写划分代码来实现样本的划分。对于中小型数据集,[7:1:2] 是一种不错的典型划分比例。对于大型数据集,也可以根据需求进行划分,例如ImageNet数据集就是一个包含128万训练数据,5万验证数据和15万测试数据的数据集。但是,无论数据集有多不同,我们都建议将数据集划分为训练集、验证集、测试集和训练验证集,同时输出一个数据集的统计信息文件以便后续使用。本章的 项目003 提供了另外一种数据集的数据列表生成范例,虽然原始数据集的文件组织结构和本小节中所介绍的数据集有所不同,但总体上依然可以使用和本小节相似的代码模板结构来实现数据清洗和数据列表生成。有兴趣的读者可以试着完成该项目。

4.2.5 练习

  1. 数据划分是对数据集进行处理的重要操作,以下数据子集可以在训练过程中用来进行模型评估的包括()。
    A. 训练集
    B. 验证集
    C. 测试集
    D. 训练验证集

  2. 以下计算机视觉任务中,通常使用一个类别标签来进行标识的任务是()。
    A. 图像分类
    B. 目标检测
    C. 图像分割
    D. 目标跟踪

  3. 以下描述,属于原始数据样本常见问题的包括()。
    A. 样本无法读取
    B. 图片尺度不相同
    C. 图片颜色通道不一致
    D. 样本存在冗余
    E. 样本目标倾斜

  4. 深度学习的训练过程如以下几点,请将这几点按照正确的顺序进行排序。()
    a. 使用训练验证集对模型进行训练
    b. 使用训练集对模型进行训练
    c. 使用测试集对模型进行评估
    d. 使用验证集对模型进行评估
    e. 将原始数据集划分为训练集、验证集、测试集和训练验证集
    A. eadbc     B. ebdac     C. ebcad     D. eacbd

  5. 图像清洗是计算机视觉最重要的操作之一,以下哪一项属于数据清洗的目的()。
    A. 消除图像中无关的信息
    B. 删除数据集中无法访问的样本
    C. 获取更美观的图像
    D. 制作训练数据集