PyTorch学习笔记(2)- 数据、可视化、GPU加速、持久化

1 数据

1.1 Dataset:数据的抽象

数据加载可通过自定义的数据集对象,继承Dataset类,并实现两个Python魔法方法:

  1. __getitem__:返回一条数据或一个样本,obj[index]等价于obj.__getitem__(index)
  2. __len__:返回样本的数量,len(obj)等价于obj.__len__()
【torchvision.transform】

torchvision是一个视觉工具包,其中transform模块提供了对PIL Image对象和Tensor对象的常用操作。

①对PIL Image的操作包括:

  • Scale:调整图片尺寸
  • CenterCrop, RandomCrop, RandomResizedCrop:裁剪图片
  • Pad:填充
  • ToTensor:将PIL Image对象转成Tensor,自动将[0,255]归一化至[0,1]

②对Tensor的操作包括:

  • Normalize:标准化
  • ToPILImage:将Tensor转为PIL Image对象

Compose:拼接对图片的多个操作,以函数形式存在,使用时调用__call__方法,类似于nn.Module

import os
from PIL import Image
import numpy as np
from torchvision import transform as T

transform = T.Compose([
	T.Resize(224), # 缩放图片,保持长宽比不变,最短边为224像素
	T.CenterCrop(224), # 从图片中切出224×224的图片
	T.ToTensor(),  # 将图片转为Tensor,归一化至[0, 1]
	T.Normalize(mean = [.5, .5, .5], std = [.5, .5, .5])]) # 标准化至[-1, 1],规定均值和方差

class DogCat(data.Dataset):
	def __init__(self, root, transforms = None):
		imgs = os.listdir(root)
		self.imgs = [os.path.join(root, img) for img in imgs]
		self.transforms = transforms

	def __getitem__(self, index):
		img_path = self.imgs[index]
		label = 1 if 'dog' in img_path.split('/')[-1] else 0
		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]
for img, label in dataset:
    print(img.size(), label)

transforms还可以通过Lamda封装自定义的转换策略,如对PIL Image进行随机旋转

trans=T.Lambda(lambda img : img.rotate(random()*360))
【常用Dataset:ImageFolder】

假设所有文件按文件夹保存,每个文件下存储同一个类别的图片,文件夹为类名,构造函数:

ImageFolder(root, transform = None, target_transform = None, loader = default_loader)
  1. root:在指定路径下寻找图片
  2. transform:对PIL Image进行转换操作,transform的输入是使用loader读取图片的返回对象
  3. target_transform:对label的转换
  4. loader:给定路径后如何读取图片,默认读取为RGB格式的PIL Image对象

label是按照文件夹名顺序排序后存成字典,即{类名:类序号(从0开始)},一般来说最好直接将文件夹命名为从0开始的数字,和ImageFolder实际的label一致,如果不是这种命名规范,通过self.class_to_idx属性了解Label和文件夹名的映射关系

dataset[0][1] #第一维是第几张图,第二维为1返回label
dataset[0][0] #第二维为0返回图片数据
dataset[0][0].size() #深度学习中图片保存为C×H×W

1.2 DataLoader

【构造和使用】

对batch操作,进行shuffle和并行加速等

DataLoader(dataset, batch_size = 1, shuffle = False, sampler = None, num_workers = 0, collate_fn = default_collate, pin_memory = False, drop_last = False)
  1. dataset:加载的数据集(Dataset对象)
  2. batch_size:batch_size
  3. shuffle:是否将数据打乱
  4. sampler:样本抽样
  5. num_workers:利用多进程加载的进程数,0代表不使用多进程
  6. collate_fn:如何将多个样本数据拼接成一个batch,一般用默认拼接方式即可
  7. pin_memory:是否将数据保存在pin memory区,pin memory中的数据转到GPU会快一些
  8. drop_last:dataset中的数据个数可能不是batch_size的整数倍,drop_last=True会将多出来不足一个batch的数据丢弃

