数据的处理对训练神经网络来说十分重要,良好的数据处理不仅会加速模型训练,更会提高模型效果(比如说去除数据的量纲,0均值化)。考虑到这点,PyTorch提供了几个高效便捷的工具,以便使用者进行数据处理或增强等操作,同时可通过并行化加速数据加载。
#在PyTorch中,数据加载可通过自定义的数据集对象。数据集对象被抽象为Dataset类,实现自定义的数据集需要继承Dataset,并实现getitem和len方法
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)
PyTorch提供了torchvision。它是一个视觉工具包,提供了很多视觉图像处理的工具.主要包含三部分:
其中transforms模块提供了对PIL Image对象和Tensor对象的常用操作。
如果要对图片进行多个操作,可通过Compose函数将这些操作拼接起来,类似于nn.Sequential。注意,这些操作定义后是以函数的形式存在,真正使用时需调用它的__call__方法,这点类似于nn.Module。例如要将图片调整为 224 × 224 224\times 224 224×224,首先应构建这个操作trans = Resize((224, 224)),然后调用trans(img)。
transform = transforms .Compose([
transforms .Resize(224), # 缩放图片(Image),保持长宽比不变,最短边为224像素
transforms .CenterCrop(224), # 从图片中间切出224*224的图片
transforms .ToTensor(), # 将图片(Image)转成Tensor,归一化至[0, 1]
transforms .Normalize(mean=[.5, .5, .5], std=[.5, .5, .5]) # 标准化至[-1, 1],规定均值和标准差
])
# 之后再创建dataset的时候再使用即可
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 = 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)
除了上述操作之外,transforms还可通过Lambda封装自定义的转换策略。例如想对PIL Image进行随机旋转,则可写成这样trans=T.Lambda(lambda img: img.rotate(random()*360))。
一个会经常使用到的Dataset,实现和上述的DogCat很相似。ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:
ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
Dataset只负责数据的抽象,一次调用__getitem__只返回一个样本。前面提到过,在训练神经网络时,最好是对一个batch的数据进行操作,同时还需要对数据进行shuffle和并行加速等。对此,PyTorch提供了DataLoader帮助我们实现这些功能。(注意和上面的loader区分)
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, num_workers=0, collate_fn=default_collate, pin_memory=False, drop_last=False)
dataloader是一个可迭代的对象,意味着我们可以像使用迭代器一样使用它。
在数据处理中,有时会出现某个样本无法读取等问题,比如某张图片损坏。这时在__getitem__函数中将出现异常,此时最好的解决方案即是将出错的样本剔除。如果实在是遇到这种情况无法处理,则可以返回None对象,然后在Dataloader中实现自定义的collate_fn,将空对象过滤掉。但要注意,在这种情况下dataloader返回的batch数目会少于batch_size,但是随机取一张图片代替,或者预先清洗数据都是个更好的选择。
DataLoader里面封装了Python的标准库multiprocessing,使其能够实现多进程加速。
所以
1.高负载的操作放在__getitem__中,如加载图片等。进程会并行的调用__getitem__函数,将负载高的放在__getitem__函数中能够实现并行加速。
2.dataset中应尽量只包含只读对象,避免修改任何可变对象,利用多线程进行操作。在多线程/多进程中,修改一个可变对象,需要加锁,就会很麻烦甚至不能实现。
PyTorch中还单独提供了一个sampler模块,用来对数据进行采样。常用的有随机采样器:RandomSampler,当dataloader的shuffle参数为True时,系统会自动调用这个采样器,实现打乱数据。默认的是采用SequentialSampler,它会按顺序一个一个进行采样。
另外一个很有用的采样方法: WeightedRandomSampler,它会根据每个样本的权重选取数据,在样本比例不均衡的问题中,可用它来进行重采样。
构建WeightedRandomSampler时需提供3个参数:
1.每个样本的权重weights,权重越大的样本被选中的概率越大.
2.共选取的样本总数num_samples,待选取的样本数目一般小于全部的样本数目.
3.以及一个可选参数replacement,用于指定是否可以重复选取某一个样本,默认为True.
在训练神经网络时,我们希望能更直观地了解训练情况,包括损失曲线、输入图片、输出图片、卷积核的参数分布等信息。这些信息能帮助我们更好地监督网络的训练过程,并为参数优化提供方向和依据。最简单的办法就是打印输出,但其只能打印数值信息,不够直观,同时无法查看分布、图片、声音等。在本节,我们将介绍两个深度学习中常用的可视化工具:Tensorboard和Visdom。
Tensorboard也是一个相对独立的工具,只要用户保存的数据遵循相应的格式,tensorboard就能读取这些数据并进行可视化。这里我们将主要介绍如何在PyTorch中使用tensorboardX1进行训练损失的可视化。 TensorboardX是将Tensorboard的功能抽取出来,使得非TensorFlow用户也能使用它进行可视化,几乎支持原生TensorBoard的全部功能。
首先启动
tensorboard --logdir /running/dir> --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)
Visdom可以创造、组织和共享多种数据的可视化,包括数值、图像、文本,甚至是视频,其支持PyTorch、Torch及Numpy。用户可通过编程组织可视化空间,或通过用户接口为生动数据打造仪表板,检查实验结果或调试代码。
Visdom中有两个重要概念:
在PyTorch中以下数据结构分为CPU和GPU两个版本:
# 交叉熵损失函数,带权重
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
以下对象可以持久化到硬盘,并能通过相应的方法加载到内存中:
if t.cuda.is_available():
a = a.cuda(1) # 把a转为GPU1上的tensor,
t.save(a,'a.pth')
# 加载为b, 存储于GPU1上(因为保存时tensor就在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'})
对于Module和Optimizer对象,这里建议保存对应的state_dict,既每个参数以及相应的值。
因为保存整个model的化,加载时会绑定到固定的类和结构上,如果代码经过比较大的重构的化,可能会出错中断。
# Module对象的保存与加载
t.save(model.state_dict(), 'squeezenet.pth')
model.load_state_dict(t.load('squeezenet.pth'))
# 优化器对象
t.save(optimizer.state_dict(), 'optimizer.pth')
optimizer.load_state_dict(t.load('optimizer.pth'))