Faster_Rcnn误检解决方案—强制负样本策略

1.概述

误检是目标检测领域的一大难点,现有的检测算法都存在误检情况.
误检一般分成两类

1.固定特征的误检.误检对象与正样本存在相似的特征,如将集装箱误检为卡车.
2.没有规律的误检.这类误检没有什么特征可寻,具有一定的随意性,如将地面/天空误检为汽车.

对于第一类误检,我们经常采用的方法是将相应的负样本加入到训练数据集中;这一做法对于yolo系列的算法改善效果较明显【需要保证一定量的负样本】,但在实际应用中,我们发现:

通过添加负样本来优化误检问题这一策略对Rcnn系列的算法的改进效果并不明显

对于漏检问题,比如特定场景下的车辆或行人等,通过增加相应的样本数据同样能够实现较好的优化,而且这个策略对yolo系列和RCNN系列的算法都适应,即:

通过添加正样本来优化漏检问题这一策略对Rcnn系列的算法的改进效果较明显

×××说明:对于数据分布不均匀导致的明显漏检问题,添加样本能够起到较好的优化效果;但不是所有的漏检都能够通过增加样本来解决,如小目标问题、遮挡问题等,这些还是得从算法的角度进行优化;

继续我们关于rcnn误检的思考,总结起来有两个问题:

  1. 为什么yolo加负样本好使而rcnn却反应不佳????
  2. 同样是加样本,为什么rcnn对漏检的改善效果明显,但对误检的改善却不行???

关于这两个问题,我从算法的角度进行了一些解读和理解【个人观点】,其主要原因还是在于两者对正负样本的处理有所区别;anchor_base系列的目标检测算法,如yolov3、faster_rcnn等,在正负样本的处理过程中都会涉及两个重要的步骤:

  1. assigner:为每一个先验框[anchor]分配属性,即这个anchor是正样本、负样本还是忽略不计
  2. sampler:采取某种策略[如随机采样]从分配好的正负样本中选出对应数量的正负样本进行训练

关于assigner和sampler的理解以及更多正负样本的匹配策略,可以参考目标检测之正负样本详解,此处不再展开;

首先关于为什么yolo加负样本好使而rcnn却反应不佳?
我们知道,yolo和faster_rcnn在模型训练过程中会生成很多的anchor,通过assigner这一步操作,这些anchor被分成了正样本、负样本或忽略不计的样本,在正负样本中,负样本的数量往往远远超过正样本的数量,因此我们会再通过sampler操作筛选出一点数量的负样本送到网络中进行训练和学习,也就是说并不是所有的负样本都会送到网络中学习;
图像中的误检目标实际是一个负样本,如果在训练过程中关于这个误检目标的负样本anchor并没有送到网络中,那网络自然也就学不到这个背景特征,而且负样本的数量往往很多,而送去学习的负样本却很少,所以如果采用随机抽样的方法进行选取,误检目标做为负样本被送进网络学习的概率很低;
在实际训练中,关于rpn参数的设置如下【可以自己调整】:

    rpn=dict(
        assigner=dict(
            type='MaxIoUAssigner',
            pos_iou_thr=0.7,
            neg_iou_thr=0.3,
            min_pos_iou=0.2,
            ignore_iof_thr=0.3),
        sampler=dict(
            type='RandomSampler',
            num=256,
            pos_fraction=0.5,
            neg_pos_ub=-1,
            add_gt_as_proposals=False),
        allowed_border=128,
        pos_weight=-1,
        debug=False),

class RandomSampler(BaseSampler):

    def __init__(self,
                 num,
                 pos_fraction,
                 neg_pos_ub=-1,
                 add_gt_as_proposals=True,
                 **kwargs):
        super(RandomSampler, self).__init__(num, pos_fraction, neg_pos_ub,
                                            add_gt_as_proposals)

    @staticmethod
    def random_choice(gallery, num):
        """Random select some elements from the gallery.

        It seems that Pytorch's implementation is slower than numpy so we use
        numpy to randperm the indices.

"""
        assert len(gallery) >= num
        if isinstance(gallery, list):
            gallery = np.array(gallery)
        cands = np.arange(len(gallery))
        np.random.shuffle(cands)
        rand_inds = cands[:num]
        if not isinstance(gallery, np.ndarray):
            rand_inds = torch.from_numpy(rand_inds).long().to(gallery.device)
        return gallery[rand_inds]

    def _sample_pos(self, assign_result, num_expected, **kwargs):
        """Randomly sample some positive samples."""
        pos_inds = torch.nonzero(assign_result.gt_inds > 0)
        if pos_inds.numel() != 0:
            pos_inds = pos_inds.squeeze(1)
        if pos_inds.numel() <= num_expected:
            return pos_inds
        else:
            return self.random_choice(pos_inds, num_expected)

    def _sample_neg(self, assign_result, num_expected, **kwargs):
        """Randomly sample some negative samples."""
        neg_inds = torch.nonzero(assign_result.gt_inds == 0)
        if neg_inds.numel() != 0:
            neg_inds = neg_inds.squeeze(1)
        if len(neg_inds) <= num_expected:
            return neg_inds
        else:
            return self.random_choice(neg_inds, num_expected)