Dataloader是一个可迭代的对象,可以像使用迭代器一样使用它,例如

for batch_datas, batch_labels in dataloader:
	train()

dataiter = iter(dataloader)
batch_datas, batch_labels = next(dataiter)
【异常情况】
  1. 在数据处理中,如果出现某个样本无法读取的问题,__getitem__函数将出现异常,可以将出错的样本剔除,返回None对象,然后在Dataloader中实现自定义的collate_fn将空对象过滤掉,这种情况下dataloader返回的batch数目会少于batch_size
class NewDogCat(DogCat):
	def __getitem__(self, index):
		try:
			return super(NewDogCat, self).__getitem__(index)
		except:
			return None, None
			
from torch.utils.data.dataloader import default_collate #默认拼接方式
def my_collate_fn(batch):
	batch = list(filter(lambda x : x[0] is not None, batch))
	if len(batch) == 0: 
		return t.Tenosr()
	return default_collate(batch) #用默认方式拼接过滤后的batch数据
  1. 最后一个batch的数据少于batch_size,可通过指定drop_last = True来丢弃最后一个不足batch_size的batch
  2. 诸如样本损坏或数据集加载异常等情况,可以随机取一张图片代替。相比丢弃异常图片而言,这种做法更好,因为这样能保证每个batch的数目仍是batch_size。但大多数情况下,最好的方式还是对数据进行彻底清洗
class NewDog(DogCat):
	def __getitem__(self, index):
		try:
			return super(NewDogCat, self).__getitem__(index)
		except:
			new_index=random.randint(0, len(self) - 1)
			return self[new_index]

1.3 Multiprocessing

DataLoader封装了Python标准库multiprocessing,使其能够进行多进程加速。建议:

  1. 高负载的操作放在__getitem__中,如加载图片,可以实现多进程并行加速;
  2. dataset中应尽量只包含只读对象,避免修改任何可变对象,如果使用可变对象可能会有意想不到的冲突,这是因为在多线程/多进程中,修改可变对象需要加锁,dataloader的设计使其很难加锁。下面使用self.num可能与预期不符
class BadDataset(Dataset):
	def __init__(self):
		self.datas = range(100)
		self.num = 0 # 取数据次数
		
	def __getitem__(self, index):
		self.num += 1
		return self.datas[index]
  1. 在使用多进程时,如果主程序异常终止(比如用Ctrl+C强行退出),相应的数据加载进程可能无法正常退出。这时你可能会发现程序已经退出了,但GPU显存和内存依旧被占用着,或通过topps aux依旧能够看到已经退出的程序,这时就需要手动强行杀掉进程。建议使用如下命令:
ps x | grep <cmdline> | awk '{print $1}' | xargs kill
  • ps x:获取当前用户的所有进程
  • grep :找到已经停止的PyTorch程序的进程,例如你是通过python train.py启动的,那你就需要写grep 'python train.py'
  • awk '{print $1}':获取进程的pid
  • xargs kill:杀掉进程,根据需要可能要写成xargs kill -9强制杀掉进程
    执行这句命令前,建议打印确认一下是否会误杀其它进程
ps x | grep <cmdline> | ps x

1.4 Sampler

sampler模块用来对数据进行采样,常用的有:

  • 随机采样器RandomSampler:当dataloader的shuffle = true时,系统会自动调用这个采样器,实现打乱数据
  • SequentialSampler:默认采用,它会按顺序一个一个进行采样
  • WeightedRandomSampler:一个很有用的采样方法,它会根据每个样本的权重选取数据,在样本比例不均衡的问题中,可用它来进行重采样
from torch.utils.data.sampler import  WeightedRandomSampler
sampler = WeightedRandomSampler(weights,\
                                num_samples=9,\
                                replacement=True)
dataloader = DataLoader(dataset,
                        batch_size=3,
                        sampler=sampler)
for datas, labels in dataloader:
    print(labels.tolist())

