ohem代码理解+anchor

对应github地址,pytorchb版本:

https://github.com/gurkirt/FPN.pytorch1.0

数据预处理

train_dataset = Detection(args, train=True, image_sets=args.train_sets, 
                            transform=Augmentation(args.input_dim, args.means, args.stds))#进行数据增强

进入Augmentation()

anchor系列

1。构造anchor

class anchorBox(object):
    """
    Compute anchorbox coordinates in center-offset form for each source
    feature map.
    共构造出75*75+38×38+19×19+10×10+5×5=7555个特征点,每个特征点上构造3个不同宽高比的box, 共有7555*3=22665个box    
    """
    def __init__(self, input_dim=600, dataset = 'coco', default_ratios= 3):
        super(anchorBox, self).__init__()
        # self.type = cfg.name
        self.image_size = input_dim
        self.anchor_set = dataset
        self.ar = default_ratios
        self.base_set = anchor_boxes_kmeaned[dataset]
        self.feature_size = feature_sizes[str(input_dim)]

    def forward(self):
        anchors = []
        for k, f in enumerate(self.feature_size):#[75, 38, 19, 10, 5],对于600*600的图片,先缩小8倍,构造出75*75的特征图,再缩小2倍,构造出38*38....
            for i, j in product(range(f), repeat=2):#第一次,遍历75*75特征图中的每一个点,共有5625个点                
                f_k = 1 / f#除以75,大特征图的边长
                # unit center x,y
                cx = (j + 0.5) * f_k #对于75*75中的每一个方格,把中心移动到方格的中心,再除以大特征图的边长,进行归一化
                cy = (i + 0.5) * f_k 
                for r in range(self.ar):  
                    anchors.append([cx, cy, self.base_set[k*3+r,2], self.base_set[k*3+r,3]])
        output = torch.FloatTensor(anchors).view(-1, 4)#reshape为4列的数组,代表一个个的box,output的维度为(22665, 4)
        # output.clamp_(max=1, min=0)
        return output

注意:这里每个anchor的构造[centx,centy,w,h]这里每个anchor的宽高是根据kmeans聚类得到的,比如我们对yolo数据集合可以聚类得到15个box的宽高,就是实际大部分目标的宽高,再换算到600的尺寸上(因为上面的centx,centy已经归一化过了),因为考虑到FPN时,前面的大特征图是预测小目标的,后面的小特征图是预测大目标的,所以对应的是前面的小anchor和后面的大点的anchor,这个代码写的更具体一些,kmeans实现可参考https://github.com/qqwweee/keras-yolo3/blob/master/kmeans.py

2。anchor与gt_boxes的匹配match,modules/box_utils.py-->match_anchors(gt_boxes, gt_labels, anchors)

函数功能:对一张图片中的所有anchors匹配出标签conf和位置loc,现在假设这张图片上有2个人,3个狗5个目标,记为gt1,gt2,gt3,gt4,gt5

入参:

gt_boxes,一张图片上所有归一化后的box坐标[xmin,ymin,xmax,ymax],

gt_labels:一张图片上所有目标的标签,比如=[人,人,狗,狗,狗]=[5,5,10,10,10],注意这里的label都没有进行one-hot编码

anchors: 根据600*600的图片尺寸构造出的的22665个anchor,这些anchor的大小,位置是固定的,不会随着图片的改变而改变

step1: 现在假设要为某个anchor_j匹配其标签,计算这个anchor_j与所有gt_boxes之间的ious=[iou1,iou2,iou3,iou4,iou5],即

       case1: 所有的ious都=0,即这个anchor_j与实际标注没有任何交集,则这个anchor_j的标签conf=0,代表是背景

      case2: 假设ious中的最大值为iou3,                  

                  若iou3<iou_threshold=0.5,说明这个anchor_j尽管覆盖了某些目标,但覆盖的较少,比如只覆盖了人的某只手,这时这个anchor_j的标签还是conf=0,代表是背景

                  若iou3>iou_threshold=0.5,说明这个anchor_j覆盖了某个目标,到底是哪个目标呢?就看iou3代表的是anchor_j与哪个gt_boxes最大,这个gt_boxes所对应的标签就成为这个anchor_j的标签。

