1 问题
探索DataLoader的属性,方法
Vscode中图标含意
list 与 iterator 的区别,尤其yield的用法
2 方法
知乎搜索DataLoader的属性,方法
pytorch基础的dataloader类是 from torch.utils.data.dataloader import Dataloader 其主要的参数如下:
datasets、batch size、shuffle、sampler、batch_sampler、num_workers、collate_fn、pin_memory、drop_last ...
(1)datasets
这可以算是dataloader最重要的参数,从本质上讲这是一个数据集映射器(DatasetMapper,这里借用了d2的概念),所谓数据集映射器,就是将数据集dataset以及其标注文件annotations,从原始的数据文件中进行提取,并转换为,网络所需要的输入数据的格式。这一步重要且繁琐,但是定义数据集dataset整体的结构还是固定的。
定义数据集dataset类的基本结构如下:
class mydataset(Dataset):# 这里可以继承torch的数据集类进行覆写,也可以不继承,直接自己去定义
def __init__():
#定义映射数据集所需要的一些基本的属性(例如数据样本的文件目录,数据的格式等等),
#如果是映射coco格式的数据集,则会使用pycocotools的COCO类进行数据提取的预定义
...
def __len__():#这里一般返回的是数据集的长度(就是数据集样本的数量)
...
def __getitem__():
#这里就是数据集映射功能的核心函数,dataloader就是通过它来对数据集的样本进行提取映射为所需要的格式
#当然,在进行数据映射的时候,也可以在这个方法中添加一些数据增强的功能,这都是无限制的。
...
# 在功能上 __getitem__ == __iter__ + __next__ 所以定义的dataset数据集,通常其实例对象本身也是一个迭代器
#这里注意区分 迭代器、可迭代对象、生成器3者之间的联系与区别:
#可迭代对象:任何一个实现了__iter__方法的对象,都是可迭代对象
#迭代器:任何一个同时实现了__iter__ 与 __next__ 方法的对象,都是一个迭代器
#生成器:任何一个同时实现了__iter__ 与 __next__ 方法的对象,并且在迭代过程中本身能够自动改变迭代值,则这种对象都是一个生成器
(生成器是一种特殊的迭代器)生成器的例子建议参看torch.utils.data.samplers 中的BatchSampler
-- 任何具有yield关键字的函数,都可以视为一种生成器,调用函数返回的是一个生成器对象,对该对象采用next()方法,才会调用该生成器函数
关于dataset类定义的例子很多,建议大家可以看看yolox中关于dataset的定义进行参考,在很多情况下,我们需要针对特定任务进行数据预处理,而这项工作大部分都是在dataset中定义完成的,有时因为涉及到的数据预处理任务比较多,可能会将dataset的定义分为两个部分,使得整个程序看上去不会那么的臃肿,一个是Mapper ,一个是augmentation,这两个部分对数据处理进行了明确的分工,一个用于格式转换映射,一个用于数据预处理增强。
(2)batch_size
这个很好理解了,就是用于提取数据的,批量大小。
(3)shuffle
这个也很好理解,提取数据的过程设置是随机还是按顺序提取,若是随机则为T,否则为F。实际上shuffle参数的设置是调用了torch中固有sampler,如果设置为T,则dataloader会自动调用RandomSampler。
(4)sampler / BatchSampler ---- 采样器 / 批量采样器
采样器是数据处理中的一个比较特别的部分,在目前大部分的情况下,我们都是使用的是torch内部固定的采样器来实现对数据的采样,但是也有少数的情况下,由于一些数据本身的一些原因,我们需要对对采样器进行继承覆写--就是自定义,如果采样器是自定义的话,则shuffle必须为F --- 都自定义了,就不要与框架实现的采样器冲突了。关于采样器自定义的格式如下(以BatchSampler为例):
class sampler(torch.utils.data.sampler):----- 一般这里要继承 torch 采样器类 并对相关的方法进行自定义重写
def __init__(self, data_source):
super().__init__():
...
def __iter__(self)-> iterator[int] 或者是 Iterator[list[int]] :用于返回所采样数据 在数据集中的索引idx
...
# 这里的__iter__方法,一般实现的是一个生成器,用yield关键字进行返回值,
#实现在迭代过程中动态改变采样数据的索引idx---就是改变返回数据的索引idx
def __len__(self)-> int:
实现的是采样器 在整个数据集的采样次数 这里的采样次数都是跟batch_size有关 总采样次数len == 数据集总样本数 / 采样的batch_size
if self.drop_last:
return len(self.sampler) // self.batch_size # type: ignore[arg-type]
else:
return (len(self.sampler) + self.batch_size - 1) // self.batch_size # type: ignore[arg-type]
...
def xxx():----- 一般这里是对上述的3个基本的方法 进行补充说明 写一些补充上述3个基本的方法的一些功能或者扩展
...
# 比如可以添加一些多尺度训练的功能,或者是其他对与采集样本的一些功能,这个就比较随机了,自己定义即可。
实现采样器,必须得有__init__, __iter__, __len__方法。
由于BatchSampler中对数据采样的方式以及batch的大小,drop_last的规定已经作了一些说明和定义,因此如果自定义了一个BatchSampler,就不需要batch size、shuffle、sampler、drop_last等参数了 ---- 原则就是自定义的功能不要与dataloader中调用的框架的固有功能冲突。
(5)num_workers ---- 子进程数量,这个在分布式的训练中要特别注意,进程的数量以及GPU数量(word—size)的把控和分配。一般而言。子进程数量越多,dataloader的执行的效率就越高,但是也越耗cpu。
(6)pin_memory -- 锁页内存,主机的内存分为 锁页 和 不锁页 两种内存,因为cuda只接受 锁页内存 的传入,因此在创建DataLoader时,设置pin_memory=True,则意味着生成的Tensor数据最开始就是属于内存中的锁页内存,这样将内存的Tensor转义到GPU的显存就会更快一些----但是缺点是对于服务器内存的要求比较高。
(7)drop_last --- 这个主要用于设置在批量提取数据时,是否要将“不能整除的”最后一部分达不到小批量数目的数据扔掉--不参加训练,默认为F,你也可以设置为T。
(8) collate_fn ---- 封装器。
dataloader通过其定义的sampler(返回采样的批量的数据样本索引idx)对定义的dataset(根据这些索引idx)进行采样并预处理数据。这样的话,dataloader批量迭代数据的结果可能就会包含一个批量batch-size中,所有样本的结果(例如batchsize设置为4,则dataloader就会返回数据集dataset中采样的4个样本的结果),而collate_fn 的作用,就是要对这些一个batch的迭代结果进行封装,使其整合为神经网络所需要的输入形式-----举个例子:就是从batchsize个(c,h,w)转化为大家最熟悉的(b,c,h,w)----- 因此很明显,collate_fn的作用就是为了对dataloader处理的批量输出进行封装整合,转化为网络所需要的输入形式。这项功能既可以自定义,也可以使用默认的框架功能。不过对于不同的框架,不同的环境和任务,很多情况下这个都是自定义的,定义的方法就是一个function,也不需要专门搞一个类的形式来定义它(除非是大型的工程,需要用到很多种封装形式),其基本的定义的格式如下:
def collate_fn(batch):
# 输入为:dataloader在迭代过程中,产生的 批量处理后的 数据样本 --- 例如:batchsize个 list / tuple / dict
# 每个list/tuple/dict 都是dataloader处理后的一个batch中的单个样本的结果
...
# 返回为:经处理整合之后的批量的数据样本,并作为dataloader最终的迭代输出结果 -- 例如:一个 list / tuple / dict
当然,如果不用这个封装功能,直接忽略该参数也是可以的,最后dataloader会使用其默认的collate_fn,并返回一个batchsize的结果 ---- 返回的结果来源于,dataset中的__getitem__返回的结果。
百度Vscode中图标含意
橙色树状结构:类 紫色立方体:方法
长方体:变量 局部变量 成员变量
命名空间。 属性 。事件
百度list 与 iterator 的区别,
1、返回的类型不同,list()返回List,iterate()返回Iterator
2、获取方式不同,list会一次性将数据库中的信息全部查询出,iterate会先把所有数据的id查询出来,然后真正要遍历某个对象的时候先到缓存中查找,如果找不到,以id为条件再发送一条sql到数据库,这样如果缓存中没有数据,则查询数据库的次数为n+1
3、iterate会查询2级缓存,list只会查询一级缓存
4、list中返回的List中每个对象都是原本的对象,iterate中返回的对象是代理对象。(debug可以发现)
百度yield的用法
1 什么是yield函数?
Python中yield函数是一个生成器(generator),可用于迭代;在函数中yield类似于return,不同的是,yield返回一个return的值并且记住这个返回值的位置,下次迭代就从记住的这个位置开始,并且下一次迭代时,从上一次迭代遇到的yield后面的代码开始执行。
2 yield函数的特点及用法。
yield函数的优点在于它可迭代,但又不直接生成返回值,如果采用return来返回值,就会直接生成返回值;如果返回的值,或者迭代的数据太大,都会使得内存消耗过大;yield函数就会很好的减少内存的消耗,但是它只可读取一次。带有yield的函数不仅仅可以用于for循环,还可以用于函数的参数,例如:
#用于for循环 def yields(n): print('yield用法:') while n<10: n+=1 yield n return 'pass' c=yields(0) print(next(c)) print(next(c)) print(next(c)) 输出: 1 2 3 #用于函数的参数 def a(): print('aaa') p = yield '123' print('bbb') k = yield '234' r = a() print(next(r)) 输出: aaa 123 |
思考一下如果将用于函数参数的yield再增加一个输出next()会发生什么情况呢?
def a(): print('aaa') p = yield '123' print('bbb') k = yield '234' r = a() print(next(r)) print(next(r)) 输出: aaa 123 bbb 234 |
由以上代码以及运行结果不难发现,每一个next返回值,都会在执行到yield函数后暂停生成,下一次next返回值则会继续从上一个暂停的位置执行,这也是yield函数的特点与用法。
3 send()与next()用法的异同。
next()函数可以不断打印yield生成器的值;
send()函数特别之处在于它可以携带参数,并修改上一个表达式的值,同时用法也与next()有很多相同之处;
3.1相同点
相同点在于,当send()所携带的参数为None(即未携带任何参数)时,用法与next()一模一样,都仅仅是来打印yield生成器的值。
3.2 不同点
不同点在于当send()所携带的参数时,就会将所带参数赋值给上一个表达式;实例:
def a(): print('send():') i = yield 123 print(i) if i==234: print("send传入的参数为234") k = yield 345 print(k) r=a() next(r) r.send(234) 输出: send(): 234 Send传入的参数为234 |
分析:首先执行next(r),当第一次遇见yield跳出输出send():;然后执行r.send(234),send()直接将234参数传给i中并从yield位置继续执行,输出i,值为234,然后输出if条件语句,当执行到下一个yield时,也就是k=yield 345时,跳出。
注意:yield的第一次执行一定为next(r)或者r.send(None)。
3 结语
了解了Dataloader 其主要的参数,有助于我更好地理解和使用这个类,从而解决图像基本信息问题。
Vscode中图标含意有助与我在日后的写代码的过程中快速的选择自己想要的函数。
list 与 iterator 的区别,yield的用法。有助于在今后代码的迭代选择时应需求选择。