on-line hard example mining in object detection

物体检测中的困难样本挖掘,之前有一个名词叫做hard negative mining困难负样本挖掘

先讨论什么是on-line hard example mining(OHEM):例如一个背景,被很大概率预测为前景了,那么这个背景就是困难样本;对于一个前景(ground truth classes为foreground),网络模型预测它很大的概率值为背景,则这个前景也是困难样本,也就是说,在线困难忘本挖掘是不区分正负样本的(但是在代码实现中,由于正样本个数较少,通常保留所有的正样本,然后按照1:3的比例采样出3倍数量的负样本),它希望更多的训练困难样本,而不是简单样本,具体的程序实现在 torchcv中的ssdloss.py中有所体现。
在SSD和FPNSSD编码器中,都是将IOU_max大于0.5的anchor设置为正样本,小于0.5的设置为负样本,

classification target为  -1  ingore   0    negative examples      1  2  3  ~  num_classes   positive examples

然后计算SSDLoss时,也是先计算出所有正样本anchor boxes和所有负样本anchor boxes的classification loss,但是size_average参数设置为False,然后从所有的正负样本中找出所有的困难样本。

ingore(classification target=-1)的classification loss=0

然后取出所有的正样本的loss,并记录当前batch size图像中positive examples总数,再对所有的负样本的classification loss进行排序,取出前3*(num positive examples)个负样本,这是因为负样本的个数要比正样本多出很多。

也就是说,SSD训练过程中的正负样本比例是1:3,只对负样本进行了困难样本挖掘,不对正样本进行困难样本挖掘,这样做的主要原因在于,设置IOU_max>0.5做为正样本,或者说在众多的anchor boxes中,正样本的个数很少,大多数都是负样本,这样会带来严重的类别不平衡问题,故而设定固定比例,取出较为困难(体现在classification loss大)的负样本,计算classification loss。在每次训练过程中的困难负样本都不一样,故而称之为在线困难样本挖掘(因为每次模型参数不一样,negative examples的classification loss排序不同)