step 2: 为每个anchor匹配gt_box,看下图所示

 可以看出对同一个anchor,有可能横向和纵向取最大值时,得出了不同的匹配,哪到底采用哪个匹配呢?代码的做法如下

1。 先纵向取出最大值,即先为每个anchor匹配一个比较好的gt_box,若当前anchor与每个gt_box的iou都=0,先给其匹配gt_box1的坐标,类别也匹配gt_box1的类别,比如上图,为anchor a4找到了匹配gt5

2。横向来看的话,若gt_box1从22665个anchor中找的最佳匹配是a4, 那么问题就来了,现在对同一个a4,上一步找到了一个匹配gt5,这里又有一个是gt1,尽管对于anchor a4来说,其与gt5的重合度>其与gt1的重合度,为保证gt1一定要有一个anchor与其对应(即一定要有一个回归目标),我们这里把anchor a4的匹配更换为gt1.,

3。对每个anchor找到其最佳匹配后,根据匹配的gt_box的坐标,计算偏差,具体为

   先找出匹配gt_box的中心坐标和宽,高,[cent_x,cent_y,w,h], 再与anchor本身的中心和宽高做差得到

 loc=[error_x,erro_y,error_w,error_h],即loc中记录的是gt_box与anchor之间的中心点和宽高的偏移量。

难样本挑选

先看下图片所示

程序实现

import torch
import math
import numpy as np
'''
目的,从给出的所有样本中,保留所有的正样本,同时按照1:3的比例,挑选出负样本,具体那些负样本会被选中,按照loss值从大到小排序选取
遗留问题,取法不好明白
'''
labels=[0,1,0,0,
        0,0,0,2,
        0,0,0,0]#模拟每个anchor的标签
labels=torch.from_numpy(np.array(labels))

loss=[1.,2,3,4,
      5,6,7,8,
      9,10,11,12]#模拟每个anchor的loss值,这里仅仅示范一下,实际上都应该在0-1之间

loss= torch.from_numpy(np.array(loss))
neg_pos_ratio=3#正负样本比例1:3
pos_mask = labels > 0#挑选出正样本
num_pos = pos_mask.long().sum(dim=0, keepdim=True)  # 求和,找出正样本个数
num_neg = num_pos * neg_pos_ratio  # 根据正负样本比例1:3,确定负样本的个数
loss[pos_mask] = -math.inf #正样本的anchor,赋值为负无穷大,这里是在筛选负样本,以保证正阳本不会被筛选到
new_loss, indexes = loss.sort(dim=0, descending=True)  # 对loss按照从大到小排队,new_loss记录了排序后什么样子,indexes记录了排队后的元素在原始列表中的位置

print('new_loss',new_loss)#[12., 11., 10.,  9.,  7.,  6.,  5.,  4.,  3.,  1., -inf, -inf],
print('indexes',indexes)#[11, 10,  9,  8,  6,  5,  4,  3,  2,  0,  1,  7]#这里期望取前面的6个index
aa, orders = indexes.sort(dim=0)  # 再对index排下队有什么意义呢?
print('aa',aa)#[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]
print('orders',orders)#[ 9, 10,  8,  7,  6,  5,  4, 11,  3,  2,  1,  0]

neg_mask = orders < num_neg

selected_index=pos_mask | neg_mask #或运算,任何一个为true时,比较结果就是true
print('pos_mask',pos_mask)#[False,  True, False, False, False, False, False,  True, False, False, False, False]
print('neg_mask',neg_mask)#[False, False, False, False, False,  True,  True, False,  True,  True, True,  True]
print(selected_index)#     #([False,  True, False, False, False,  True,  True,  True,  True,  True,True,  True])
selected_sample=labels[selected_index]

print(selected_sample)#[1, 0, 0, 2, 0, 0, 0, 0],可以看出这里除了两个正样本之外,还选择了2*3=6个负样本,