构建WeightedRandomSampler时需提供两个参数:

  1. 每个样本的权重weights:权重越大的样本被选中的概率越大
  2. 共选取的样本总数num_samples:待选取的样本数目一般小于全部的样本数目
  3. 一个可选参数replacement:用于指定是否可以重复选取某一个样本,默认为True,即允许在一个epoch中重复采样某一个数据。如果设为False,则当某一类的样本被全部选取完,但其样本数目仍未达到num_samples时,sampler将不会再从该类中选择数据,此时可能导致weights参数失效。

2 torchvision

为了方便研究者使用,PyTorch团队专门开发了一个视觉工具包torchvision,独立于Python,通过pip install torchvision安装。

  • torchvision主要包括三部分:
  1. models:提供深度学习中各种经典网络的网络结构以及预训练好的模型,包括AlexNet, VGG系列,ResNet系列,inception系列等
from torchvision import models
from torch import nn
# 加载预训练好的模型,如果不存在会进行下载
# 预训练好的模型保存在 ~/.torch/models/下面
resnet34 = models.squeezenet1_1(pretrained=True, num_classes=1000)

# 修改最后的全连接层为10分类问题(默认是ImageNet上的1000分类)
resnet34.fc=nn.Linear(512, 10)
  • datasets:提供常用的数据集加载,设计上都是继承torch.utils.data.Dataset,主要包括MNIST, CIFAR10/100, ImageNet, COCO等
from torchvision import datasets
# 指定数据集路径为data,如果数据集不存在则进行下载
# 通过train=False获取测试集
dataset = datasets.MNIST('data/', download=True, train=False, transform=transform)
  • transforms:提供常用的数据预处理操作,主要包括对Tensor以及PIL Image对象的操作
from torchvision import transforms 
to_pil = transforms.ToPILImage()
to_pil(t.randn(3, 64, 64))
  • torchvision还提供了两个常用的函数:
  1. make_grid:能将多张图片拼接成一个网格中
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) # 拼成4*4网格图片,且会转成3通道
to_img(img)
  1. save_img:能将Tensor保存成图片
save_image(img, 'a.png')
Image.open('a.png')

3 可视化工具

想要了解训练情况,包括损失曲线、输入图片、输出图片、卷积核的参数分布等信息,以更好地监督网络的训练过程,并为参数优化提供方向和依据。

3.1 Tensorboard

作为和TensorFlow深度集成的工具,Tensorboard能够展现TensorFlow网络计算图,绘制图像生成的定量指标图以及附加数据。同时Tensorboard也是一个相对独立的工具,只要用户保存的数据遵循相应的格式,tensorboard就能读取这些数据并进行可视化。

【安装】

  1. 安装TensorFlow:如果电脑中已经安装完TensorFlow可以跳过这一步,如果电脑中尚未安装,建议安装CPU-Only的版本,或使用pip直接安装,推荐使用清华的软件源
  2. 安装tensorboard: pip install tensorboard
  3. 安装tensorboardX:pip install tensorboardX

【启动】

tensorboard --logdir --port

【效果】

from tensorboardX import SummaryWriter
# 构建logger对象,logdir用来指定log文件的保存路径
# flush_secs用来指定刷新同步间隔
logger = SummaryWriter(log_dir='experimient_cnn', flush_secs=2)

for ii in range(100):
    logger.add_scalar('data/loss', 10-ii**0.5)
    logger.add_scalar('data/accuracy', ii**0.5/10)
  • 打开浏览器输入http://localhost:6006(其中6006应改成你的tensorboard所绑定的端口)即可看到效果,左侧的Horizontal Axis下有三个选项,分别是:
  1. Step:根据步长来记录,log_value时如果有步长,则将其作为x轴坐标描点画线
  2. Relative:用前后相对顺序描点画线,可认为logger自己维护了一个step属性,每调用一次log_value就自动加1
  3. Wall:按时间排序描点画线

