Date: 2020/10/12
Coder: CW
Foreword:
近两天仔细看了下 RetinaNet 的 Pytorch 源码实现,发现其中有些编码实现挺值得借鉴的,一部分是模型训练中常用的套路,如学习率策略(lr_schduler)的使用、自定义的数据采样器(Sampler)、Dataloader 中使用自定义 collate_fn、图像缩放、固定 BN 层的统计量、通过权重初始化预测先验概率、coco api 的使用等,另一部分是我自己觉得常用的一些CV算法,如 FPN 在 ResNet 中的使用、Focal Loss 和 Smooth L1 Loss 的实现、mAP 的计算等。CW会在本文中对这些内容进行记录,在方便自己日后温习的同时也可让炼丹者们作为参考。
学习率策略 ReduceLROnPlateau
在模型训练中使用的学习率策略是 ReduceLROnPlateau,CW认为这种策略最大的优点在于能够根据评估指标来调整学习率。通俗地说,就是:
若本次训练迭代(通常是周期,epoch)结束后,取得的评估效果(可以是loss,也可以是其它评估指标如准确率等)优于之前,则保持当前的学习率继续进行下一迭代的训练;否则,在连续经历一定次数迭代后,取得的评估效果都没有变好,就减小学习率再进行下一迭代的训练。
ReduceLROnPlateau 初始化方法的签名如下:
def __init__(self, optimizer, mode='min', factor=0.1, patience=10, verbose=False, threshold=1e-4, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-8)
其中,mode 表示评估指标是越小越好(min)还是越大越好(max);factor 是学习率的衰减系数,必须小于1.0;patience 指连续经历多少个迭代后评估效果都没有变好就调整学习率;threshold 指评估指标至少要变化(根据mode来指示变小或变大)这么多的数值才算取得了更好的效果;threshold_mode 指threshold那部分数值是相对值(rel)还是绝对值(abs),如果是相对值,若mode是min,则评估项至少要不大于之前最优的 (1-threshold) 倍才算取得了更好的效果(从而学习率不需调整);相反地,若mode是max,则评估项至少要不小于之前最优的 (1+threshold) 倍才算取得了更好的效果(从而学习率不需调整)。
在使用时,step()方法需要有个作为评估指标的参数,比如:
optimizer = optim.Adam(retinanet.parameters(), lr=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, patience=3, verbose=True
)
scheduler.step(loss)
在以上示例中,使用loss作为评估指标。若连续3个周期loss都没有下降,那么就会减小学习率。
自定义数据采样器(Sampler)
这个自定义数据采样器的作用是将宽高比相近的图像聚集到同一个batch,它继承了Pytorch自带的Sampler类:torch.utils.data.sampler.Sampler。
Sampler(i)这里的data_source就是 torch.utils.data.Dataset 类的一个实例。
__iter__() 方法是Sampler类中最重要的方法之一,它返回了数据采样后的输出顺序。
Sampler(ii)group_images()方法决定了哪些图片将被分组到同一个批次(batch),作者在data_source对应的类里实现了image_aspect_ratio()方法,它能够返回每张图像的宽高比。因此,由以上代码可知,这个方法先将数据按照图片的宽高比进行排序,然后再根据batch size将它们分组为一个个batch。
Dataloader中使用自定义collate_fn
这个自定义的整理函数(collate_fn)是将一个batch内的图像统一到相同的尺寸,并且将它们对应的标注数量也统一到一致(也就是使得一个batch内各张图片拥有相同数量的标注物体)。
collate_fn(i)图像填充部分的像素值使用0填充,填充的标注物体的位置和类别均使用-1填充,代表这些物体并非图片上实际存在的目标物体。
collate_fn(ii) collate_fn(iii)这样,一个batch内各个张量的shape就变为一致。
图像缩放
这种缩放方式是按比例将图像的短边缩放到不超过608、长边缩放到不超过1024。
resize(i)注意下scale的设置,是取短边比例与长边比例的较小者。这样,宽、高按同一比例缩放,同时使得缩放后短边不超过608,长边不超过1024。
resize(ii)缩放后,还将边长pad到32的整数倍。最后,注意标注框的坐标也要按对应比例缩放。
resize(iii)CW觉得之所以要将边长pad为32的整数倍是因为作者设置anchor负责检测的最小尺寸是32x32。
固定BN层的统计量
简单提一下BN的操作过程,在每次前向过程中,其会统计当前批次数据在各个通道的均值和方(标准)差,然后对该批次的数据做归一化,从而使得数据呈均值为0、方(标准)差为1的分布。
于是,当batch size较小时,统计量难免会与实际整体数据的偏差较大,因此BN的效果就可能不好。在该版本的 RetinaNet 实现中,我看到作者设置的batch size为2,所以他将BN层的统计量给固定住了。backbone使用的是预训练模型,因此训练过程中BN的统计量就固定为预训练后得到的值。
freeze_bn注意,这里的实现方式是将BN层置为eval()模式,在这种模式下,梯度依然能够反向传播,BN层的weight和bias依然会在训练过程中得到更新。
通过权重初始化预测先验概率
RetinaNet 对分类和回归进行了解耦,使用两个不同分支来完成预测。在源码实现中,我看到了一个有意思的地方——对分类模块最后一个卷积层的权重(我这里说的权重不仅仅指weight,还包含了偏差bias)初始化。
权重初始化刚开始看到这里的时候CW是一脸懵逼,直呼:这是神马操作!?冷静下来后,我认真研究了下,发现这招真是妙!通过源码我知道,在这个卷积层后会经过Sigmoid函数将输出转换为概率,Sigmoid函数公式如下:
Sigmoid由于权重初始化为0,那么刚开始时输出就会等于偏差的初始化值,于是我试着将这个值代入到Sigmoid函数公式中,发现得到的结果就是prior!我get到作者的意思了!
这个意思是,作者希望模型一开始时输出的分类预测概率等于一个先验概率,变量名prior恰好就是这个意思,而作者将其设为0.01,也就是说模型一开始时会预测每个anchor都是小概率的前景,通常背景占大部分,那么这些背景产生的loss(-log(1-p))就会相对较小,而遇到一个前景时,产生的loss(-log(p))就会很大。因此,这招可以在训练初始阶段起到平衡正负样本损失的作用,666!
Smooth L1 Loss
以作为分界点,当时使用MSE,否则使用MAE。
Smooth L1 Loss这里主要说下如何确定二次函数(对应MSE公式)和一次函数(对应MAE公式)的系数:
i). 在分界点处,两条曲线相切,导数相等;
ii). 同时,在分界点处,两个函数的值相等
利用以上两条性质解方程组即可。
Focal Loss
先计算各个anchor与GT的IoU,以区分正负样本,进而设置对应的目标类别,正样本为1、负样本为0,还有一批设置为-1不参与计算。我发现许多实现都不会是“非正既负”的现象,而是会将正负样本的区分度变得大一些,这里也是:
IoU<0.4为负样本,IoU>=0.5为正样本,而介于0.4和0.5之间的那批则忽略不计。
这样,正负样本之间有个区分间隔,我估计这么做的目的是使得模型更加容易区分两者,利于训练和收敛。
Focal Loss(i)这里给出 focal loss 的公式,方便对照理解(RetinaNet的原始实现中):
focal loss 公式接下来,设置正样本对应的loss系数为、负样本的为,并且设置focal loss权重:
对于正样本,置信度越高,权重越低;而负样本则是置信度越低,权重越低。
这样,易区分的样本loss就相对较小,从而达到平衡难易样本的目的。
Focal Loss(ii)最后,忽略类别目标值为-1的那些样本产生的loss。
Focal Loss(iii)FPN在ResNet中的应用
FPN在这个年头已经是常规操作了,同时也被玩得花里胡哨,衍生了很多不同的形式,其实现并不难,这里就不详细给出实现的源码了。
FPN我们主要来看看它是如何嵌入到ResNet中使用的。
FPN在ResNet中的应用fpn_sizes是一个list,其中3个元素分别对应FPN的C3、C4、C5的输入通道数,从以上代码可以知道,这里是将ResNet的layer2、layer3、layer4的最后一个block的输出分别作为FPN的C3、C4、C5。
coco api 的使用
coco api 封装了mAP的计算,使用它可以很方便地对模型性能进行评估。
coco api的使用coco_true是 pycocotools.coco.COCO 类的一个实例,其中是数据标注;coco_pred 通过coco_true的loadRes()方法得到,需要加载一个含有预测结果的json文件;coco_eval是 pycocotools.cocoeval.COCOeval 类的一个实例,使用其中的 evaluate()、accumulate()、summarize() 三连即可对预测结果进行评估。
接下来看看含有预测结果的json文件的格式:
json文件内容文件内容由一个包含多个dict的list构成,每个dict的内容如上,其中bbox是(x,y,w,h)的形式,注意,x、y是左上角坐标而非中心坐标。最后,将预测结果写入到json:
json.dump(results, open('{}_bbox_results.json'.format(dataset.set_name), 'w'), indent=4)
set_name是如 train_2017、val_2017 等,指示数据集的类型。
mAP的计算
如果不使用coco api,那么就需要我们自己实现mAP的计算。mAP(mean Average Precision)是各目标类别下平均精度的均值,AP的计算有两种方式,现在常用的是计算PR曲线的面积,这里给出的也是这一种实现。
首先,我们需要分类别统计 TP、FP 以及 TP+FN 即目标物体数量:
mAP(i)区分TP、FP的根据是预测结果与标注的IoU:
mAP(ii)IoU不小于阀值的记为TP,否则为FP。
mAP(iii)np.cumsum()是累加,在这里起到以各置信度为分类阀值统计得到TP和FP的效果。
mAP(iv)接下来就可以计算召回率(recall)和精度(precision)了,:
mAP(v)最后来看看PR曲线面积的计算:
mAP(vi)在描述PR曲线的二维坐标系中,recall作为横轴,precision作为纵轴,先在它们中分别加入两个端点作为哨兵值,由于recall通常随着预测数量增加而增加、precision则通常是先增后减(不严格),因此recall加入的两个端点值分别为0、1,而precision加入的两个端点值均为0。
接着,需要将PR曲线平滑,平滑的方法是将每个recall对应的precision值设置为大于等于当前recall时对应的最大precision值。
然后,由于多个recall可能对应1个precision,因此需要去重,仅保留不同的recall。
最后,PR曲线面积由一个个曲线下的矩形面积相加得到。
PR曲线(来源:https://blog.csdn.net/Xueting_B/article/details/105473923)End
越来越发现,阅读别人的源码实现能学到许多东西,虽然自己写也能写出来,但是别人的实现会提供不同的思路,相当于站在他人的视角下去看待问题、从另一个角度去解决问题,这往往会让你得到新的启发,也能让自己的思路突破原有的束缚。
网友评论