跟着问题学5——深度学习中的数据集详解(1)
深度学习数据集的创建与读取
数据 (计算机术语)
数据(data)是事实或观察的结果,是对客观事物的逻辑归纳,是用于表示客观事物的未经加工的的原始素材。
数据可以是连续的值,比如声音、图像,称为模拟数据。也可以是离散的,如符号、文字,称为数字数据。
在计算机系统中,数据以二进制信息单元0,1的形式表示。
数据 (汉语词语)
数据就是数值,也就是我们通过观察、实验或计算得出的结果。数据有很多种,最简单的就是数字。数据也可以是文字、图像、声音等。数据可以用于科学研究、设计、查证等。
数据的形式在深度学习中是什么样的呢?数据在深度学习中是以多维数组的形式存储的,而数据代表的就是样本的数字信息特征,一般来说,对于单一样本的特征,可以像影响房价的因素那样是一维向量数组,也可以是图像特征那样是二维数组,还可以是后面介绍的语言文字那样是一维词向量数组,在训练的时候,通常会批量读取训练,所以数据集会在原有的维度基础上加一个batch_size的维度。
一直在强调,深度学习中最重要的因素之一是数据集,而刚刚入门深度学习的时候也经常会在数据集上无从下手。一般来说,针对数据集,我们需要明确2个要素,以什么样的方式存储在哪里,怎么读取用于训练。
数据存储与数据划分
将数据按照一定的格式存储起来形成的集合就是数据集。
深度学习中,接下来我们将结合pytorch的实例详细介绍一下数据集相关知识。
除非使用已经封装好的数据集如MNIST等可以直接调用api接口导入数据,我们在网上下载也好,自己收集标注也好,都需要将图片,音频,视频,语言文字等模拟数据存储在电脑或服务器特定的路径位置下。
另一点值得注意的是下载和收集的数据要符合特定的格式(尤其不同的模型可能要求数据的格式是不同的,比如MASK-RCNN 和YOLO),这样才能方便代码读取,这里暂时不做过多的展开介绍,后面进行补充,这篇文章重点还是介绍数据的读取过程。
数据集的两个重要因素一是原始数据特征,比如图片,文字等,二是标签,比如类别或者物体分割时的位置等,在存储时要分清他们的位置,便于读取。 物体分割时比如MASK-RCNN 和YOLO,需要使用标注工具label等手动获取标签;对于分类任务,一般可以将不同类别的图片划分到不同的子文件夹里,并命名为类别或数字,读取标签的时候直接读取子文件夹名称即可。如下图是表情分类任务的数据集,以不同的数字代表表情类别。
数据集一般会划分为训练,测试和验证三个子集,网上下载好的数据一般是划分好的,若没有划分好或者是自己收集制作的,可以自己划分。最快速上手的方式就是手动分割,不过代码具有泛用性,数据划分主要是路径的处理和文件的遍历,下面以具体例子说明,最开始是data文件夹下有2个类别的子文件夹0和1,
现在我们想要将左图中的原始数据切割成右图中的分train,test,valid的数据集,关键点就是遍历左边的数据,然后设置比例分为三份,并按比例将其复制到右边。这里边涉及几个重要的文件处理函数,一个是os.makedirs(“path”),在检测到路径无文件夹存在时调用创建文件夹;一个是os.listdir(path),会将路径下的文件或文件夹名称储存到列表里;一个是os.walk(root_dir),是深度遍历的方式遍历自根文件夹下的所有子文件夹及文件,返回的是三元组(root,subdirs,filenames),
每一层遍历:
root保存的就是当前遍历的文件夹的绝对路径;
subdirs保存当前文件夹下的所有子文件夹的名称(仅一层,孙子文件夹不包括)
filenames保存当前文件夹下的所有文件的名称
举个例子,
-dir1
-1.jpg
-subdir1
--2.jpg
--3.jpg
-subdir2
--4.jpg
--5.jpg
os.walk() 之后遍历的结果就是
第一次遍历输出三元组
dir1 [subdir1,subdir2],1.jpg
第二次遍历输出三元组
dir/subdir1,[],[2.jpg,3.jpg]
第三次遍历输出三元组
dir/subdir2,[],[4.jpg,5.jpg]
而且需要注意的是,这三个输出是在同一个for path,dirnames,filenames in os.walk(root)下递归执行的
检测路径是否存在,若不存在,则创建此路径。
def makedir(new_dir):
if not os.path.exists(new_dir):
os.makedirs(new_dir)
设置路径,将它们组合在一起。相对于Python文件所在位置的相对路径。
dataset_dir = os.path.join("..", "..", "data", "RMB_data")
深度遍历,获取根文件夹和子文件夹下的文件,并对其按照设置的比例存储,以遍历第一个子文件夹为例说明一下:
#进入深度遍历
for root, dirs, files in os.walk(dataset_dir):
#开始遍历第一个子文件夹
for sub_dir in dirs:
#将此子文件夹下的文件名存储到列表里
imgs = os.listdir(os.path.join(root, sub_dir))
imgs = list(filter(lambda x: x.endswith('.jpg'), imgs))
random.shuffle(imgs)
img_count = len(imgs)
#设置划分比例
train_point = int(img_count * train_pct)
valid_point = int(img_count * (train_pct + valid_pct))
#开始按照划分比例切分图片文件
for i in range(img_count):
if i < train_point:
#根据归属于不同的比例填入或创建目标路径
out_dir = os.path.join(train_dir, sub_dir)
elif i < valid_point:
out_dir = os.path.join(valid_dir, sub_dir)
else:
out_dir = os.path.join(test_dir, sub_dir)
#创建目标路径
makedir(out_dir)
#将文件从源文件复制到目标文件位置
target_path = os.path.join(out_dir, imgs[i])
src_path = os.path.join(dataset_dir, sub_dir, imgs[i])
shutil.copy(src_path, target_path)
shutil.copy()Python中的方法用于将源文件的内容复制到目标文件或目录。它还会保留文件的权限模式,但不会保留文件的其他元数据(例如文件的创建和修改时间)。源必须代表文件,但目标可以是文件或目录。如果目标是目录,则文件将使用源中的基本文件名复制到目标中。另外,目的地必须是可写的。如果目标是文件并且已经存在,则将其替换为源文件,否则将创建一个新文件。
数据的变换
在图像数据集的训练中,经常会利用torchvision.transforms.Compose()将对图像预处理的操作以列表[,,]的形式组合在一起,然后赋值给到data_transform,并作为后续创建dataset时的参数,最常用的分为两类:
一是图像本身的变换,如随机裁剪和翻转,图像变换的时候要注意维度保持一致。
(1)transforms.CenterCrop(size)
从图片中心截取size大小的图片。
(2)transforms.RandomCrop(size,padding,padding_mode)
随机裁剪区域。
(3)transforms.RandomResizedCrop(size,scale,ratio)
随机大小,随机长宽比的裁剪。
(1)transforms.RandomHorizationalFlip(p)
依据概率p水平翻转。
(2)transforms.RandomVerticalFlip(p)
依据概率p垂直翻转。
(3)transforms.RandomRotation(degrees,resample,expand)
二是将“图像”转化为“tensor”,我们知道,任何数据(包括图像音频等)在计算机上的计算都是要转化成数字信息的,所以我们需要将图像数据转化成pytorch中数字信息以及存储格式tensor。
transforms.Totensor()
将图像数据转换为tensor。
transforms.Normalize(mean,std,inplace)
逐通道的标准化,每个通道先求出平均值和标准差,然后标准化。Inplace表示是否原地操作。
下面代码为常用的数据变换格式,还有很多其它变换方法,可以在需要使用的时候查询。
train_transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])
数据集转换dataset
获得并划分处理好的格式数据之后,需要利用代码将其转变为模型训练可用的数据集,所谓数据集,其实就是一个负责处理索引(index)到样本(sample)映射的一个类(class)。
数据集的三个核心参数便是从格式数据传递的存储路径,给每一个数据加的索引,以及数据集的大小(数据集总数)。
dataset(继承于Dataset)是可以我们自己用代码实现的一个类,这个类中主要包括__init__(),__getitem__(),__len__()这三个函数:
1)__init__():构造函数,该函数里面会定义存放数据的路径,一般由人给定输入。
2)__getitem__():这个函数把传入的索引Index和数据(标签)对应起来,返回数据(标签);
3)__len__():这个函数获取数据集中样本的总个数。
Pytorch提供两种数据集: Map式数据集 Iterable式数据集
目前深度学习将数据转换成训练可用的数据集有三种形式,
PyTorch集成的数据集接口
实际上,PyTorch提供了很多常用数据集的接口,如果使用这些数据集的话,可以直接使用对应的包加载,会方便很多,比如:
- torchvision.datasets 就提供了很多视觉方向的数据集:https://pytorch.org/docs/stable/torchvision/datasets.html?highlight=torchvision datasets
- torchtext 则提供了很多文本处理方向的数据集
- torchaudio 提供了很多音频处理方向的数据集
当然 PyTorch也可以配合其他包来获得数据以及对数据进行处理,比如:
- 对于视觉方面,配合 Pillow、OpenCV等
- 对于音频处理方面,配合 scipy、librosa等
- 对于文本处理方面,配合 Cython、NLTK、SpaCy等
实际例子来说,比如MNIST,CIFAR10等,这时可以直接利用一句代码读取出来:
在PyTorch中CIFAR10是一个写好的Dataset,我们使用时只需以下代码:
data = datasets.CIFAR10("./data/", transform=transform, train=True, download=True)
此时,代码会检查当前路径下“./data”是否存在cifar10数据集,若不存在,download为true时则启动下载,transform后面会介绍。
ImageFolder 等API
二是网络上一些公开的数据集,这时可以直接下载下来,然后利用定好的方法将其转化为模型训练的数据集,比如我们下载一些图片数据,放到一个文件夹下,这时可以使用ImageFolder这个方便的API。
FaceDataset = datasets.ImageFolder('./data', transform=img_transform)
这个是应用比较广泛的方法,因为很多时候我们会将图片放到特定的文件夹路径下。这里需要注意标签是利用api自动读取的文件夹的名称并返回列表的。
自定义一个数据集
三则是自定义一个数据集,torch.utils.data.Dataset 是一个表示数据集的抽象类。任何自定义的数据集都需要继承这个类并覆写相关方法。这个其实是更深入的了解一下dataset数据集的本质。
Map式数据集
一个Map式的数据集必须要重写getitem(self, index),len(self) 两个内建方法,用来表示从索引到样本的映射(Map).
Map式的数据集dataset,举个例子,当使用dataset[idx]命令时,可以在你的硬盘中读取你的数据集中第idx张图片以及其标签(如果有的话);len(dataset)则会返回这个数据集的容量。
自定义类大致是这样的:
class CustomDataset(data.Dataset):#需要继承data.Dataset
def __init__(self):
# 可以初始化文件路径或者数据转换格式等其它处理信息
def __getitem__(self, index):
# 根据输入的索引参数读取一个数据和标签,可以将数据进行处理后返回数据和标签
def __len__(self):
# 返回数据集的大小
例子-1: 图片文件储存在“./data/faces/”文件夹下,图片的名字并不是从1开始,而是从final_train_tag_dict.txt这个文件保存的字典中读取,label信息也是用这个文件中读取。
from torch.utils import data
import numpy as np
from PIL import Image
class face_dataset(data.Dataset):
def __init__(self,label_dict):
self.file_path = './data/faces/'
self.label_dict=label_dict
def __getitem__(self,index):
label = list(self.label_dict.values())[index-1]
img_id = list(self.label_dict.keys())[index-1]
img_path = self.file_path+str(img_id)+".jpg"
img = np.array(Image.open(img_path))
return img,label
def __len__(self):
return len(self.label_dict)
Iterable式数据集
一个Iterable(迭代)式数据集是抽象类data.IterableDataset的子类,并且覆写了iter方法成为一个迭代器。这种数据集主要用于数据大小未知,或者以流的形式的输入,本地文件不固定的情况,需要以迭代的方式来获取样本索引。
小结
在实际的应用中,可以根据具体的任务或调用API,或自己重写数据集类,一般会在创建数据集的步骤中将数据变换列表transforms作为参数写进去,直接对数据进行变换处理。
数据样本加载DataLoader
创建完成数据集dataset后,还不能直接用于网络训练,下一步需要构建迭代器DataLoader作为网络训练时读取数据的方式,而数据集dataset是DataLoader实例化的一个参数。一般格式为:
train_dataloader=DataLoader(train_dataset,batch_size=16,shuffle=True)
Dataloader的一些常用参数
Dataloader的一些重要的参数如下,除了第一个 dataset参数外,一般会设置batch_size,shuffle,其他均视情况可选:
- dataset(第一个参数,必须的参数):一个 Dataset的实例,即前面三种方式创建传入的数据集(或者其他可迭代对象)
- batch_size:整数值,每个 batch的样本数量,即 batch大小,默认为1
- shuffle:bool值,如果设置为 True,则在每个 epoch开始的时候,会对数据集的数据进行重新排序,默认 False
- sampler:传入一个自定义的 Sampler实例,定义从数据集中取样本的策略,Sampler每次返回一个索引,默认为 None
- batch_sampler:也是传入一个自定义的 Sampler实例,但是与 sampler参数不同的是,它接收的 Sampler是一次返回一个 batch的索引,默认为 None
- num_workers:整数值,定义有几个进程来处理数据。0意味着所有的数据都会被加载进主进程,默认0
- collate_fn:传入一个函数,它的作用是将一个 batch的样本打包成一个大的 tensor,tensor的第一维就是这些样本,如果没有特殊需求可以保持默认即可(后边会详细介绍)
- pin_memory:bool值,如果为 True,那么将加载的数据拷贝到 CUDA中的固定内存中。
- drop_last:bool值,如果为 True,则对最后的一个 batch来说,如果不足 batch_size个样本了就舍弃,如果为 False,也会继续正常执行,只是最后的一个 batch可能会小一点(剩多少算多少),默认 False
- timeout:如果是正数,表明等待从加载一个 batch等待的时间,若超出设定的时间还没有加载完,就放弃这个 batch,如果是0,表示不设置限制时间。默认为0
Dataloader参数之间的互斥
值得注意的是,Dataloader的参数之间存在互斥的情况,这些初步了解即可。主要针对自己定义的采样器:
- sampler:如果自行指定了 sampler参数,则 shuffle必须保持默认值,即 False
- batch_sampler:如果自行指定了 batch_sampler参数,则 batch_size、shuffle、sampler、drop_last 都必须保持默认值
如果没有指定自己是采样器,那么默认的情况下(即 sampler和 batch_sampler均为 None的情况下),Dataloader的采样策略是如何的呢: - sampler:shuffle = True:sampler采用 RandomSampler,即随机采样
shuffle = Flase:sampler采用 SequentialSampler,即按照顺序采样
- batch_sampler:采用 BatchSampler,即根据 batch_size 进行 batch采样
Sampler类是一个很抽象的父类,其主要用于设置从一个序列中返回样本的规则,即采样的规则。
Dataloader其实还有一个比较重要的参数是 collate_fn,它接收一个 callable对象,比如一个函数,它的作用是将每次迭代出来的数据打包成 batch。
举个例子,如果我们在 Dataloader中设置了 batch_size为8,实际上,从 Dataloader所读取的数据集Dataset中取出数据时得到的是单独的数据,比如我们的例子中,每次采样得到一个 tuple:(image, label),因此 collate_fn 的作用就有了,它负责包装 batch,即每从数据集中抽出8个这样的 tuple,它负责把8个 (image, label)包装成一个 list: [images, labels],这个 list有两个元素,每一个是一个 tensor,比如第一个元素,实际上是一个 8×size(image) 的tensor,即给原来的数据增加了一维,也就是最前边的 batch的维度,labels也同理。
有时候我们可能会需要实现自己的包装逻辑,所以需要自定义一个函数来完成定制化的如上的内容,只要将该函数名传递给 collate_fn参数即可。
DataLoader读取数据样本的流程
综上,概括一下DataLoader读取数据样本的流程:
DataLoader首先创建一个迭代器DataloaderIter,
然后这个迭代器会去访问Sampler得到读取数据的索引值;
根据这个索引值去访问创建好的数据集DataSet;
利用DataSet的getitem函数读取对应索引值的样本数据;
然后利用collate_fn将独立的样本数据打包成batch_size大小的Batch Data。
深度学习数据集的创建与读取流程
而整个PyTorch中深度学习数据读取的流程是这样的:
1. 创建Dateset
2. Dataset传递给DataLoader
3. DataLoader迭代产生训练数据提供给模型
对应的一般都会有这三部分代码
# 创建Dateset(可以自定义)
dataset = face_dataset # Dataset部分自定义过的face_dataset
# Dataset传递给DataLoader
dataloader = torch.utils.data.DataLoader(dataset,batch_size=64,shuffle=False,num_workers=8)
# DataLoader迭代产生训练数据提供给模型
for i in range(epoch):
for index,(img,label) in enumerate(dataloader):
pass
到这里应该就PyTorch的数据集和数据传递机制应该就比较清晰明了了。Dataset负责建立索引到样本的映射,DataLoader负责以特定的方式从数据集中迭代的产生一个个batch的样本集合。在enumerate过程中实际上是dataloader按照其参数sampler规定的策略调用了其dataset的getitem方法。其中,还会涉及数据的变化形式。
参考资料
torch.utils.data — PyTorch 2.5 documentation
https://gladdduck.github.io/2022/11/09/笔记-Pytorch数据集加载/
深度之眼 pytorch学习课程