迁移学习
多数情况,想要较好地复现他人的训练精度,是需要借助他人提供的主干网络权重的,如复现Yolo则需要获取对应版本的Darknet53主干网络权重,因为特征是通用的。而从0开始训练目标检测等较为复杂的网络,首先需要的是良好的学习率策略,其次也会因权值初始化带来的不稳定性让结果变得较为不可控。
下方将展示通常情况下Torch迁移学习的代码实现:
model_dict = model.state_dict()
pretrained_dict = torch.load(model_path, map_location=device)
load_key, no_load_key, temp_dict = [], [], {}
for k, v in pretrained_dict.items():
if k in model_dict.keys() and np.shape(model_dict[k]) == np.shape(v):
temp_dict[k] = v
load_key.append(k)
else:
no_load_key.append(k)
model_dict.update(temp_dict)
model.load_state_dict(model_dict)
model是torch框架下搭建的网络,利用state_dict方法使其变为字典形式。而后通过load方法载入已有的权重(一般为pth后缀)。遍历载入的字典格式的权重文件,首先判定两个字典是否存在同样的键,而后查看相同键对应的值是否具有相同的shape,满足条件后给temp_dict字典加上同样的键与值,代码中load与no_load列表非必须,可用于记录载入权值文件中被载入与未被载入的部分。
当然前提条件是载入的pth文件需要和搭建的网络部分具有相同的结构,即保证其结构、层数相同才能进行迁移学习。
学习率策略
(1)常用优化器及初始学习率设置
在学习了yolo,ssd等目标检测网络后,发现较为常用的是sgd和adam优化器,sgd是过去十分常用的优化器,但收敛较慢。adam通常情况下可能较易于收敛,但是也有其问题,具体可参考以下博客来对比两种优化器的差异。
而关于初始学习率,对于目标检测网络而言,SGD的初始学习率为1e-2,Adam为1e-3或6e-4。当然这是仅供参考的大致范围。不具有很好的普适性。
下图为学习率调整与优化器设置。学习率自适应调整是在yolo里使用的方式,除了初始设置,也会与batch size相关联。相当于在yolo中输入optimizer的初始学习率和学习率变化策略不仅是初始设置,也和batchsize相关。
参照作者建议来设置, 选用adam,init_lr为1e-3,batch_size为16,最终结果Init_lr_fit为3e-4。其实直接自行设置也并非不可行。
nbs = 64
lr_limit_max = 1e-3 if optimizer_type == 'adam' else 5e-2
lr_limit_min = 3e-4 if optimizer_type == 'adam' else 5e-4
# Init_lr * 0.01 = Min_lr
Init_lr_fit = min(max(batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max)
Min_lr_fit = min(max(batch_size / nbs * Min_lr, lr_limit_min * 1e-2), lr_limit_max * 1e-2)
同时也在设置优化器上,进行了小设计。
pg0, pg1, pg2 = [], [], []
for k, v in model.named_modules():
if hasattr(v, "bias") and isinstance(v.bias, nn.Parameter):
pg2.append(v.bias)
if isinstance(v, nn.BatchNorm2d) or "bn" in k:
pg0.append(v.weight)
elif hasattr(v, "weight") and isinstance(v.weight, nn.Parameter):
pg1.append(v.weight)
optimizer = {
'adam': optim.Adam(pg0, Init_lr_fit, betas=(momentum, 0.999)),
'sgd': optim.SGD(pg0, Init_lr_fit, momentum=momentum, nesterov=True)
}[optimizer_type]
optimizer.add_param_group({"params": pg1, "weight_decay": weight_decay})
optimizer.add_param_group({"params": pg2})
当然也可以使用较为简单的方式,如直接将model.parameters作为optimezer的第一个参数进行初始化。如下所示是pfld代码的优化器初始化,是和上方的add_param_group作用相同,但细节不同,一个是两个分支网络分开,一个是将网络的bias, weight,和BN层分开。
pfld_backbone = PFLDInference().to(device)
auxiliarynet = AuxiliaryNet().to(device)
criterion = PFLDLoss()
optimizer = torch.optim.Adam([{
'params': pfld_backbone.parameters()
}, {
'params': auxiliarynet.parameters()
}],
lr=args.base_lr,
weight_decay=args.weight_decay)
(2)训练学习率策略
训练过程中通常学习率变化可分为两个阶段, warm_up阶段和学习率下降阶段。warmup和学习率下降的意义可另行查询,此处仅介绍用法和实现。同样以yolo为例,来介绍其cos与线性学习率策略。
首先,输入参数为经过上述策略或自行得到的最终初始学习率和最小学习率(一般二者为0.01的关系),lr_decay_type为选择使用哪一种策略,total_iters为总迭代次数,warmup_iters_ratio为自适应计算warmup的epoch个数的系数,warmup_lr_ratio为计算warmup过程最开始学习率的系数。现在我们计算出来了需要几个epoch进行warmup并计算得到了no_aug的epoch数(最后一段需要学习率不变的epoch数),并把上述所有参数利用partial方法得到已选定大部分参数的yolox_warm_cos_lr函数,唯一所需参数为当前迭代数iters。
WARM_UP阶段:我们设iter为x,正式开始学习率为lr1, warmup开始学习率为limit(极小值),总warmup的epoch数为e1。warmup阶段学习率变化如下所示:
中间阶段:设最小学习率为lr2, 设学习率不变的epoch数为e2, 总epoch数为epoch, 学习率变化如下所示:
最后阶段:学习率恒定为minlr 即为开始训练的lr * 0.01。
通过计算得到no_augepoch = 5, warmup epoch = 3, warmup_start_lr = 5e-5, lr = 3e-4, minlr = 3e-6 按照上述数据绘制了下方图片
WARMUP:
TRAINING:
def get_lr_scheduler(lr_decay_type, lr, min_lr, total_iters, warmup_iters_ratio=0.05, warmup_lr_ratio=0.1,
no_aug_iter_ratio=0.05, step_num=10):
def yolox_warm_cos_lr(lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter, iters):
if iters <= warmup_total_iters:
# lr = (lr - warmup_lr_start) * iters / float(warmup_total_iters) + warmup_lr_start
lr = (lr - warmup_lr_start) * pow(iters / float(warmup_total_iters), 2) + warmup_lr_start
elif iters >= total_iters - no_aug_iter:
lr = min_lr
else:
lr = min_lr + 0.5 * (lr - min_lr) * (
1.0 + math.cos(
math.pi * (iters - warmup_total_iters) / (total_iters - warmup_total_iters - no_aug_iter))
)
return lr
def step_lr(lr, decay_rate, step_size, iters):
if step_size < 1:
raise ValueError("step_size must above 1.")
n = iters // step_size
out_lr = lr * decay_rate ** n
return out_lr
if lr_decay_type == "cos":
warmup_total_iters = min(max(warmup_iters_ratio * total_iters, 1), 3)
warmup_lr_start = max(warmup_lr_ratio * lr, 1e-6)
no_aug_iter = min(max(no_aug_iter_ratio * total_iters, 1), 15)
func = partial(yolox_warm_cos_lr, lr, min_lr, total_iters, warmup_total_iters, warmup_lr_start, no_aug_iter)
else:
decay_rate = (min_lr / lr) ** (1 / (step_num - 1))
step_size = total_iters / step_num
func = partial(step_lr, lr, decay_rate, step_size)
return func
上述详细讲述了cos情况下warmup和training时学习率策略,至于线性时情况则十分简单,将首位学习率线性连接即可。