PyTorch学习笔记(七)进阶训练技巧

目录​​​​​​​

自定义损失函数

以函数方式定义

以类方式定义

动态调整学习率

使用官方scheduler

自定义scheduler

模型微调-torchvision

模型微调的流程

使用已有模型结构

训练特定层

模型微调-timm

使用和修改预训练模型

模型的保存

半精度训练

使用argparse进行调参

argparse简介

argparse的使用

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

总结


自定义损失函数

以函数方式定义

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

以类方式定义

Dice Loss是一种在分割领域常见的损失函数,定义如下:

DSC = \frac{2\left | X\cap Y \right |}{\left | X \right |+\left | Y \right |}

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)

更多的损失函数参考 Loss Function Library - Keras & PyTorch | Kaggle

注:在自定义损失函数时,涉及到数学运算时,最好使用Pytorch提供的张量计算接口,这样就不需要实现自动求导功能并且直接调用cuda。

动态调整学习率

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

使用官方scheduler

  • 了解官方提供的API

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

  1. lr_scheduler.LambdaLR LambdaLR — PyTorch 1.12 documentation
  2. lr_scheduler.MultiplicativeLR MultiplicativeLR — PyTorch 1.12 documentation
  3. lr_scheduler.StepLR StepLR — PyTorch 1.12 documentation
  4. lr_scheduler.MultiStepLR MultiStepLR — PyTorch 1.12 documentation
  5. lr_scheduler.ExponentialLR ExponentialLR — PyTorch 1.12 documentation
  6. lr_scheduler.CosineAnnealingLR CosineAnnealingLR — PyTorch 1.12 documentation
  7. lr_scheduler.ReduceLROnPlateau ReduceLROnPlateau — PyTorch 1.12 documentation
  8. lr_scheduler.CyclicLR CyclicLR — PyTorch 1.12 documentation
  9. lr_scheduler.OneCycleLR OneCycleLR — PyTorch 1.12 documentation
  10. lr_scheduler.CosineAnnealingWarmRestartsCosineAnnealingWarmRestarts — PyTorch 1.12 documentation
  • 使用官方API
#选择一种优化器
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

我们的方法是自定义函数adjust_learning_rate来改变param_group中lr的值。

简单示例:需要学习率每30轮下降为原来的1/10,需要自定义函数来实现学习率的改变。

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

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

模型微调的流程

  1. 在源数据集上预训练一个神经网络模型,即源模型;
  2. 创建一个新的神经网络模型,即目标模型;
  3. 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数;
  4. 在目标数据集上训练目标模型。将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

使用已有模型结构

  • 实例化网络
import torchvision.models as models
resnet18 = models.resnet18()
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. 模型的扩展为.pt或.pth。
  2. 可以通过使用torch.utils.model_zoo.load_url()设置权重的下载地址。
  3. 可以将权重下载下来放到同文件夹下,然后再将参数加载网络。
  4. 如果中途强行停止下载的话,一定要去对应路径下将权重文件删除干净,要不然可能会报错。
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))

训练特定层

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

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

github链接:GitHub - rwightman/pytorch-image-models: PyTorch image models, scripts, pretrained weights -- ResNet, ResNeXT, EfficientNet, EfficientNetV2, NFNet, Vision Transformer, MixNet, MobileNet-V3/V2, RegNet, DPN, CSPNet, and more

官网链接:

https://fastai.github.io/timmdocs/

https://rwightman.github.io/pytorch-image-models/

timm的安装:pip install timm

如何查看预训练模型种类

  • 查看timm提供的预训练模型

timm提供的预训练模型已经达到了592个,我们可以通过timm.list_models()方法查看timm提供的预训练模型。

import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)
  • 查看特定模型的所有种类

每一种系列可能对应着不同方案的模型,比如ResNet系列就包括了ResNet18等,可以在timm.list_models()传入想查询的模型特征(模糊查询)。

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, 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())
  • 修改模型(将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'))

半精度训练

GPU的性能主要分为两部分:算力和显存,前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。在可以使用的显存数量一定的情况下,每次训练能够加载的数据更多(batch_size更大),则可以提高训练效率。因此,合理使用显存也就显得十分重要。

观察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)
    ...

注:半精度训练主要适用于数据本身的size比较大;当数据本身的size并不大时,使用半精度训练则可能不会带来显著的提升。

使用argparse进行调参

argparse简介

argparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以直接在命令行中就可以向程序中传入参数。argparse的作用是将命令行传入的其他参数进行解析、保存和使用。在使用argparse后,在命令行输入的参数就可以以python file.py --lr 1e-4 --batch_size 32来完成对常见超参数的设置。

argparse的使用

总的来说,可以将argparse的使用归纳为三个步骤:

  • 创建ArgumentParser()对象
  • 调用add_argument()方法添加参数
  • 使用parse_args()解析参数
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}')

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

This is some output
learning rate: 3e-4

argparse的参数主要可以分为可选参数和必选参数。可选参数在未输入的情况下会设置为默认值。必选参数跟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修改超参数

通常为了使代码更加简洁和模块化,一般会将有关超参数的操作写在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'cuds 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)实现参数的保存和模型输出的记录。

参考链接:第六章:PyTorch进阶训练技巧 — 深入浅出PyTorch

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值