PyTorch–常用的工具
在训练神经网络的过程中需要用到很多工具,其中最重要的三部分是数据,可视化和GPU加速
一.数据处理
数据的处理对训练神经网络来说十分重要,良好的数据处理不仅加速模型训练,也会提高模型效果
数据加载
在PyTorch中,数据加载可通过自定义的数据集对象实现。数据集对象被抽象为Dataset类,实现自定义的数据集需要继承Dataset,并实现两个python魔法方法:
- getitemgetitem**(index)
- lenlen**()
import torch as t
from torch.utils import data
import os
from PIL import Image
import numpy as np
class DogCat(data.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->0
label=1 if 'dog' in img_path.split('/')[-1] else 0
pil_img=Image.open(img_path)
array=np.asarray(pil_img)
data=t.from_numpy(array)
return data,label
def __len__(self):
return len(self.imgs)
dataset=DogCat('./data/dogcat/')
img,label=dataset[0] # 相当于调用dataset.__getitem__(0)
for img,label in dataset:
print(img.size(),img.float().mean(),label)
torch.Size([500, 497, 3]) tensor(106.4915) 0
torch.Size([499, 379, 3]) tensor(171.8085) 0
torch.Size([236, 289, 3]) tensor(130.3004) 0
torch.Size([374, 499, 3]) tensor(115.5177) 0
torch.Size([375, 499, 3]) tensor(116.8139) 1
torch.Size([375, 499, 3]) tensor(150.5080) 1
torch.Size([377, 499, 3]) tensor(151.7174) 1
torch.Size([400, 300, 3]) tensor(128.1550) 1
通过上面的代码,我们学习了如何自定义自己的数据集,并可以,依次获取。但这里,返回的数据不适合实际使用,因其具有如下两方面问题:
- 返回样本的形状不一,每张图片的大小不一样,这对于需要取batch训练的神经网络来说很不友好
- 返回样本的数值较大,未归一化至[-1,1]
针对上述问题,PyTorch提供了torchvision。它是一个视觉工具包,提供了很多视觉图像处理的工具,其中transforms模块提供了对PIL Image对象和Tensor对象的常用操作
对PIL Image的常见操作如下:
- Scale:调整图片尺寸,长宽比保持不变
- CenterCrop,RandomGrop,RandomSizedGrop:裁剪图片
- Pad:填充
- ToTensor:将PIL Image对象转成Tensor,会自动将[0,255]归一化至[0,1]
对Tensor的常见操作如下: - Normalize:标准化,即减均值,除以标准差
- ToPILImage:将Tensor转为PIL Image对象
下面我们就用transforms的这些操作来优化上面实现的dataset
import os
from PIL import Image
import numpy as np
from torchvision import transforms as T
transform=T.Compose([
# 缩放图片(Image),保持长宽比不变,最短边为224像素
T.Scale(224),
# 从图片中间切出224*224的图片
T.CenterCrop(224),
# 将图片(Image)转成Tensor,归一化至[0,1]
T.ToTensor(),
# 标准化至[-1,1],规定均值和标准差
T.Normalize(mean=[.5,.5,.5],std=[.5,.5,.5])
])
class DogCat(data.Dataset):
def __init__(self,root,transforms=None):
imgs=os.listdir(root)
# 所有图片的绝对路径
# 这里不实际加载图片,只是指定路径,当调用__getitem__时才会真正读图片
self.imgs=[os.path.join(root,img) for img in imgs]
self.transforms=transforms
def __getitem__(self,index):
img_path=self.imgs[index]
label=0 if 'dog' in img_path.split('/')[-1] else 1
data=Image.open(img_path)
if self.transforms:
data=self.transforms(data)
return data,label
def __len__(self):
return len(self.imgs)
dataset=DogCat('./data/dogcat/',transforms=transform)
img,label=dataset[0] # 相当于调用dataset.__getitem__(0)
for img,label in dataset:
print(img.size(),img.float().mean(),label)
E:\Anaconda\lib\site-packages\torchvision\transforms\transforms.py:207: UserWarning: The use of the transforms.Scale transform is deprecated, please use transforms.Resize instead.
warnings.warn("The use of the transforms.Scale transform is deprecated, " +
torch.Size([3, 224, 224]) tensor(-0.1654) 1
torch.Size([3, 224, 224]) tensor(0.3892) 1
torch.Size([3, 224, 224]) tensor(0.0711) 1
torch.Size([3, 224, 224]) tensor(-0.0462) 1
torch.Size([3, 224, 224]) tensor(-0.0649) 0
torch.Size([3, 224, 224]) tensor(0.1176) 0
torch.Size([3, 224, 224]) tensor(0.2234) 0
torch.Size([3, 224, 224]) tensor(-0.0267) 0
除了上述操作之外,transforms还可以通过lambda封装自定义的转换策略。例如,想对PIL Image进行随机旋转,则可写成trans=T.Lambda(lambda img:img.rotate(random()*360))
torchvision已经预先实现了常用的Dataset,包括前面使用过的CIFAR-10,以及ImageNet,COCO,MNIST,LSUN等数据集,可通过调用torchvision.datasets下相应对象来调用相关数据集
ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
ImageFolder(root,transform=None,target_transform=None,loader=default_loader)
它主要有以下四个参数:
- root:在root指定的路径下寻找图片
- transform:对PIL Image进行转换操作,transform的输入是使用loader读取图片的返回对象
- target_transform:对label的转换
- loader:指定加载图片的函数,默认操作是读取为PIL Image对象
label是按照文件夹名顺序后存成字典的,即{类名:类序号(从0开始)},一般来说最好直接将文件夹命名为从0开始的数字,这样会和ImageFolder实际的label一致,如果不是这种命名规范,建议通过self.class_to_idx属性了解label和文件夹名的映射关系
from torchvision.datasets import ImageFolder
dataset=ImageFolder("data/dogcat_2/")
# cat文件夹的图片对应label 0,dog对应1
dataset.class_to_idx
{'cat': 0, 'dog': 1}
# 所有图片的路径和对应的label
dataset.imgs
[('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)]
# 没有任何的transform,所以返回的还是PIL Image对象
# 第一维是第几张图,第二维为1返回label
dataset[0][1]
# 为0返回图片数据,返回的Image对象
dataset[0][0]
from torchvision import transforms as T
# 加上transform
normalize=T.Normalize(mean=[0.4,0.4,0.4],std=[0.2,0.2,0.2])
transform=T.Compose([
T.RandomSizedCrop(224),
T.RandomHorizontalFlip(),
T.ToTensor(),
normalize,
])
dataset=ImageFolder("data/dogcat_2/",transform=transform)
# 深度学习中图片数据一般保存成CxHxW,即通道数x图片高x图片宽
dataset[0][0].size()
torch.Size([3, 224, 224])
to_img=T.ToPILImage()
# 0.2和0.4是标准差和均值的近似
to_img(dataset[0][0]*0.2+0.4)
Dataset只负责数据的抽象,一次调用getitem只返回一个样本。前面提到过,在训练神经网络时,是对一个batch的数据操作,同时还需要对数据进行shuffle和并行加速。对此,PyTorch提供了DataLoader帮助我们实现这些功能
DataLoader的函数定义如下:
DataLoader(dataset,batch_size=1,shuffle=False,smapler=None,num_workers=0,collate_fn=default_collate,pin_memory=False,drop_last=False)
- 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=DataLoader(dataset,batch_size=3,shuffle=True,num_workers=0,drop_last=False)
dataiter=iter(dataloader)
imgs,labels=next(dataiter)
# batch_size,channel,height,weight
imgs.size()
torch.Size([3, 3, 224, 224])
dataloader是一个可迭代的对象,我们可以像使用迭代器一样使用它,例如:
for batch_datas,batch_labels in dataloader:
train()
# 或
dataiter=iter(dataloader)
batch_datas,batch_labelsl=next(dataiter)
在数据处理中,有时会出现某个样本无法读取等问题。这时在getitem函数中将出现异常,此时最好的解决方案即是将出错的样本剔除。如果遇到这种情况实在无法处理,则可以返回None对象,然后在DataLoader中实现自定义的collate_fn,将空对象过滤掉。但要注意,在这种情况下dataloader返回的一个batch的样本数目会少于batch_size
class NewDogCat(DogCat):
def __getitem__(self,index):
try:
# 调用父类的获取函数,即DogCat.__getitem__(self,index)
return super(NewDogCat,self).__getitem__(index)
except:
return None,None
# 导入默认的拼接方式
from torch.utils.data.dataloader import default_collate
def my_collate_fn(batch):
"""
batch中的每个元素形如(data,label)
"""
# 过滤为None的数据
batch=list(filter(lambda x:x[0] is not None,batch))
return default_collate(batch)
dataset=NewDogCat("data/dogcat_wrong/",transforms=transform)
dataset[5]
(tensor([[[-0.5098, -0.8824, -1.1176, ..., 1.8431, 1.4902, 0.4314],
[-0.6078, -0.9412, -1.0196, ..., 1.8431, 1.5098, 0.4706],
[-0.7647, -1.0196, -0.9608, ..., 1.8235, 1.5294, 0.4902],
...,
[ 2.9216, 2.9608, 2.9020, ..., 1.0588, 1.0588, 1.0588],
[ 2.8824, 2.9216, 2.8431, ..., 1.4118, 1.3725, 1.3725],
[ 2.8431, 2.8824, 2.8039, ..., 1.5294, 1.5294, 1.5490]],
[[-0.0392, -0.4314, -0.6275, ..., 1.4706, 1.1176, 0.1373],
[-0.1373, -0.4902, -0.5294, ..., 1.4706, 1.1569, 0.1569],
[-0.2941, -0.5686, -0.5098, ..., 1.4902, 1.1765, 0.1765],
...,
[ 2.9216, 2.9608, 2.9020, ..., 1.3725, 1.3529, 1.3529],
[ 2.9216, 3.0000, 2.9216, ..., 1.6667, 1.6078, 1.6078],
[ 2.9020, 2.9804, 2.9216, ..., 1.7843, 1.7843, 1.8039]],
[[ 0.8824, 0.5294, 0.3529, ..., 0.4902, 0.1373, -0.7647],
[ 0.7843, 0.4706, 0.4314, ..., 0.5098, 0.1961, -0.7255],
[ 0.6078, 0.3922, 0.4706, ..., 0.5098, 0.2157, -0.6863],
...,
[ 2.9216, 2.9608, 2.9020, ..., 1.0196, 1.0392, 1.0784],
[ 2.9020, 2.9804, 2.9020, ..., 1.4706, 1.4118, 1.4118],
[ 2.8824, 2.9608, 2.8824, ..., 1.5882, 1.5882, 1.6078]]]), 0)
dataloader=DataLoader(dataset,2,collate_fn=my_collate_fn,num_workers=1)
for batch_datas,batch_labels in dataloader:
print(batch_datas.size(),batch_labels.size())
二.视觉工具包:torchvision
torchvision主要包括以下三部分:
- models:提供深度学习中各种经典网络的网络结构及预训练好的模型,包括AlexNet,VGG系列,ResNet系列,Inception系列等
- datasets:提供常用的数据集加载,设计上都是继承torch.utils.data.Dataset,主要包括MNIST,CIFAR10/100,ImageNet,COCO等
- transforms:提供常用的数据预处理操作,主要包括对Tensor及PIL Image对象的操作
from torchvision import models
from torch import nn
# 加载预训练好的模型,如果不存在会下载
# 预训练好的模型保存在~/.torch/models/下面
resnet34=models.resnet34(pretrained=True,num_classes=1000)
# 修改最后的全连接层为10分类问题(默认是ImageNet上的1000分类)
resnet34.fc=nn.Linear(512,10)
from torchvision import datasets
# 指定数据集路径为data,如果数据集不存在则进行下载
# 通过train=False获取测试集
dataset=datasets.MNIST("data/",download=True,train=False,transform=transform)
Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to data/MNIST\raw\train-images-idx3-ubyte.gz
100%|█████████▉| 9871360/9912422 [00:13<00:00, 1188747.47it/s]
Extracting data/MNIST\raw\train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to data/MNIST\raw\train-labels-idx1-ubyte.gz
0it [00:00, ?it/s]
0%| | 0/28881 [00:00<?, ?it/s]
57%|█████▋ | 16384/28881 [00:00<00:00, 59945.44it/s]
32768it [00:00, 38427.66it/s]
Extracting data/MNIST\raw\train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to data/MNIST\raw\t10k-images-idx3-ubyte.gz
0it [00:00, ?it/s]
0%| | 0/1648877 [00:00<?, ?it/s]
1%| | 16384/1648877 [00:00<00:26, 62465.55it/s]
3%|▎ | 49152/1648877 [00:01<00:21, 74382.81it/s]
6%|▌ | 98304/1648877 [00:01<00:16, 92296.04it/s]
10%|▉ | 163840/1648877 [00:01<00:12, 115207.62it/s]
14%|█▍ | 229376/1648877 [00:01<00:10, 138903.86it/s]
24%|██▍ | 393216/1648877 [00:01<00:06, 182372.38it/s]
26%|██▋ | 434176/1648877 [00:02<00:07, 172328.72it/s]
41%|████ | 679936/1648877 [00:02<00:04, 231085.39it/s]
48%|████▊ | 794624/1648877 [00:02<00:03, 273772.31it/s]
56%|█████▌ | 917504/1648877 [00:02<00:02, 320331.10it/s]
64%|██████▎ | 1048576/1648877 [00:03<00:01, 410840.31it/s]
68%|██████▊ | 1122304/1648877 [00:03<00:01, 408075.09it/s]
73%|███████▎ | 1196032/1648877 [00:03<00:01, 393825.71it/s]
81%|████████ | 1335296/1648877 [00:03<00:00, 435269.59it/s]
90%|████████▉ | 1482752/1648877 [00:03<00:00, 475917.06it/s]
99%|█████████▉| 1630208/1648877 [00:04<00:00, 560651.29it/s]
1654784it [00:04, 395803.27it/s]
Extracting data/MNIST\raw\t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to data/MNIST\raw\t10k-labels-idx1-ubyte.gz
0it [00:00, ?it/s]
0%| | 0/4542 [00:00<?, ?it/s]
8192it [00:00, 16627.37it/s]
Extracting data/MNIST\raw\t10k-labels-idx1-ubyte.gz
Processing...
Done!
9920512it [00:30, 1188747.47it/s]
Transform中涵盖了大部分对Tensor和PIL Image的常用处理。需要注意的是转换分为两步,第一步:构建转换操作,例如transform=transforms.Normalize(mean=x,std=y);第二步:执行转换操作,例如ouput=transf(input).另外还可将多个处理操作用Compose拼接起来,构成一个处理转换流程
from torchvision import transforms
to_pil=transforms.ToPILImage()
to_pil(t.randn(3,64,64))
torchvision还提供了两个常用的函数。一个是make_grid,它能将多张图片拼接在一个网格中;另一个是save_image,它能将Tensor保存成图片
len(dataset)
10000
dataloader=DataLoader(dataset,shuffle=True,batch_size=16)
from torchvision.utils import make_grid,save_image
dataiter=iter(dataloader)
img=make_grid(next(dataiter)[0],4)
to_img(img)
save_image(img,'a.png')
Image.open('a.png')
三.可视化工具
介绍两个深度学习中常用的可视化工具:Tensorboard和visdom
1.Tensorboard
最初,Tensorboard是作为Tensorflow的可视化工具迅速流行起来的。作为和TensorFlow深度集成的工具,Tensorboard能够展现Tensorflow网络计算图,绘制图像生成的定量指标图及附加数据。在PyTorch中使用Tensorboard_logger进行训练损失的可视化。Tensorboard_logger是TeamHG-Memex开发的一款轻量级工具,它将Tensorboard的功能抽取出来,使非Tensorflow用户也能使用它进行可视化,但其支持的功能有限
tensorboard_logger的安装主要分为以下两步:
- 安装tensorflow:建议安装CPU-Only的版本
- 安装tensorboard_logger:使用pip install tensorboard_logger
tensorboard_logger的使用非常简单。首先用如下命令启动Tensorboard:
tensorboard --logdir <your/running/dir> --port <your_bind_port>
from tensorboard_logger import Logger
# 构建logger对象,logdir用来指定log文件的保存路径
# flush_secs用来指定刷新同步间隔
logger=Logger(logdir='experimient_cnn',flush_secs=2)
for ii in range(100):
logger.log_value('loss',10-ii**0.5,step=ii)
logger.log_value('accuracy',ii**0.5/10)
输入:tensorboard --logdir F:\Python\Pytest\DL\pytorch\experimient_cnn --port 6006,打开浏览器http://localhost:6006
左侧的Horizontal Axis下有如下三个选项:
- step:根据步长来记录,log_value是指如果有步长,则将其作为x轴坐标描点画线
- Relative:用前后相对顺序描点画线,可认为logger自己维护了一个step属性,每调用一次log_value就自动加1
- Wall:按时间排序描点画线
左侧的Smoothing条可以左右拖动,用来调节平滑的幅度。单击页面右上角的刷新按钮可立即刷新结果,默认是每30秒自动刷新新数据。Tensorboard_logger的使用十分简单,但它只能统计简单的数值信息,不支持其他功能
除了tensorboard_logger,还有专门针对PyTorch开发的TensorboardX,它封装更多的Tensorboard接口,支持记录标量,图片,直方图,声音,文本,计算图和embedding等信息,几乎包括和Tensorflow的Tensorboard完全一样的功能
2.visdom
visdom是Facebook专门为PyTorch开发的一款可视化工具,visdom十分轻量级,却支持非常丰富的功能,能胜任大多数的科学运算可视化任务
visdom中有以下两个重要概念:
- env:环境。不同环境的可视化结果相互隔离,互不影响,在使用时如果不指定env,默认使用main
- pane:窗格。窗格可用于可视化图像,数值或打印文本等,其可以拖动,缩放,保存和关闭
通过命令pip install visdom即可完成visdom的安装。安装完成后,需通过python -m visdom.server命令启动visdom服务
import visdom
# 新建一个连接客户端
# 指定env=u'test1',默认端口为8097,host为'localhost'
vis=visdom.Visdom(env=u'test1')
x=t.arange(1,30,0.01)
y=t.sin(x)
vis.line(X=x,Y=y,win='sinx',opts={'title':'y=sin(x)'})
WARNING:root:Setting up a new session...
'sinx'
下面我们逐一分析这几行代码:
- vis=visdom.Visdom(env=u’test1’):用于构建一个客户端,客户端除指定env外,还可以指定host,port等参数
- vis作为一个客户端对象,可以使用如下常见的画图函数
-
- line:类似MATLAB中的plot操作,用于记录某些标量的变化,例如损失,准确率等
-
- image:可视化图片,可以是输入的图片,也可以是GAN生成的图片,还可以是卷积核的信息
-
- text:用于记录日志等文字信息,支持HTML格式
-
- histgram:可视化分布,主要是查看数据,参数的分布
-
- scatter:绘制散点图
-
- bar:绘制柱状图
-
- pie:绘制饼状图
image的画图功能可分为如下两类:
- iamge接收一个二维或三维向量,H_W或3_H*W,前者是黑白图像,后者是彩色图像
- images接收一个四维向量N_C_H*W,C可以是1或3,分别代表黑白和彩色图像
# 可视化一张随机的黑白图片
vis.image(t.randn(64,64).numpy())
# 可视化一张随机的彩色图片
vis.image(t.randn(3,64,64).numpy(),win='random2')
# 可视化36张随机的彩色图片,每一张6张
vis.images(t.randn(36,3,64,64).numpy(),nrow=6,win='random3',opts={'title':'random_imgs'})
'random3'
vis.text用于可视化文本,它支持所有的html标签
vis.text(u'''<h1>Hello visdom</h1><br>visdom是Facebook专门
为<b>PyTorch</b>开发的一款可视化工具,
visdom十分轻量级,却支持非常丰富的功能,
能胜任大多数的科学运算可视化任务''',win='visdom',opts={'title':u'vidsom简介'})
'visdom'