PyTorch(五)——PyTorch进阶训练技巧

文章目录

    • 自定义损失函数
      • 以函数方式定义
      • 以类方式定义
    • 动态调整学习率
      • 使用官方scheduler
      • 自定义scheduler
    • 模型微调-torchvision
      • 模型微调的流程
      • 使用已有模型结构
      • 训练特定层
    • 模型微调-timm
      • 如何查看预训练模型种类
      • 使用和修改预训练模型
      • 模型的保存
      • 推荐资料
    • 半精度训练
      • 半精度训练的设置
    • 数据增强-imgaug
      • imgaug简介和安装
      • imgaug的使用
        • 单张图片处理
        • 对批次图片进行处理
        • 对不同大小的图片进行处理
      • imgaug在PyTorch的应用
      • 总结
    • 使用argparse进行调参
      • argparse简介
      • argparse的使用
      • 更加高效使用argparse修改超参数
      • 总结
    • - PyTorch模型定义与进阶训练技巧

datawhale深入浅出pytorch

自定义损失函数

以函数方式定义

事实上,损失函数仅仅是一个函数而已,因此我们可以通过直接以函数定义的方式定义一个自己的函数,如下所示:

def my_loss(output, target):
    loss = torch.mean((output - target)**2)
    return loss

以类方式定义

虽然以函数定义的方式很简单,但是以类方式定义更加常用,在以类方式定义损失函数时,我们如果看每一个损失函数的继承关系我们就可以发现Loss函数部分继承自_loss, 部分继承自_WeightedLoss, 而_WeightedLoss继承自_loss_loss继承自 nn.Module。我们可以将其当作神经网络的一层来对待,同样地,我们的损失函数类就需要继承自nn.Module类,在下面的例子中我们以DiceLoss为例向大家讲述。
Dice Loss是一种在分割领域常见的损失函数,定义如下:
D S C = 2 ∣ X ∩ Y ∣ ∣ X ∣ + ∣ Y ∣ DSC = \frac{2|X∩Y|}{|X|+|Y|} DSC=X+Y2∣XY
实现代码如下:

class DiceLoss(nn.Module):
    def __init__(self,weight=None,size_average=True):
        super(DiceLoss,self).__init__()
        
    def forward(self,inputs,targets,smooth=1):
        inputs = F.sigmoid(inputs)       
        inputs = inputs.view(-1)
        targets = targets.view(-1)
        intersection = (inputs * targets).sum()                   
        dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)  
        return 1 - dice

# 使用方法    
criterion = DiceLoss()
loss = criterion(input,targets)

除此之外,常见的损失函数还有BCE-Dice Loss,Jaccard/Intersection over Union (IoU) Loss,Focal Loss…

动态调整学习率

学习率的选择是深度学习中一个困扰人们许久的问题,学习速率设置过小,会极大降低收敛速度,增加训练时间;学习率太大,可能导致参数在最优解两侧来回振荡。但是当我们选定了一个合适的学习率后,经过许多轮的训练后,可能会出现准确率震荡或loss不再下降等情况,说明当前学习率已不能满足模型调优的需求。此时我们就可以通过一个适当的学习率衰减策略来改善这种现象,提高我们的精度。这种设置方式在PyTorch中被称为scheduler。

使用官方scheduler

在训练神经网络的过程中,学习率是最重要的超参数之一,作为当前较为流行的深度学习框架,PyTorch已经在torch.optim.lr_scheduler为我们封装好了一些动态调整学习率的方法供我们使用,如下面列出的这些scheduler。

  • lr_scheduler.LambdaLR
  • lr_scheduler.MultiplicativeLR
  • lr_scheduler.StepLR
  • lr_scheduler.MultiStepLR
  • lr_scheduler.ExponentialLR
  • lr_scheduler.CosineAnnealingLR
  • lr_scheduler.ReduceLROnPlateau
  • lr_scheduler.CyclicLR
  • lr_scheduler.OneCycleLR
  • lr_scheduler.CosineAnnealingWarmRestarts