在git上的实现为modules/box_utils.py/hard_negative_mining()

def hard_negative_mining(loss, labels, neg_pos_ratio):#ohem难样本挖掘
    """
    It used to suppress the presence of a large number of negative prediction.
    It works on image level not batch level.
    For any example/image, it keeps all the positive predictions and
     cut the number of negative predictions to make sure the ratio
     between the negative examples and positive examples is no more
     the given ratio for an image.
    Args:
        loss (N, num_anchors): the loss for each example.N=batch_size,预测值,经过了softmax
        labels (N, num_anchors): the labels.#真实值,这里到底是让回归什么呢?
        neg_pos_ratio:  the ratio between the negative examples and positive examples.
    """   
    pos_mask = labels > 0#这里的label其维度为(batch_size,22665),具体元素取值为0,1,2,...,class_num+1, >0是代表正样本,并没有进行one-hot编码,   
    num_pos = pos_mask.long().sum(dim=1, keepdim=True)#求和,找出每张图片上正样本个数    
    num_neg = num_pos * neg_pos_ratio#根据正负样本比例1:3,确定负样本的个数   
    loss[pos_mask] = -math.inf #把预测值的对应位置,即pos_mask对应位置=True,是正样本的anchor,赋值为负无穷大
    _, indexes = loss.sort(dim=1, descending=True)#对loss按照从大到小排队,indexes记录了排队后的元素在原始列表中的位置    
    _, orders = indexes.sort(dim=1)#再对index拍下队有什么意义呢?    
    neg_mask = orders < num_neg   
    return pos_mask | neg_mask

总体的损失函数实现为

class MultiBoxLoss(nn.Module):
    def __init__(self, neg_pos_ratio=3):
        """Implement SSD Multibox Loss.
        Basically, Multibox loss combines classification loss
         and Smooth L1 regression loss.
        """
        super(MultiBoxLoss, self).__init__()
        self.neg_pos_ratio = neg_pos_ratio #正负样本比例=1:3

    def forward(self, confidence, predicted_locations, gts, anchors):
        # cls_out, reg_out, anchor_gt_labels, anchor_gt_locations
        
        """


        Compute classification loss and smooth l1 loss.
        Args:
            confidence (batch_size, num_anchors, num_classes): class predictions.
            predicted_locations (batch_size, num_anchors, 4*seq_len): predicted locations.
            gts (batch_size, num_anchors): real labels of all the anchors.指一张图片中目标的实际位置和类别,比如一张图片中有1个人,在batch_size=1时,

           gts= [tensor([[ 0.4896,  0.2048,  0.6805,  0.6860, 11.0000]], device='cuda:0'),

            anchors(batch_size, num_anchors, 4*seq_len): real boxes corresponding all the anchors.维度为[batch_size,22665 4],

        """
        
        num_classes = confidence.size(2)#batch_size=4, 在train.py中设置,num_classes=21,voc共有20类,+背景num_anchors=22665,这个如何计算出来的,
        #答:在modules/anchor_box_kmeans.py中规定了feature_sizes['600'] = [75, 38, 19, 10, 5],即把输入图片都resize到了(600,600)大小,然后缩小8,2,2,2,得到
        #图像金字塔,共构造了(75*75*+38*38+19*19+10*10+5*5)* 3=22665个anchor

        # print('confidence',confidence.size(0),confidence.size(1),confidence.size(2))
        gt_locations = []
        labels = []
        # print('anchors',anchors.size(0),anchors.size(1))
        with torch.no_grad():#这里面的数据不需要计算梯度,也不会进行反向传播
            # torch.cuda.synchronize()
            # t0 = time.perf_counter()
            for b in range(len(gts)):#=batch_size
                gt_boxes = gts[b][:,:4]#取出每个batch ,即每张图片的所有anchors 的box坐标,这里是(xmin,ymin,xmax,ymax)还是?
                gt_labels = gts[b][:,4]#anchor 的label
                gt_labels = gt_labels.type(torch.cuda.LongTensor)#为什么要换成这种数据格式呢?
                conf, loc = box_utils.match_anchors(gt_boxes, gt_labels, anchors)

                labels.append(conf)
                gt_locations.append(loc)
            gt_locations = torch.stack(gt_locations, 0)
            labels = torch.stack(labels, 0)
            # torch.cuda.synchronize()
            # t1 = time.perf_counter()
            # print(gt_locations.size(), labels.size(), t1 - t0)
            # derived from cross_entropy=sum(log(p))
            loss = -F.log_softmax(confidence, dim=2)[:, :, 0]#预测值,
            mask = box_utils.hard_negative_mining(loss, labels, self.neg_pos_ratio)
        
        # pdb.set_trace()
        confidence = confidence[mask, :]
        classification_loss = F.cross_entropy(confidence.reshape(-1, num_classes), labels[mask], reduction='sum')#labels[mask]只选取了那些loss
        #值比较大的负样本进行计算
        pos_mask = labels > 0
        predicted_locations = predicted_locations[pos_mask, :].reshape(-1, 4)#因为负样本中没有目标,所以不可能有box,所以box回归中不需要负样本筛选
        gt_locations = gt_locations[pos_mask, :].reshape(-1, 4)
        smooth_l1_loss = F.smooth_l1_loss(predicted_locations, gt_locations, reduction='sum')
        num_pos = pos_mask.sum()
        return smooth_l1_loss/num_pos, classification_loss/num_pos

