【深入浅出pytorch-task4】-第六章学习笔记-pytorch训练进阶方法

学习网站在此:https://datawhalechina.github.io/thorough-pytorch/

以下是对知识的记录(都记下来)

一、自定义损失函数

许多损失函数并未出现在官方库中,需要我们自己来实现,尤其在日新月异的ai领域,模型与相关算法的更新很快,同时,在科学研究领域,当提出全新的损失函数时,这也要求我们自行定义,以验证其在数据集上的好坏

1.1函数方式

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

1.2 类方式

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

只需要init部分以及forward部分就可以定义一个损失函数,来比较inputs和targets
涉及数学运算的方面最好使用pytorch提供的接口,可以自动求导,十分方便

二、动态调整学习率

我们设置在训练过程中视情况进行增加或减小学习率,使得学习率达到不同时刻的最优学习率,从而提高模型训练的精度,这种方式叫做scheduler

2.1使用官方scheduler

torch.optim.lr_scheduler中包含多种动态调整学习率的方法

代码注释也比较完整,如下

# 选择一种优化器
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()
    # 调整完学习率之后,进入下一轮训练

2.2 自定义scheduler

假设我们现在正在做实验,需要学习率每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

三、模型微调

3.1 微调是啥

利用已经训练好的模型,相当于在该领域其他方向已经非常牛的“大佬”,再来学习一个新的方向,会比小白从头学习这个新的方向要强,利用这个思想,我们只需要对“大佬”的方向思想进行一些细微的调整(可能以前做床识别,现在做椅子识别),就可以适用于这个方向,我们对“大佬”喂入一些“作业”来改变一些参数,这比重新训练要快的多,同时大大减少了数据量的需求(”大佬“在其他领域已经把”数学基础“搞得很牢实了,来这里自然不需要再通过大量实例来推出背后的潜在道理,一点点实例就足够”大佬“入门了)

流程如下

我们把原模型的参数拿过来,只在输出层进行重新训练(毕竟下游任务不可能完全一样)

这样我们直接训练特定层

3.1 使用torchvision提供的模型

首先要冻结梯度,因为我们不想改变已经训练好的参数,只想训练特定层

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
# 定义线性层,输入是原模型倒数第二层的输出,输出改为4
model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)

3.2 使用timm库中的SOTA模型

还有一个常见的预训练模型库,叫做timm,这个库是由来自加拿大温哥华Ross Wightman创建的。里面提供了许多计算机视觉的SOTA模型,可以当作是torchvision的扩充版本,并且里面的模型在准确度上也较高

3.2.1 安装

通过

pip install timm

或者

git clone https://github.com/rwightman/pytorch-image-models
cd pytorch-image-models && pip install -e .

安装

3.2.2 查看所有预训练模型

import timm
all_models = timm.list_models(pretrained=True)

3.2.3 模糊查询

 timm.list_models("*densenet*")

返回所有带有关键字"densenet"模型的列表

3.2.4 查看模型具体参数

# 此处定义num_classes就是定义了输出为10,如果你有一个100分类的任务,那么不妨将它设置为100
model = timm.create_model('resnet34',num_classes=10,pretrained=True)

model.default_cfg

3.2.5使用、修改预训练模型

import timm
import torch
# 通过create_model 创建模型,设置pretrained=True来获取预训练好的权重参数,否则只会传给你一个模型
model = timm.create_model('resnet34'
						  ,num_classes=10# 将1000分类修改为10分类
						  ,pretrained=True
						  # 修改图片的传入时的通道数,加入你的图片只有单通道,那么设置为1
						  ,in_chans=1)
x = torch.randn(1,3,224,224)
output = model(x)
output.shape

3.2.6 模型保存与加载读取权重参数

# 保存 model.state_dict()是model的权重参数,保存到pth文件中
torch.save(model.state_dict(),'./checkpoint/timm_model.pth')
# 读取参数到model中
model.load_state_dict(torch.load('./checkpoint/timm_model.pth'))

四、半精度训练

PyTorch默认的浮点数存储方式用的是torch.float32,小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果
这对于高分辨率的照片来说,大大减少了显存的占用,使得我们能够增加bs,提高训练效率

代码

# 导入autocast
from torch.cuda.amp import autocast

模型设置,使用了python装饰器


@autocast()   
def forward(self, x):
    ...
    return x

只需在训练过程中,把数据输入模型之后的部分放在with autocast后面即可

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

五、数据增强imgaug

5.1 安装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

5.2 读取图片

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)

5.3 使用数据增强的方法

Affine,即仿射变换。 将图像旋转-45°和+ 45°之间的随机值

from imgaug import augmenters as iaa
# 实例化方法
rotate = iaa.Affine(rotate=(-45,45))
img_aug = rotate(image=img)
ia.imshow(img_aug)

5.4 组合数据增强(使用多种方法)

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

# 构建处理序列
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)

5.5 对批量图片进行处理

# 将待处理图片放入一个list中
images = [img,img,img,img,]
images_aug = rotate(images=images)
ia.imshow(np.hstack(images_aug))

5.6 对批次的图片分部分处理

我们可以对一个批次的部分图片进行a处理,其余部分进行b处理

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)

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

上面提到的图片都是基于相同的图像。以下的示例具有不同图像大小的情况,我们从维基百科加载三张图片,将它们作为一个批次进行扩充,然后一张一张地显示每张图片。具体的操作跟单张的图片都是十分相似,因此不做过多赘述

# 构建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]]))

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

六、使用argparse进行调参

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

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

  • 创建ArgumentParser()对象
  • 调用add_argument()方法添加参数
  • 使用parse_args()解析参数

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

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)

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