关于如何使用这些动态调整学习率的策略,PyTorch官方也很人性化的给出了使用实例代码帮助大家理解。

# 选择一种优化器
optimizer = torch.optim.Adam(...) 
# 选择上面提到的一种或多种动态调整学习率的方法
scheduler1 = torch.optim.lr_scheduler.... 
scheduler2 = torch.optim.lr_scheduler....
...
schedulern = torch.optim.lr_scheduler....
# 进行训练
for epoch in range(100):
    train(...)
    validate(...)
    optimizer.step()
    # 需要在优化器参数更新之后再动态调整学习率
	scheduler1.step() 
	...
    schedulern.step()

我们在使用官方给出的torch.optim.lr_scheduler时,需要将scheduler.step()放在optimizer.step()后面进行使用。

自定义scheduler

虽然PyTorch官方给我们提供了许多的API,但是在实验中也有可能碰到需要我们自己定义学习率调整策略的情况,而我们的方法是自定义函数adjust_learning_rate来改变param_grouplr的值,在下面的叙述中会给出一个简单的实现。
假设我们现在正在做实验,需要学习率每30轮下降为原来的1/10,假设已有的官方API中没有符合我们需求的,那就需要自定义函数来实现学习率的改变。

def adjust_learning_rate(optimizer, epoch):
    lr = args.lr * (0.1 ** (epoch // 30))
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

有了adjust_learning_rate函数的定义,在训练的过程就可以调用我们的函数来实现学习率的动态变化

def adjust_learning_rate(optimizer,...):
    ...
optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9)
for epoch in range(10):
    train(...)
    validate(...)
    adjust_learning_rate(optimizer,epoch)

模型微调-torchvision

模型微调的流程

PyTorch(五)——PyTorch进阶训练技巧_第1张图片

使用已有模型结构

这里我们以torchvision中的常见模型为例,列出了如何在图像分类任务中使用PyTorch提供的常见模型结构和参数。对于其他任务和网络结构,使用方式是类似的:

import torchvision.models as models
resnet18 = models.resnet18()
# resnet18 = models.resnet18(pretrained=False)  等价于与上面的表达式
alexnet = models.alexnet()
vgg16 = models.vgg16()
squeezenet = models.squeezenet1_0()
densenet = models.densenet161()
inception = models.inception_v3()
googlenet = models.googlenet()
shufflenet = models.shufflenet_v2_x1_0()
mobilenet_v2 = models.mobilenet_v2()
mobilenet_v3_large = models.mobilenet_v3_large()
mobilenet_v3_small = models.mobilenet_v3_small()
resnext50_32x4d = models.resnext50_32x4d()
wide_resnet50_2 = models.wide_resnet50_2()
mnasnet = models.mnasnet1_0()

通过True或者False来决定是否使用预训练好的权重,在默认状态下pretrained = False,意味着我们不使用预训练得到的权重,当pretrained = True,意味着我们将使用在一些数据集上预训练得到的权重。

import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)
alexnet = models.alexnet(pretrained=True)
squeezenet = models.squeezenet1_0(pretrained=True)
vgg16 = models.vgg16(pretrained=True)
densenet = models.densenet161(pretrained=True)
inception = models.inception_v3(pretrained=True)
googlenet = models.googlenet(pretrained=True)
shufflenet = models.shufflenet_v2_x1_0(pretrained=True)
mobilenet_v2 = models.mobilenet_v2(pretrained=True)
mobilenet_v3_large = models.mobilenet_v3_large(pretrained=True)
mobilenet_v3_small = models.mobilenet_v3_small(pretrained=True)
resnext50_32x4d = models.resnext50_32x4d(pretrained=True)
wide_resnet50_2 = models.wide_resnet50_2(pretrained=True)
mnasnet = models.mnasnet1_0(pretrained=True)

