目录
0 完整章节内容
1 自定义损失函数
PyTorch在torch.nn模块为我们提供了许多常用的损失函数,比如:MSELoss,L1Loss,BCELoss...... 但是随着深度学习的发展,出现了越来越多的非官方提供的Loss,比如DiceLoss,HuberLoss,SobolevLoss...... 这些Loss Function专门针对一些非通用的模型,PyTorch不能将他们全部添加到库中去,因此这些损失函数的实现则需要我们通过自定义损失函数来实现。另外,在一些算法实现中,研究者往往会提出全新的损失函数来提升模型的表现,这时我们既无法使用PyTorch自带的损失函数,也没有相关的博客供参考,此时自己实现损失函数就显得更为重要了。
经过本节的学习,你将收获:
-
掌握如何自定义损失函数
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是一种在分割领域常见的损失函数,定义如下:
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)
除此之外,常见的损失函数还有BCE-Dice Loss,Jaccard/Intersection over Union (IoU) Loss,Focal Loss......
class DiceBCELoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(DiceBCELoss, 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_loss = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
Dice_BCE = BCE + dice_loss
return Dice_BCE
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
ALPHA = 0.8
GAMMA = 2
class FocalLoss(nn.Module):
def __init__(self, weight=None, size_average=True):
super(FocalLoss, self).__init__()
def forward(self, inputs, targets, alpha=ALPHA, gamma=GAMMA, smooth=1):
inputs = F.sigmoid(inputs)
inputs = inputs.view(-1)
targets = targets.view(-1)
BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
BCE_EXP = torch.exp(-BCE)
focal_loss = alpha * (1-BCE_EXP)**gamma * BCE
return focal_loss
# 更多的可以参考链接1
注:
在自定义损失函数时,涉及到数学运算时,我们最好全程使用PyTorch提供的张量计算接口,这样就不需要我们实现自动求导功能并且我们可以直接调用cuda,使用numpy或者scipy的数学运算时,操作会有些麻烦,大家可以自己下去进行探索。关于PyTorch使用Class定义损失函数的原因,可以参考PyTorch的讨论区(链接6)
1.3 本节参考
- Loss Function Library - Keras & PyTorch | Kaggle
- https://www.zhihu.com/question/66988664/answer/247952270
- pytorch系列12 --pytorch自定义损失函数custom loss function_loss function gpu-优快云博客
- 自定义损失函数 - image processing
- pytorch教程之损失函数详解——多种定义损失函数的方法_如何自己设计损失函数-优快云博客
- Should I define my custom loss function as a class? - PyTorch Forums
2 动态调整学习率
学习率的选择是深度学习中一个困扰人们许久的问题,学习速率设置过小,会极大降低收敛速度,增加训练时间;学习率太大,可能导致参数在最优解两侧来回振荡。但是当我们选定了一个合适的学习率后,经过许多轮的训练后,可能会出现准确率震荡或loss不再下降等情况,说明当前学习率已不能满足模型调优的需求。此时我们就可以通过一个适当的学习率衰减策略来改善这种现象,提高我们的精度。这种设置方式在PyTorch中被称为scheduler(调度器),也是我们本节所研究的对象。
我在之前吴恩达老师机器学习课程的笔记里也提到过学习率设置的重要性,包括:
- 学习率 α 的选取将会对梯度下降的效率产生巨大影响。
- 如何设置学习率呢?
- 在梯度下降的过程中,学习率为啥是由大变小的?
可以参见下方链接:
经过本节的学习,你将收获:
-
如何根据需要选取已有的学习率调整策略
-
如何自定义设置学习调整策略并实现
2.1 使用官方scheduler
2.1.1 了解官方提供的API
在训练神经网络的过程中,学习率是最重要的超参数之一,作为当前较为流行的深度学习框架,PyTorch已经在torch.optim.lr_scheduler
为我们封装好了一些动态调整学习率的方法供我们使用,如下面列出的这些scheduler。
这些scheduler都是继承自_LRScheduler
类,我们可以通过help(torch.optim.lr_scheduler)
来查看这些类的具体使用方法,也可以通过help(torch.optim.lr_scheduler._LRScheduler)
来查看_LRScheduler
类的具体使用方法。
2.1.2 使用官方API
关于如何使用这些动态调整学习率的策略,PyTorch
官方也很人性化的给出了使用实例代码帮助大家理解,我们也将结合官方给出的代码来进行解释。
loader, optimizer, model, loss_fn = ...
swa_model = torch.optim.swa_utils.AveragedModel(model)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=300)
swa_start = 160
swa_scheduler = SWALR(optimizer, swa_lr=0.05)
for epoch in range(300):
for input, target in loader:
optimizer.zero_grad()
loss_fn(model(input), target).backward()
optimizer.step()
if epoch > swa_start:
swa_model.update_parameters(model)
swa_scheduler.step()
else:
scheduler.step()# scheduler的优化是在每一轮后面进行的
# Update bn statistics for the swa_model at the end
torch.optim.swa_utils.update_bn(loader, swa_model)
# Use swa_model to make predictions on test data
preds = swa_model(test_input)
注:我们在使用官方给出的torch.optim.lr_scheduler
时,需要将scheduler.step()
放在optimizer.step()
后面进行使用。
2.2 自定义scheduler
虽然PyTorch官方给我们提供了许多的API,但是在实验中也有可能碰到需要我们自己定义学习率调整策略的情况,而我们的方法是自定义函数adjust_learning_rate
来改变param_group
中lr
的值,在下面的叙述中会给出一个简单的实现。
假设我们现在正在做实验,需要学习率每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)
2.3 本节参考
3 模型微调-torchvision
随着深度学习的发展,模型的参数越来越大,许多开源模型都是在较大数据集上进行训练的,比如Imagenet-1k,Imagenet-11k,甚至是ImageNet-21k等。但在实际应用中,我们的数据集可能只有几千张,这时从头开始训练具有几千万参数的大型神经网络是不现实的,因为越大的模型对数据量的要求越大,过拟合无法避免。
假设我们想从图像中识别出不同种类的椅⼦,然后将购买链接推荐给用户。一种可能的方法是先找出100种常见的椅子,为每种椅子拍摄1000张不同⻆度的图像,然后在收集到的图像数据集上训练一个分类模型。这个椅子数据集虽然可能比Fashion-MNIST数据集要庞⼤,但样本数仍然不及ImageNet数据集中样本数的十分之⼀。这可能会导致适用于ImageNet数据集的复杂模型在这个椅⼦数据集上过拟合。同时,因为数据量有限,最终训练得到的模型的精度也可能达不到实用的要求。
为了应对上述问题,一个显⽽易⻅的解决办法是收集更多的数据。然而,收集和标注数据会花费大量的时间和资⾦。例如,为了收集ImageNet数据集,研究人员花费了数百万美元的研究经费。虽然目前的数据采集成本已降低了不少,但其成本仍然不可忽略。
另外一种解决办法是应用迁移学习(transfer learning),将从源数据集学到的知识迁移到目标数据集上。例如,虽然ImageNet数据集的图像大多跟椅子无关,但在该数据集上训练的模型可以抽取较通用的图像特征,从而能够帮助识别边缘、纹理、形状和物体组成等。这些类似的特征对于识别椅子也可能同样有效。
迁移学习的一大应用场景是模型微调(finetune)。简单来说,就是我们先找到一个同类的别人训练好的模型,把别人现成的训练好了的模型拿过来,换成自己的数据,通过训练调整一下参数。 在PyTorch中提供了许多预训练好的网络模型(VGG,ResNet系列,mobilenet系列......),这些模型都是PyTorch官方在相应的大型数据集训练好的。学习如何进行模型微调,可以方便我们快速使用预训练模型完成自己的任务。
经过本节的学习,你将收获:
-
掌握模型微调的流程
-
了解PyTorch提供的常用model
-
掌握如何指定训练模型的部分层
3.1 模型微调的流程
-
在源数据集(如ImageNet数据集)上预训练一个神经网络模型,即源模型。
-
创建一个新的神经网络模型,即目标模型。它复制了源模型上除了输出层外的所有模型设计及其参数。我们假设这些模型参数包含了源数据集上学习到的知识,且这些知识同样适用于目标数据集。我们还假设源模型的输出层跟源数据集的标签紧密相关,因此在目标模型中不予采用。
-
为目标模型添加一个输出⼤小为⽬标数据集类别个数的输出层,并随机初始化该层的模型参数。
-
在目标数据集上训练目标模型。我们将从头训练输出层,而其余层的参数都是基于源模型的参数微调得到的。
3.2 使用已有模型结构
这里我们以torchvision中的常见模型为例,列出了如何在图像分类任务中使用PyTorch提供的常见模型结构和参数。对于其他任务和网络结构,使用方式是类似的:
3.2.1 实例化网络
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()
3.2.2 传递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)
注意事项:
-
通常PyTorch模型的扩展为
.pt
或.pth
,程序运行时会首先检查默认路径中是否有已经下载的模型权重,一旦权重被下载,下次加载就不需要下载了。 -
一般情况下预训练模型的下载会比较慢,我们可以直接通过迅雷或者其他方式去 这里 查看自己的模型里面
model_urls
,然后手动下载,预训练模型的权重在Linux
和Mac
的默认下载路径是用户根目录下的.cache
文件夹。在Windows
下就是C:\Users\<username>\.cache\torch\hub\checkpoint
。我们可以通过使用 torch.utils.model_zoo.load_url()设置权重的下载地址。 -
如果觉得麻烦,还可以将自己的权重下载下来放到同文件夹下,然后再将参数加载网络。
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
-
如果中途强行停止下载的话,一定要去对应路径下将权重文件删除干净,要不然可能会报错。
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属性,我们完成了指定训练模型的特定层的目标,这对实现模型微调非常重要。
3.4 本节参考
4 模型微调 - timm
除了使用torchvision.models
进行预训练以外,还有一个常见的预训练模型库,叫做timm
,这个库是由Ross Wightman创建的。里面提供了许多计算机视觉的SOTA模型,可以当作是torchvision的扩充版本,并且里面的模型在准确度上也较高。在本章内容中,我们主要是针对这个库的预训练模型的使用做叙述,其他部分内容(数据扩增,优化器等)如果大家感兴趣,可以参考以下两个链接。
4.1 timm的安装
关于timm的安装,我们可以选择以下两种方式进行:
-
通过pip安装
pip install timm
-
通过源码编译安装
git clone https://github.com/rwightman/pytorch-image-models
cd pytorch-image-models && pip install -e .
4.2 如何查看预训练模型种类
1)查看timm提供的预训练模型
截止到2024.12.17日为止,timm提供的预训练模型已经达到了1525个,我们可以通过timm.list_models()
方法查看timm提供的预训练模型(注:本章测试代码均是在jupyter notebook上进行)
import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)
1525
2)查看特定模型的所有种类
每一种系列可能对应着不同方案的模型,比如Resnet系列就包括了ResNet18,50,101等模型,我们可以在timm.list_models()
传入想查询的模型名称(模糊查询),比如我们想查询densenet系列的所有模型。
all_densnet_models = timm.list_models("*densenet*")
all_densnet_models
['densenet121',
'densenet161',
'densenet169',
'densenet201',
'densenet264d',
'densenetblur121d']
3)查看模型的具体参数
当我们想查看下模型的具体参数的时候,我们可以通过访问模型的default_cfg
属性来进行查看,具体操作如下(记得挂clash)
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'}
除此之外,我们可以通过访问这个链接 查看提供的预训练模型的准确度等信息。
4.3 使用和修改预训练模型
在得到我们想要使用的预训练模型后,我们可以通过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)
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'))
4.5 参考材料
5 半精度训练
我之前在“手把手带你自学Transformers”这篇笔记中详细介绍了NLP中低精度训练的一些内容,比较完整,如果你对这些感兴趣,欢迎浏览下面的笔记:
手把手带你实战Transformers(学习笔记)_transformers学习笔记-优快云博客
此外,李宏毅老师《生成式人工智能》这门课也提到了“模型参数与显存的关系,以及不同精度的影响”,笔记链接如下:
HW4-补充4:模型参数与显存的关系,以及不同精度的影响_大模型参数规模和显存的关系-优快云博客
我们提到PyTorch时候,总会想到要用硬件设备GPU的支持。而GPU的性能主要分为两部分:算力和显存,前者决定了显卡计算的速度,后者则决定了显卡可以同时放入多少数据用于计算。在可以使用的显存数量一定的情况下,每次训练能够加载的数据更多(也就是batch size更大),则也可以提高训练效率。另外,有时候数据本身也比较大(比如3D图像、视频等),显存较小的情况下可能甚至batch size为1的情况都无法实现。因此,合理使用显存也就显得十分重要。
我们观察PyTorch默认的浮点数存储方式用的是torch.float32
,小数点后位数更多固然能保证数据的精确性,但绝大多数场景其实并不需要这么精确,只保留一半的信息也不会影响结果,也就是使用torch.float16
格式。由于数位减了一半,因此被称为“半精度”,具体如下图:
显然半精度能够减少显存占用,使得显卡可以同时加载更多数据进行计算。本节会介绍如何在PyTorch中设置使用半精度计算。
经过本节的学习,你将收获:
-
如何在PyTorch中设置半精度训练
-
使用半精度训练的注意事项
5.1 半精度训练的设置
在PyTorch中使用autocast配置半精度训练,同时需要在下面三处加以设置:
-
import autocast
from torch.cuda.amp import autocast
-
模型设置
在模型定义中,使用python的装饰器方法,用autocast装饰模型中的forward函数。关于装饰器的使用,可以参考这里:
-
训练过程
在训练过程中,只需在将数据输入模型及其之后的部分放入"with autocast():"即可:
# Creates model and optimizer in default precision
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)
for input, target in data:
optimizer.zero_grad()
# Enables autocasting for the forward pass (model + loss)
with torch.autocast(device_type="cuda"):
output = model(input)
loss = loss_fn(output, target)
# Exits the context manager before backward()
loss.backward()
optimizer.step()
注意:
半精度训练主要适用于数据本身的size比较大(比如说3D图像、视频等)。当数据本身的size并不大时(比如手写数字MNIST数据集的图片尺寸只有28*28),使用半精度训练则可能不会带来显著的提升。
6 数据增强-imgaug
我之前在吴恩达老师深度学习课程中也介绍过“3.25 计算机视觉中常用的数据增强操作 ”,链接如下:
在机器学习/深度学习中,我们经常会遇到模型过拟合的问题,为了解决过拟合问题,我们可以通过加入正则项或者减少模型学习参数来解决,但是最简单的避免过拟合的方法是增加数据,但是在许多场景我们无法获得大量数据,例如医学图像分析。数据增强技术的存在是为了解决这个问题,这是针对有限数据问题的解决方案。数据增强一套技术,可提高训练数据集的大小和质量,以便我们可以使用它们来构建更好的深度学习模型。 在计算视觉领域,生成增强图像相对容易。即使引入噪声或裁剪图像的一部分,模型仍可以对图像进行分类,数据增强有一系列简单有效的方法可供选择,有一些机器学习库来进行计算视觉领域的数据增强,比如:imgaug 官网它封装了很多数据增强算法,给开发者提供了方便。通过本章内容,您将学会以下内容:
-
imgaug的简介和安装
-
使用imgaug对数据进行增强
6.1 imgaug简介和安装
6.1.1 简介
imgaug
是计算机视觉任务中常用的一个数据增强的包,相比于torchvision.transforms
,它提供了更多的数据增强方法,因此在各种竞赛中,人们广泛使用imgaug
来对数据进行增强操作。除此之外,imgaug官方还提供了许多例程让我们学习,本章内容仅是简介,希望起到抛砖引玉的功能。
6.1.2 安装
imgaug的安装方法和其他的Python包类似,我们可以通过以下两种方式进行安装
conda:
conda config --add channels conda-forge
conda install 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
6.2 imgaug的使用
imgaug仅仅提供了图像增强的一些方法,但是并未提供图像的IO操作,因此我们需要使用一些库来对图像进行导入,建议使用imageio进行读入,如果使用的是opencv进行文件读取的时候,需要进行手动改变通道,将读取的BGR图像转换为RGB图像。除此以外,当我们用PIL.Image进行读取时,因为读取的图片没有shape的属性,所以我们需要将读取到的img转换为np.array()的形式再进行处理。因此官方的例程中也是使用imageio进行图片读取。
6.2.1 单张图片处理
在该单元,我们仅以几种数据增强操作为例,主要目的是教会大家如何使用imgaug来对数据进行增强操作。
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 # 导入 imgaug 库中的图像增强模块
# 设置随机数种子,保证增强的随机性结果可复现
ia.seed(4)
# 实例化仿射变换方法,这里设置旋转角度范围为 -4 到 45 度
rotate = iaa.Affine(rotate=(-4, 45))
# 对图像进行旋转增强,`image` 是输入图像
# 请确保 img 是一个有效的图像数组(如 NumPy 数组形式)
img_aug = rotate(image=img)
# 显示增强后的图像
ia.imshow(img_aug)
这是对一张图片进行一种操作方式,但实际情况下,我们可能对一张图片做多种数据增强处理。这种情况下,我们就需要利用imgaug.augmenters.Sequential()
来构造我们数据增强的pipline,该方法与torchvison.transforms.Compose()
相类似。
iaa.Sequential(
children=None, # Augmenter 集合,可以是单个 augmenter,也可以是 augmenter 的列表。
random_order=False, # 是否对每个 batch 随机调整 Augmenter 的顺序。默认 False,按顺序执行。
name=None, # Augmenter 名称,用于标识(可选)。
deterministic=False, # 是否将增强过程设为确定性。若 True,则同一输入会得到相同的输出。
random_state=None, # 随机数生成器的种子,控制随机行为(可选)。
)
from imgaug import augmenters as iaa # 导入 imgaug 库中的增强模块
# 构建增强处理序列
aug_seq = iaa.Sequential( # 定义一个增强器序列,按顺序应用以下操作
[
iaa.Affine(rotate=(-25, 25)), # 随机旋转角度在 -25 到 25 度之间
iaa.AdditiveGaussianNoise(
scale=(10, 60)
), # 添加高斯噪声,噪声强度随机选择在 10 到 60 之间
iaa.Crop(percent=(0, 0.2)), # 随机裁剪图像,裁剪范围为 0% 到 20% 的比例
]
)
# 对图像进行增强处理
# 注意:这里 image 参数必须写为单数形式 'image',表示对单张图像进行增强
image_aug = aug_seq(image=img)
# 显示增强后的图像
ia.imshow(image_aug)
总的来说,对单张图片处理的方式基本相同,我们可以根据实际需求,选择合适的数据增强方法来对数据进行处理。
6.2.2 对批次图片进行处理
1)对批次的图片以同一种方式进行处理
对一批次的图片进行处理时,我们只需要将待处理的图片放在一个list
中,并将函数的image改为images即可进行数据增强操作,具体实际操作如下:
# 准备输入图像列表,包含多张相同的图像
import numpy as np
images = [img, img, img, img] # 图像列表,每张图像都为 img
# 对图像列表进行增强处理
# 注意:这里使用 `images` 参数,表示对多张图像进行增强
images_aug = rotate(images=images)
# 将增强后的多张图像水平拼接到一起,便于可视化对比
# np.hstack: 将数组按水平方向堆叠
ia.imshow(np.hstack(images_aug)) # 显示增强后的图像拼接结果
我们就可以得到如下的展示效果:
在上述的例子中,我们仅仅对图片进行了仿射变换,同样的,我们也可以对批次的图片使用多种增强方法,与单张图片的方法类似,我们同样需要借助Sequential
来构造数据增强的pipline。
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)
# images_aug = aug_seq(images = images)
ia.imshow(np.hstack(images_aug))
2)对批次的图片分部分处理
imgaug相较于其他的数据增强的库,有一个很有意思的特性,即就是我们可以通过imgaug.augmenters.Sometimes()
对batch中的一部分图片应用一部分Augmenters,剩下的图片应用另外的Augmenters。
iaa.Sometimes(
p=0.5, # 代表划分比例,表示以 p 的概率应用 `then_list` 中的增强器。
then_list=None, # 在 p 的概率下应用的增强器集合,可以是单个 augmenter 或列表。
else_list=None, # 在 1-p 的概率下应用的增强器集合,可以是单个 augmenter 或列表。
name=None, # 可选参数,为该 augmenter 指定名称,便于调试和标识。
deterministic=False, # 是否将增强过程设为确定性。若为 True,则相同输入会始终产生一致的输出。
random_state=None, # 随机数生成器的种子,控制随机行为(可选)。
)
Sometimes(p=Binomial(Deterministic(float 0.50000000)), name=UnnamedSometimes, then_list=None, else_list=None, deterministic=False)
import numpy as np
from imgaug import augmenters as iaa
# 定义增强器
sometimes = iaa.Sometimes(
p=0.5, # 50% 的概率选择旋转增强
then_list=[
iaa.Affine(rotate=(-25, 25)),
iaa.AdditiveGaussianNoise(scale=(10, 60)),
],
else_list=[
iaa.AdditiveGaussianNoise(scale=(10, 60)),
iaa.Crop(percent=(0, 0.2)),
],
)
# 对整批图片应用增强
processed_images = sometimes(images=images)
# 拼接并显示增强后的图片
ia.imshow(np.hstack(processed_images)) # 水平拼接显示图片
6.2.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]]))
Image 0 (input shape: (257, 286, 3), output shape: (257, 286, 3))
Image 1 (input shape: (536, 800, 3), output shape: (536, 800, 3))
Image 2 (input shape: (289, 520, 3), output shape: (289, 520, 3))
6.3 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)
6.4 总结
数据扩充是我们需要掌握的基本技能,除了imgaug以外,我们还可以去学习其他的数据增强库,包括但不局限于Albumentations,Augmentor。除去imgaug以外,我还强烈建议大家学下Albumentations,因为Albumentations跟imgaug都有着丰富的教程资源,大家可以有需求访问Albumentations教程。
6.5 参考资料
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的使用方法。
import argparse # 引入 argparse 模块,用于解析命令行参数
# 创建 ArgumentParser() 对象
parser = argparse.ArgumentParser(description="用于测试 argparse 的命令行参数解析示例")
# 添加参数 -o 或 --output,用于控制是否显示输出
parser.add_argument(
"-o", # 短选项
"--output", # 长选项
action="store_true", # 不接受值,提供该选项时值为 True,默认值为 False
help="是否显示输出(布尔类型)", # 参数说明
)
# 添加参数 --lr,用于指定学习率,类型为 float,默认值为 3e-5
parser.add_argument(
"--lr", # 参数名称
type=float, # 参数类型为浮点数
default=3e-5, # 设置默认值
help="选择学习率,默认值为 3e-5", # 参数说明
)
# 添加参数 --batch_size,用于指定批次大小,类型为 int,必须提供该参数
parser.add_argument(
"--batch_size", # 参数名称
type=int, # 参数类型为整数
required=True, # 设置为必填项
help="输入批次大小(必须提供)", # 参数说明
)
# 使用 parse_args() 解析命令行参数
args = parser.parse_args()
# 打印解析后的参数对象
print(args)
# 根据 --output 参数的值,控制输出逻辑
if args.output:
print("This is some output")
print(f"Learning rate: {args.lr}") # 输出学习率
我们在命令行使用python SZ/demo.py --lr 3e-4 --batch_size 32 -o
,就可以看到以下的输出
Namespace(batch_size=32, lr=0.0003, output=True)
This is some output
Learning rate: 0.0003
argparse的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr
参数相类似,未输入的情况下会设置为默认值。必选参数就跟我们的batch_size
参数相类似,当我们给参数设置required =True
后,我们就必须传入该参数,否则就会报错。看到我们的输入格式后,我们可能会有这样一个疑问,我输入参数的时候不使用--可以吗?答案是肯定的,不过我们需要在设置上做出一些改变。
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")
当我们不使用--后,将会严格按照参数位置进行解析。
Peter is 23 years old
总的来说,argparse的使用很简单,以上这些操作就可以帮助我们进行参数的修改,在下面的部分,我将会分享我是如何在模型训练中使用argparse进行超参数的修改。
7.3 更加高效使用argparse修改超参数
每个人都有着不同的超参数管理方式,在这里我将分享我使用argparse管理超参数的方式,希望可以对大家有一些借鉴意义。通常情况下,为了使代码更加简洁和模块化,我一般会将有关超参数的操作写在config.py
,然后在train.py
或者其他文件导入就可以。具体的config.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
等其他文件,我们就可以使用下面的这样的结构来调用参数。
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)
7.4 总结
argparse给我们提供了一种新的更加便捷的方式,而在一些大型的深度学习库中人们也会使用json、dict、yaml等文件格式去保存超参数进行训练。如果大家还想进一步的了解argparse的使用,大家可以点击下面提供的连接进行更深的学习和了解。