class SSDLoss(nn.Module):#损失函数
    def __init__(self, num_classes):
        super(SSDLoss, self).__init__()
        self.num_classes = num_classes#类别总数,对于VOC数据集而言,是21类

    def _hard_negative_mining(self, cls_loss, pos):
        '''Return negative indices that is 3x the number as postive indices.

        Args:
          cls_loss: (tensor) cross entroy loss between cls_preds and cls_targets, sized [N,#anchors].  分类损失值
          pos: (tensor) positive class mask, sized [N,#anchors].

        Return:
          (tensor) negative indices, sized [N,#anchors].
        '''
        cls_loss = cls_loss * (pos.float() - 1)   #对于正样本,损失值为0,得到对于负样本计算出的损失值,损失值越大的负样本,cls_loss值越小
        #正样本损失值   0
        #负样本损失值=之前的负样本损失值*(-1)
        #这是因为_hard_negative_mining只返回所有的负样本classification loss
        #从所有的负样本中采样出前(3*num_positive)个负样本的loss
        #这些负样本的classification loss最大,是困难的负样本

        _, idx = cls_loss.sort(1)  # sort by negative losses
        '''
       cls_loss: [N,#anchors]  正样本的损失值为0,对于负样本,损失值越大,cls_loss越小
       tensor.sort方法返回sort之后的按升序排列的tensor和对应的indices
       对每一行,遍历所有的列,则得到的每一行按照升序排列,即对于每个input images,得到其按照升序排列的分类损失idx
       
       idx同样是[N,#anchors].的tensor,其中的每一行的值范围为  [0,1,2,……,8732]    
       表示当前input image 的所有anchors的负样本的分类损失 由大到小的索引排序
       
       '''
        _, rank = idx.sort(1)      # [N,#anchors]

        num_neg = 3*pos.sum(1)  # [N,]
        #num_neg为长度为batch size 的tensor,其中的每个元素表示3*当前input image中的正样本个数



        neg = rank < num_neg[:,None]   # [N,#anchors]  neg中的数值为1或者0  如果是hard  negative examples,则对应位置处的值为1

        '''
        对于当前batch size张图像中的每一张(每一张图像中的正样本不同)
        找到是当前图像中正样本数量3倍的负样本,并且固定数量的负样本是通过在线困难样本挖掘得到的
        这主要是为了解决计算分类损失函数时样本不均衡的问题,因为比如说SSD300这种模型中8732个default boxes
        中的正样本数量很少(与ground  truth 的overlap大于0.5,在box_coder.encode函数中设置)
        为了保证在同一张图像中的正负样本比例在1:3,故而使用在线困难样本挖掘(在线指的是在训练过程中,这意味着
        在每次训练过程中,每次挖掘到的困难负样本可能是不同的,要根据网络模型预测的输出值决定)
        算法如下:
        首先取出所有的负样本,对于当前batch_size*#anchors  ,对于每一行(每张训练图像)的分类损失值进行排序
        按照当前图像中正样本的数量的3倍取出loss值排在前面的负样本)
        负样本的分类损失值计算:np.log(p)  小    p小,就是说对于负样本预测为背景类的概率值小,就是预测为前景的概率值大
        这些是很容易被分类错的负样本,被称为困难负样本,这些样本的loss值很大,对于网络模型的参数更新非常有效
        而那些很容易就能被分类正确的负样本对于最终权值更新效果不大,故而舍弃
        '''
        return neg

    def forward(self, loc_preds, loc_targets, cls_preds, cls_targets):
        '''Compute loss between (loc_preds, loc_targets) and (cls_preds, cls_targets).
        计算分类损失和回归损失

        Args:
          loc_preds: (tensor) predicted locations, sized [N, #anchors, 4].
          对于当前batch size的图像所预测出来的localization N=batch_size,#anchors表示default boxes的数量
          loc_targets: (tensor) encoded target locations, sized [N, #anchors, 4].
          cls_preds: (tensor) predicted class confidences, sized [N, #anchors, #classes].
          对于当前batch size的图像所预测出来的classification  N=batch_size,#anchors表示default boxe数量,#classes表示数据集类别总数
          cls_targets: (tensor) encoded target labels, sized [N, #anchors].
          batch_size行,#anchors列,第i行第j列的元素表示对于第i个训练样本图像,SSD预测出来的第j个default boxes的GT类别标号(一个int类型整数)

        loss:
          (tensor) loss = SmoothL1Loss(loc_preds, loc_targets) + CrossEntropyLoss(cls_preds, cls_targets).
                              位置回归损失                              交叉熵分类损失
        '''
        pos = cls_targets > 0  # [N,#anchors]  pos中的数值是  0 1
        '''
       cls_targets是经过编码之后的classification ground truth
       表示与ground truth bounding boxes的IOU值最大或者大于一定的阈值的anchor boxes则会被认为是正样本,为1
       负样本为-1 
       
       在encoder阶段,
             
       '''

        batch_size = pos.size(0)#每个batch 中包含多少张训练图片
        num_pos = pos.sum().item()#对pos 2-Dtensor求和,得到当前batch size的训练图片中共有多少个anchor boxes为正样本
        #当前batch size 数量的输入图像中,positive examples(这里的正样本指的是default boxes而不是一整张图像)的数量

        #===============================================================
        # loc_loss = SmoothL1Loss(pos_loc_preds, pos_loc_targets)
        #===============================================================
        mask = pos.unsqueeze(2).expand_as(loc_preds)       # [N,#anchors,4]
        loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=False)#只对正样本进行回归损失的计算
        #mask是# [N,#anchors,4]的3-dimension tensor,扩展的第2维度与之前的数值相同,即对于正样例(batch size中的第i幅图片中的第j个anchors)
        #mask[i,j,:]=1,如果为负样本则mask[i,j,:]=0
        #mask作下标则表示其中元素值为1的下标,即所有的正样本所在的下标(4)

        #===============================================================
        # cls_loss = CrossEntropyLoss(cls_preds, cls_targets)
        #===============================================================
        cls_loss = F.cross_entropy(cls_preds.view(-1,self.num_classes), \
                                   cls_targets.view(-1), reduce=False)  # [N*#anchors*num_classes,]
        '''
       cls_preds:[N,#anchors,num_classes]   view    cls_preds:[(N*#anchors),num_classes]
       cls_targets:[N*#anchors,]
       计算多分类的交叉损失函数是cross_entropy,reduce参数为false,则返回值cls_loss维度为(N*#anchors)
       分别给出了这一个batch size中每张图像所有anchor boxes的分类损失值得
       '''
        cls_loss = cls_loss.view(batch_size, -1)#cls_loss:[N,#anchors]
        cls_loss[cls_targets<0] = 0  # set ignored loss to 0 现将所有负样本的分类损失变成0,这是为了使用hard negative mining算法挑选出困难负样本
        neg = self._hard_negative_mining(cls_loss, pos)  # [N,#anchors]
        cls_loss = cls_loss[pos|neg].sum()
        '''
        正样本具有分类损失和回归损失,SSD中的正样本包括最大的IOU和IOU值大于0.5的region proposal
        一般的负样本没有分类损失,也没有回归损失
        hard negative examples具有分类损失,不具有回归损失
        实际上训练时采用的正负样本是所有的正样本和所有的hard negative examples,   
       '''

        print('loc_loss: %.3f | cls_loss: %.3f' % (loc_loss.item()/num_pos, cls_loss.item()/num_pos), end=' | ')
        loss = (loc_loss+cls_loss)/num_pos
        return loss

这里想先记录一下,老板说在物体检测或者特定物体检测的问题中,数据增强中的尺度变换非常重要,可能直接对于最后的结果带来3%的提升,其次是随机旋转-10~10度,可能带来 1%的提升,最后是常规的水平方向翻转的操作。

猜你喜欢

转载自blog.csdn.net/WYXHAHAHA123/article/details/86670135