注意事项:

  1. 通常PyTorch模型的扩展为.pt.pth,程序运行时会首先检查默认路径中是否有已经下载的模型权重,一旦权重被下载,下次加载就不需要下载了。
  2. 一般情况下预训练模型的下载会比较慢,我们可以直接通过迅雷或者其他方式去这里查看自己的模型里面model_urls,然后手动下载,预训练模型的权重在LinuxMac的默认下载路径是用户根目录下的.cache文件夹。在Windows下就是C:\Users\\.cache\torch\hub\checkpoint。我们可以通过使用 torch.utils.model_zoo.load_url()设置权重的下载地址。
  3. 如果觉得麻烦,还可以将自己的权重下载下来放到同文件夹下,然后再将参数加载网络。
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
  1. 如果中途强行停止下载的话,一定要去对应路径下将权重文件删除干净,要不然可能会报错。

训练特定层

在默认情况下,参数的属性.requires_grad = True,如果我们从头开始训练或微调不需要注意这里。但如果我们正在提取特征并且只想为新初始化的层计算梯度,其他参数不进行改变。那我们就需要通过设置requires_grad = False来冻结部分层。在PyTorch官方中提供了这样一个例程。

def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

在下面我们仍旧使用resnet18为例的将1000类改为4类,但是仅改变最后一层的模型参数,不改变特征提取的模型参数;注意我们先冻结模型参数的梯度,再对模型输出部分的全连接层进行修改,这样修改后的全连接层的参数就是可计算梯度的。

import torchvision.models as models
# 冻结参数的梯度
feature_extract = True
model = models.resnet18(pretrained=True)
set_parameter_requires_grad(model, feature_extract)
# 修改模型
num_ftrs = model.fc.in_features
model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)

之后在训练过程中,model仍会进行梯度回传,但是参数更新则只会发生在fc层。通过设定参数的requires_grad属性,我们完成了指定训练模型的特定层的目标,这对实现模型微调非常重要。

模型微调-timm

除了使用torchvision.models进行预训练以外,还有一个常见的预训练模型库,叫做timm,这个库是由来自加拿大温哥华Ross Wightman创建的。里面提供了许多计算机视觉的SOTA模型,可以当作是torchvision的扩充版本,并且里面的模型在准确度上也较高。在本章内容中,我们主要是针对这个库的预训练模型的使用做叙述,其他部分内容(数据扩增,优化器等)如果大家感兴趣,可以参考以下两个链接。

  • Github链接:https://github.com/rwightman/pytorch-image-models
  • 官网链接:https://fastai.github.io/timmdocs/ https://rwightman.github.io/pytorch-image-models/

如何查看预训练模型种类

  • 查看timm提供的预训练模型,我们可以通过timm.list_models()方法查看timm提供的预训练模型
import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)
  • 看特定模型的所有种类 每一种系列可能对应着不同方案的模型,比如Resnet系列就包括了ResNet18,50,101等模型,我们可以在timm.list_models()传入想查询的模型名称(模糊查询),比如我们想查询densenet系列的所有模型。
all_densnet_models = timm.list_models("*densenet*")
all_densnet_models

我们发现以列表的形式返回了所有densenet系列的所有模型。

['densenet121',
 'densenet121d',
 'densenet161',
 'densenet169',
 'densenet201',
 'densenet264',
 'densenet264d_iabn',
 'densenetblur121d',
 'tv_densenet121']
  • 查看模型的具体参数 当我们想查看下模型的具体参数的时候,我们可以通过访问模型的default_cfg属性来进行查看,具体操作如下
model = timm.create_model('resnet34',num_classes=10,pretrained=True)
model.default_cfg
{'url': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/resnet34-43635321.pth',
 'num_classes': 1000,
 'input_size': (3, 224, 224),
 'pool_size': (7, 7),
 'crop_pct': 0.875,
 'interpolation': 'bilinear',
 'mean': (0.485, 0.456, 0.406),
 'std': (0.229, 0.224, 0.225),
 'first_conv': 'conv1',
 'classifier': 'fc',
 'architecture': 'resnet34'}

除此之外,我们可以通过访问这个链接查看提供的预训练模型的准确度等信息。

使用和修改预训练模型