也就是说,用于rpn训练的正负样数一共才256个,而实际的负样本数量达到几万至几十万个设置更多,采用随机抽样的方法,误检目标的负样本被抽中的概率就比较低了;
对应后续阶段的rcnn训练,送到二阶段的目标框是rpn的输出结果,rpn会根据其预测目标框置信度得分,取前12000个结果经过nms操作后,最后得到2000个框【最大值,可能比这个少】送到rcnn阶段进行处理;rcnn对于这些框,同样会经过assigner和sampler操作,最后选出对应数量的正负样本送到网络学习,其参数设置如下:

    rpn_proposal=dict(
        nms_across_levels=False,
        nms_pre=12000,
        nms_post=2000,
        max_num=2000,
        nms_thr=0.7,
        min_bbox_size=0),
    rcnn=dict(
        assigner=dict(
            type='MaxIoUAssigner',
            pos_iou_thr=0.5,
            neg_iou_thr=0.5,
            min_pos_iou=0.5,
            ignore_iof_thr=0.3),
        sampler=dict(
            type='OHEMSampler',
            num=512,
            pos_fraction=0.25,
            neg_pos_ub=-1,
            add_gt_as_proposals=True),
        pos_weight=-1,
        debug=False))

也就是说rpn筛选的2000个框会经过进一步的筛选,最后选出512个框送去训练;
所以经过这两步的操作,误检负样本能够被选中送到网络中学习的概率就相当于小了,这样即使我们加入了对应的负样本数据集,网络也不一定能够学到对应的特征;
以上是faster_rcnn的处理过程,那么yolo又是如何操作的?为什么加入负样本数据集对yolo来说就比较有效。
这是由于yolo会对所有的正负样本都计算一个置信度损失,如下图所示,对于正样本而言其置信度标签C=1,对于负样本而言,其负样本置信度标签C=0;这就相当于对每个正负样本都计算了一个是前景还是背景的二分类操作,这当中就包括了一些包含误检目标的负样本,因此网络能够学到这些误检目标是背景的特征,从而得到改进和优化;

Faster_Rcnn误检解决方案---强制负样本策略
同样是加样本,为什么rcnn对漏检的改善效果明显,但对误检的改善却不行?
同样的处理逻辑,收集漏检场景的数据集,并对漏检目标进行标注;不同之处在于由于漏检目标标注完成后对应的是正样本,经过assigner和sampler能够保证将漏检的正样本送到网络中进行学习,从而实现对网络的优化,最终实现对漏检的改进。
  1. 如何改进Faster_rcnn的误检问题?

既然知道了问题缘由就是误检目标的负样本很难被选择送到网络中学习,我们就可以对症进行改进:

方案1:修改超参数设置

我们前面提到了rpn阶段用于训练的正负样本数量一个才256个,rcnn用于训练的样本数才512个,那我们可不可以增大这个参数,这样一来,误检目标的负样本被选中的概率就会增加;
理论上是这样的,但是我们前面提到了实际的负样本数量一般是几万到几十万甚至更多,如果希望误检目标被选中,那这个数量得增加多少才有效?如果只是简单得增加几倍,这样是否会有效?
那么我们取一个极值,将所有得负样本都送去训练是否可行呢?第一,这样操作会显著增加计算量和训练时间,让本来在速度上处于劣势的faster_rcnn变得更慢;第二,如果这样做,其实这已经不能算faster_rcnn了,faster_rcnn精度高的一个重要原因在于正负样本的比例控制的很好,也就是送去学习的负样本数量不会比正样本多很多,而如果我们把所有的负样本都送去学习,负样本的loss则会主导模型的训练,模型的精度会显著下降
笔者在这方面也做过一些简单试验,单纯扩大3-4倍的样本数,对误检的优化仍然取不到啥效果;

方案2:强制负样本策略

