[PyTorch] 5 种高级训练技术(损失函数、学习率、模型微调、半精度训练、数据增强、超参数设置)
PyTorch:5-进阶训练技巧
注:所有资料来源且归属于thorough-pytorch(https://datawhalechina.github.io/thorough-pytorch/),下文仅为学习记录
5.1:自定义损失函数
PyTorch在torch.nn模块提供了许多常用的损失函数,比如:MSELoss,L1Loss,BCELoss
非官方提供的Loss,比如:DiceLoss,HuberLoss,SobolevLoss
5.1.1:以函数方式定义
损失函数仅是一个函数
def my_loss(output, target):
loss = torch.mean((output - target)**2)
return loss
5.1.2:以类方式定义
如果看每一个损失函数的继承关系,可以发现Loss
函数部分继承自_loss
,部分继承自_WeightedLoss
,而_WeightedLoss
继承自_loss
, _loss
继承自 nn.Module。
损失函数类需要继承自nn.Module类。
【案例:DiceLoss】
应用:分割
D
i
c
e
L
o
s
s
=
2
∣
X
∩
Y
∣
∣
X
∣
+
∣
Y
∣
DiceLoss=\frac{2|X∩Y|}{|X|+|Y|}
DiceLoss=∣X∣+∣Y∣2∣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)
在自定义损失函数时,可能涉及到数学运算,最好全程使用PyTorch提供的张量计算接口,这样就不需要实现自动求导功能,并且可以直接调用cuda
【案例:IoULoss】
实现代码:
class IoULoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(IoULoss, 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()
total = (inputs + targets).sum()
union = total - intersection
IoU = (intersection + smooth)/(union + smooth)
return 1 - IoU
5.2:动态调整学习率
学习率过小:极大降低收敛速度,增加训练时间
学习率过大:导致参数在最优解两侧来回振荡
scheduler——通过一个适当的学习率衰减策略来改善这种现象
5.2.1:官方scheduler
PyTorch已经在torch.optim.lr_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
- lr_scheduler.ConstantLR
- lr_scheduler.LinearLR
- lr_scheduler.PolynomialLR
- lr_scheduler.ChainedScheduler
- lr_scheduler.SequentialLR
这些scheduler都是继承自_LRScheduler
类。
使用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()
# 需要在优化器参数更新之后再动态调整学习率
# scheduler的优化是在每一轮后面进行的
scheduler1.step()
...
schedulern.step()
在使用官方给出的torch.optim.lr_scheduler
时,需要将scheduler.step()
放在optimizer.step()
后面进行使用。
5.2.2:自定义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)
5.3:模型微调——torchvision
迁移学习(transfer learning):将从源数据集学到的知识迁移到目标数据集上。
模型微调(finetune):先找到一个同类的别人训练好的模型,把别人现成的训练好了的模型拿过来,换成自己的数据,通过训练调整一下参数。
5.3.1:模型微调流程
- 在源数据集上预训练一个神经网络模型,即源模型。
- 创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。
- 为目标模型添加一个输出大小为目标数据集类别个数的输出层,并随机初始化该层的模型参数。
- 在目标数据集上训练目标模型。从头训练输出层,而其余层的参数基于源模型的参数微调。
5.3.2:使用已有模型结构
实例化模型
import torchvision.models as models
resnet18 = models.resnet18()
传递参数
通过True
或者False
来决定是否使用预训练好的权重,在默认状态下pretrained = False
import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)
可以将自己的权重下载下来放到同文件夹下,然后再将参数加载网络。
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
如果中途强行停止下载的话,一定要去对应路径下将权重文件删除干净,要不然可能会报错。
5.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
修改示例:resnet输出从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)
5.4:模型微调——timm
5.4.1:依赖安装
通过pip
pip install timm
通过源码编译
git clone https://github.com/rwightman/pytorch-image-models
cd pytorch-image-models && pip install -e .
5.4.2:查看预训练模型种类
通过timm.list_models()
方法查看timm提供的预训练模型
import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)
查看特定模型的所有种类
在timm.list_models()
传入想查询的模型名称(模糊查询)
案例:查询densenet系列的所有模型
all_densnet_models = timm.list_models("*densenet*")
all_densnet_models
"""
['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'}
"""
5.4.3:使用和修改预训练模型
得到预训练模型后,通过timm.create_model()
的方法创建新模型。
通过传入参数pretrained=True
,使用预训练模型。
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)]
"""
修改模型
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])
"""
改变输入通道数
model = timm.create_model('resnet34',num_classes=10,pretrained=True,in_chans=1)
# 通过添加in_chans=1来改变
x = torch.randn(1,1,224,224)
output = model(x)
5.4.4:模型保存
timm库所创建的模型是torch.model
的子类
可以直接使用torch库中内置的模型参数保存和加载的方法
torch.save(model.state_dict(),'./checkpoint/timm_model.pth')
model.load_state_dict(torch.load('./checkpoint/timm_model.pth'))
5.5:半精度训练
GPU的性能主要分为两部分:算力和显存。
前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。
PyTorch默认的浮点数存储方式用的是torch.float32
。
绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果,即使用torch.float16
格式。
数位减了一半,因此被称为**“半精度”**。半精度能够减少显存占用,使显卡可以同时加载更多数据进行计算。
5.5.1:半精度训练的设置
使用autocast配置半精度训练,需要在下面三处加以设置:
【1】import package
from torch.cuda.amp import autocast
【2】模型
使用python的装饰器方法,用autocast装饰模型中的forward函数
@autocast()
def forward(self, x):
...
return x
【3】训练过程
在将数据输入模型及其之后的部分放入“with autocast():“即可
for x in train_loader:
x = x.cuda()
with autocast():
output = model(x)
...
半精度训练适用于数据本身的size比较大(3D图像、视频等)。
5.6:数据增强——imgaug
最简单的避免过拟合的方法是增加数据
imgaug封装了很多数据增强算法
5.6.1:imgaug简介
imgaug
是计算机视觉任务中常用的一个数据增强的包,相比于torchvision.transforms
,它提供了更多的数据增强方法。
pip安装:
pip install imgaug
conda安装:
conda config --add channels conda-forge
conda install imgaug
5.6.2:imgaug使用
imgaug仅提供了图像增强的一些方法,但是并未提供图像的IO操作,因此需要使用一些库来对图像进行导入。
建议使用imageio进行读入,如果使用的是opencv进行文件读取的时候,需要进行手动改变通道,将读取的BGR图像转换为RGB图像。
用PIL.Image进行读取时,因为读取的图片没有shape的属性,所以需要将读取到的img转换为np.array()的形式再进行处理。
【1】单张图片处理
读取图像:
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)
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)
做多种数据增强处理时,需要利用imgaug.augmenters.Sequential()
来构造数据增强的pipeline,该方法与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)
【2】批次图片处理
可以将图形数据按照NHWC的形式,或者由列表组成的HWC的形式对批量的图像进行处理。
主要分为以下两部分,对批次的图片以同一种方式处理和对批次的图片进行分部分处理。
对批次的图片以同一种方式处理
案例1:
images = [img,img,img,img,]
images_aug = rotate(images=images)
ia.imshow(np.hstack(images_aug))
案例2:
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)
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)
【3】不同大小图片处理
# 构建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.6.3:imgaug应用
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
上一篇: 宫圈人设表•壹