PyTorch:进阶训练技巧

1. 自定义损失函数

torch.nn 模块中提供了许多常用的损失函数,比如:MSELoss,L1Loss,BCELoss等。

然而,随着深度学习的发展,出现了越来越多的非官方提供的 Loss,比如 DiceLoss,HuberLoss,SobolevLoss等。这些Loss Function专门针对一些非通用的模型,可能没必要也难以及时被 PyTorch 全部添加到库中。另外,在科学研究中,我们往往会提出全新的损失函数来提升模型的表现,比如增加一些惩罚项来避免过拟合问题。

因此,我们需要学会如何自定义损失函数。

我们可以通过直接以函数定义的方式定义一个自己的函数,如下所示:

def my_loss(output, target):
    loss = torch.mean((output - target)**2)  # MSE(均方误差)
    return loss

虽然以函数定义的方式很简单,但是以类方式定义更加常用。DiceLoss 的实现代码如下所示:

class DiceLoss(nn.Module):
    def __init__(self,weight=None,size_average=True):
        super(DiceLoss,self).__init__()
        # weight: 给予每个类别不同的权重,默认是相同权重,若赋值,则 shape 要与 targets 一致
        # size_average: 是否要对 loss 求平均
        
    def forward(self,inputs,targets,smooth=1):
        inputs = F.sigmoid(inputs)   #离散化为 0 或 1    
        inputs = inputs.view(-1)   # 平铺为向量
        targets = targets.view(-1)   # 平铺为向量
        intersection = (inputs * targets).sum()   # 计算交集,越大越好                
        dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)  # smooth 避免分子和分母为 0 的情况
        return 1 - dice

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

注:在自定义损失函数时,我们最好全程使用 PyTorch 提供的张量计算接口,这样就不需要我们实现自动求导功能。

2. 动态调整学习率

学习率的选择是深度学习中一个困扰人们许久的问题,学习速率设置过小,会极大降低收敛速度,增加训练时间;学习率太大,可能导致参数在最优解两侧来回振荡。

一般来说,我们希望在训练初期学习率大一些,使得网络收敛迅速,在训练后期学习率小一些,使得网络更好的收敛到最优解。我们可以通过一个适当的学习率调整方法来完成这个任务。

2.1 Optimizer 机制

每个 Optimizer 都维护一个 param_groups 的 list,该 list 中维护需要优化的参数以及对应的属性设置,结构如下:

-param_groups    
    -0(dict)  # 第一组参数        
        params:  # 维护要更新的参数        
        lr:  # 该组参数的学习率        
        betas:        
        eps:  # 该组参数的学习率最小值        
        weight_decay:  # 该组参数的权重衰减系数        
        amsgrad:      
    -1(dict)  # 第二组参数    
    -2(dict)  # 第三组参数    
    ...

Optimizer 是用来更新网络参数的,而 Optimizer 中的学习率 lr 值会影响参数更新后的值。也就是说,通过修改 Optimizer 中各个参数的 lr,可以影响训练的快慢,也可以对不同的参数设置不同的学习率,甚至只更新一部分参数。

我们需要新的一种方法,能像 Optimizer 更新 model 的参数一样,去更新 Optimizer 的属性,比如 lr。这个方法在 PyTorch 中被称为 scheduler。

2.2 使用官方 scheduler

PyTorch 已经在 torch.optim.lr_scheduler 封装好了一些动态调整学习率的方法供我们使用,下面是最常用的四种:

  • StepLR:学习率每隔一定步数(或者epoch)就减少为原来的gamma分之一
  • MultiStepLR:按照非固定的步长更新学习率
  • ExponentialLR:学习率按照指数的形式衰减,参数gamma表示衰减的底数
  • CosineAnnealingLR:学习率按照周期变化,并不是衰减

以指数衰减策略为例,实现代码如下:

import torch.optim.Adamimport torch.optim.SGD

class Net(nn.Module): 
  ...

model = Net()  # 待训练的模型
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)  # 优化器定义,绑定待训练的模型
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.98)  # 更新器定义,绑定优化器

# 调用顺序
loss.backward()  # 反向传播,计算梯度
optimizer.step()  # 根据梯度和学习率等更新参数
scheduler.step()  # 更新优化器中的学习率(一般根据 epoch 来调整)
optimizer.zero_grad()  # 将梯度归零(并不会更改已经更新的学习率)

2.3 自定义 scheduler

在实验中可能碰到需要我们自己定义学习率调整策略的情况:自定义函数 adjust_learning_rate 来改变 param_group 中 lr 的值,如下所示:

def adjust_learning_rate(optimizer, epoch, args.lr):
    lr = args.lr * (0.1 ** (epoch // 30))  # 学习率每 30 轮下降为原来的 1/10; 这行代码是最关键的,可以自定义
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

args.lr = 0.0005
optimizer = torch.optim.SGD(model.parameters(), lr = args.lr ,momentum = 0.9) # 赋值,初始化

for epoch in range(100):
    train(...)
    validate(...)
    adjust_learning_rate(optimizer, epoch, args.lr)  # 更新学习率

3. 模型微调(Finetune)

我们可以先找到一个同类的训练好的模型,然后换成自己的数据通过训练调整一下参数,这样可以方便我们快速完成自己的任务。

在PyTorch中提供了许多预训练好的网络模型(VGG,ResNet系列,mobilenet系列等),这些模型都是PyTorch官方在相应的大型数据集训练好的。

3.1 模型微调的流程

  1. 在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
  2. 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
  3. 为目标模型添加一个输出⼤小为⽬标数据集类别个数的输出层,并随机初始化该层的模型参数。
  4. 在目标数据集上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。

3.2 使用已有模型结构

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

import torchvision.models as models  # 有很多预训练模型可供选择

resnet18 = models.resnet18()
# 使用 pretrained 来决定是否使用预训练好的权重,默认状态下为 False
# 若模型或模型权重的下载速度慢,可手动下载并手动载入,使用 model.load_state_dict()

3.3 训练特定层

在默认情况下,参数的属性.requires_grad = True。如果我们只想为新初始化的层计算梯度,其他参数不进行改变,那我们需要通过设置requires_grad = False来冻结部分层。PyTorch官方中提供了这样一个例程:

# 当 feature_extracting 为 TRUE 时,冻结所有层的参数更新
def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

下面我们将输出改为 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 为 True。

除了使用torchvision.models进行预训练以外,还有一个常见的预训练模型库,叫做 timm。timm 提供了许多计算机视觉的SOTA模型,可以当作是torchvision的扩充版本,并且里面的模型在准确度上也较高。

4. 数据增强

深度学习最重要的是数据。我们需要大量数据才能避免模型的过度拟合。但是我们在许多场景无法获得大量数据,例如医学图像分析。

数据增强技术的存在是为了解决这个问题,可提高训练数据集的大小和质量,以便我们可以使用它们来构建更好的深度学习模型。

本节以 imgaug 库为例,介绍图像数据的增强算法。

4.1 图像数据读取

import imageio
import imgaug as ia
%matplotlib inline

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

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

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

4.2 单张图像单种处理

from imgaug import augmenters as iaa

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

# 实例化方法
rotate = iaa.Affine(rotate=(-4,45)) # 仿射变换(Affine),可以达到旋转的效果,从-4到45随机选择一个旋转的角度
img_aug = rotate(image=img) # image不可以省略,也不能写成images(因为是单张图像)
ia.imshow(img_aug)

iaa 还有很多种图像增强的方法可供使用,使用方法基本一样,注意下每个方法的参数即可。

4.3 单张图像多种处理

实际情况下,我们可能对一张图片做多种数据增强处理。这种情况下,我们就需要利用 imgaug.augmenters.Sequential() 来构造我们数据增强的 pipline

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))
]) # 只输入了 children 参数

image_aug = aug_seq(image=img)
ia.imshow(image_aug)

总的来说,对单张图片处理的方式基本相同,我们可以根据实际需求,选择合适的数据增强方法来对数据进行处理。

4.4 多张图像的处理

只需要将待处理的图片放在一个list中,并将 image 改为 images 即可进行数据增强操作,具体实际操作如下:

import numpy as np

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

同样的,我们也可以对批次的图片使用多种增强方法,与单张图片的方法类似。

images_aug = aug_seq(images=images)
ia.imshow(np.hstack(images_aug))

我们还可以通过 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)

4.5 imgaug在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)

custom_ds = CustomDataset(n_images=50, n_classes=10, transform=tfs)

5. 使用argparse进行调参

在深度学习中时,超参数的修改和保存是非常重要的一步,尤其是当我们在服务器上跑我们的模型时。

argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直接在命令行中就可以向程序中传入参数。

我们可以使用 python file.py 来运行python文件。而argparse的作用就是将命令行传入的其他参数进行解析、保存和使用。在使用argparse后,我们在命令行输入的参数就可以以 python file.py --lr 1e-4 --batch_size 32 这种形式来完成对常见超参数的设置。

5.1 argparse 的使用

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

  1. 创建ArgumentParser()对象
  2. 调用add_argument()方法添加参数
  3. 使用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=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的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr参数相类似,未输入的情况下会设置为默认值。必选参数就跟我们的batch_size参数相类似,当我们给参数设置required =True后,我们就必须传入该参数,否则就会报错。

5.2 一种值得参考的使用方式

将有关超参数的操作写在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)

参考资料

posted @   Junwei_Kuang  阅读(465)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示