Towards Accurate One-Stage Object Detection with AP-Loss
一. 论文简介
笔者部分不理解,仅记录自己理解部分。
解决目标检测(一阶段)样本不均衡问题,统一loss和评价指标直接联系。
主要做的贡献如下(可能之前有人已提出):
- 设计一个AP-Loss(包括前向和后向传播)
二. 模块详解
2.1 论文思路简介
这里以一张图像为例子:
核心思想:(a)当前预测的正样本概率((score))尽可能大于所有负样本的概率((score)),(b)尽可能的大于其他正样本的概率((score))
只满足(a): inference置信度概率很低,因为负样本概率很低。
只满足(b): 精度很低,且非常难训练,因为样本不均衡。
- 获得一张分类feature
- 正负样本分开
- 按(score)排序(从小到大)正样本
- 循环排序后的正样本
- 计算当前正样本在所有正样本中的次序(source code1),计做 (alpha)
- 计算当前正样本在所有负样本中的次序(source code2),计做 (eta)
- 细节2.2节进行说明
- (alpha) 当做loss,(eta) 当做梯度(权重、方向)
- 求和 (eta * alpha) 当做梯度进行回传
代码是以batch为单位进行计算,将batch中所有的正样本看做一张图的正样本即可,和一张图处理一样。
2.2 具体实现
2.2.1 理论部分
论文核心思想再提一次,尽量使得当前的目标score排序靠前,接下来以此为基础进行推导公式
为了求取当前正样本score在所有正样本score中的次序,先使用公式(1)得到差值,其中 ( heta) 是权重,所有值默认为1.0。(x_{ij}<0) 表示当前目标(x_i)靠前,等于越小越好。
公式(2)是label转换函数,(t) 是一个二值数[0或1],(i) 当前目标,(j) 其它目标
公式(3)统计当前排序在总体中的占比(只是一种统计方式,当然也可以使用其它方式表示),越小越好(公式(3、4)直接推导即可)。分母是归一化的值,其中(+1)代表自身 (x_{ii})。(k) 是正负样本总体区域,(j) 是正样本区域。(P) 表示positive sample,(N) 表示negtive sample。
公式(5)计算近似的AP(和传统AP不同),越大越好。(rank^+/rank) 分别表示当前正样本在正样本中的次序和当前正样本在总体样本中的次序。注意这里分子(+1),而公式(3)分子没有(+1),因为公式(3)计算的是在负样本中的占比,公式(5)计算的是在正样本中的占比。前者全部都是 (i eq j) ,后者是是存在 (i=j)。这里作者在github已经回答source 3
这里 (y_{ij}) 是表示只计算正样本概率,不管负样本概率。代码中直接直接使用index进行过滤了。
由于上述公式 (L_{x}) 是阶跃信号不可导,所以使用其它方式进行代替。
公式(8)是正常求解loss的情况,(L_{ij}^*) 表示label,(L_{ij}) 表示计算值
公式(9)
注释: (vartriangle x_{ij}) 可导,而(L(x))不可导,有点奇怪?
2.2.2 具体实现
内部已经经过注释,这里不再赘述
import numpy as np
import torch
import torch.nn as nn
from .. import config
from ..util.calc_iou import calc_iou
class APLoss(torch.autograd.Function):
@staticmethod
def forward(ctx, classifications, regressions, anchors, annotations):
batch_size = classifications.shape[0]
regression_losses = [] # 回归总loss
regression_grads=torch.zeros(regressions.shape).cuda()
p_num=torch.zeros(1).cuda()
labels_b=[] # 分类总loss
anchor = anchors[0, :, :].type(torch.cuda.FloatTensor) # 每个图像的anchor都是一样的,没必要生成那么多(这里取[0]获取其中一个)
anchor_widths = anchor[:, 2] - anchor[:, 0]+1.0
anchor_heights = anchor[:, 3] - anchor[:, 1]+1.0
anchor_ctr_x = anchor[:, 0] + 0.5 * (anchor_widths-1.0)
anchor_ctr_y = anchor[:, 1] + 0.5 * (anchor_heights-1.0)
for j in range(batch_size): # 单独计算每个图像
classification = classifications[j, :, :].cuda()
regression = regressions[j, :, :].cuda()
bbox_annotation = annotations[j].cuda() # N * 5
bbox_annotation = bbox_annotation[bbox_annotation[:, 4] != -1]
if bbox_annotation.shape[0] == 0: # 没有目标,loss为0
regression_losses.append(torch.tensor(0).float().cuda())
labels_b.append(torch.zeros(classification.shape).cuda())
continue
# 每个anchor和目标的交集
IoU = calc_iou(anchors[0, :, :], bbox_annotation[:, :4]) # num_anchors(32736) x num_annotations
IoU_max, IoU_argmax = torch.max(IoU, dim=1) # num_anchors x 1,每个anchor的最大交集目标
# compute the loss for classification
targets = torch.ones(classification.shape) * -1
targets = targets.cuda()
######
gt_IoU_max, gt_IoU_argmax = torch.max(IoU, dim=0) # 1 * num_annotations,每个目标的最大交集anchor
gt_IoU_argmax=torch.where(IoU==gt_IoU_max)[0]
positive_indices = torch.ge(torch.zeros(IoU_max.shape).cuda(),1) # 用于存储正样本ID,torch.ge无意义
positive_indices[gt_IoU_argmax.long()] = True # 每个anchor负责一个最大交集的目标
######
# [0.4,0.5)之间不做处理(不进行反向传播)
# FIXME | 修改为 & 源代码好像用错了
positive_indices = positive_indices | torch.ge(IoU_max, 0.5) # 正样本: 交集大于等于0.5 同时 处于最大交集
negative_indices = torch.lt(IoU_max, 0.4)# 负样本: 交集小于0.4
p_num+=positive_indices.sum() # 正样本数量
assigned_annotations = bbox_annotation[IoU_argmax, :] # 每个anchor最大交集目标
targets[negative_indices, :] = 0
targets[positive_indices, :] = 0
targets[positive_indices, assigned_annotations[positive_indices, 4].long()] = 1 # 整合成one-hot形式
labels_b.append(targets)
# compute the loss for regression
if positive_indices.sum() > 0:
assigned_annotations = assigned_annotations[positive_indices, :]
anchor_widths_pi = anchor_widths[positive_indices]
anchor_heights_pi = anchor_heights[positive_indices]
anchor_ctr_x_pi = anchor_ctr_x[positive_indices]
anchor_ctr_y_pi = anchor_ctr_y[positive_indices]
gt_widths = assigned_annotations[:, 2] - assigned_annotations[:, 0]+1.0
gt_heights = assigned_annotations[:, 3] - assigned_annotations[:, 1]+1.0
gt_ctr_x = assigned_annotations[:, 0] + 0.5 * (gt_widths-1.0)
gt_ctr_y = assigned_annotations[:, 1] + 0.5 * (gt_heights-1.0)
# clip widths to 1
gt_widths = torch.clamp(gt_widths, min=1)
gt_heights = torch.clamp(gt_heights, min=1)
targets_dx = (gt_ctr_x - anchor_ctr_x_pi) / anchor_widths_pi
targets_dy = (gt_ctr_y - anchor_ctr_y_pi) / anchor_heights_pi
targets_dw = torch.log(gt_widths / anchor_widths_pi)
targets_dh = torch.log(gt_heights / anchor_heights_pi)
targets2 = torch.stack((targets_dx, targets_dy, targets_dw, targets_dh))
targets2 = targets2.t()
targets2 = targets2/torch.Tensor([[0.1, 0.1, 0.2, 0.2]]).cuda() # 编码之后的label bbox
#negative_indices = ~ positive_indices
# bbox直接使用 Smooth L1 loss
regression_diff = regression[positive_indices, :] - targets2
regression_diff_abs= torch.abs(regression_diff)
regression_loss = torch.where(
torch.le(regression_diff_abs, 1.0 / 1.0),
0.5 * 1.0 * torch.pow(regression_diff_abs, 2),
regression_diff_abs - 0.5 / 1.0
)
regression_losses.append(regression_loss.sum())
# 将绝对值小于1.0的值使用sin(x)代替,其中sin(x) < x,相当于偏差大不变,偏差小的更小
regression_grad=torch.where(
torch.le(regression_diff_abs,1.0/1.0),
1.0*regression_diff,
torch.sign(regression_diff))
regression_grads[j,positive_indices,:]=regression_grad
else:
regression_losses.append(torch.tensor(0).float().cuda())
p_num=torch.clamp(p_num,min=1)# 一个batch中所有正样本个数
regression_grads/=(4*p_num) # 不知道*4啥意思,应该是多次训练得到的大概值
########################AP-LOSS##########################
labels_b=torch.stack(labels_b)
classification_grads,classification_losses=AP_loss(classifications.cuda(),labels_b)
#########################################################
ctx.save_for_backward(classification_grads,regression_grads) # 这里是权重(bbox偏差值)不是梯度
return classification_losses, torch.stack(regression_losses).sum(dim=0, keepdim=True)/p_num # 这里是loss的值,也不是梯度
@staticmethod
def backward(ctx, out_grad1, out_grad2): # forward的输出和backward的输入一致
g1,g2=ctx.saved_tensors # 获取ctx.save_for_backward
return g1*out_grad1,g2*out_grad2,None,None # 输出和forward的输出一致,因为这里求得结果是forward输入的梯度
def AP_loss(logits,targets):
delta=1.0 # 负样本控制参数
grad=torch.zeros(logits.shape).cuda()
metric=torch.zeros(1).cuda()
if torch.max(targets)<=0:
return grad, metric
labels_p=(targets==1)
fg_logits=logits[labels_p]
threshold_logit=torch.min(fg_logits)-delta # 决策最小边界(影响负样本,不影响正样本)
######## Ignore those negative j that satisfy (L_{ij}=0 for all positive i), to accelerate the AP-loss computation.
valid_labels_n=((targets==0)&(logits>=threshold_logit)) # 忽略偏差太大的负样本
valid_bg_logits=logits[valid_labels_n]
valid_bg_grad=torch.zeros(len(valid_bg_logits)).cuda()
########
fg_num=len(fg_logits)
prec=torch.zeros(fg_num).cuda()
order=torch.argsort(fg_logits) # 排序正样本,获取Index
max_prec=0
for ii in order:
# 正样本操作
tmp1=fg_logits-fg_logits[ii]
tmp1=torch.clamp(tmp1/(2*delta)+0.5,min=0,max=1) # 正常输出是sigmoid函数,[-1,+1]*0.5+0.5->>[0,1]之间
# 负样本操作
tmp2=valid_bg_logits-fg_logits[ii]
tmp2=torch.clamp(tmp2/(2*delta)+0.5,min=0,max=1) # 同上
# tmp1里面包含ii项,论文公式(5)里是i不等于j,所以多了一个0/(2*1)+0.5=0.5,再加一个0.5正好为1
a=torch.sum(tmp1)+0.5
b=torch.sum(tmp2)
tmp2/=(a+b) # 梯度归一化(论文公式3),当前正样本在所有负样本中的排名
current_prec=a/(a+b) # 正确率归一化(论文公式5中间),当前正样本在所有正样本中的排名
if (max_prec<=current_prec):
max_prec=current_prec
else:
tmp2*=((1-max_prec)/(1-current_prec)) # 插值得到实际最大值
valid_bg_grad+=tmp2
prec[ii]=max_prec
grad[valid_labels_n]=valid_bg_grad
grad[labels_p]=-(1-prec)
fg_num=max(fg_num,1)
grad /= (fg_num)
metric=torch.sum(prec,dim=0,keepdim=True)/fg_num
return grad, 1-metric
三. 参考文献
- 原始论文
- 比较全面的一个资料