# 六、PyTorch进阶训练技巧

@[TOC]

## 1. 自定义损失函数

### 1.1. 函数定义

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

### 1.2. 类定义

#### 1.2.1. DiceLoss

Dice Loss是一种在分割领域常见的损失函数。Dice系数，是一种集合相似度度量函数，通常用于计算两个样本点的相似度（值范围为[0, 1]），|X⋂Y| 表示 X 和 Y 之间的交集；|X| 和 |Y| 分别表示 X 和 Y 的元素个数. 其中，分子中的系数 2，是因为分母存在重复计算 X 和 Y 之间的共同元素的原因。

$$Dice \ Loss = 1 - \frac{2| X \cap Y |}{|X| +|Y|}$$

$$S = \frac{2| X \cap Y |}{|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)

#### 1.2.2. DiceBCELoss

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

#### 1.2.3. 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

#### 1.2.4. FocalLoss

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

## 2. 动态调整学习率

scheduler的使用有两种方式：

### 2.1. 使用官方提供的scheduler

PyTorch已经在torch.optim.lr_scheduler为我们封装好了一些动态调整学习率的方法

# 选择一种优化器
# 选择上面提到的一种或多种动态调整学习率的方法
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

def adjust_learning_rate(optimizer, epoch):
lr = args.lr * (0.1 ** (epoch // 30))
for param_group in optimizer.param_groups:
param_group['lr'] = lr
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)

## 3. 模型微调-torchvision

### 3.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()
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()

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)
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. 通常PyTorch模型的扩展为.pt.pth，程序运行时会首先检查默认路径中是否有已经下载的模型权重，一旦权重被下载，下次加载就不需要下载了。

2. 一般情况下预训练模型的下载会比较慢，我们可以直接通过迅雷或者其他方式去 这里 查看自己的模型里面model_urls，然后手动下载，预训练模型的权重在LinuxMac的默认下载路径是用户根目录下的.cache文件夹。在Windows下就是C:\Users\&lt;username&gt;\.cache\torch\hub\checkpoint。我们可以通过使用 torch.utils.model_zoo.load_url()设置权重的下载地址。

3. 如果觉得麻烦，还可以将自己的权重下载下来放到同文件夹下，然后再将参数加载网络。

self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
4. 如果中途强行停止下载的话，一定要去对应路径下将权重文件删除干净，要不然可能会报错。

### 3.2 训练特定层

def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False

import torchvision.models as models
# 冻结参数的梯度
feature_extract = True
model = models.resnet18(pretrained=True)
# 修改模型
num_ftrs = model.fc.in_features
model.fc = nn.Linear(in_features=512, out_features=4, bias=True)

## 4. 模型微调 - timm

### 4.1. timm的安装

1. 通过pip安装
pip install timm
1. 通过git与pip进行安装
git clone https://github.com/rwightman/pytorch-image-models
cd pytorch-image-models && pip install -e .

### 4.2. 如何查看预训练模型种类

#### 4.2.1. 查看timm提供的预训练模型

import timm
avail_pretrained_models = timm.list_models(pretrained=True)
len(avail_pretrained_models)
592

#### 4.2.2. 查看特定模型的所有种类

all_densnet_models = timm.list_models("*densenet*")
all_densnet_models

['densenet121',
'densenet121d',
'densenet161',
'densenet169',
'densenet201',
'densenet264',
'densenet264d_iabn',
'densenetblur121d',
'tv_densenet121']

#### 4.2.3. 查看模型的具体参数

model = timm.create_model('resnet34',num_classes=10,pretrained=True)
model.default_cfg
'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. 使用和修改预训练模型

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,

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

## 5. 半精度训练

• 如何在PyTorch中设置半精度训练
• 使用半精度训练的注意事项

### 5.1. 半精度训练的设置

• import autocast
from torch.cuda.amp import autocast
• 模型设置

