YOLO和SSD可以算one-stage算法里的佼佼者,加上R-CNN系列算法,这几种算法可以说是目标检测领域非常经典的算法了。这几种算法在提出之后经过数次改进,都得到了很高的精确度,但是one-stage的算法总是稍逊two-stage算法一筹,在Focal Loss这篇论文中中,作者认为one-stage精确度不如two-stage是因为下面的原因:
正负样本比例极度不平衡。由于one-stage detector没有专门生成候选框的子网络,无法将候选框的数量减小到一个比较小的数量级(主流方法可以将候选框的数目减小到数千),导致了绝大多数候选框都是背景类,大大分散了放在非背景类上的精力;
梯度被简单负样本主导。我们将背景类称为负样本。尽管单个负样本造成的loss很小,但是由于它们的数量极其巨大,对loss的总体贡献还是占优的,而真正应该主导loss的正样本由于数量较少,无法真正发挥作用。这样就导致收敛不到一个好的结果。
既然负样本数量众多,one-stage detector又不能减小负样本的数量,那么很自然的,作者就想到减小负样本所占的权重,使正样本占据更多的权重,这样就会使训练集中在真正有意义的样本上去,相应的有很多 解决正负样本不平衡的方法。
在SSD和RCNN中,通常都会设置默认框(8732),经过计算交并比,将IOU>0.5的默认框置为正样本,<阈值的样本置为负样本,但是每张图片的gt_truth比较少,所以正负样本严重不平衡,所以使用了hard negative mining这个方法来帮助我们训练。
具体做法:
SSD 代码实现
def match(threshold, truths, priors, variances, labels, loc_t, conf_t, idx):
# 返回任意两个box之间的交并比, overlaps[i][j] 代表box_a中的第i个box与box_b中的第j个box之间的交并比.
overlaps = jaccard(
truths,
point_form(priors)
)
# overlaps[gt][default_box] 记录每个GT和每个默认框的交并比
# max(dim=1) 取每个GT最匹配的默认框 [8,1] 若GT_num = 8
# max(dim=0) 取每个默认框最匹配的GT [1,8732]
#[num_objs,1], 得到对于每个 gt box 来说的匹配度最高的 prior box, 前者存储交并比, 后者存储prior box在num_priors中的位置
best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)
#[1, num_priors], 即[1,8732], 同理, 得到对于每个 prior box 来说的匹配度最高的 gt box
best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)
best_truth_idx.squeeze_(0)
best_truth_overlap.squeeze_(0) # 8732
best_prior_idx.squeeze_(1)
best_prior_overlap.squeeze_(1) # num_objs 如 8
#强制 每一个GT都匹配其IOU最大的默认框
#每个默认框都会匹配一个GT,可能一个默认框对于8个GT,交并比都很小,但是还是会取最大的GT作为目标,
best_truth_overlap.index_fill_(0, best_prior_idx, 2)
for j in range(best_prior_idx.size(0)):
best_truth_idx[best_prior_idx[j]] = j
'''
label = torch.tensor([1,2,3,4])
idx = torch.tensor([1,1,1,2,1,2,3])
print(label[idx])
tensor([2, 2, 2, 3, 2, 3, 4])
'''
# 根据前面计算,每个默认框都已经匹配一个GT,将label、box分配给每个默认框作为target, 为后面计算loss使用
matches = truths[best_truth_idx]
conf = labels[best_truth_idx] + 1
# 小于阈值的默认框 直接置为负样本,这个地方会产生大量的负样本,因为每张图片上GT一般都比较少, 需要后面进行负样本挖掘
conf[best_truth_overlap < threshold] = 0 # label as background
#
loc = encode(matches, priors, variances)
loc_t[idx] = loc # [num_priors,4] encoded offsets to learn
conf_t[idx] = conf # [num_priors] top class label for each prior
class MultiBoxLoss(nn.Module):
def forward(self, predictions, targets):
loc_data, conf_data, priors = predictions
num = loc_data.size(0) # batch
num_priors = (priors.size(0))
num_classes = self.num_classes
# loc_t conf_t 分别记录 每张图片(batch)上每个默认框匹配到的GT信息(box,label)
loc_t = torch.Tensor(num, num_priors, 4)
conf_t = torch.LongTensor(num, num_priors)
best_priors = []
# 在图片级别进行GT和默认框匹配算法,不是在batch级别上
for idx in range(num):
# num_objs 为每个图片上的GT数量
truths = targets[idx][:, :-1].data # [num_objs, 4]
labels = targets[idx][:, -1].data # [num_objs]
defaults = priors.data
match(self.threshold, truths, defaults, self.variance, labels,loc_t, conf_t, idx)
pos = conf_t > 0 # label>0 统计正样本数量
num_pos = pos.sum(dim=1, keepdim=True)
# 回归损失 只对正样本进行回归损失
pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
loc_p = loc_data[pos_idx].view(-1, 4)
# 真实标签,经过编码处理,所以网络预测(loc_p)的是相对于GT的平移缩放
loc_t = loc_t[pos_idx].view(-1, 4)
loss_l = F.smooth_l1_loss(loc_p, loc_t, size_average=False)
# 分类损失 需要对负样本进行挖掘,否则大量负样本主导梯度,
# loss_c 用来计算每个样本的CE损失,用来排序选取最难负样本
batch_conf = conf_data.view(-1, self.num_classes)
loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))
# Hard Negative Mining
loss_c = loss_c.view(num, -1)
loss_c[pos] = 0 # 将正样本置零,排序时不考虑正样本
# 排序并选择出 前top_N个负样本,N为 比例*正样本数量
_, loss_idx = loss_c.sort(1, descending=True)
_, idx_rank = loss_idx.sort(1)
num_pos = pos.long().sum(1, keepdim=True)
num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
neg = idx_rank < num_neg.expand_as(idx_rank)
# 这样就计算出了正负样本pos_idx和neg_idx,然后计算CE,并进行梯度更新
pos_idx = pos.unsqueeze(2).expand_as(conf_data)
neg_idx = neg.unsqueeze(2).expand_as(conf_data)
conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, self.num_classes)
targets_weighted = conf_t[(pos+neg).gt(0)]
loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
N = num_pos.data.sum()
loss_l /= N
loss_c /= N
整个代码写的十分清晰,大家可以去仔细研读一下源码,我把主要步骤基本都注释了;
总结:正样本为默认框与真实框根据iou匹配得到,负样本为分类loss值排序得到
注意事项,在匹配时,每个anchor最多匹配一个GT,但是可以多个anchor匹配同一个GT,就像 你只能喜欢一个帅哥,但是可以有很多人同时都喜欢你的帅哥,如果你同时喜欢多个帅哥(同时匹配多个GT),那么你就不知道你的目标到底是什么,网络不会收敛
在RCNN系列中的hard negative mining和这个有点不同,不过不用太在意,理解这个我感觉就可以了,我贴一下RCNN中的操作
简单来说,就是先用负样本的子集进行训练,这样正负样本比例就是平衡,然后训练第一版网络,拿这个网络去预测没有经过训练的负样本集,筛选出预测大于一定阈值的负样本(即预测错误的负样本),然后把这部分加入负样本集合中(错题集),然后再次训练第二版网络,然后再拿第二版网络更新负样本(错题集),循环训练即可;
FL认为取3:1的比例比较粗鲁,对于一些难样本可能游离于3:1边界处,而OHEM会完全忽略这部分样本(作者验证修改这个比例会产生影响),而FL计算了所有样本的loss,由于简单样本(p比较大)数量较多,会占据主导梯度,所以FL设置了权重系数,对于不同的样本设置不同的损失权重系数,难样本权重大,易样本权重小,则可以很好的平衡,即保留了其它负样本的梯度,又不至于让其占主导梯度;
ssd按照ohem选出了loss较大的,但忽略了那些loss较小的easy的负样本,虽然这些easy负样本loss很小,但数量多,加起来的loss较大,对最终loss有一定贡献。作者想把这些loss较小的也融入到loss计算中。但如果直接计算所有的loss,loss会被那些easy的负样本主导,因为数量太多,加起来的loss就大了。也就是说,作者是想融入一些easy example,希望他们能有助于训练,但又不希望他们主导loss。这个时候就用了公式进行衰减那些easy example,让他们对loss做贡献,但又不至于主导loss,并且通过balanced crossentropy平衡类别。
提出了解决正负样本比例和easy example 问题的Focal loss:
核心思想很简单, 就是在优化过程中逐渐减低那些easy example的权重, 这样会使得训练优化过程对更有意义的样本有更高的偏置.
alpha可以平衡正负样本的重要性,但是无法解决简单与困难样本的问题,因此针对难分样本的Gamma也必不可少。Gamma调节简单样本权重降低的速率,当Gamma为0时即为交叉熵损失函数,当Gamma增加时,调整因子的影响也在增加。