目标检测 样本不平衡

YOLO和SSD可以算one-stage算法里的佼佼者,加上R-CNN系列算法,这几种算法可以说是目标检测领域非常经典的算法了。这几种算法在提出之后经过数次改进,都得到了很高的精确度,但是one-stage的算法总是稍逊two-stage算法一筹,在Focal Loss这篇论文中中,作者认为one-stage精确度不如two-stage是因为下面的原因:

  1. 正负样本比例极度不平衡。由于one-stage detector没有专门生成候选框的子网络,无法将候选框的数量减小到一个比较小的数量级(主流方法可以将候选框的数目减小到数千),导致了绝大多数候选框都是背景类,大大分散了放在非背景类上的精力;

  2. 梯度被简单负样本主导。我们将背景类称为负样本。尽管单个负样本造成的loss很小,但是由于它们的数量极其巨大,对loss的总体贡献还是占优的,而真正应该主导loss的正样本由于数量较少,无法真正发挥作用。这样就导致收敛不到一个好的结果。

既然负样本数量众多,one-stage detector又不能减小负样本的数量,那么很自然的,作者就想到减小负样本所占的权重,使正样本占据更多的权重,这样就会使训练集中在真正有意义的样本上去,相应的有很多 解决正负样本不平衡的方法。

hard negative mining

在SSD和RCNN中,通常都会设置默认框(8732),经过计算交并比,将IOU>0.5的默认框置为正样本,<阈值的样本置为负样本,但是每张图片的gt_truth比较少,所以正负样本严重不平衡,所以使用了hard negative mining这个方法来帮助我们训练。

具体做法:

  1. 首先计算 默认框和gt_truth的交并比,将大于阈值的框设为正样本,剩余为负样本
  2. 计算所有框的交叉熵损失(CE),对负样本的损失进行排序,这个loss不用来更新网络,仅仅为了排序难负样本
  3. 根据正样本数量,设置选取的负样本数量(1:3),然后选择前top_N个负样本,为难负样本
  4. 对正、负样本(筛选后)计算CE_loss,并更新分类损失
  5. 回归损失的话,只计算 正样本的回归,因为负样本回归不具有意义

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中的操作

  1. 目标检测中如何根据有标签的数据划分正负训练集?
    用带标签的图像随机生成图像块,iou大于某一个阈值的图像块做为正样本,否则为负样本。但一般负样本远远多于正样本,为避免训练出来的模型会偏向预测为负例,需要保持样本均衡,所以初始负样本训练集需要选择负样本集的子集,一般正:负=1:3。
  2. 有了正负训练集就可以训练神经网络了。经过训练后,就可以用这个训练出的模型预测其余的负样本了(就是没有加入训练集的那些负样本)。模型在预测一张图像块后会给出其属于正负的概率,在这里设置一个阈值,预测为正的概率大于这个阈值,就可以把这个图像块加入复样本训练集了。
  3. 正样本训练集不变,负样本训练集除了初始的那些,还有新加入的。拿着这个新的训练集,就可以开始新的一轮训练了。(这里运用了解决样本不平衡欠采样的方法之一)
  4. 跳到第二步(这个过程是重复的)

简单来说,就是先用负样本的子集进行训练,这样正负样本比例就是平衡,然后训练第一版网络,拿这个网络去预测没有经过训练的负样本集,筛选出预测大于一定阈值的负样本(即预测错误的负样本),然后把这部分加入负样本集合中(错题集),然后再次训练第二版网络,然后再拿第二版网络更新负样本(错题集),循环训练即可;

OHEM

Focal Loss

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的权重, 这样会使得训练优化过程对更有意义的样本有更高的偏置.
目标检测 样本不平衡_第1张图片
alpha可以平衡正负样本的重要性,但是无法解决简单与困难样本的问题,因此针对难分样本的Gamma也必不可少。Gamma调节简单样本权重降低的速率,当Gamma为0时即为交叉熵损失函数,当Gamma增加时,调整因子的影响也在增加。

  1. 为什么Focal Loss没有用在Two Stage方法上面? 这是因为以RCNN为代表的一系列Two Stage会在区域候选推荐阶段采用两个问题来降低正负样本比例easy example问题带来的影响:
  • 采用NMS算法将候选框降低到一到两千个,更重要的是,这一到两千个可能位置并不是随机选取的,它们移除了大量的易分类负样本(背景框)
  • 采用了biased-minibatch的采样策略, 比如,保证正样本和负样本的比例为1:3进行训练(这其实相当于起到了 alpha 因子的作用

目标检测 样本不平衡_第2张图片
这个解释很棒

目标检测 样本不平衡_第3张图片

你可能感兴趣的:(Pytorch_Mxnet)