@autocast()
def forward(self, x):
...
return x
• 训练过程

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

## 6. 数据增强-imgaug

• imgaug的简介和安装
• 使用imgaug对数据进行增强

### 6.1.1. imgaug简介

imgaug是计算机视觉任务中常用的一个数据增强的包，相比于torchvision.transforms，它提供了更多的数据增强方法，因此在各种竞赛中，人们广泛使用imgaug来对数据进行增强操作。除此之外，imgaug官方还提供了许多例程让我们学习，本章内容仅是简介，希望起到抛砖引玉的功能。

1. Github地址：imgaug
3. 官方提供notebook例程：notebook

### 6.1.2 imgaug的安装

imgaug的安装方法和其他的Python包类似，我们可以通过以下两种方式进行安装

#### conda

conda config --add channels conda-forge
conda install imgaug

#### pip

#  install imgaug either via pypi

pip install imgaug

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进行图片读取。

### 单张图片处理

import imageio
import imgaug as ia
%matplotlib inline

# 图片的读取

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

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

from imgaug import augmenters as iaa

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

# 实例化方法
rotate = iaa.Affine(rotate=(-4,45))
img_aug = rotate(image=img)
ia.imshow(img_aug)

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.Crop(percent=(0,0.2))
])
# 对图片进行处理，image不可以省略，也不能写成images
image_aug = aug_seq(image=img)
ia.imshow(image_aug)

### 对批次图片进行处理

#### 对批次的图片以同一种方式处理

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

aug_seq = iaa.Sequential([
iaa.Affine(rotate=(-25, 25)),
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))

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

imgaug相较于其他的数据增强的库，有一个很有意思的特性，即就是我们可以通过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)

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

# 构建pipline
seq = iaa.Sequential([
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 = [
]

# 对图片进行增强
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]]))

### 6.3. imgaug在PyTorch的应用

import numpy as np
from imgaug import augmenters as iaa
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)
num_workers=4, pin_memory=True,
worker_init_fn=worker_init_fn)

## 7. 使用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的使用

• 创建ArgumentParser()对象
• 调用add_argument()方法添加参数
• 使用parse_args()解析参数 在接下来的内容中，我们将以实际操作来学习argparse的使用方法。
# demo.py
import argparse

# 创建ArgumentParser()对象
parser = argparse.ArgumentParser()

# 添加参数
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} ")

This is some output
learning rate: 3e-4

argparse的参数主要可以分为可选参数和必选参数。可选参数就跟我们的lr参数相类似，未输入的情况下会设置为默认值。必选参数就跟我们的batch_size参数相类似，当我们给参数设置required =True后，我们就必须传入该参数，否则就会报错。看到我们的输入格式后，我们可能会有这样一个疑问，我输入参数的时候不使用--可以吗？答案是肯定的，不过我们需要在设置上做出一些改变。

# positional.py
import argparse

# 位置参数
parser = argparse.ArgumentParser()

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进行超参数的修改。 ### 7.3. 更加高效使用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'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:

# 导入必要库
...
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给我们提供了一种新的更加便捷的方式，在后面我们将结合其他Python标准库（pickle，json，logging）实现参数的保存和模型输出的记录。如果大家还想进一步的了解argparse的使用，大家可以点击下面提供的连接进行更深的学习和了解。

## 参考资料

[6.1 自定义损失函数 — 深入浅出PyTorch (datawhalechina.github.io)](https://datawhalechina.github.io/thorough-pytorch/第六章/6.1 自定义损失函数.html)

[6.2 动态调整学习率 — 深入浅出PyTorch (datawhalechina.github.io)](https://datawhalechina.github.io/thorough-pytorch/第六章/6.2 动态调整学习率.html)

kaggle-data-augmentation-packages-overview

how to use imgaug with pytorch

Kaggle知识点：数据扩增方法

PyTorch Classification Model Based On Imgaug.

Tutorial Notebooks