左侧的Smoothing条可以左右拖动,用来调节平滑的幅度。点击右上角的刷新按钮可立即刷新结果,默认是每30s自动刷新数据。可见tensorboard_logger的使用十分简单,但它只能统计简单的数值信息,不支持其它功能。

3.2 Visdom

【Visdom基础概念】
  • Visdom:可以创造、组织、共享多种数据可视化,包括数值、图像、文本、视频,支持PyTorch、Torch、Numpy,用户可通过编程组织可视化空间,检查实验结果或调试代码
  • Visdom的两个重要概念:
  1. env: 环境,不同环境的可视化结果互相隔离,互不影响,使用时如果不指定env,默认使用main,不同用户、不同程序使用不同的env
  2. pane: 窗格,用于可视化图像、数值、打印文本等,一个程序可以使用同一个env中不同pane,每个pane可视化或记录某一信息
【安装】

pip install visdom

【启动】
  • 安装完成后:通过python -m visdom.server启动visdom服务,或通过nohup python -m visdom.server &将服务放至后台运行。
  • Visdom服务是一个web server服务,默认绑定8097端口,客户端与服务器间通过tornado进行非阻塞交互。

【使用注意】

  1. 需手动指定保存env,可在web界面点击save按钮或在程序中调用save方法,否则visdom服务重启后,env等信息会丢失。
  2. 客户端与服务器之间的交互采用tornado异步框架,可视化操作不会阻塞当前程序,网络异常也不会导致程序退出。
【vis操作】
  • 常用操作
%%sh
# 启动visdom服务器
# nohup python -m visdom.server &
import torch as t
import visdom

# 新建一个连接客户端
# 指定env = u'test1',默认端口为8097,host是‘localhost'
vis = visdom.Visdom(env = u'test1', use_incoming_socket = False) # 用于构建一个客户端,客户端除指定env之外,还可以指定host、port等参数

x = t.arange(1, 30, 0.01)
y = t.sin(x)
vis.line(X = x, Y = y, win = 'sinx', opts = {'title' : 'y = sin(x)'})
  1. line:类似Matlab中的plot,记录某些标量的变化,损失、准确率等
  2. image:可视化图片,可以是输入图片,也可以是GAN生成图片,还可以是卷积核信息
  3. text:记录日志等文字信息,用于可视化文本,支持所有html标签,也支持所有html语法标准
  4. histgram:可视化分布,查看数据、参数分布
  5. scatter:绘制散点图
  6. bar:绘制柱状图
  7. pie:绘制饼状图
  • Visdom同时支持PyTorch的tensor和Numpy的ndarray两种数据类型,不支持Python的int, float等数据类型,每次传入时需先将数据转成ndarray或tensor,有两个参数是大多数操作都具备的:
  1. win:指定pane的名字,如果不指定,visdom将自动分配一个新的pane,建议每次操作都重新指定win
  2. opts:选项,接受一个字典,常见options包括title, xlabel, ylabel, width等,用于设置pane的显示格式
#append追加数据
for ii in range(0, 10):
	x = t.Tensor([ii])
	y = x
	vis.line(X = x, Y = y, win = 'polynomial', update = 'append' if ii > 0 else None)
  • image的画图功能可分为如下两类:
  1. image接收一个二维或三维向量,H×W或3×H×W
  2. images接收一个四维向量N×C×H×W,可实现类似torchvision中make_grid功能,将多张图片拼在一起
#可视化一个随机黑白照片
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'})
  • vis.text用于可视化文本,支持所有的html标签,同时也遵循着html的语法标准。例如,换行需使用
    标签,\r\n无法实现换行
vis.text(u'''

Hello Visdom


Visdom是Facebook专门为PyTorch开发的一个可视化工具, 在内部使用了很久,在2017年3月份开源了它。 Visdom十分轻量级,但是却有十分强大的功能,支持几乎所有的科学运算可视化任务'''
, win='visdom', opts={'title': u'visdom简介' } )

