欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Pytorch学习笔记-第五章

程序员文章站 2024-03-01 18:49:58
...


记录一下个人学习和使用Pytorch中的一些问题。强烈推荐 《深度学习框架PyTorch:入门与实战》.写的非常好而且作者也十分用心,大家都可以看一看,本文记录按照该书学习Pytorch时在第五章常用模块遇到的一些问题。

数据处理

数据的处理对训练神经网络来说十分重要,良好的数据处理不仅会加速模型训练,更会提高模型效果(比如说去除数据的量纲,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)

视觉工具包torchvision

PyTorch提供了torchvision。它是一个视觉工具包,提供了很多视觉图像处理的工具.主要包含三部分:

  1. models:提供深度学习中各种经典网络的网络结构以及预训练好的模型,包括AlexNet、VGG系列、ResNet系列、Inception系列等。
  2. datasets:提供常用的数据集加载,设计上都是继承torhc.utils.data.Dataset,主要包括MNIST、CIFAR10/100、ImageNet、COCO等。
  3. transforms:提供常用的数据预处理操作,主要包括对Tensor以及PIL Image对象的操作。
    torchvision还提供了两个常用的函数。一个是make_grid,它能将多张图片拼接成一个网格中;另一个是save_img,它能将Tensor保存成图片。

其中transforms模块提供了对PIL Image对象和Tensor对象的常用操作。
Pytorch学习笔记-第五章
如果要对图片进行多个操作,可通过Compose函数将这些操作拼接起来,类似于nn.Sequential。注意,这些操作定义后是以函数的形式存在,真正使用时需调用它的__call__方法,这点类似于nn.Module。例如要将图片调整为224×224224\times 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))。

ImageFolder

一个会经常使用到的Dataset,实现和上述的DogCat很相似。ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:

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

DataLoader

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)
  • dataset:加载的数据集(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的数据丢弃

dataloader是一个可迭代的对象,意味着我们可以像使用迭代器一样使用它。
在数据处理中,有时会出现某个样本无法读取等问题,比如某张图片损坏。这时在__getitem__函数中将出现异常,此时最好的解决方案即是将出错的样本剔除。如果实在是遇到这种情况无法处理,则可以返回None对象,然后在Dataloader中实现自定义的collate_fn,将空对象过滤掉。但要注意,在这种情况下dataloader返回的batch数目会少于batch_size,但是随机取一张图片代替,或者预先清洗数据都是个更好的选择。

多进程

DataLoader里面封装了Python的标准库multiprocessing,使其能够实现多进程加速。
所以
1.高负载的操作放在__getitem__中,如加载图片等。进程会并行的调用__getitem__函数,将负载高的放在__getitem__函数中能够实现并行加速。
2.dataset中应尽量只包含只读对象,避免修改任何可变对象,利用多线程进行操作。在多线程/多进程中,修改一个可变对象,需要加锁,就会很麻烦甚至不能实现。

Sample

PyTorch中还单独提供了一个sampler模块,用来对数据进行采样。常用的有随机采样器:RandomSampler,当dataloader的shuffle参数为True时,系统会自动调用这个采样器,实现打乱数据。默认的是采用SequentialSampler,它会按顺序一个一个进行采样。
另外一个很有用的采样方法: WeightedRandomSampler,它会根据每个样本的权重选取数据,在样本比例不均衡的问题中,可用它来进行重采样。
构建WeightedRandomSampler时需提供3个参数:
1.每个样本的权重weights,权重越大的样本被选中的概率越大.
2.共选取的样本总数num_samples,待选取的样本数目一般小于全部的样本数目.
3.以及一个可选参数replacement,用于指定是否可以重复选取某一个样本,默认为True.

可视化工具

在训练神经网络时,我们希望能更直观地了解训练情况,包括损失曲线、输入图片、输出图片、卷积核的参数分布等信息。这些信息能帮助我们更好地监督网络的训练过程,并为参数优化提供方向和依据。最简单的办法就是打印输出,但其只能打印数值信息,不够直观,同时无法查看分布、图片、声音等。在本节,我们将介绍两个深度学习中常用的可视化工具:Tensorboard和Visdom。

Tensorboard

Tensorboard也是一个相对独立的工具,只要用户保存的数据遵循相应的格式,tensorboard就能读取这些数据并进行可视化。这里我们将主要介绍如何在PyTorch中使用tensorboardX1进行训练损失的可视化。 TensorboardX是将Tensorboard的功能抽取出来,使得非TensorFlow用户也能使用它进行可视化,几乎支持原生TensorBoard的全部功能。
首先启动

tensorboard --logdir <your/running/dir> --port <your_bind_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

Visdom可以创造、组织和共享多种数据的可视化,包括数值、图像、文本,甚至是视频,其支持PyTorch、Torch及Numpy。用户可通过编程组织可视化空间,或通过用户接口为生动数据打造仪表板,检查实验结果或调试代码。
Visdom中有两个重要概念:

  • env:环境。不同环境的可视化结果相互隔离,互不影响,在使用时如果不指定env,默认使用main。不同用户、不同程序一般使用不同的env。
  • pane:窗格。窗格可用于可视化图像、数值或打印文本等,其可以拖动、缩放、保存和关闭。一个程序中可使用同一个env中的不同pane,每个pane可视化或记录某一信息。
    Pytorch学习笔记-第五章

GPU加速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()所起的作用一致。tensor.to(device)方法,能够实现设备透明,便于实现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

使用建议

  • GPU运算很快,但对于很小的运算量来说,并不能体现出它的优势,因此对于一些简单的操作可直接利用CPU完成
  • 数据在CPU和GPU之间,以及GPU与GPU之间的传递会比较耗时,应当尽量避免
  • 在进行低精度的计算时,可以考虑HalfTensor,它相比于FloatTensor能节省一半的显存,但需千万注意数值溢出的情况。

并行化

Pytorch学习笔记-第五章

固化数据

以下对象可以持久化到硬盘,并能通过相应的方法加载到内存中:

  • Tensor
  • Variable
  • nn.Module
  • Optimizer
    本质上上述这些信息最终都是保存成Tensor。Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。

一般Tensor

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'})

modle和Optimizer

对于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'))
相关标签: DL 深度学习