在得到我们想要使用的预训练模型后,我们可以通过timm.create_model()的方法来进行模型的创建,我们可以通过传入参数pretrained=True,来使用预训练模型。同样的,我们也可以使用跟torchvision里面的模型一样的方法查看模型的参数,类型

import timm
import torch

model = timm.create_model('resnet34',pretrained=True)
x = torch.randn(1,3,224,224)
output = model(x)
output.shape
torch.Size([1, 1000])
  • 查看某一层模型参数(以第一层卷积为例)
model = timm.create_model('resnet34',pretrained=True)
list(dict(model.named_children())['conv1'].parameters())
[Parameter containing:
 tensor([[[[-2.9398e-02, -3.6421e-02, -2.8832e-02,  ..., -1.8349e-02,
            -6.9210e-03,  1.2127e-02],
           [-3.6199e-02, -6.0810e-02, -5.3891e-02,  ..., -4.2744e-02,
            -7.3169e-03, -1.1834e-02],
            ...
           [ 8.4563e-03, -1.7099e-02, -1.2176e-03,  ...,  7.0081e-02,
             2.9756e-02, -4.1400e-03]]]], requires_grad=True)]
  • 修改模型(将1000类改为10类输出)
model = timm.create_model('resnet34',num_classes=10,pretrained=True)
x = torch.randn(1,3,224,224)
output = model(x)
output.shape
torch.Size([1, 10])
  • 改变输入通道数(比如我们传入的图片是单通道的,但是模型需要的是三通道图片) 我们可以通过添加in_chans=1来改变
model = timm.create_model('resnet34',num_classes=10,pretrained=True,in_chans=1)
x = torch.randn(1,1,224,224)
output = model(x)

模型的保存

timm库所创建的模型是torch.model的子类,我们可以直接使用torch库中内置的模型参数保存和加载的方法,具体操作如下方代码所示

torch.save(model.state_dict(),'./checkpoint/timm_model.pth')
model.load_state_dict(torch.load('./checkpoint/timm_model.pth'))

推荐资料

  1. https://www.aiuai.cn/aifarm1967.html
  2. https://towardsdatascience.com/getting-started-with-pytorch-image-models-timm-a-practitioners-guide-4e77b4bf9055
  3. https://chowdera.com/2022/03/202203170834122729.html

半精度训练

我们提到PyTorch时候,总会想到要用硬件设备GPU的支持,也就是“卡”。GPU的性能主要分为两部分:算力和显存,前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。在可以使用的显存数量一定的情况下,每次训练能够加载的数据更多(也就是batch size更大),则也可以提高训练效率。另外,有时候数据本身也比较大(比如3D图像、视频等),显存较小的情况下可能甚至batch size为1的情况都无法实现。因此,合理使用显存也就显得十分重要。
我们观察PyTorch默认的浮点数存储方式用的是torch.float32,小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果,也就是使用torch.float16格式。由于数位减了一半,因此被称为“半精度”,具体如下图:
PyTorch(五)——PyTorch进阶训练技巧_第2张图片显然半精度能够减少显存占用,使得显卡可以同时加载更多数据进行计算。本节会介绍如何在PyTorch中设置使用半精度计算。

半精度训练的设置

在PyTorch中使用autocast配置半精度训练,同时需要在下面三处加以设置:

  • import autocast
from torch.cuda.amp import autocast
  • 模型设置

在模型定义中,使用python的装饰器方法,用autocast装饰模型中的forward函数。

@autocast()   
def forward(self, x):
    ...
    return x
  • 训练过程

在训练过程中,只需在将数据输入模型及其之后的部分放入“with autocast():“即可:

 for x in train_loader:
	x = x.cuda()
	with autocast():
        output = model(x)
        ...

半精度训练主要适用于数据本身的size比较大(比如说3D图像、视频等)。当数据本身的size并不大时(比如手写数字MNIST数据集的图片尺寸只有28*28),使用半精度训练则可能不会带来显著的提升。

数据增强-imgaug