4 使用GPU加速:cuda

【.cuda】
  • PyTorch中以下数据结构分为CPU和GPU两个版本:
  1. Tensor
  2. nn.Module(包括layer, loss function, 容器Sequential等)
    调用.cuda()方法可将其转为GPU对象,注意,tensor.cuda会返回一个新对象,这个新对象的数据已转移至GPU,而之前的tensor还在原来的设备上(CPU)。而module.cuda则会将所有的数据都迁移至GPU,并返回自己。所以module = module.cuda()module.cuda()所起的作用一致。
  • 为什么将数据转移至GPU的方法叫.cuda而不是.gpu?
    因为GPU的编程接口采用CUDA,目前并不是所有的GPU都支持CUDA,只有部分Nvidia的GPU才支持。PyTorch未来可能会支持AMD的GPU,而AMD GPU的编程接口采用OpenCL,因此PyTorch还预留着.cl方法,用于以后支持AMD等GPU。
tensor = t.Tensor(3, 4)
# 返回一个新的tensor,保存在第1块GPU上,但原来的tensor并没有改变
tensor.cuda(0)
tensor.is_cuda # False
# 不指定所使用的GPU设备,将默认使用第1块GPU
tensor = tensor.cuda()
tensor.is_cuda # True
module = nn.Linear(3, 4)
module.cuda(device = 1)
module.weight.is_cuda # True
  • 将不同的计算分布到不同的GPU中:
class VeryBigModule(nn.Module):
    def __init__(self):
        super(VeryBigModule, self).__init__()
        self.GiantParameter1 = t.nn.Parameter(t.randn(100000, 20000)).cuda(0)
        self.GiantParameter2 = t.nn.Parameter(t.randn(20000, 100000)).cuda(1)
    
    def forward(self, x):
        x = self.GiantParameter1.mm(x.cuda(0))
        x = self.GiantParameter2.mm(x.cuda(1))
        return x
  • 关于使用GPU的一些建议:
  1. GPU运算很快,但对于很小的运算量来说,并不能体现出它的优势,因此对于一些简单的操作可直接利用CPU完成
  2. 数据在CPU和GPU之间,以及GPU与GPU之间的传递会比较耗时,应当尽量避免
  3. 在进行低精度的计算时,可以考虑HalfTensor,它相比于FloatTensor能节省一半的显存,但需千万注意数值溢出的情况。
  • 损失函数调用criterion.cuda:大部分的损失函数也都属于nn.Moudle,但在使用GPU时,很多时候我们都忘记使用它的.cuda方法,这在大多数情况下不会报错,因为损失函数本身没有可学习的参数(learnable parameters)。但在某些情况下会出现问题,为了保险起见同时也为了代码更规范,应记得调用criterion.cuda
# 交叉熵损失函数,带权重
criterion = t.nn.CrossEntropyLoss(weight=t.Tensor([1, 3]))
input = t.randn(4, 2).cuda()
target = t.Tensor([1, 0, 0, 1]).long().cuda()

# 下面这行会报错,因weight未被转移至GPU
# loss = criterion(input, target)

# 这行则不会报错
criterion.cuda()
loss = criterion(input, target)

criterion._buffers
  • torch.cuda.device(1):指定默认使用哪一块GPU
  • torch.set_default_tensor_type:使程序默认使用GPU,不需要手动调用cuda
# 如果未指定使用哪块GPU,默认使用GPU 0
x = t.cuda.FloatTensor(2, 3)
# x.get_device() == 0
y = t.FloatTensor(2, 3).cuda()
# y.get_device() == 0

# 指定默认使用GPU 1
with t.cuda.device(1):    
    # 在GPU 1上构建tensor
    a = t.cuda.FloatTensor(2, 3)

    # 将tensor转移至GPU 1
    b = t.FloatTensor(2, 3).cuda()
    print(a.get_device() == b.get_device() == 1 )

    c = a + b
    print(c.get_device() == 1)

    z = x + y
    print(z.get_device() == 0)

    # 手动指定使用GPU 0
    d = t.randn(2, 3).cuda(0)
    print(d.get_device() == 2)