这一方案是笔者自创的方法。所谓的强制负样本策略,就是在数据标注过程中将误检目标也进行标注并给一个单独的类别【如背景类】,这个类别在assigner阶段的处理与其它正样本类似,通过iou计算也会找到一些与之匹配的anchor,我们将这些anchor进行特殊标记处理;在sampler选择负样本时,我们将这些特殊标记的anchor拿出来,强制做为负样本优先进行选取,这样就可以保证包含误检目标的负样本一定能够送到网络中学习;
关于网络部分的核心修改,主要还是在assigner和sampler阶段,核心修改代码主要如下:


 def assign_wrt_overlaps(self, overlaps, gt_labels=None):
        """Assign w.r.t. the overlaps of bboxes with gts.

        Args:
            overlaps (Tensor): Overlaps between k gt_bboxes and n bboxes,
                shape(k, n).

            gt_labels (Tensor, optional): Labels of k gt_bboxes, shape (k, ).

        Returns:
            :obj:AssignResult: The assign result.

"""

        num_gts, num_bboxes = overlaps.size(0), overlaps.size(1)

        assigned_gt_inds = overlaps.new_full((num_bboxes, ),
                                             -1,
                                             dtype=torch.long)
        if num_gts == 0 or num_bboxes == 0:
            # No ground truth or boxes, return empty assignment
            max_overlaps = overlaps.new_zeros((num_bboxes, ))
            if num_gts == 0:
                assigned_gt_inds[:] = 0
            if gt_labels is None:
                assigned_labels = None
            else:
                assigned_labels = overlaps.new_zeros((num_bboxes, ),
                                                     dtype=torch.long)
            return AssignResult(
                num_gts,
                assigned_gt_inds,
                max_overlaps,
                labels=assigned_labels)

        # for each anchor, which gt best overlaps with it
        # for each anchor, the max iou of all gts
        max_overlaps, argmax_overlaps = overlaps.max(dim=0)
        # for each gt, which anchor best overlaps with it
        # for each gt, the max iou of all proposals
        gt_max_overlaps, gt_argmax_overlaps = overlaps.max(dim=1)

        # 2. assign negative: below
        if isinstance(self.neg_iou_thr, float):#将与gt iou大于0小于负样本阈值的anchor设置为0【即负样本】
            assigned_gt_inds[(max_overlaps >= 0)
                             & (max_overlaps < self.neg_iou_thr)] = 0
        elif isinstance(self.neg_iou_thr, tuple):
            assert len(self.neg_iou_thr) == 2
            assigned_gt_inds[(max_overlaps >= self.neg_iou_thr[0])
                             & (max_overlaps < self.neg_iou_thr[1])] = 0

        # 3. assign positive: above positive IoU threshold
        pos_inds = max_overlaps >= self.pos_iou_thr
        assigned_gt_inds[pos_inds] = argmax_overlaps[pos_inds] + 1 #将与gt iou大于正样本阈值的anchor设置为对于的正样本索引【即正样本】

        # 4. assign fg: for each gt, proposals with highest IoU
        for i in range(num_gts):
            if gt_max_overlaps[i] >= self.min_pos_iou:
                if self.gt_max_assign_all:  #true
                    max_iou_inds = overlaps[i, :] == gt_max_overlaps[i]
                    assigned_gt_inds[max_iou_inds] = i + 1
                else:
                    assigned_gt_inds[gt_argmax_overlaps[i]] = i + 1

        if gt_labels is not None:
            force_neg_ind = ((gt_labels[argmax_overlaps] == 0) & (assigned_gt_inds > 0))

            assigned_gt_inds[force_neg_ind] = -2
            assigned_labels = assigned_gt_inds.new_zeros((num_bboxes, ))
            pos_inds = torch.nonzero(assigned_gt_inds > 0).squeeze()
            if pos_inds.numel() > 0:
                assigned_labels[pos_inds] = gt_labels[
                    assigned_gt_inds[pos_inds] - 1]
            for i in range(num_gts):
                if gt_labels[i] == 0:
                    num_gts -= 1
        else:
            assigned_labels = None

        return AssignResult(
            num_gts, assigned_gt_inds, max_overlaps, labels=assigned_labels)

    def _sample_neg(self, assign_result, num_expected, **kwargs):
        neg_inds_1 = torch.nonzero(assign_result.gt_inds == -2)

        neg_inds_2 = torch.nonzero(assign_result.gt_inds == 0)

        neg_inds_1 = neg_inds_1.squeeze(1)
        if neg_inds_2.numel() != 0:
            neg_inds_2 = neg_inds_2.squeeze(1)
        if neg_inds_1.numel() <= num_expected:
            num_expected -= neg_inds_1.numel()
            if len(neg_inds_2) <= num_expected:
                return torch.cat([neg_inds_1 + neg_inds_2],dim=0)
            else:
                return torch.cat([neg_inds_1,self.random_choice(neg_inds_2,num_expected)],dim=0)
        else:
            return self.random_choice(neg_inds_1, num_expected)

通过以上强制负样本策略,能够保证误检的背景特征都能够送到网络中学习,从而得到改善和优化;
笔者在自己的项目中也进行了试验,通过引入这一方法,固定特征的误检改善十分明显;但有两点需要注意:

  1. 这一策略还带来一些额外的计算,训练时间会略有增加【不会很多】,编写代码时最好进行兼容,正常训练时建议关掉此功能;
    2.这个策略的引入也需要一定量的数据才能取得较好的效果,笔者的训练数据大概有十万左右,标注的负样本数据集大概1500,并进行了一倍过采样;

Original: https://blog.csdn.net/qq_44804542/article/details/122016565
Author: 硝烟_1994
Title: Faster_Rcnn误检解决方案—强制负样本策略

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/686850/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球