位置损失的计算采用了smooth_L1_Loss损失函数,

smooth_L1_Loss也叫Huber Losses, 是Faster rcnn提出来的计算距离的loss,文章中提到对噪声点更加鲁棒,其在pytorch中的实现为

smooth_l1_loss(input, target, size_average=None, reduce=None, reduction='mean')

误差在(-1,1)上是平方损失,其他情况是L1损失

loss(x,y)=\frac{1}{N}\left\{\begin{matrix} \frac{1}{2} (x_i-y_i)^2&if ~~|x_i-y_i|<1 \\ |x_i-y_i|-\frac{1}{2}&otherwise \end{matrix}\right.

网络预测部分

网络预测结果

 loc_data, sf_conf = net(images)

其中loc_data的维度为 [1 22665],也就是对一张600*600的图片,预测出22665个位置box,按照原来的理解,每个

box=[error_x,error_y,error_w,error_h]

也就是预测了与人为构造的22665个anchor的中心点和宽高偏差.

主要的特征提取与解析部分在demo.py-->extract_boxes()

def extract_boxes(args, images, net, softmax, anchors):

    
    images = images.cuda(0, non_blocking=True)
    
    loc_data, sf_conf = net(images)#位置的预测loc_data,维度(1,22665,4),类别预测loc_data维度(1 22665 21)
    sf_conf = torch.softmax(sf_conf, 2)#分类得分,维度(1, 22665, 21)
    
    b=0
    decoded_boxes = decode(loc_data[b], anchors, [0.1, 0.2])#loc_data[b]的维度为(22665 4),根据网络预测,解码预测的box在600*600的图片上的相对位置
    # decoded_boxes中元素大概如下[ 0.6798,  0.5099,  1.1242,  1.2886]
    #sf_conf[b,:, 0]取出来了sf_conf的第一列,第一列表征的是这个box是背景的概率,若是背景的概率大,当然就是包含物体的概率小,所以1-sf_conf[b,:, 0]
    #之后,计算出了这个box中有没有物体的概率
    obj_boxes, obj_ids, cls_scores, bin_scores = apply_nms(decoded_boxes, 1-sf_conf[b,:, 0], sf_conf[b], 10, args.conf_thresh, args.nms_thresh)

    return  obj_boxes, cls_scores, bin_scores, obj_ids

其中,根据预测的类别值,通过激活函数softmax计算每个box的分类得分

sf_conf = torch.softmax(sf_conf, 2)

step1:解码预测的box在600×600的图像上的相对位置,函数modules/box_utils.py-->decode(loc,anchors,variances)

      1。根据原来encode过程,网络回归目标pred-->=loc=[gt-anchor]=[gt_x-an_x,gt_y-an_y,log(gt_w/an_w),log(gt_h/an_h)],即理想情况下,网络预测的是真实box与anchor的中心偏差和宽高偏差,那么预测时,目标的实际位置

    gt_box=pred+anchor=[pred_x+an_x,pred_y+an_y,exp(pred_w)*an_w,exp(pred_h)*an_h]

其中:pred_x,pred_y ,pred_w,pred_h分别为预测的box的中心点x坐标,y坐标,box宽,box高;

          an_x,an_y,an_w,an_h分别为人为预先构造的固定的anchor的中心的x坐标,y坐标,anchor宽,anchor高;

def decode(loc, anchors, variances):
    """Decode locations from predictions using anchors to undo
    the encoding we did for offset regression at train time.
    Args:
        loc (tensor): location predictions for loc layers,
            Shape: [num_anchors,4]
        anchors (tensor): anchor boxes in center-offset form.
            Shape: [num_anchors,4].
        variances: (list[float]) Variances of anchorboxes
    Return:
        decoded bounding box predictions
    """
    #pdb.set_trace()
    boxes = torch.cat((
        anchors[:, :2] + loc[:, :2] * variances[0] * anchors[:, 2:],
        anchors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1) #box=[cent_x,cent_y,w,h]
    boxes[:, :2] -= boxes[:, 2:] / 2 #xmin=centx-w/2, ymin=cent_y-h/2,经过这一步,box已经更新为box=[xmin,ymin,w,h]
    boxes[:, 2:] += boxes[:, :2] #xmax=xmin+w, xmax=ymin+h
    return boxes

step2: 对上述的22665个预测box做nms, demo.py-->apply_nms(boxes,scores,sf_conf,num_nodes=10,conf_thresh=0.01,nms_thresh=0.5)

        入参:boxes预测的box的位置

                 scores:预测的每个box的置信度,其计算方式为score=1-sf_conf[b,:, 0],sf_conf[b,:, 0]取出来了sf_conf的第一列,第一列表征的是这个box是背景的概率,若是背景的概率大,当然就是包含物体的概率小,所以1-sf_conf[b,:, 0]之后,计算出了这个box中有没有物体的概率

                 sf_conf:预测的box的类别得分,维度 [22665,21]

step3:根据解析出的box,根据置信度,画出预测的结果

 

            for ik in range(obj_boxes.size(0)):
                if bin_scores[ik]>args.final_thresh: ## only plot boxes that has higher score than this threshold
                    count += 1 #bin_scores反映的是这个box的置信度
                    scores = cls_scores[ik, 1:].squeeze().cpu().numpy()#cls_scores本质是网络的预测值,维度[num_box,21],第一列表示置信度,后面的是具体术语哪个类的概率
                    win_class = np.argmax(scores)#找出这个box是哪个类别
                    win_label = args.classes[win_class]
                    box = obj_boxes[ik,:].cpu().numpy()
                    x1 = box[0] * width; x2 = box[2] * width #网络预测值经过上述处理后还是相对值,这里乘以图片尺寸,成为绝对值

 

问题:
1。为什么loss大了就是hard sample了呢?因为loss=-F.log_softmax(pred)
答: #若网络预测值soft[a1,a2,a3]后,预测值在0-1之间,若对a2预测越小,即非常不能确定这个样本a2属于这个类别的概率,再取log之后就是很小的负数,然后取负号之后就是比较大的loss

2。挑选难样本时,用的损失是loss = -F.log_softmax(confidence, dim=2)[:, :, 也就是说这个loss值就完全是网络的预测值,跟这个anchor的真实标签没有任何关系,那我如何判断网络对这个anchor的分类是否对呢?为什么不对net的预测值,做过softmax后,再跟label结合起来,一个个的计算交叉熵,再根据这个loss挑选hard sample呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值