深度学习最重要的是数据。我们需要大量数据才能避免模型的过度拟合。但是我们在许多场景无法获得大量数据。数据增强技术的存在是为了解决这个问题,这是针对有限数据问题的解决方案。数据增强有一系列简单有效的方法可供选择,有一些机器学习库来进行计算视觉领域的数据增强,比如:imgaug 官网它封装了很多数据增强算法,给开发者提供了方便。

imgaug简介和安装

imgaug是计算机视觉任务中常用的一个数据增强的包,相比于torchvision.transforms,它提供了更多的数据增强方法,因此在各种竞赛中,人们广泛使用imgaug来对数据进行增强操作。除此之外,imgaug官方还提供了许多例程让我们学习,本章内容仅是简介,希望起到抛砖引玉的功能。

  1. Github地址:imgaug
  2. Readthedocs:imgaug
  3. 官方提供notebook例程:notebook

imgaug的安装方法和其他的Python包类似,我们可以通过以下两种方式进行安装
conda

conda config --add channels conda-forge
conda install imgaug

pip

#  install imgaug either via pypi

pip install imgaug

#  install the latest version directly from github

pip install git+https://github.com/aleju/imgaug.git

imgaug的使用

imgaug仅仅提供了图像增强的一些方法,但是并未提供图像的IO操作,因此我们需要使用一些库来对图像进行导入,建议使用imageio进行读入,如果使用的是opencv进行文件读取的时候,需要进行手动改变通道,将读取的BGR图像转换为RGB图像。除此以外,当我们用PIL.Image进行读取时,因为读取的图片没有shape的属性,所以我们需要将读取到的img转换为np.array()的形式再进行处理。因此官方的例程中也是使用imageio进行图片读取。

单张图片处理

在该单元,我们仅以几种数据增强操作为例,主要目的是教会大家如何使用imgaug来对数据进行增强操作。

import imageio
import imgaug as ia
%matplotlib inline

# 图片的读取
img = imageio.imread("./Lenna.jpg")

# 使用Image进行读取
# img = Image.open("./Lenna.jpg")
# image = np.array(img)
# ia.imshow(image)

# 可视化图片
ia.imshow(img)

PyTorch(五)——PyTorch进阶训练技巧_第3张图片
现在我们已经得到了需要处理的图片,imgaug包含了许多从Augmenter继承的数据增强的操作。在这里我们以Affine为例子。

from imgaug import augmenters as iaa

# 设置随机数种子
ia.seed(4)

# 实例化方法
rotate = iaa.Affine(rotate=(-4,45))
img_aug = rotate(image=img)
ia.imshow(img_aug)

PyTorch(五)——PyTorch进阶训练技巧_第4张图片
这是对一张图片进行一种操作方式,但实际情况下,我们可能对一张图片做多种数据增强处理。这种情况下,我们就需要利用imgaug.augmenters.Sequential()来构造我们数据增强的pipline,该方法与torchvison.transforms.Compose()相类似。

iaa.Sequential(children=None, # Augmenter集合
               random_order=False, # 是否对每个batch使用不同顺序的Augmenter list
               name=None,
               deterministic=False,
               random_state=None)
# 构建处理序列
aug_seq = iaa.Sequential([
    iaa.Affine(rotate=(-25,25)),
    iaa.AdditiveGaussianNoise(scale=(10,60)),
    iaa.Crop(percent=(0,0.2))
])
# 对图片进行处理,image不可以省略,也不能写成images
image_aug = aug_seq(image=img)
ia.imshow(image_aug)

PyTorch(五)——PyTorch进阶训练技巧_第5张图片
总的来说,对单张图片处理的方式基本相同,我们可以根据实际需求,选择合适的数据增强方法来对数据进行处理。

对批次图片进行处理

在实际使用中,我们通常需要处理更多份的图像数据。此时,可以将图形数据按照NHWC的形式或者由列表组成的HWC的形式对批量的图像进行处理。主要分为以下两部分,对批次的图片以同一种方式处理和对批次的图片进行分部分处理。
对批次的图片以同一种方式处理
对一批次的图片进行处理时,我们只需要将待处理的图片放在一个list中,并将image改为image即可进行数据增强操作,具体实际操作如下:

images = [img,img,img,img,]
images_aug = rotate(images=images)
ia.imshow(np.hstack(images_aug))

PyTorch(五)——PyTorch进阶训练技巧_第6张图片
在上述的例子中,我们仅仅对图片进行了仿射变换,同样的,我们也可以对批次的图片使用多种增强方法,与单张图片的方法类似,我们同样需要借助Sequential来构造数据增强的pipline。

aug_seq = iaa.Sequential([
    iaa.Affine(rotate=(-25, 25)),
    iaa.AdditiveGaussianNoise(scale=(10, 60)),
    iaa.Crop(percent=(0, 0.2))
])

# 传入时需要指明是images参数
images_aug = aug_seq.augment_images(images = images)
#images_aug = aug_seq(images = images) 
ia.imshow(np.hstack(images_aug))


对批次的图片分部分处理
imgaug相较于其他的数据增强的库,有一个很有意思的特性,即就是我们可以通过imgaug.augmenters.Sometimes()对batch中的一部分图片应用一部分Augmenters,剩下的图片应用另外的Augmenters。

iaa.Sometimes(p=0.5,  # 代表划分比例
              then_list=None,  # Augmenter集合。p概率的图片进行变换的Augmenters。
              else_list=None,  #1-p概率的图片会被进行变换的Augmenters。注意变换的图片应用的Augmenter只能是then_list或者else_list中的一个。
              name=None,
              deterministic=False,
              random_state=None)

对不同大小的图片进行处理

上面提到的图片都是基于相同的图像。以下的示例具有不同图像大小的情况,我们从维基百科加载三张图片,将它们作为一个批次进行扩充,然后一张一张地显示每张图片。

# 构建pipline
seq = iaa.Sequential([
    iaa.CropAndPad(percent=(-0.2, 0.2), pad_mode="edge"),  # crop and pad images
    iaa.AddToHueAndSaturation((-60, 60)),  # change their color
    iaa.ElasticTransformation(alpha=90, sigma=9),  # water-like effect
    iaa.Cutout()  # replace one squared area within the image by a constant intensity value
], random_order=True)

# 加载不同大小的图片
images_different_sizes = [
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/e/ed/BRACHYLAGUS_IDAHOENSIS.jpg"),
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/c/c9/Southern_swamp_rabbit_baby.jpg"),
    imageio.imread("https://upload.wikimedia.org/wikipedia/commons/9/9f/Lower_Keys_marsh_rabbit.jpg")
]

# 对图片进行增强
images_aug = seq(images=images_different_sizes)

# 可视化结果
print("Image 0 (input shape: %s, output shape: %s)" % (images_different_sizes[0].shape, images_aug[0].shape))
ia.imshow(np.hstack([images_different_sizes[0], images_aug[0]]))

print("Image 1 (input shape: %s, output shape: %s)" % (images_different_sizes[1].shape, images_aug[1].shape))
ia.imshow(np.hstack([images_different_sizes[1], images_aug[1]]))

print("Image 2 (input shape: %s, output shape: %s)" % (images_different_sizes[2].shape, images_aug[2].shape))
ia.imshow(np.hstack([images_different_sizes[2], images_aug[2]]))

Image 0 (input shape: (257, 286, 3), output shape: (257, 286, 3))
PyTorch(五)——PyTorch进阶训练技巧_第7张图片
Image 1 (input shape: (536, 800, 3), output shape: (536, 800, 3))
PyTorch(五)——PyTorch进阶训练技巧_第8张图片
Image 2 (input shape: (289, 520, 3), output shape: (289, 520, 3))
PyTorch(五)——PyTorch进阶训练技巧_第9张图片

imgaug在PyTorch的应用

关于PyTorch中如何使用imgaug每一个人的模板是不一样的,我在这里也仅仅给出imgaug的issue里面提出的一种解决方案,大家可以根据自己的实际需求进行改变。 具体链接:how to use imgaug with pytorch

