0. 引言
AI性能 = 数据(70%)+ 模型(20%)+ 技巧(10%),数据决定了AI的上限,模型和trick只是去逼近这个上限。我们首先要进行数据分析,进行一些预处理使数据合理化,根据数据调整模型的感受野、anchor、训练尺度等;之后进行模型的选取,模型结构的修改;最后使用一些技巧使模型的性能进一步提升。
1. 训练技巧
(1) Backbone 和 Heads 的不同学习率
目标检测Backbone和Heads在结构上不同,使用不同的学习率是可以有效的使得网络整理达到更好,更稳定的收敛效果。下面以resnet18为例,展示学习率如何设置。
import torch
import torchvision.models as models
model = models.resnet18()
print([layer[0] for layer in model._modules.items()])
['conv1', 'bn1', 'relu', 'maxpool', 'layer1', 'layer2', 'layer3', 'layer4', 'avgpool', 'fc']
知道了模型每一层分别是什么以后,选取不同的层设置不同的学习率。
parameters1_key = ['conv1', 'bn1', 'relu', 'maxpool']
parameters2_key = ['layer1', 'layer2', 'layer3', 'layer4', 'avgpool', 'fc']
parameters1 = []
parameters2 = []
for k, v in model.named_parameters():
if any(key in k for key in parameters1_key):
parameters1.append(v)
else:
parameters2.append(v)
params = [
{'params': parameters1, 'lr':0.01},
{'params': parameters2, 'lr':0.001}
]
optimizer = torch.optim.SGD(params, momentum=0.9, weight_decay=1e-5)
另一种方式:
optimizer2 = torch.optim.SGD(params=parameters1, lr=0.01, momentum=0.9, weight_decay=1e-5)
optimizer2.add_param_group(params[1])
(2) Warmup
warmup 的学习率为 warmup_lr, 动量为 warmup_momentum,当前批次为 batch,一共进行 warmup 的 batch 为 warmup_total_batches,使用线性插值进行学习率和动量的选取
for j, x in enumerate(self.optimizer.param_groups):
x['lr'] = np.interp(
batch, [0, warmup_total_batches], [warmup_lr, lr])
if 'momentum' in x:
x['momentum'] = np.interp(batch, [0, warmup_total_batches], [warmup_momentum, momentum])
(3) clip gradients(梯度裁剪)
随着网络向前传播的层数越多,可能出现梯度爆炸,所以需要梯度裁剪,避免模型越过最优点。
第一种:确定一个范围 [-clip_value, clip_value],如果参数的gradient超过了,直接裁剪
torch.nn.utils.clip_grad.clip_grad_value_(parameters, clip_value)
第二种:如果所有参数的 gradient 组成的向量的 L 2 n o r m {L_2\ norm} L2 norm 大于 m a x n o r m {max\ norm} max norm,那么需要根据 L 2 n o r m / m a x _ n o r m {L_2\ norm/max\_norm} L2 norm/max_norm 进行缩放。从而使得 L 2 n o r m {L_2\ norm} L2 norm 小于预设的 m a x n o r m {max\ norm} max norm
torch.nn.utils.clip_grad.clip_grad_norm_(self.model.parameters(), max_norm=10.0, norm_type=2)
(4)多loss加权混合
loss函数的组合的权重是需要自己去摸索
(5)余弦退火算法
torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=150, eta_min=0)
T_max 表示余弦函数的半个周期(学习率下降到最小);eta_min 表示学习率的最小值
(6)指数移动平均(EMA)
class ModelEMA:
def __init__(self, model, decay=0.9999, tau=2000, updates=0):
# Create EMA
self.ema = deepcopy(de_parallel(model)).eval() # FP32 EMA
self.updates = updates # number of EMA updates
self.decay = lambda x: decay * (1 - math.exp(-x / tau)) # decay exponential ramp (to help early epochs)
for p in self.ema.parameters():
p.requires_grad_(False)
self.enabled = True
def update(self, model):
# Update EMA parameters
if self.enabled:
self.updates += 1
d = self.decay(self.updates)
msd = de_parallel(model).state_dict() # model state_dict
for k, v in self.ema.state_dict().items():
if v.dtype.is_floating_point: # true for FP16 and FP32
v *= d
v += (1 - d) * msd[k].detach()
# assert v.dtype == msd[k].dtype == torch.float32, f'{k}: EMA {v.dtype}, model {msd[k].dtype}'
ema = ModelEMA(model)
# 训练过程中,更新完参数后
ema.update(self.model)
(7)seed(42)
随机种子数42,没啥道理,但是就是要这样干。
# 固定CPU和CUDA的随机种子,使结果可以复现
torch.manual_seed(42)
# 如果为True,每次返回的卷积算法将是确定的,即默认算法
torch.backends.cudnn.deterministic = True
# 如果为True,则使cuDNN对多个卷积算法进行基准测试并选择最快的。
torch.backends.cudnn.benchmark = False
(8)混合精度训练
from torch.cuda import amp
# 是否进行混合精度训练
amp = device.type != 'cpu'
scaler = amp.GradScaler(enabled=amp)
with torch.cuda.amp.autocast(amp):
preds = model(img)
optimizer.zero_grad()
# 反向传播计算得到每个参数的梯度值
# 计算的梯度都是缩放后的梯度
scaler.scale(loss).backward()
# 如果要使用梯度值,首先应该进行反缩放操作。
# 如进行梯度裁剪时,首先应该对计算的梯度进行反缩放再和给定的阈值进行比较,
# 当然也可以对阈值按照同样的系数进行缩放而不对计算的梯度反缩放,效果是一致的。
scaler.unscale_(self.optimizer) # unscale gradients
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=10.0) # clip gradients
# 参数更新
scaler.step(optimizer)
scaler.update()
(9)多尺度训练
# Actual multiscale ranges: [640 - 5 * 32, 640 + 5 * 32].
# To disable multiscale training, set the value to 0.
multiscale_range = 5
# You can uncomment this line to specify a multiscale range
# self.random_size = (14, 26)
input_size = (640, 640)
def random_resize():
tensor = torch.LongTensor(2).cuda()
size_factor = input_size[1] * 1.0 / input_size[0]
# if not hasattr(self, 'random_size'):
min_size = int(input_size[0] / 32) - multiscale_range
max_size = int(input_size[0] / 32) + multiscale_range
random_size = (min_size, max_size)
size = random.randint(*random_size)
size = (int(32 * size), 32 * int(size * size_factor))
tensor[0] = size[0]
tensor[1] = size[1]
input_size = (tensor[0].item(), tensor[1].item())
return input_size
# 输入分别是图像,图像标签,随机的尺度
def preprocess(inputs, targets, tsize):
scale_y = tsize[0] / input_size[0]
scale_x = tsize[1] / input_size[1]
if scale_x != 1 or scale_y != 1:
inputs = nn.functional.interpolate(
inputs, size=tsize, mode="bilinear", align_corners=False
)
targets[..., 1::2] = targets[..., 1::2] * scale_x
targets[..., 2::2] = targets[..., 2::2] * scale_y
return inputs, targets
(10)Focalloss
在一些对长尾,和稀有类别特别关注的任务和指标上有所作为。
(11)对抗训练
FGM,PGD,能提点,就是训练慢
(12)Rdrop等对比学习方法
两次前向推断+KL loss约束。
在训练阶段,dropout是开启的,多次推断dropout是有随机性的。模型如果鲁棒的话,同一个样本,即使推断时候,开启dropout,结果也应该差不多。
(13)CPU 和 GPU 之间频繁的数据传输
x = x.cuda(non_blocking=True)
.cuda()的作用是进行数据传输。当 non-blocking=True 时,也就是说数据传输kernel一启动,控制权就直接回到 CPU 上了,即 CPU 不需要等数据从CPU传输到GPU了,可以进行接下来在 CPU 上的操作,但是GPU上的不行。
(14)使用梯度积累
在调⽤ optimizer.step() 之前,只进行反向传播,梯度就会累积。
# 将当前模型参数的梯度置为0
model.zero_grad()
for i, (inputs, labels) in enumerate(training_set):
predictions = model(inputs)
loss = loss_function(predictions, labels)
# accumulation_steps 积累梯度的次数
loss = loss / accumulation_steps
# 反向传播计算梯度
loss.backward()
if (i+1) % accumulation_steps == 0:
# 根据累计的梯度更新网络参数
optimizer.step()
# 清空之前的梯度
model.zero_grad()
if (i+1) % evaluation_steps == 0:
evaluate_model()
(15)使⽤ .as_tensor() 而不是 .tensor()
torch.tensor() 总是会复制数据。如果你要转换⼀个 numpy 数组,使⽤ torch.as_tensor() 或 torch.from_numpy() 来避免复制数据。
(16)cudnn 基准
如果你的模型架构保持不变、输⼊⼤⼩保持不变,设置 torch.backends.cudnn.benchmark=True。
(17)使用梯度 / 激活 checkpointing
Checkpointing 的⼯作原理是⽤计算换内存,并不存储整个计算图的所有中间激活⽤于 backward pass,⽽是重新计算这些激活。可以将其应⽤于模型的任何部分。