🏷sec0403_Datasets_DataReading
深度学习模型需要大量的数据来完成训练和评估,这些数据样本可能是图片(image)、文本(text)、语音(audio)等多种类型,而模型训练过程实际是数学计算过程,因此数据样本在送入模型前需要经过一系列处理,如转换数据格式、划分数据集、变换数据形状(shape)、构建数据迭代读取器等以备分批训练。在前面的内容和项目中,我们已经尝试过多种不同的数据读取和载入方法,其中就包括最简单的Paddle高级API等。但在工业实践中,我们可能面临的任务和数据是千差万别的。要将这些多样化的数据输入到模型中,最好是能够设计一套数据定义和读取流程,以规范数据的读取和预处理。这种做法的好处是,在设计模型的时候不需要去关心数据的基本情况,只需要按照统一的接口去调用数据就可以了。
在从硬盘等存储设备上进行数据读取的时候,一般可以使用两种方法来进行读取,一种称之为同步数据读取,一种称之为异步数据读取。在对于数据量大、数据读取较慢的场景,建议采用异步数据读取方式进行读取。二者的对比如下图所示:
图4-6 深度学习数据读取方式对比图
在Paddle框架中,进行数据读取一般包含四个步骤:
paddle.vision.datasets
库和 paddle.text
库中已经内置了一些经典数据集类,我们可以直接进行调用。当然,我们也可以全手工的方式来定义数据类,本小节中内容将采用这种自定义的方法进行数据集类的定义。下面我们将基于同步数据读取模式来获取十二生肖数据集Zodiac的四个数据子集中的数据,并进行简单的测试。
# codes04003_synchronous_initialization import os import numpy as np import cv2 import json import paddle # 1. 定义数据集列表文件路径 dataset_name = 'Zodiac' dataset_path = 'D:\\Workspace\\ExpDatasets\\' dataset_root_path = os.path.join(dataset_path, dataset_name) 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') # 2. 图像基本信息 input_size = [3, 227, 227] # 定义图像输入模型时的尺寸 mean_value = [0.485, 0.456, 0.406] # Imagenet均值 std_value = [0.229, 0.224, 0.225] # Imagenet标准差
在同步数据读取模式下,我们需要手动编写python代码来实现数据的读取。在 程序清单4-4 中,我们定义了两个函数 transforms
和 data_reader
分别用来实现数据预处理和数据读取。在 transforms
函数中,可以进行 第4.2小节 中关于数据规约的相关配置,例如像素归一化、均值消除、通道变换、数据类型转换等。在 data_reader
函数中,主要实现从数据列表中读取数据,并将图像和类别标签进行分割。下面是这部分内容的示例代码。
# codes04004_synchronous_create_reader # 0. 在CPU多进程处理方法 from multiprocessing import cpu_count # 1. 定义数据预处理方法 def transforms(sample): img, label = sample img = cv2.imread(img, 1) # 将图像尺度resize为指定尺寸 img = cv2.resize(img, [input_size[1], input_size[2]]) # 将图像数据类型转化为float32 img = img.astype('float32') # 将像素值归一化到[0,1]之间 img = img/255.0 # 数据标准化(均值消除) img = (img - mean_value) / std_value # 将图像数据类型转化为float32 img = img.reshape(1, 3, input_size[1], input_size[2]) return img, label # 2. 定义数据器eader,用于从列表文件中批量获取图像 def data_reader(data_list_path): # 定义读取函数,从列表文件中读取 def reader(): with open(data_list_path, 'r') as f: lines = f.readlines() for line in lines: img_path, label = line.split('\t') yield img_path, int(label) # 通过用户自定义的映射器mapper来映射reader返回的样本 # cpu_count()可以实现多线程方式运行 return paddle.reader.xmap_readers(transforms, reader, cpu_count(), 512)
依托数据集读取器所获取的样本是大批量的,当数据量比较大的时候,设备可能会无法一次性容纳整个数据集。因此,我们还需要将数据集划分成若干个批次来分批进行训练。因此,为了实现基于小批量的训练,我们还需要创建基于批量的数据迭代读取器。数据迭代读取器可以有效实现将数据进行小批量划分以及在数据被送入模型前的打乱操作。此处,我们需要为四个数据子集分别创建数据迭代读取器。
# codes04005_synchronous_create_iterative_reader trainval_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(trainval_list), buf_size=256), batch_size=64, drop_last=False) train_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(train_list), buf_size=256), batch_size=64, drop_last=False) val_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(val_list), buf_size=256), batch_size=64, drop_last=False) test_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(test_list), buf_size=256), batch_size=64, drop_last=False)
在数据迭代读取器创建完成后,就可以直接进行训练了。但为了保证数据送入模型的正确性,我们可以先对数据迭代器进行一定的测试,例如输出数据的形态。
# codes04006_synchronous_print_reader # 迭代的读取数据并打印数据的形状 for i, data in enumerate(val_reader()): if i < 1: print('验证集batch_{}的图像形态: {}'.format(i, data[0][0].shape)) else: break
验证集batch_0的图像形态: (1, 3, 224, 224)
相比同步数据读取,异步数据读取更为常用。特别是对于更喜欢大数据的深度学习来说,异步数据读取具有速度快的优势。在Paddle中,我们可以使用内置的 paddle.io
库来实现异步数据读取,这个过程主要包括两个步骤:
paddle.io.Dataset
类;paddle.io.DataLoader
创建异步数据迭代读取器。下面我们仍然以十二生肖数据集Zodiac为例,来构建基于异步数据读取模式的数据读取框架。此处,我们同样将实现四个数据子集的获取,包括训练集、验证集、训练验证集和测试集。
在异步数据读取模式下,全局参数的定义和必要库的导入基本上和同步的一致,但需要增加一个 paddle.vision.transforms
类来实现数据预处理和后续的数据增广操作。
# codes04007_asynchronous_initialization import os import cv2 import json import numpy as np import matplotlib.pyplot as plt import sys sys.path.append(r'D:\WorkSpace\DeepLearning\WebsiteV2') # 定义课程自定义模块保存位置 from codes.paddle import common, datasets import paddle import paddle.vision.transforms as T # 1. 定义数据集基本信息 dataset_name = 'Zodiac' dataset_path = 'D:\\Workspace\\ExpDatasets\\' dataset_root_path = os.path.join(dataset_path, dataset_name) # 2. 图像基本信息 args = { 'input_size': [3, 227, 227], # 定义图像输入模型时的尺寸 'mean_value': [0.485, 0.456, 0.406], # Imagenet均值 'std_value': [0.229, 0.224, 0.225], # Imagenet标准差 }
在实际的场景中,一般需要使用自有的数据来构建和定义数据集,此时可以通过继承 paddle.io.Dataset
基类来实现自定义数据集类的创建。该类主要实现以下三个函数:
__init__
):完成数据集初始化操作以及基本参数的定义,并将磁盘中的样本文件路径和对应标签映射到一个列表中。__getitem__
):定义指定索引(index)时如何获取样本数据,最终返回对应 index 的单条数据,包括样本数据和其对应的类别标签。__len__
):返回数据集的样本总数。paddle.io.Dataset
类来定义数据集类。该类主要包括三个内置的函数,分别是 __init__
, __getitem__
和 __len__
。# codes04008_asynchronous_create_dataset class DatasetZodiac(paddle.io.Dataset): # 1. 初始化数据集,并将样本和标签映射到列表中 def __init__(self, dataset_root_path, mode='test'): # 2. 定义数据获取函数,返回单条数据(样本数据、对应的标签) def __getitem__(self, index): # 3. 定义样本总数获取函数 def __len__(self):
均值减除和方差(标准差)归一化是图像标准化最常见的方法,它可以通过消除相邻像素之间的相似性来突出目标的显著性。均值减除指将图像的均值变为0,标准差归一化指将标准差置为1。不过根据UFLDL的描述,对于自然图像,即使不做标准差归一化操作,图像也基本上满足1标准差的要求。下面代码给出的标准化参数是在百万级别的Imagenet数据集上计算出的均值和方差,对于自然图像,通常情况都使用该值作为默认值。在 RGB色彩体系下,我们将这两个超参数定义为:mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225] 或 mean=[124, 117, 104], std=[58,57,57]。
def __init__(self, dataset_root_path, mode='test', args=None, isTransforms=2): assert mode in ['train', 'val', 'test', 'trainval'] self.data = [] # 定义数据序列,用于保存数据的路径和标签 self.args = args # 定义超参数列表 self.isTransforms = isTransforms # 定义transforms类型 # 读取数据列表文件,将每一行都按照路径和标签进行拆分成两个字段的序列,并将序列依次保存至data序列中 # 1) 若列表信息长度为2,则表示包含路径和标签信息。 # 2) 若列表信息长度为1,则表示只包含路径,不包含标签。一般正式的测试文件都只包含路径,不包含标签。 with open(os.path.join(dataset_root_path, mode+'.txt')) as f: for line in f.readlines(): info = line.strip().split('\t') # 拆分从列表文件中读取到数据信息 image_path = info[0].strip() # 信息的[0]位置为路径 if len(info) == 2: # 判断信息的长度,若包含标签则写入image_label image_label = info[1].strip() elif len(info) == 1: # 判断信息的长度,若不包含标签,则用"-1"表示 image_label = -1 self.data.append([image_path, image_label]) # 将路径和标签写入[data]容器 # 对训练数据和验证、测试数据采用不同的数据预处理方法 # 1) train和trainval:执行随机裁剪,并完成标准化预处理 # 2) train和trainval:直接执行尺度缩放,并完成标准化预处理 inputSize = self.args['input_size'][1:3] if len(self.args['input_size'])==3 else self.args['input_size'] if self.isTransforms == 0: self.transforms = T.Compose([ # 0) 必要数据规约 T.Resize(inputSize), # 直接尺度缩放 T.ToTensor(), # 转换成Paddle规定的Tensor格式 ]) elif self.isTransforms == 1 or (self.isTransforms == 2 and mode in ['val', 'test']): self.transforms = T.Compose([ # 1) 基本数据预处理,不含数据增广 T.Resize(inputSize), # 直接尺度缩放 T.ToTensor(), # 转换成Paddle规定的Tensor格式 T.Normalize(mean=self.args['mean_value'], # 均值方差归一化 std=self.args['std_value']) ]) elif self.isTransforms == 2 and mode in ['train', 'trainval']: self.transforms = T.Compose([ # 2) 训练数据预处理,包含数据增广 T.Resize((256, 256)), # 直接尺度缩放 T.RandomResizedCrop(inputSize), # 随机裁剪 T.RandomHorizontalFlip(prob=0.5), # 水平翻转 T.RandomRotation(15), # 随机旋转 T.ColorJitter(brightness=0.4, # 色彩扰动:亮度、对比度、饱和度和色度 contrast=0.4,
def __getitem__(self, index): image_path, label = self.data[index] # 根据索引,从列表中取出指定[index]图像,并将数据拆分成路径和列表 img = cv2.imread(image_path, 1) # 使用cv2进行数据读取,0为灰度模式,1为彩色模式 img = self.transforms(img) # 执行数据预处理 label = np.array(label, dtype='int64') # 将标签转换为64位整型 return img, label
def __len__(self): return len(self.data)
在上面的代码中,我们自定义了一个数据集类 DatasetZodiac
,该类继承自 paddle.io.Dataset
基类,并且实现了 __init__
,__getitem__
和 __len__
三个函数。
__init__
函数中完成了对标签文件的读取和解析,并将所有的图像路径 image_path 和对应的标签 label 存放到一个列表 data_list 中。同时,还可以根据数据子集的作用来实现一些基本的数据预处理操作,如像素归一化、数据类型转换、均值消除等操作,这些操作将在 __getitem__
函数中被应用到样本上。此外,在飞桨框架的 paddle.vision.transforms
类中内置了几十种图像数据处理方法,详细使用方法可以参考 第4.4节 数据增广 。__getitem__
函数中定义了指定索引获取对应图像数据的方法,完成了图像的读取以及均值消除、尺度缩放等数据规约操作,并最终返回32位浮点型的图像(image)和其对应的64位整型的标签值( label)。在该函数内,我们还增加了一个标志 isTransforms
来判断是否使用数据预处理和数据增广。在一些特殊的情况下,例如可视化数据集中的原始图像时不需要执行数据预处理和数据增广。__len__
函数中返回 __init__
函数中初始化好的数据集列表 data_list 的长度。创建了数据集类之后,就可以通过实例化的方法来将数据集中的数据载入到内存中。
# codes04009_asynchronous_create_reader dataset_train = DatasetZodiac(dataset_root_path, args=args, mode='train') dataset_val = DatasetZodiac(dataset_root_path, args=args, mode='val') dataset_trainval = DatasetZodiac(dataset_root_path, args=args, mode='trainval') dataset_test = DatasetZodiac(dataset_root_path, args=args, mode='test')
Zodiac数据集由12个类别的图像组成,每个类别由训练集(train dataset)中的600张图像、验证集(val dataset)中的54张图像和测试集中55张图像组成。因此,训练集、验证集和测试集分别包含7190、650和660张图像。测试数据集和验证数据集不会用于训练,只用于评估模型性能。注意部分数据可能存在损坏的情况,因此需要通过数据清洗操作将其进行实现排除(参考 4.2小节 )。
print('train:{}, val:{}, trainval:{}, test:{}'.format(len(dataset_train),len(dataset_val), len(dataset_test), len(dataset_trainval)))
train:7190, val:650, test:660, trainval:7840
该数据集中的每个输入图像的高度和宽度都在数据集类中被约束成224像素,这也是分类任务的一种典型配置。所有输入图像都由彩色图像组成,其通道数为3,即红绿蓝(RGB)三个通道。为了简洁起见,本书将高度h像素和宽度w像素的图像记为 h×w 或 (h,w)。
dataset_train[0][0].shape
[3, 224, 224]
Zodiac数据集中总共包含的12个类别,分别是dog(狗)、dragon(龙)、goat(羊)、horse(马)、monkey(猴)、ox(牛)、pig(猪)、rabbit(兔)、ratt(鼠)、rooster(鸡)、snake(蛇)、tiger(虎)。以下函数用于在数字标签中索引及其文本名称之间进行转换。
#@save dataasets.get_Zodiac_labelname_from_labelID @TODO:4.3 def get_Zodiac_labelname_from_labelID(label_id): """根据标签ID,返回Zodiac数据集的文本标签""" 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'} return label_dict[str(label_id)]
dataset_info_path = os.path.join(dataset_root_path, 'dataset_info.json') print('标签 0 的名称为:{}。'.format(datasets.get_Zodiac_labelname_from_labelID_by_json(0, dataset_info_path)))
标签 0 的名称为:dog。
此外,我们也可以通过读取 4.3节 中生成数据集信息文件 dataset_info.json
来获取标签字典。对于自定义数据集推荐使用这种方法来获取数据标签。
#@save dataasets.get_Zodiac_labelname_from_labelID_by_json @TODO:4.3 def get_Zodiac_labelname_from_labelID_by_json(label_id, dataset_info_path): """根据标签ID,返回Zodiac数据集的文本标签,标签信息来源于dataset_info.json""" dataset_info = json.load(open(dataset_info_path, 'r', encoding='utf-8')) label_dict = dataset_info['label_dict'] return label_dict[str(label_id)]
dataset_info_path = os.path.join(dataset_root_path, 'dataset_info.json') print('标签 0 的名称为:{}。'.format(datasets.get_Zodiac_labelname_from_labelID_by_json(0, dataset_info_path)))
标签 0 的名称为:dog。
接下来,我们定义一个图片可视化函数来显示数据集中的图像。为了比较好的可视化数据集中的图像,我们需要关闭读取器的数据预处理功能(isTransforms=0),同时开启小批量迭代读取器的打乱功能(shuffle=True)。注意,对于测试集数据一般不需要进行打乱操作。
#@save common.show_dataset_images @TODO:4.3 def show_dataset_images(reader, num_rows=2, num_cols=6, scale=1.5): "显示数据集中的图像文件" _, (image, label) = next(enumerate(reader)) num_images = num_rows*num_cols image = np.transpose(image[0:num_images], (0,2,3,1)) label = label[0:num_images] plt.figure(figsize = (num_cols*scale, num_rows*scale+1)) for i in range(1, num_rows+1): for j in range(1, num_cols+1): n = num_cols*(i-1)+j ax = plt.subplot(num_rows, num_cols, n) ax.set_title(datasets.get_Zodiac_labelname_from_labelID(int(label[n-1]))) img = cv2.cvtColor(image[n-1].numpy(), cv2.COLOR_BGR2RGB) plt.imshow(img)
dataset_test = DatasetZodiac(dataset_root_path, mode='test', args=args,isTransforms=0) test_reader = paddle.io.DataLoader(dataset_test, batch_size=32, shuffle=True, drop_last=False) common.show_dataset_images(test_reader, num_rows=2, num_cols=6, scale=3)
为了使模型在读取训练集和测试集时更容易,推荐使用内置的数据迭代器API来创建小批量数据迭代读取器,而不是从零开始创建。在每次迭代训练时,数据加载器都会读取一个小批量的数据,其大小为 batch_size。此外,通过内置数据迭代器,我们可以随机打乱所有样本,从而无偏见地读取小批量样本,并提供给模型进行迭代训练。
# codes04013_asynchronous_create_dataLoader train_reader = paddle.io.DataLoader(dataset_train, batch_size=64, shuffle=True,drop_last=True) val_reader = paddle.io.DataLoader(dataset_val, batch_size=64, shuffle=False, drop_last=False) trainval_reader = paddle.io.DataLoader(dataset_trainval, batch_size=64, shuffle=True, drop_last=True) test_reader = paddle.io.DataLoader(dataset_test, batch_size=64, shuffle=False, drop_last=False)
在上述方法中,我们实例化了一个训练数据读取器 dataset_train
。依托于该实例,我们可以使用 paddle.io.DataLoader
类来创建一个数据迭代读取器 train_reader
。函数 DataLoader
可以返回一个异步的批次数据迭代器。它主要包含以下几个关键字段:
定义好数据读取器之后,就可以使用 for 循环来迭代地读取批次数据并用于模型训练了。值得注意的是,如果使用高层API中的 paddle.Model.fit
函数来训练数据,则只需定义数据集类 DatasetZodiac
,而不需要再单独定义迭代读取器。因为 paddle.Model.fit
中实际已经封装了 DataLoader
的功能。下面给出测试迭代读取器的函数。
# codes04014_asynchronous_print_reader for i, (image, label) in enumerate(val_reader()): if i < 2: print('验证集batch_{}的图像形态:{}, 标签形态:{}' .format(i, image.shape, label.shape)) else: break
验证集batch_0的图像形态:[64, 3, 224, 224], 标签形态:[64] 验证集batch_1的图像形态:[64, 3, 224, 224], 标签形态:[64]
现在我们定义 load_dataset_Zodiac
函数,用于完整的获取和读取Zodiac数据集。这个函数返回数据集的验证集、训练集、测试集和训练验证集四个子集的批量数据迭代器。此外,这个函数还接受两个可选参数batch_size和transform,前者用于设置迭代器的批量大小,后者则用于接受各种数据预处理参数。
# codes04015_load_dataset_Zodiac def load_dataset_Zodiac(batch_size=64, transformArgs=args, isTransforms=2): """载入Zodiac数据集并对其进行基本预处理""" # 1. 实例化数据类 dataset_train = datasets.DatasetZodiac(dataset_root_path, args=transformArgs, isTransforms=isTransforms, mode='train') dataset_val = datasets.DatasetZodiac(dataset_root_path, args=transformArgs, isTransforms=isTransforms, mode='val') dataset_trainval = datasets.DatasetZodiac(dataset_root_path, args=transformArgs, isTransforms=isTransforms, mode='trainval') dataset_test = datasets.DatasetZodiac(dataset_root_path, args=transformArgs, isTransforms=isTransforms, mode='test') # 2. 创建小批量数据迭代读取器 # 使用paddle.io.DataLoader 定义DataLoader对象用于加载Python生成器产生的数据, # DataLoader 返回的是一个批次数据迭代器,并且是异步的。 train_reader = paddle.io.DataLoader(dataset_train, batch_size=batch_size, shuffle=True, drop_last=False) val_reader = paddle.io.DataLoader(dataset_val, batch_size=batch_size, shuffle=False, drop_last=False) trainval_reader = paddle.io.DataLoader(dataset_trainval, batch_size=batch_size, shuffle=True, drop_last=False) test_reader = paddle.io.DataLoader(dataset_test, batch_size=batch_size, shuffle=False, drop_last=False) return train_reader, val_reader, trainval_reader, test_reader
下面,我们通过设置transform参数来测试load_dataset_Zodiac函数的参数调整功能。
# codes04016_Zodiac_create_dataLoader train_reader, val_reader, train_reader, test_reader = load_dataset_Zodiac(batch_size=32, transformArgs=args, isTransforms=2) for i, (image, label) in enumerate(val_reader): if i < 2: print('验证集batch_{}的图像形态:{}, 标签形态:{}'.format(i, image.shape, label.shape)) else: break
验证集batch_0的图像形态:[32, 3, 100, 100], 标签形态:[32] 验证集batch_1的图像形态:[32, 3, 100, 100], 标签形态:[32]
一般来说,异步数据读取模型需要在大规模的数据集上运行才能看出其明显的效果。不过,这两种模式都并不影响模型训练的精度。为了规范化,还是建议使用异步数据读取方法进行数据读取。在后面的项目中,我们都将使用异步读取模式进行数据读取。此外,数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代读取器,可以实现深度学习训练中最重要的设计——基于小批量数据的训练。这种技术可以应对单一显卡容量不足的问题,并且可以有效利用高性能并行计算来提高训练速度。
尝试对其他数据集进行数据集类定义和数据读取器的定义,并完成相关的测试。
以下描述符合异步数据读取的包括:()。
A. 适合数据量较大、数据读取较慢的场景
B. 数据的读取和模型的训练以串行方式进行
C. 模型直接从缓存队列获取数据
D. 在进行模型训练前需要一次性读取整个数据集的样本
以下函可以用来实现小批量数据迭代读取的是()。
A. paddle.reader.shuffle
B. paddle.vision.transforms
C. paddle.io.DataLoader
D. cv2.imread
E. paddle.reader.xmap_reader
在对训练数据和测试数据进行处理的时候,都需要将原始样本Resize到模型要求的固定尺寸(例如224×224)。那么,对于训练集和验证集来说,它们所使用的Resize方法必须保持一致。
A. 正确
B. 错误
在创建小批量数据迭代读取器的时候,通常需要将批次数进行打乱操作,下列哪些数据子集必须执行打乱。()
A. 训练集
B. 验证集
C. 测试集
D. 训练验证集
OpenCV中默认的色彩空间存储格式是()。
A. RGB
B. HSV
C. BGR
D. HSL
对于一个神经网络模型,规定它的输出为28×28的RGB图像,输入张量形状为[8,3,28,28],这里8表示()。
A. batch_size
B. channel
C. height
D. epoch