import numpy as np
from imgaug import augmenters as iaa
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms

# 构建pipline
tfs = transforms.Compose([
    iaa.Sequential([
        iaa.flip.Fliplr(p=0.5),
        iaa.flip.Flipud(p=0.5),
        iaa.GaussianBlur(sigma=(0.0, 0.1)),
        iaa.MultiplyBrightness(mul=(0.65, 1.35)),
    ]).augment_image,
    # 不要忘记了使用ToTensor()
    transforms.ToTensor()
])

# 自定义数据集
class CustomDataset(Dataset):
    def __init__(self, n_images, n_classes, transform=None):
		# 图片的读取,建议使用imageio
        self.images = np.random.randint(0, 255,
                                        (n_images, 224, 224, 3),
                                        dtype=np.uint8)
        self.targets = np.random.randn(n_images, n_classes)
        self.transform = transform

    def __getitem__(self, item):
        image = self.images[item]
        target = self.targets[item]

        if self.transform:
            image = self.transform(image)

        return image, target

    def __len__(self):
        return len(self.images)


def worker_init_fn(worker_id):
    imgaug.seed(np.random.get_state()[1][0] + worker_id)


custom_ds = CustomDataset(n_images=50, n_classes=10, transform=tfs)
custom_dl = DataLoader(custom_ds, batch_size=64,
                       num_workers=4, pin_memory=True, 
                       worker_init_fn=worker_init_fn)

关于num_workers在Windows系统上只能设置成0,但是当我们使用Linux远程服务器时,可能使用不同的num_workers的数量,这是我们就需要注意worker_init_fn()函数的作用了。它保证了我们使用的数据增强在num_workers>0时是对数据的增强是随机的。

总结

数据扩充是我们需要掌握的基本技能,除了imgaug以外,我们还可以去学习其他的数据增强库,包括但不局限于Albumentations,Augmentor。除去imgaug以外,我还强烈建议大家学下Albumentations,因为Albumentations跟imgaug都有着丰富的教程资源,大家可以有需求访问Albumentations教程。
扩展资料:

  1. kaggle-data-augmentation-packages-overview
  2. how to use imgaug with pytorch
  3. Kaggle知识点:数据扩增方法
  4. PyTorch Classification Model Based On Imgaug.
  5. Tutorial Notebooks

使用argparse进行调参

argparse简介

在深度学习中时,超参数的修改和保存是非常重要的一步,尤其是当我们在服务器上跑我们的模型时,如何更方便的修改超参数是我们需要考虑的一个问题。这时候,要是有一个库或者函数可以解析我们输入的命令行参数再传入模型的超参数中该多好。到底有没有这样的一种方法呢?答案是肯定的,这个就是 Python 标准库的一部分:Argparse。
argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直接在命令行中就可以向程序中传入参数。我们可以使用python file.py来运行python文件。而argparse的作用就是将命令行传入的其他参数进行解析、保存和使用。在使用argparse后,我们在命令行输入的参数就可以以这种形式python file.py --lr 1e-4 --batch_size 32来完成对常见超参数的设置。

argparse的使用

总的来说,我们可以将argparse的使用归纳为以下三个步骤。

  • 创建ArgumentParser()对象
  • 调用add_argument()方法添加参数
  • 使用parse_args()解析参数
# demo.py
import argparse

# 创建ArgumentParser()对象
parser = argparse.ArgumentParser()

# 添加参数
parser.add_argument('-o', '--output', action='store_true', 
    help="shows output")
# action = `store_true` 会将output参数记录为True
# type 规定了参数的格式
# default 规定了默认值
parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=3e-5') 

parser.add_argument('--batch_size', type=int, required=True, help='input batch size')  
# 使用parse_args()解析函数
args = parser.parse_args()

if args.output:
    print("This is some output")
    print(f"learning rate:{args.lr} ")

我们在命令行使用python demo.py --lr 3e-4 --batch_size 32,就可以看到以下的输出

This is some output
learning rate: 3e-4

