数据加载可通过自定义的数据集对象,继承Dataset类,并实现两个Python魔法方法:
__getitem__
:返回一条数据或一个样本,obj[index]
等价于obj.__getitem__(index)
__len__
:返回样本的数量,len(obj)
等价于obj.__len__()
torchvision是一个视觉工具包,其中transform模块提供了对PIL Image对象和Tensor对象的常用操作。
①对PIL Image的操作包括:
②对Tensor的操作包括:
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))
假设所有文件按文件夹保存,每个文件下存储同一个类别的图片,文件夹为类名,构造函数:
ImageFolder(root, transform = None, target_transform = None, loader = default_loader)
root
:在指定路径下寻找图片transform
:对PIL Image进行转换操作,transform的输入是使用loader读取图片的返回对象target_transform
:对label的转换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
对batch操作,进行shuffle和并行加速等
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_sizeshuffle
:是否将数据打乱sampler
:样本抽样num_workers
:利用多进程加载的进程数,0代表不使用多进程collate_fn
:如何将多个样本数据拼接成一个batch,一般用默认拼接方式即可pin_memory
:是否将数据保存在pin memory区,pin memory中的数据转到GPU会快一些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)
collate_fn
将空对象过滤掉,这种情况下dataloader返回的batch数目会少于batch_sizeclass 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数据
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]
DataLoader封装了Python标准库multiprocessing,使其能够进行多进程加速。建议:
__getitem__
中,如加载图片,可以实现多进程并行加速;class BadDataset(Dataset):
def __init__(self):
self.datas = range(100)
self.num = 0 # 取数据次数
def __getitem__(self, index):
self.num += 1
return self.datas[index]
top
、ps aux
依旧能够看到已经退出的程序,这时就需要手动强行杀掉进程。建议使用如下命令:ps x | grep <cmdline> | awk '{print $1}' | xargs kill
ps x
:获取当前用户的所有进程grep
:找到已经停止的PyTorch程序的进程,例如你是通过python train.py启动的,那你就需要写grep 'python train.py'
awk '{print $1}'
:获取进程的pidxargs kill
:杀掉进程,根据需要可能要写成xargs kill -9
强制杀掉进程ps x | grep <cmdline> | ps x
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时需提供两个参数:
为了方便研究者使用,PyTorch团队专门开发了一个视觉工具包torchvision,独立于Python,通过pip install torchvision
安装。
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))
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)
save_img
:能将Tensor保存成图片save_image(img, 'a.png')
Image.open('a.png')
想要了解训练情况,包括损失曲线、输入图片、输出图片、卷积核的参数分布等信息,以更好地监督网络的训练过程,并为参数优化提供方向和依据。
作为和TensorFlow深度集成的工具,Tensorboard能够展现TensorFlow网络计算图,绘制图像生成的定量指标图以及附加数据。同时Tensorboard也是一个相对独立的工具,只要用户保存的数据遵循相应的格式,tensorboard就能读取这些数据并进行可视化。
pip install tensorboard
pip install tensorboardX
tensorboard --logdir
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)
左侧的Smoothing条可以左右拖动,用来调节平滑的幅度。点击右上角的刷新按钮可立即刷新结果,默认是每30s自动刷新数据。可见tensorboard_logger的使用十分简单,但它只能统计简单的数值信息,不支持其它功能。
pip install visdom
python -m visdom.server
启动visdom服务,或通过nohup python -m visdom.server &
将服务放至后台运行。%%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)'})
#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)
#可视化一个随机黑白照片
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(u'''Hello Visdom
Visdom是Facebook专门为PyTorch开发的一个可视化工具,
在内部使用了很久,在2017年3月份开源了它。
Visdom十分轻量级,但是却有十分强大的功能,支持几乎所有的科学运算可视化任务''',
win='visdom',
opts={'title': u'visdom简介' }
)
tensor.cuda
会返回一个新对象,这个新对象的数据已转移至GPU,而之前的tensor还在原来的设备上(CPU)。而module.cuda
则会将所有的数据都迁移至GPU,并返回自己。所以module = module.cuda()
和module.cuda()
所起的作用一致。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
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
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)
:指定默认使用哪一块GPUtorch.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
tensor.cuda()
方法会将tensor保存到第一块GPU上,等价于tensor.cuda(0)
,此时如果想使用第二块GPU,需要手动指定tensor.cuda(1)
,需要修改大量代码。两种替代方法:先调用t.cuda.set_device(1)
指定使用第二块GPU,后续.cuda()
都无需更改,切换GPU只需这一行代码
推荐设置环境变量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兼容
tensor.to(device)
方法,能够实现设备透明,便于实现CPU/GPU兼容new_module = nn.DataParallel(module, device_ids)
默认把模型分布到所有的卡上,机制:
在绝大多数情况下,new_module的用法和module一致,除了极其特殊的情况(RNN中的PackedSequence)。另外想获取原始的单卡模型,需要通过new_module.module
访问。
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)
加载保存的数据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'])