PyTorch进阶练习技巧

我报名参加金石方案1期挑战——瓜分10万奖池,这是我的第9篇文章,点击检查活动详情

1.自界说丢失函数

1.1 以函数办法界说

事实上,丢失函数仅仅是一个函数罢了,因而咱们能够经过直接以函数界说的办法界说一个自己的函数,如下所示:

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

1.2 以类办法界说

虽然以函数界说的办法很简略,可是以类办法界说愈加常用,在以类办法界说丢失函数时,咱们如果看每一个丢失函数的承继关系咱们就能够发现Loss函数部分承继自_loss, 部分承继自_WeightedLoss, 而_WeightedLoss承继自_loss _loss承继自 nn.Module。咱们能够将其当作神经网络的一层来对待,同样地,咱们的丢失函数类就需求承继自nn.Module类,鄙人面的比方中咱们以DiceLoss为例向大家叙述。

Dice Loss是一种在分割领域常见的丢失函数,界说如下:

DSC=2∣X∩Y∣∣X∣+∣Y∣DSC = \frac{2|X∩Y|}{|X|+|Y|}

完成代码如下:

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)

2 动态调整学习率

学习率的挑选是深度学习中一个困扰人们良久的问题,学习速率设置过小,会极大降低收敛速度,增加练习时刻;学习率太大,或许导致参数在最优解两边来回振动。可是当咱们选定了一个合适的学习率后,经过许多轮的练习后,或许会呈现准确率震荡或loss不再下降等状况,阐明当时学习率已不能满足模型调优的需求。此时咱们就能够经过一个恰当的学习率衰减战略来改进这种现象,进步咱们的精度。这种设置办法在PyTorch中被称为scheduler,也是咱们本节所研究的目标。

经过本节的学习,你将收成:

  • 怎么依据需求选取已有的学习率调整战略
  • 怎么自界说设置学习调整战略并完成

2.1 运用官方scheduler

  • 了解官方供给的API

在练习神经网络的进程中,学习率是最重要的超参数之一,作为当时较为盛行的深度学习框架,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
  • 运用官方API

关于怎么运用这些动态调整学习率的战略,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()后面进行运用。

2.2 自界说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)

3 模型微调 – timm

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

  • Github链接:github.com/rwightman/p…
  • 官网链接:fastai.github.io/timmdocs/
    rwightman.github.io/pytorch-ima…

3.1运用和修正预练习模型

在得到咱们想要运用的预练习模型后,咱们能够经过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)

3.2模型的保存

timm库所创立的模型是torch.model的子类,咱们能够直接运用torch库中内置的模型参数保存和加载的办法,详细操作如下方代码所示

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

4.模型微调-torchvision

跟着深度学习的开展,模型的参数越来越大,许多开源模型都是在较大数据集上进行练习的,比方Imagenet-1k,Imagenet-11k,乃至是ImageNet-21k等。但在实际运用中,咱们的数据集或许只要几千张,这时从头开始练习具有几千万参数的大型神经网络是不现实的,因为越大的模型对数据量的要求越大,过拟合无法避免。

  • 实例化网络
    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()
  • 传递pretrained参数

经过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,然后手动下载,预练习模型的权重在Linux和Mac的默许下载途径是用户根目录下的.cache文件夹。在Windows下便是C:\Users<username>.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’))
  4. 如果中途强行中止下载的话,一定要去对应途径下将权重文件删除干净,要不然或许会报错。

6.3.3 练习特定层

在默许状况下,参数的特点.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特点,咱们完成了指定练习模型的特定层的目标,这对完成模型微调非常重要。

5.半精度练习

咱们说到PyTorch时分,总会想到要用硬件设备GPU的支持,也便是“卡”。GPU的性能首要分为两部分:算力和显存,前者决议了显卡核算的速度,后者则决议了显卡能够一起放入多少数据用于核算。在能够运用的显存数量一定的状况下,每次练习能够加载的数据更多(也便是batch size更大),则也能够进步练习功率。别的,有时分数据本身也比较大(比方3D图画、视频等),显存较小的状况下或许乃至batch size为1的状况都无法完成。因而,合理运用显存也就显得十分重要。

们观察PyTorch默许的浮点数存储办法用的是torch.float32,小数点后位数更多固然能确保数据的准确性,但绝大多数场景其实并不需求这么准确,只保留一半的信息也不会影响成果,也便是运用torch.float16格局。因为数位减了一半,因而被称为“半精度”。

在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)
        ...

6.数据增强

  • imgaug的简介和装置
  • 运用imgaug对数据进行增强

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时是对数据的增强是随机的。

7. 运用argparse进行调参

在深度学习中时,超参数的修正和保存是非常重要的一步,尤其是当咱们在服务器上跑咱们的模型时,怎么更便利的修正超参数是咱们需求考虑的一个问题。这时分,要是有一个库或者函数能够解析咱们输入的命令行参数再传入模型的超参数中该多好。究竟有没有这样的一种办法呢?答案是必定的,这个便是 Python 规范库的一部分:Argparse。那么下面让咱们看看他是多么便利。经过本节课,您将会收成以下内容

  • argparse的简介
  • argparse的运用
  • 怎么运用argparse修正超参数

7.1 argparse简介

argsparse是python的命令行解析的规范模块,内置于python,不需求装置。这个库能够让咱们直接在命令行中就能够向程序中传入参数。咱们能够运用python file.py来运转python文件。而argparse的作用便是将命令行传入的其他参数进行解析、保存和运用。在运用argparse后,咱们在命令行输入的参数就能够以这种形式python file.py --lr 1e-4 --batch_size 32来完成对常见超参数的设置。

7.2 argparse的运用

总的来说,咱们能够将argparse的运用概括为以下三个步骤。

  • 创立ArgumentParser()目标
  • 调用add_argument()办法增加参数
  • 运用parse_args()解析参数
    在接下来的内容中,咱们将以实际操作来学习argparse的运用办法。
# 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=1e-3') 
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} ")

7.3 愈加高效运用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()
# 导入必要库
...
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)