t.set_default_tensor_type('torch.cuda.FloatTensor') # 指定默认tensor的类型为GPU上的FloatTensor
a = t.ones(2, 3)
a.is_cuda
  • 如果服务器有多个GPU,tensor.cuda()方法会将tensor保存到第一块GPU上,等价于tensor.cuda(0),此时如果想使用第二块GPU,需要手动指定tensor.cuda(1),需要修改大量代码。两种替代方法:
  1. 先调用t.cuda.set_device(1)指定使用第二块GPU,后续.cuda()都无需更改,切换GPU只需这一行代码

  2. 推荐设置环境变量CUDA_VISIBLE_DEVICES,例如当export CUDA_VISIBLE_DEVICE = 1,只使用第二块物理GPU,在程序中这块GPU被看成第一块逻辑GPU,此时调用tensor.cuda()会将Tensor转移至第二块物理GPU。CUDA_VISIBLE_DEVICES还可以指定多个GPU,如export CUDA_VISIBLE_DEVICES = 0, 2, 3,那么第一、三、四块物理GPU会被映射成第一、二、三块逻辑GPU,tensor.cuda(1)会将Tensor转移到第三块GPU上

    设置CUDA_VISIBLE_DEVICES有两种方法,一种直接在命令行中CUDA_VISIBLE_dEVICES = 0, 1 python main.py,一种在程序中import os; os.environ["CUDA_VISIBLE_DEVICES"] = "2"。如果使用IPython或Jupyter Notebook,还可以使用%env CUDA_VISIBLE_dEVICES = 1, 2设置环境变量

    tensor.to(device),能够实现设备透明,便于实现CPU/GPU兼容

  • 从 0.4 版本开始,pytorch新增了tensor.to(device)方法,能够实现设备透明,便于实现CPU/GPU兼容
【单机多卡并行】

new_module = nn.DataParallel(module, device_ids) 默认把模型分布到所有的卡上,机制:

  1. 将模型复制到每一张卡上
  2. 将形状为(N, C, H, W)的输入均等分为n份(n张卡),每一份形状是(N/n, C, H, W),然后在每张卡前向传播,反向传播,梯度求平均,要求batch_size大于等于卡的个数(N>=n)

在绝大多数情况下,new_module的用法和module一致,除了极其特殊的情况(RNN中的PackedSequence)。另外想获取原始的单卡模型,需要通过new_module.module访问。

【多机分布式】

5 持久化

  • 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)
if t.cuda.is_available():
	a = a.cuda(1) #把a转为GPU1上的tensor
	t.save(a, 'a.pth')
	#加载为b,存储于GPU1上
	b = t.load('a.pth')
	#加载为c,存储于CPU
	c = t.load('a.pth', map_location = lambda storage, loc: storage)
	#加载为d,存储于GPU0上
	d = t.load('a.pth', map_location = {'cuda:1' : 'cuda:0'})
t.set_default_tensor_type('torch.FloatTensor')
from torchvision.models import SqueezeNet
model = SqueezeNet()
#module的state_dict是一个字典
model.state_Dict().keys()

#Module对象的保存和加载
t.save(model.state_dict(), 'squeezenet.pth')
model.load_state_dict(t.load('squeezenet.pth'))

#Optimizer对象的保存和加载
optimizer = t.optim.Adam(model.parameters(), lr=0.1)
t.save(optimizer.state_dict(), 'optimizer.pth')
optimizer.load_state_dict(t.load('optimizer.pth'))
all_data = dict(optimizer = optimizer.state_dict(), model = model.state_dict(), info = '模型和优化器的所有参数')
all_data = t.load('all.pth')
all_data.keys()
#dict_keys(['optimizer','model','info'])

你可能感兴趣的:(Python,pytorch,学习,python)