最新网站备案,杭州做网站费用,网站建设与管理试卷,免费手机网站商城文章目录 前言1 数据处理1.1 Dataset1.2 DataLoader 前言
在训练神经网络的过程中需要用到很多的工具#xff0c;最重要的是数据处理、可视化和GPU加速。本章主要介绍PyTorch在这些方面常用的工具模块#xff0c;合理使用这些工具可以极大地提高编程效率。
由于内容较多最重要的是数据处理、可视化和GPU加速。本章主要介绍PyTorch在这些方面常用的工具模块合理使用这些工具可以极大地提高编程效率。
由于内容较多本文分成了五篇文章1数据处理2预训练模型3TensorBoard4Visdom5CUDA与小结。
整体结构如下
1 数据处理 1.1 Dataset1.2 DataLoader 2 预训练模型3 可视化工具3.1 TensorBoard3.2 Visdom4 使用GPU加速CUDA5 小结
全文链接
PyTorch中常用的工具1数据处理PyTorch常用工具2预训练模型PyTorch中常用的工具3TensorBoardPyTorch中常用的工具4VisdomPyTorch中常用的工具5使用GPU加速CUDA
1 数据处理
解决深度学习问题的过程中往往需要花费大量的精力去处理数据包括图像、文本、语音或其他二进制数据等。数据的处理对训练神经网络来说十分重要良好的数据处理不仅会加速模型训练而且会提高模型效果。考虑到这一点PyTorch提供了几个高效便捷的工具帮助使用者进行数据处理、数据增强等操作同时可以通过并行化加速数据加载的过程。
1.1 Dataset
在PyTorch中数据加载可以通过自定义的数据集对象实现。数据集对象被抽象为Dataset类实现自定义的数据集需要继承Dataset并实现以下两个Python魔法方法。
__getitem__()返回一条数据或一个样本。obj[index]等价于obj.__getitem__(index)。__len__()返回样本的数量。len(obj)等价于obj.__len__()。
下面以Kaggle经典挑战赛Dogs vs. Cats的数据为例详细讲解如何进行数据预处理。Dogs vs. Cats是一个分类问题它的任务是判断一张图片是狗还是猫。在该问题中所有图片都存放在一个文件夹下可以根据文件名的前缀得到它们的标签值狗或者猫。
In: %env LS_COLORS None !tree --charset ascii data/dogcat/Out: env: LS_COLORSNonedata/dogcat/|-- cat.12484.jpg|-- cat.12485.jpg|-- cat.12486.jpg|-- cat.12487.jpg|-- dog.12496.jpg|-- dog.12497.jpg|-- dog.12498.jpg-- dog.12499.jpg0 directories, 8 filesIn: import torch as tfrom torch.utils.data import Datasetprint(t.__version__)Out: 1.8.0In: import osfrom PIL import Imageimport numpy as npclass DogCat(Dataset):def __init__(self, root):imgs os.listdir(root)# 所有图片的绝对路径# 这里不实际加载图片只是指定路径当调用__getitem__时才会真正读取图片self.imgs [os.path.join(root, img) for img in imgs]def __getitem__(self, index):img_path self.imgs[index]# dog-1 cat-0label 1 if dog in img_path.split(/)[-1] else 0pil_img Image.open(img_path)array np.asarray(pil_img)data t.tensor(array)return data, labeldef __len__(self):return len(self.imgs)In: dataset DogCat(./data/dogcat/)img, label dataset[0] # 相当于调用dataset.__getitem__(0)for img, label in dataset:print(img.size(), img.float().mean(), label)Out: torch.Size([374, 499, 3]) tensor(115.5177) 0torch.Size([377, 499, 3]) tensor(151.7174) 1torch.Size([400, 300, 3]) tensor(128.1550) 1torch.Size([499, 379, 3]) tensor(171.8085) 0torch.Size([375, 499, 3]) tensor(116.8139) 1torch.Size([500, 497, 3]) tensor(106.4915) 0torch.Size([375, 499, 3]) tensor(150.5079) 1torch.Size([236, 289, 3]) tensor(130.3004) 0上面的代码讲解了如何定义自己的数据集并对数据集进行遍历。然而这里返回的数据并不适合实际使用主要存在以下两个问题。 返回样本的形状不统一也就是每张图片的大小不一样这对于按batch训练的神经网络来说很不友好。 返回样本的数值较大没有进行归一化。
针对上述问题PyTorch提供了torchvision工具包。torchvision是一个视觉工具包它提供了很多视觉图像处理的工具其中transforms模块提供了一系列数据增强的操作。本章仅对它的部分操作进行介绍完整内容可参考官方相关文档。
仅支持PIL Image对象的常见操作如下。 RandomChoice在一系列transforms操作中随机执行一个操作。 RandomOrder以随意顺序执行一系列transforms操作。
仅支持Tensor对象的常见操作如下。
Normalize标准化即减去均值除以标准差。RandomErasing随机擦除Tensor中一个矩形区域的像素。ConvertImageDtype将Tensor转换为指定的类型并进行相应的缩放。
PIL Image对象与Tensor对象相互转换的操作如下。
ToTensor将 H × W × C H\times W\times C H×W×C形状的PIL Image对象转换成形状为 C × H × W C\times H\times W C×H×W的Tensor同时会自动将[0, 255]归一化至[0, 1]。ToPILImage将Tensor转为PIL Image对象。
既支持PIL Image对象又支持Tensor对象的常见操作如下。 Resize调整图片尺寸。 CenterCrop、RandomCrop、RandomResizedCrop、FiveCrop 按照不同规则对图像进行裁剪。 RandomAffine随机进行仿射变换保持图像中心不变。 RandomGrayscale随机将图像变为灰度图。 RandomHorizontalFlip、RandomVerticalFlip、RandomRotation随机水平翻转、垂直翻转、旋转图像。
如果需要对图片进行多个操作那么可以通过transforms.Compose将这些操作拼接起来这点类似于nn.Sequential**。注意这些操作定义后以对象的形式存在真正使用时需要调用__call__方法这点类似于nn.Module。**例如要将图片的大小调整至 224 × 224 224\times 224 224×224首先应构建操作trans Resize((224, 224))然后调用trans(img)。下面使用transforms的这些操作来优化上面的Dataset
In: import osfrom PIL import Imageimport numpy as npfrom torchvision import transforms as Ttransform T.Compose([T.Resize(224), # 缩放图片(Image)保持长宽比不变最短边为224像素T.CenterCrop(224), # 从图片中间切出224×224的图片T.ToTensor(), # 将图片(Image)转成Tensor归一化至[0, 1]T.Normalize(mean[.5, .5, .5], std[.5, .5, .5]) # 标准化至[-1, 1]规定均值和标准差])class DogCat(Dataset):def __init__(self, root, transformsNone):imgs os.listdir(root)self.imgs [os.path.join(root, img) for img in imgs]self.transforms transformsdef __getitem__(self, index):img_path self.imgs[index]label 0 if dog in img_path.split(/)[-1] else 1data Image.open(img_path)if self.transforms:data self.transforms(data)return data, labeldef __len__(self):return len(self.imgs)dataset DogCat(./data/dogcat/, transformstransform)img, label dataset[0]for img, label in dataset:print(img.size(), label)Out: torch.Size([3, 224, 224]) 1torch.Size([3, 224, 224]) 0torch.Size([3, 224, 224]) 0torch.Size([3, 224, 224]) 1torch.Size([3, 224, 224]) 0torch.Size([3, 224, 224]) 1torch.Size([3, 224, 224]) 0torch.Size([3, 224, 224]) 1除了上述操作transforms还可以通过Lambda封装自定义的转换策略。例如如果要对PIL Image对象进行随机旋转那么可以写成trans T.Lambda(lambda img: img.rotate(random() * 360))。
与torch.nn以及torch.nn.functional类似torchvision将transforms分解为torchvision.transforms以及torchvision.transforms.functional。相比于transformstransforms.functional为用户提供了更加灵活的操作读者在使用时需要自己指定所有的参数。部分transforms.functional提供的操作如下完整内容可参考官方文档。
adjust_brightnessadjust_contrast调整图像的亮度、对比度。cropcenter_cropfive_cropten_crop对图像按不同规则进行裁剪。normalize标准化即减均值除以标准差。to_tensor将PIL Image对象转成Tensor。
可以看出transforms.functional中的操作与transforms十分类似。相对于transforms而言transforms.functional可以对多个对象以相同的参数进行操作举例说明如下
import torchvision.transforms.functional as TF
import randomdef transforms_rotate(image1, image2):angle random.randint(0, 360)image1 TF.rotate(image1, angle)image2 TF.rotate(image2, angle)return image1, image2除了对数据进行增强操作的transformstorchvision还预先实现了常用的dataset包括前面使用过的CIFAR-10以及ImageNet、COCO、MNIST、LSUN等数据集用户可以通过诸如torchvision.datasets.CIFAR10的命令进行调用具体使用方法请参考官方文档。本节介绍一个读者会经常使用到的Dataset——ImageFolder它的实现和上述的DogCat十分类似。ImageFolder假设所有的图片按文件夹保存每个文件夹下存储同一个类别的图片文件夹名为类名它的构造函数如下
ImageFolder(root, transformNone, target_transformNone, loaderdefault_loader, is_valid_fileNone)它主要有以下五个参数。
root在root指定的路径下寻找图片。transform对PIL Image进行相关数据增强transform的输入是使用loader读取图片的返回对象。target_transform对label的转换。loader指定加载图片的函数默认操作是读取为PIL Image对象。is_valid_file获取图像路径检查文件的有效性。
在生成数据的label时首先按照文件夹名进行顺序排序然后将文件夹名保存为字典即{类名类序号从0开始}。一般来说最好直接将文件夹命名为从0开始的数字这样会和ImageFolder实际的label一致。如果不是这种命名规范那么建议通过self.class_to_idx属性了解label和文件夹名的映射关系。
In: !tree --charset ASCII data/dogcat_2/Out: data/dogcat_2/|-- cat| |-- cat.12484.jpg| |-- cat.12485.jpg| |-- cat.12486.jpg| -- cat.12487.jpg-- dog|-- dog.12496.jpg|-- dog.12497.jpg|-- dog.12498.jpg-- dog.12499.jpg2 directories, 8 filesIn: from torchvision.datasets import ImageFolderdataset ImageFolder(data/dogcat_2/)# cat文件夹的图片对应label 0dog对应1dataset.class_to_idxOut: {cat: 0, dog: 1}In: # 所有图片的路径和对应的labeldataset.imgsOut: [(data/dogcat_2/cat/cat.12484.jpg, 0),(data/dogcat_2/cat/cat.12485.jpg, 0),(data/dogcat_2/cat/cat.12486.jpg, 0),(data/dogcat_2/cat/cat.12487.jpg, 0),(data/dogcat_2/dog/dog.12496.jpg, 1),(data/dogcat_2/dog/dog.12497.jpg, 1),(data/dogcat_2/dog/dog.12498.jpg, 1),(data/dogcat_2/dog/dog.12499.jpg, 1)]In: # 没有任何的transforms操作所以返回的还是PIL Image对象print(dataset[0][1]) # 第一维是第几张图第二维为1返回labeldataset[0][0] # 第二维为0返回图片数据Out: 0In: # 加上transformstransform T.Compose([T.RandomResizedCrop(224),T.RandomHorizontalFlip(), # 水平翻转T.ToTensor(),T.Normalize(mean[.5, .5, .5], std[.5, .5, .5]),])In: dataset ImageFolder(data/dogcat_2/, transformtransform)# 深度学习中图片数据一般保存成C×H×W即通道数×图片高×图片宽dataset[0][0].size()Out: torch.Size([3, 224, 224])In: to_img T.ToPILImage()# 0.2和0.4是标准差和均值的近似to_img(dataset[0][0] * 0.2 0.4)1.2 DataLoader
Dataset只负责数据的抽象调用一次__getitem__返回一个样本。然而在训练神经网络时一次处理的对象是一个batch的数据同时还需要对一批数据进行打乱顺序和并行加速等操作。考虑到这一点PyTorch提供了DataLoader实现这些功能。
DataLoader的定义如下
DataLoader(dataset, batch_size1, shuffleFalse, samplerNone, batch_samplerNone, num_workers0, collate_fnNone, pin_memoryFalse, drop_lastFalse, timeout0, worker_init_fnNone, multiprocessing_contextNone, generatorNone, *, prefetch_factor2, persistent_workersFalse)它主要有以下几个参数。
dataset加载的数据集Dataset对象。batch_size一个batch的大小。shuffle是否将数据打乱。sampler样本抽样后续会详细介绍。batch_sampler与sampler类似一次返回一个batch的索引该参数与batch_size、shuffle、sampler和drop_last不兼容。num_workers使用多进程加载的进程数0代表不使用多进程。collate_fn 如何将多个样本数据拼接成一个batch一般使用默认的拼接方式即可。pin_memory是否将数据保存在pin memory区pin memory中的数据转移到GPU速度更快。drop_lastdataset中的数据个数可能不是batch_size的整数倍若drop_last为True则将多出来不足一个batch的数据丢弃。timeout进程读取数据的最大时间若超时则丢弃数据。worker_init_fn每个worker的初始化函数。prefetch_factor每个worker预先加载的样本数。
下面举例说明DataLoader的使用方法
In: from torch.utils.data import DataLoaderdataloader DataLoader(dataset, batch_size3, shuffleTrue, num_workers0, drop_lastFalse)dataiter iter(dataloader)imgs, labels next(dataiter)imgs.size() # batch_size, channel, height, widthOut: torch.Size([3, 3, 224, 224])DataLoader是一个可迭代iterable对象可以像使用迭代器一样使用它例如
for batch_datas, batch_labels in dataloader:train()
或
dataiter iter(dataloader)
batch_datas, batch_labels next(dataiter)在数据处理中有时会出现某个样本无法读取等问题例如某张图片损坏。此时在__getitem__函数中会抛出异常最好的解决方案是将出错的样本剔除。如果不便于处理这种情况那么可以返回None对象然后在Dataloader中实现自定义的collate_fn将空对象过滤掉。注意这种情况下DataLoader返回的一个batch的样本数目会少于batch_size。
In: class NewDogCat(DogCat): # 继承前面实现的DogCat数据集def __getitem__(self, index):try:# 调用父类的获取函数即 DogCat.__getitem__(self, index)return super().__getitem__(index)except:return None, Nonefrom torch.utils.data.dataloader import default_collate # 导入默认的拼接方式def my_collate_fn(batch):batch是一个list每个元素是dataset的返回值形如(data, label)# 过滤为None的数据batch [_ for _ in batch if _[0] is not None]if len(batch) 0: return t.Tensor()return default_collate(batch) # 用默认方式拼接过滤后的batch数据In: dataset NewDogCat(data/dogcat_wrong/, transformstransform)dataset[8]Out: (None, None)In: dataloader DataLoader(dataset, 2, collate_fnmy_collate_fn, num_workers0, shuffleTrue)for batch_datas, batch_labels in dataloader:print(batch_datas.size(), batch_labels.size())Out: torch.Size([1, 3, 224, 224]) torch.Size([1])torch.Size([2, 3, 224, 224]) torch.Size([2])torch.Size([2, 3, 224, 224]) torch.Size([2])torch.Size([2, 3, 224, 224]) torch.Size([2])torch.Size([1, 3, 224, 224]) torch.Size([1])从上述输出中可以看出第1个batch的batch_size为1这是因为有一张图片损坏无法正常返回。最后1个batch的batch_size也为1这是因为共有9张包括损坏的文件图片无法整除2batch_size所以最后一个batch的样本数目小于batch_size。
对于样本损坏或数据集加载异常等情况还可以通过其他方式解决例如随机取一张图片代替出现异常的图片
class NewDogCat(DogCat):def __getitem__(self, index):try:return super().__getitem__(index)except:new_index random.randint(0, len(self) - 1)return self[new_index]相比于丢弃异常图片而言这种做法会更好一些它能保证每个batch的样本数目仍然是batch_size但是在大多数情况下最好的方式还是对数据进行彻底清洗。
DataLoader中没有太多的魔法方法它封装了Python的标准库Multiprocessing能够实现多进程加速下面对DataLoader的多进程并行原理进行简要介绍。
DataLoader默认使用单进程加载数据这样的加载方式较慢但在系统资源有限、数据集较小能够直接加载时十分推荐。这是因为在单进程的工作模式下若发生异常用户在调试时能够获取更多错误信息。当数据量较大时可以通过num_workers参数进行多进程的数据读取多进程并行流程如下图所示。 在多进程加载数据时每一个进程都会拷贝Dataset对象并执行_worker_loop函数。首先主进程生成一个batch的数据索引并保存在队列index_queue中。然后每个子进程执行_worker_loop函数根据index_queue在拷贝的Dataset对象中执行__getitem__函数获取数据。最后每个子进程将自身获取的数据放至work_result_queue队列中通过collate_fn处理数据最终得到一个batch的数据data_queue。重复执行上述流程DataLoader就实现了多进程的数据加载更多细节读者可以参考DataLoader的相关源码。
在Dataset和DataLoader的使用方面有以下建议。 将高负载的操作放在__getitem__中例如加载图片等。在多进程加载数据时程序会并行地调用__getitem__函数将负载高的操作放在__getitem__函数中能够实现并行加速。 Dataset中应当尽量仅包含只读对象避免修改任何可变对象。在多进程加载数据时每个子进程都会拷贝Dataset对象。如果某一个进程修改了部分数据那么在另外一个进程的拷贝中这部分数据并不会被修改。下面是一个不好的例子希望self.idxs返回的结果是[0,1,2,3,4,5,6,7,8]实际上4个进程最终的self.idxs分别是[0,4,8],[1,5],[2,6],[3,7]。而dataset.idxs则是[] 因为它并未参与迭代并行处理的是它的四个拷贝。
class BadDataset:def __init__(self):self.idxs [] # 取数据的次数def __getitem__(self, index):self.idxs.append(index)return self.idxsdef __len__(self):return 9
dataset BadDataset()
dl t.utils.data.DataLoader(dataset, num_workers4)
for item in dl:print(item) # 注意这里self.idxs的数值
print(idxs of main, dataset.idxs) # 注意这里的idxs和__getitem__返回的idxs的区别使用Multiprocessing库时还有另外一个问题在使用多进程加载中如果主程序异常终止例如使用快捷键“CtrlC”强行退出那么相应的数据加载进程可能无法正常退出。虽然发现程序已经退出了但是GPU显存和内存仍然被占用着通过top、ps aux也能够看到已经退出的程序这时就需要手动强行杀掉进程建议使用如下命令
ps x | grep cmdline | awk {print $1} | xargs killps x获取当前用户的所有进程。grep cmdline找到已经停止的PyTorch程序的进程例如通过python train.py启动程序需要写成grep python train.py。awk {print $1}获取进程的pid。xargs kill杀掉进程根据需要可能需要写成xargs kill -9强制杀掉进程。
在执行这句命令之前建议先确认仍有未停止进程
ps x | grep cmdline PyTorch中还单独提供了一个Sampler模块用来对数据进行采样。常用的有随机采样器RandomSampler当DataLoader的shuffle参数为True时系统会自动调用这个采样器打乱数据。默认的采样器是SequentialSampler它会按顺序一个一个进行采样。这里介绍另外一个很有用的采样方法WeightedRandomSampler它会根据每个样本的权重选取数据在样本比例不均衡的问题中可用它进行重采样。
构建WeightedRandomSampler时需提供两个参数每个样本的权重weights、选取的样本总数num_samples以及一个可选参数replacement。权重越大的样本被选中的概率越大待选取的样本数目一般小于全部的样本数目。replacement用于指定是否可以重复选取某一个样本默认为True即允许在一个epoch中重复采样某一个数据。如果设为False那么当某一类的样本被全部选取完但样本数目仍然未达到num_samples时sampler不会再从该类中选择数据此时可能导致weights参数失效。下面举例说明
In: dataset DogCat(data/dogcat/, transformstransform)# 假设狗的图片被取出的概率是猫的概率的两倍# 两类图片被取出的概率与weights的绝对大小无关只和比值有关weights [2 if label 1 else 1 for data, label in dataset]weightsOut: [2, 1, 1, 2, 1, 2, 1, 2]In: from torch.utils.data.sampler import WeightedRandomSamplersampler WeightedRandomSampler(weights,\num_samples9,\replacementTrue)dataloader DataLoader(dataset,\batch_size3,\samplersampler)for datas, labels in dataloader:print(labels.tolist())Out: [1, 1, 0][0, 1, 1][1, 1, 0]可以看出猫狗样本比例约为12。同时一共只有8个样本但是却返回了9个说明有样本被重复返回这就是replacement参数的作用。下面将replacement设为False
In: sampler WeightedRandomSampler(weights, 8, replacementFalse)dataloader DataLoader(dataset, batch_size4, samplersampler)for datas, labels in dataloader:print(labels.tolist())Out: [1, 0, 1, 0][1, 1, 0, 0]在replacement为False的情况下num_samples等于dataset的样本总数。为了不重复选取Sampler会将每个样本都返回weight参数不再生效。
从上面的例子中可以看出Sampler在样本采样中的作用如果指定了Sampler那么shuffle参数不再生效并且sampler.num_samples会覆盖dataset的实际大小即一个epoch返回的图片总数取决于sampler.num_samples。
本小节介绍了数据加载中两个常见的操作Dataset与DataLoader并结合实际数据对它们的魔法方法与底层原理进行了详细介绍。数据准备与加载是神经网络训练中最基本的环节之一读者应该熟悉其常见操作。