argparse的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr参数相类似,未输入的情况下会设置为默认值。必选参数就跟我们的batch_size参数相类似,当我们给参数设置required =True后,我们就必须传入该参数,否则就会报错。看到我们的输入格式后,我们可能会有这样一个疑问,我输入参数的时候不使用–可以吗?答案是肯定的,不过我们需要在设置上做出一些改变。

# positional.py
import argparse

# 位置参数
parser = argparse.ArgumentParser()

parser.add_argument('name')
parser.add_argument('age')

args = parser.parse_args()

print(f'{args.name} is {args.age} years old')

当我们不使用–后,将会严格按照参数位置进行解析。

$ positional_arg.py Peter 23
Peter is 23 years old

总的来说,argparse的使用很简单,以上这些操作就可以帮助我们进行参数的修改,在下面的部分,我将会分享我是如何在模型训练中使用argparse进行超参数的修改。

更加高效使用argparse修改超参数

每个人都有着不同的超参数管理方式,在这里我将分享我使用argparse管理超参数的方式,希望可以对大家有一些借鉴意义。通常情况下,为了使代码更加简洁和模块化,我一般会将有关超参数的操作写在config.py,然后在train.py或者其他文件导入就可以。具体的config.py可以参考如下内容。

import argparse  
  
def get_options(parser=argparse.ArgumentParser()):  
  
    parser.add_argument('--workers', type=int, default=0,  
                        help='number of data loading workers, you had better put it '  
                              '4 times of your gpu')  
  
    parser.add_argument('--batch_size', type=int, default=4, help='input batch size, default=64')  
  
    parser.add_argument('--niter', type=int, default=10, help='number of epochs to train for, default=10')  
  
    parser.add_argument('--lr', type=float, default=3e-5, help='select the learning rate, default=1e-3')  
  
    parser.add_argument('--seed', type=int, default=118, help="random seed")  
  
    parser.add_argument('--cuda', action='store_true', default=True, help='enables cuda')  
    parser.add_argument('--checkpoint_path',type=str,default='',  
                        help='Path to load a previous trained model if not empty (default empty)')  
    parser.add_argument('--output',action='store_true',default=True,help="shows output")  
  
    opt = parser.parse_args()  
  
    if opt.output:  
        print(f'num_workers: {opt.workers}')  
        print(f'batch_size: {opt.batch_size}')  
        print(f'epochs (niters) : {opt.niter}')  
        print(f'learning rate : {opt.lr}')  
        print(f'manual_seed: {opt.seed}')  
        print(f'cuda enable: {opt.cuda}')  
        print(f'checkpoint_path: {opt.checkpoint_path}')  
  
    return opt  
  
if __name__ == '__main__':  
    opt = get_options()
$ python config.py

num_workers: 0
batch_size: 4
epochs (niters) : 10
learning rate : 3e-05
manual_seed: 118
cuda enable: True
checkpoint_path:

随后在train.py等其他文件,我们就可以使用下面的这样的结构来调用参数。

# 导入必要库
...
import config

opt = config.get_options()

manual_seed = opt.seed
num_workers = opt.workers
batch_size = opt.batch_size
lr = opt.lr
niters = opt.niters
checkpoint_path = opt.checkpoint_path

# 随机数的设置,保证复现结果
def set_seed(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    random.seed(seed)
    np.random.seed(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True

...


if __name__ == '__main__':
	set_seed(manual_seed)
	for epoch in range(niters):
		train(model,lr,batch_size,num_workers,checkpoint_path)
		val(model,lr,batch_size,num_workers,checkpoint_path)

总结

argparse给我们提供了一种新的更加便捷的方式,在后面我们将结合其他Python标准库(pickle,json,logging)实现参数的保存和模型输出的记录。如果大家还想进一步的了解argparse的使用,大家可以点击下面提供的连接进行更深的学习和了解。

  1. Python argparse 教程
  2. argparse 官方教程

- PyTorch模型定义与进阶训练技巧

你可能感兴趣的:(Pytorch,pytorch,深度学习,python)