PyTorch常用工具模块
在训练神经网络过程中,需要用到很多工具,其中最重要的三部分是:数据、可视化和GPU加速。本章主要介绍Pytorch在这几方面的工具模块,合理使用这些工具能够极大地提高编码效率。
数据处理
在解决深度学习问题的过程中,往往需要花费大量的精力去处理数据,包括图像、文本、语音或其它二进制数据等。数据的处理对训练神经网络来说十分重要,良好的数据处理不仅会加速模型训练,更会提高模型效果。考虑到这点,PyTorch提供了几个高效便捷的工具,以便使用者进行数据处理或增强等操作,同时可通过并行化加速数据加载。
数据加载
在PyTorch中,数据加载可通过自定义的数据集对象。数据集对象被抽象为Dataset
类,实现自定义的数据集需要继承Dataset,并实现两个Python魔法方法:
__getitem__
:返回一条数据,或一个样本。obj[index]
等价于obj.__getitem__(index)
__len__
:返回样本的数量。len(obj)
等价于obj.__len__()
这里我们以Kaggle经典挑战赛"Dogs vs. Cat"的数据为例,来了解如何处理数据。"Dogs vs. Cats"是一个分类问题,判断一张图片是狗还是猫,其所有图片都存放在一个文件夹下,根据文件名的前缀判断是狗还是猫。
import torch as t |
通过上面的代码,我们学习了如何自定义自己的数据集,并可以依次获取。但这里返回的数据不适合实际使用,因其具有如下两方面问题:
- 返回样本的形状不一,因每张图片的大小不一样,这对于需要取batch训练的神经网络来说很不友好
- 返回样本的数值较大,未归一化至[-1, 1]
针对上述问题,PyTorch提供了torchvision。它是一个视觉工具包,提供了很多视觉图像处理的工具,其中transforms
模块提供了对PIL Image
对象和Tensor
对象的常用操作。
对PIL Image的操作包括:
Scale
:调整图片尺寸,长宽比保持不变CenterCrop
、RandomCrop
、RandomSizedCrop
: 裁剪图片Pad
:填充ToTensor
:将PIL Image对象转成Tensor,会自动将[0, 255]归一化至[0, 1]
对Tensor的操作包括:
- Normalize:标准化,即减均值,除以标准差
- ToPILImage:将Tensor转为PIL Image对象
如果要对图片进行多个操作,可通过Compose
函数将这些操作拼接起来,类似于nn.Sequential
。注意,这些操作定义后是以函数的形式存在,真正使用时需调用它的__call__
方法,这点类似于nn.Module
。例如要将图片调整为,首先应构建这个操作trans = Resize((224, 224))
,然后调用trans(img)
。下面我们就用transforms的这些操作来优化上面实现的dataset。
import os |
除了上述操作之外,transforms还可通过Lambda
封装自定义的转换策略。例如想对PIL Image进行随机旋转,则可写成这样trans=T.Lambda(lambda img: img.rotate(random()*360))
。
torchvision已经预先实现了常用的Dataset,包括前面使用过的CIFAR-10,以及ImageNet、COCO、MNIST、LSUN等数据集,可通过诸如torchvision.datasets.CIFAR10
来调用,具体使用方法请参看官方文档。在这里介绍一个会经常使用到的Dataset——ImageFolder
,它的实现和上述的DogCat
很相似。ImageFolder
假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
ImageFolder(root, transform=None, target_transform=None, loader=default_loader) |
它主要有四个参数:
root
:在root指定的路径下寻找图片transform
:对PIL Image进行的转换操作,transform的输入是使用loader读取图片的返回对象target_transform
:对label的转换loader
:给定路径后如何读取图片,默认读取为RGB格式的PIL Image对象
label是按照文件夹名顺序排序后存成字典,即{类名:类序号(从0开始)},一般来说最好直接将文件夹命名为从0开始的数字,这样会和ImageFolder实际的label一致,如果不是这种命名规范,建议看看self.class_to_idx
属性以了解label和文件夹名的映射关系。
from torchvision.datasets import ImageFolder |
# cat文件夹的图片对应label 0,dog对应1 |
# 所有图片的路径和对应的label |
# 没有任何的transform,所以返回的还是PIL Image对象 |
# 加上transform |
dataset = ImageFolder('data/dogcat_2/', transform=transform) |
# 深度学习中图片数据一般保存成CxHxW,即通道数x图片高x图片宽 |
|
Dataset
只负责数据的抽象,一次调用__getitem__
只返回一个样本。前面提到过,在训练神经网络时,最好是对一个batch的数据进行操作,同时还需要对数据进行shuffle和并行加速等。对此,PyTorch提供了DataLoader
帮助我们实现这些功能。
DataLoader的函数定义如下:
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, num_workers=0, collate_fn=default_collate, pin_memory=False, drop_last=False)
- dataset:加载的数据集(Dataset对象)
- batch_size:batch size
- shuffle::是否将数据打乱
- sampler: 样本抽样,后续会详细介绍
- num_workers:使用多进程加载的进程数,0代表不使用多进程
- collate_fn: 如何将多个样本数据拼接成一个batch,一般使用默认的拼接方式即可
- pin_memory:是否将数据保存在pin memory区,pin memory中的数据转到GPU会快一些
- drop_last:dataset中的数据个数可能不是batch_size的整数倍,drop_last为True会将多出来不足一个batch的数据丢弃
from torch.utils.data import DataLoader |
dataloader是一个可迭代的对象,意味着我们可以像使用迭代器一样使用它,例如:
for batch_datas, batch_labels in dataloader: |
或
dataiter = iter(dataloader) |
在数据处理中,有时会出现某个样本无法读取等问题,比如某张图片损坏。这时在__getitem__
函数中将出现异常,此时最好的解决方案即是将出错的样本剔除。如果实在是遇到这种情况无法处理,则可以返回None对象,然后在Dataloader
中实现自定义的collate_fn
,将空对象过滤掉。但要注意,在这种情况下dataloader返回的batch数目会少于batch_size。
class NewDogCat(DogCat): # 继承前面实现的DogCat数据集 |
来看一下上述batch_size的大小。其中第2个的batch_size为1,这是因为有一张图片损坏,导致其无法正常返回。而最后1个的batch_size也为1,这是因为共有9张(包括损坏的文件)图片,无法整除2(batch_size),因此最后一个batch的数据会少于batch_szie,可通过指定drop_last=True
来丢弃最后一个不足batch_size的batch。
对于诸如样本损坏或数据集加载异常等情况,还可以通过其它方式解决。例如但凡遇到异常情况,就随机取一张图片代替:
class NewDogCat(DogCat): |
相比较丢弃异常图片而言,这种做法会更好一些,因为它能保证每个batch的数目仍是batch_size。但在大多数情况下,最好的方式还是对数据进行彻底清洗。
DataLoader里面并没有太多的魔法方法,它封装了Python的标准库multiprocessing
,使其能够实现多进程加速。在此提几点关于Dataset和DataLoader使用方面的建议:
- 高负载的操作放在
__getitem__
中,如加载图片等。 - dataset中应尽量只包含只读对象,避免修改任何可变对象,利用多线程进行操作。
第一点是因为多进程会并行的调用__getitem__
函数,将负载高的放在__getitem__
函数中能够实现并行加速。
第二点是因为dataloader使用多进程加载,如果在Dataset
实现中使用了可变对象,可能会有意想不到的冲突。在多线程/多进程中,修改一个可变对象,需要加锁,但是dataloader的设计使得其很难加锁(在实际使用中也应尽量避免锁的存在),因此最好避免在dataset中修改可变对象。例如下面就是一个不好的例子,在多进程处理中self.num
可能与预期不符,这种问题不会报错,因此难以发现。如果一定要修改可变对象,建议使用Python标准库Queue
中的相关数据结构。
class BadDataset(Dataset): |
使用Python multiprocessing
库的另一个问题是,在使用多进程时,如果主程序异常终止(比如用Ctrl+C强行退出),相应的数据加载进程可能无法正常退出。这时你可能会发现程序已经退出了,但GPU显存和内存依旧被占用着,或通过top
、ps aux
依旧能够看到已经退出的程序,这时就需要手动强行杀掉进程。建议使用如下命令:
ps x | grep <cmdline> | awk '{print $1}' | xargs kill |
ps x
:获取当前用户的所有进程grep <cmdline>
:找到已经停止的PyTorch程序的进程,例如你是通过python train.py启动的,那你就需要写grep 'python train.py'
awk '{print $1}'
:获取进程的pidxargs kill
:杀掉进程,根据需要可能要写成xargs kill -9
强制杀掉进程
在执行这句命令之前,建议先打印确认一下是否会误杀其它进程
ps x | grep <cmdline> | ps x |
PyTorch中还单独提供了一个sampler
模块,用来对数据进行采样。常用的有随机采样器:RandomSampler
,当dataloader的shuffle
参数为True时,系统会自动调用这个采样器,实现打乱数据。默认的是采用SequentialSampler
,它会按顺序一个一个进行采样。这里介绍另外一个很有用的采样方法:
WeightedRandomSampler
,它会根据每个样本的权重选取数据,在样本比例不均衡的问题中,可用它来进行重采样。
构建WeightedRandomSampler
时需提供两个参数:每个样本的权重weights
、共选取的样本总数num_samples
,以及一个可选参数replacement
。权重越大的样本被选中的概率越大,待选取的样本数目一般小于全部的样本数目。replacement
用于指定是否可以重复选取某一个样本,默认为True,即允许在一个epoch中重复采样某一个数据。如果设为False,则当某一类的样本被全部选取完,但其样本数目仍未达到num_samples时,sampler将不会再从该类中选择数据,此时可能导致weights
参数失效。下面举例说明。
dataset = DogCat('data/dogcat/', transforms=transform) |
计算机视觉工具包:torchvision
计算机视觉是深度学习中最重要的一类应用,为了方便研究者使用,PyTorch团队专门开发了一个视觉工具包torchvion
,这个包独立于PyTorch,需通过pip instal torchvision
安装。在之前的例子中我们已经见识到了它的部分功能,这里再做一个系统性的介绍。torchvision主要包含三部分:
- models:提供深度学习中各种经典网络的网络结构以及预训练好的模型,包括
AlexNet
、VGG系列、ResNet系列、Inception系列等。 - datasets: 提供常用的数据集加载,设计上都是继承
torhc.utils.data.Dataset
,主要包括MNIST
、CIFAR10/100
、ImageNet
、COCO
等。 - transforms:提供常用的数据预处理操作,主要包括对Tensor以及PIL Image对象的操作。
|
from torchvision import datasets |
Transforms中涵盖了大部分对Tensor和PIL Image的常用处理,这些已在上文提到,这里就不再详细介绍。需要注意的是转换分为两步,第一步:构建转换操作,例如transf = transforms.Normalize(mean=x, std=y)
,第二步:执行转换操作,例如output = transf(input)
。另外还可将多个处理操作用Compose拼接起来,形成一个处理转换流程。
from torchvision import transforms |
torchvision还提供了两个常用的函数。一个是make_grid
,它能将多张图片拼接成一个网格中;另一个是save_img
,它能将Tensor保存成图片。
dataloader = DataLoader(dataset, shuffle=True, batch_size=16) |
持久化
在PyTorch中,以下对象可以持久化到硬盘,并能通过相应的方法加载到内存中:
- Tensor
- Variable
- nn.Module
- Optimizer
本质上上述这些信息最终都是保存成Tensor。Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。
我们可以通过t.save(obj, file_name)
等方法保存任意可序列化的对象,然后通过obj = t.load(file_name)
方法加载保存的数据。对于Module和Optimizer对象,这里建议保存对应的state_dict
,而不是直接保存整个Module/Optimizer对象。Optimizer对象保存的主要是参数,以及动量信息,通过加载之前的动量信息,能够有效地减少模型震荡,下面举例说明。
a = t.Tensor(3, 4) |
t.set_default_tensor_type('torch.FloatTensor') |
# Module对象的保存与加载 |
optimizer = t.optim.Adam(model.parameters(), lr=0.1) |
t.save(optimizer.state_dict(), 'optimizer.pth') |
all_data = dict( |
all_data = t.load('all.pth') |