🏷sec0402_Datasets_DataPreparation
数据对于机器学习的重要性就像水之于鱼,空气之于人。数据的质量直接关系到模型的有效性和可用性。一种普遍的观点认为,神经网络模型是用数据喂出来的,也就是说神经网络是一种数据驱动型的模型。然而在深度学习的工程实践中,我们得到的数据通常是不能直接使用的。一方面,我们我们需要对它进行索引和标注;另一方面,可能还需要对它进行一定的预处理。大多数情况下,我们所得到的数据都是脏数据,会存在缺失、重复、噪声、不一致、不可用等问题。同时,也可能会存在数据量小、多样性不足等问题。数据预处理(Data Preprocessing) 是指在使用数据进行训练或推理前对数据进行的一些基本处理,具体包括数据清洗、数据标注、数据集成、数据列表生成、数据规约、数据增广六个部分,它们的主要功能如下:
在本书中,我们将数据清洗、数据标注、数据集成和数据列表生成统称为数据准备(Data Preparation),它们一般在模型训练之前执行。数据归约和数据增广通常可以与模型训练同步完成。数据准备是深度学习训练、测试等工作的基础,特别是对于自建数据集来说,数据清洗、数据标注、数据列表生成都是必不可少的工作。数据准备通常是一项工程性非常强的工作,每个数据集都需要针对性地撰写代码。在本小节中,我们将主要聚焦于介绍一些典型的数据清洗和数据列表生成方法,用来快速生成可以直接用于训练的数据。此外,我们将分别在 第4.3节 数据读取 和 第4.4节 数据增广 中介绍有关数据规约和数据增广的相关知识和实现方法。
数据清洗,顾名思义就是将原始数据集中的脏数据给去除掉。深度学习兴起的二十一世纪是一个大数据的时代,我们在获取海量数据的同时,总是会伴随着大量脏数据的产生。一般来说,数据集的来源主要有三种。第一种是在互联网公开发布的数据集,这类数据集通常都是由作者进行整理过后进行发布的。这些数据有些是经过数据清洗,并且包含数据列表的,例如ImageNet数据集;当然,也有一些数据集依然是未处理状态,例如Clickture数据集。第二种是使用网络爬虫程序从互联网上进行批量采集获得,这类数据集通常噪声数据较多,且可能存在大量的冗余数据。第三种是由开发人员自行拍摄或者整理获得,这类数据集相对干净,但由于来源多样化,也可能存在损坏的数据。因此,在我们进行模型训练前,很有必要对数据进行清洗操作,以排除那些可能会影响模型精度或者训练进程正常执行的样本。数据样本常见的问题主要包括以下几种:
cv2.imread(src, cv2.IMREAD_COLOR)
就可以直接实现按照彩色模式或者灰度模式进行样本读取。在了解了数据样本常见的问题之后,我们再来看看数据清洗常用方法。在传统机器学习中,数据大多数是简单的文本、表格数字或者简单的图片数据。因此,我们常用的处理方法包括直接删除错误数据,为缺失数据赋常量、均值或中位数,利用插值法或者建模法补充或修复数据等。然而对于现在复杂度较高的图像和视频等视觉数据来说,补充数据、修复数据是比较困难的。因此,常见的数据清洗一般有以下三种方法:
下面我们以 十二生肖分类数据集Zodiac 为例,对该数据集进行数据清洗。图4-5 给出了该数据集一些示例图片,不难看出该数据集是有一定难度的,各种类型的样本都有,包括真实照片、漫画、动漫、雕塑等。此外,该数据集中存在几张无法访问的坏数据。因此,我们需要通过数据清洗操作来将这些损坏的图片给找出来,并将它们的名称存入到一个坏文件列表,以方便在后续的数据列表生成时将它们进行排除。在下一小节中,我们还将依托该数据集来实现数据集的划分,并生成用来训练和评估模型时读取数据所使用的数据列表。十二生肖数据集Zodiac可以通过百度AIStudio的数据仓库获得,下载地址为:
https://aistudio.baidu.com/aistudio/datasetdetail/71363。
图4-5 十二生肖数据集Zodiac部分样例图
程序清单4-1 是进行数据清洗的示例程序,主要包括参数初始化、定义并检查坏文件列表、执行数据清洗以及输出统计结果四个部分。该代码清单的主要功能是找到并索引无法读取的坏样本,并将索引结果保存到bad.txt文件中。程序设置了一个 exclusion 变量来保存扫描文件夹时需要自动跳过的文件或文件夹,默认情况下,我们会自动跳过.DS_Store
和.ipynb_checkpoints
。大家可以根据实际需求来进行设置。在对文件进行是否损坏判断时,我们采用试错法来完成。简单说就是使用OpenCV库的cv2.imread()
函数来尝试读取图片并输出图片形态。如果该操作可以正常运行,则证明图片是完好无损的;如果无法操作,则说明样本是损坏的。该方法实现简单,但比较有效。只是因为需要对每个样本图片都进行IO操作,因此需要耗费一定的时间。十二生肖数据集大约需要5-6分钟来完成(Intel i7-7700K@4.2G)数据清洗。有兴趣的读者也可以尝试调用CPU多线程并行来对下面的代码进行加速。值得注意的是,对于一个数据集来说,数据清洗通常只需要执行一次。
# 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个无法正常读取的图片,并通过索引法将这些图片的信息写入到了一个固定的文件中。利用该文件,我们就可以在生成数据集列表的时候实现有针对性地排除,以防止这些无法读取的坏数据影响模型的训练。试错法仅仅只是一种检测文件是否损坏的方法,并非固定模式。处理损坏文件基本上也是所有数据集预处理时必须的操作之一。
在对数据集进行清洗之后,就可以正式使用数据集了。根据前面的介绍,我们知道模型应当在训练集上进行训练,然后在测试集上进行评估。然而深度学习的模型训练是一个非常主观的过程。换句话说,在训练过程中存在大量的超参数需要选择,如何选择这些超参数并对这些超参数进行合理的组合是非常经验化的。更多的时候只能需要通过反复的实验来搜索和验证。因此我们需要一个与训练集和测试集完全不相交的数据集合来实现这些超参数的选择。这个数据集合就是验证集。
在深度学习的工程任务中,通常会将数据集划分为四个子集,分别是训练集、验证集、测试集和训练验证集,其中训练验证集是训练集和验证集的组合。下面,我们先来看看这四个数据子集是怎么进行划分的,并且它们之间有什么的关联和区别。
根据数据集的划分原则,我们可以得到这四个数据集的划分方法和使用时机。具体而言,这也是深度学习的基本训练过程,流程如下:
深度学习训练过程
对于深度学习中数据集的使用,还有几点需要特别注意。
(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进行四个数据子集的列表文件生成,主要包括参数初始化、数据集相关路径定义、数据划分以及输出统计结果四个部分。
# codes04002_generate_annotation_Zodiac import os import json import codecs
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': {} # 类别标签字典 }
# 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])) # 从训练集文件夹中获取类别名称标签
训练集+测试集
,另一种是训练集+验证集+测试集+训练验证集
。在很多示例代码中,我们都会看到第一种划分方式;但是,在实际应用中第二种方式才更为规范。在本书中,我们将统一使用第二种方法对数据集进行划分。在进行训练列表输出的时候,我们可以分别定义训练、验证和测试三个条件分支,并将每一个样本都根据一定的条件,按照三种不同的设置进行划分。注意训练验证列表将同时属于训练和验证两个分支。在数据划分代码部分,将同时输出四个子集列表文件。# 遍历子文件夹,并分别输出四个不同的数据子集列表 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
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'}}
数据准备是数据预处理和模型训练必不可少的环节。正如本小节中所介绍的内容,这个过程是一个非常工程化的过程,它需要根据数据集的实际情况来选择合适的处理方法。本小节中给出的代码也只是一种具有针对性的典型设置。不同的数据集可能会存在不同的设置方法,在一些数据集中,样本数据可能会被统一放到一个文件夹中,并且没有事先进行划分。此时,就是需要我们手工编写划分代码来实现样本的划分。对于中小型数据集,[7:1:2] 是一种不错的典型划分比例。对于大型数据集,也可以根据需求进行划分,例如ImageNet数据集就是一个包含128万训练数据,5万验证数据和15万测试数据的数据集。但是,无论数据集有多不同,我们都建议将数据集划分为训练集、验证集、测试集和训练验证集,同时输出一个数据集的统计信息文件以便后续使用。本章的 项目003 提供了另外一种数据集的数据列表生成范例,虽然原始数据集的文件组织结构和本小节中所介绍的数据集有所不同,但总体上依然可以使用和本小节相似的代码模板结构来实现数据清洗和数据列表生成。有兴趣的读者可以试着完成该项目。
数据划分是对数据集进行处理的重要操作,以下数据子集可以在训练过程中用来进行模型评估的包括()。
A. 训练集
B. 验证集
C. 测试集
D. 训练验证集
以下计算机视觉任务中,通常使用一个类别标签来进行标识的任务是()。
A. 图像分类
B. 目标检测
C. 图像分割
D. 目标跟踪
以下描述,属于原始数据样本常见问题的包括()。
A. 样本无法读取
B. 图片尺度不相同
C. 图片颜色通道不一致
D. 样本存在冗余
E. 样本目标倾斜
深度学习的训练过程如以下几点,请将这几点按照正确的顺序进行排序。()
a. 使用训练验证集对模型进行训练
b. 使用训练集对模型进行训练
c. 使用测试集对模型进行评估
d. 使用验证集对模型进行评估
e. 将原始数据集划分为训练集、验证集、测试集和训练验证集
A. eadbc B. ebdac C. ebcad D. eacbd
图像清洗是计算机视觉最重要的操作之一,以下哪一项属于数据清洗的目的()。
A. 消除图像中无关的信息
B. 删除数据集中无法访问的样本
C. 获取更美观的图像
D. 制作训练数据集