希望通过本文能了解到:
- generalized focal loss的背景和原理;
- 代码的实现,如何加在自己的模型中;
generalized focal loss v1- 2020.6.8
generalized focal loss v2- 2020.11.25
1.背景
v1主要解决:
- 大多数目标检测模型中,要同时预测目标置信度 s c o r e o b j score_{obj} scoreobj和类别置信度 s c o r e i ( i = 1... n ) score_{i}(i =1...n) scorei(i=1...n),训练的时候分开单独训练,test时却相乘作为nms的目标框质量分, 0.9 ∗ 0.3 > 0.5 ∗ 0.5 0.9*0.3 > 0.5* 0.5 0.9∗0.3>0.5∗0.5;
- 目标检测一贯的模式都是直接预测边框参数,会不会太生硬了,如图,两个下边框似乎都还行,标注框就一定绝对正确么?
v1贡献:
- 提出用一个小数统一代表目标框得分,虽然作者把它解释成了 s c o r e o b j ∗ s c o r e i score_{obj}*score_{i} scoreobj∗scorei,结果本来目标框 b b o x − s c o r e bbox - score bbox−score,以前的score是两部分,而且不是0,就是1,现在就是一个小数了;
- 提出用预测一个分布图来代替习以为常的绝对预测;
- 为了训练提出quality focal loss和distribution focal loss来训练;
- loss定义可以去看论文,大概就是把focal loss只能训练0-1标签的问题改成了可以训练小数label,建议可以手动画下qfl的二维曲线图看看;
- dfl定义中y是gt_label, y i + 1 y_{i+1} yi+1和 y i y_{i} yi是由预测融合出的数的下取整和上取整(间隔取1时),log部分是一个数与一组分布向量的交叉熵。
- 比较有意思的是df loss,对于一个参数
p
j
p_{j}
pj的预测向量:
X
=
{
x
i
}
X = \{x_{i}\}
X={xi},则加权系数为
W
=
{
w
i
}
W=\{w_{i}\}
W={wi},其中
i
=
1...
l
e
n
g
t
h
i=1...length
i=1...length则:
p j = X W T p_{j}=XW^T pj=XWT
有一种多项式组合的感觉,其中 w i = i w_{i}=i wi=i即可,然后在放缩到自己需要的范围。
v2主要解决:
- v1中的分布就只是用来融合出一个数,比如宽,高,偏移,作者感觉没完全把信息利用起来;
v2贡献:
- 把分布图的信息叠加到了类别预测分支上
- 这里得解释的就是gfl的模型可以看做最后的特征输出了两个头,一个输出 b a t c h ∗ c ∗ w ∗ h batch * c* w * h batch∗c∗w∗h的4维目标,一个输出 b a t c h ∗ ( 4 ∗ n ) ∗ w ∗ h batch * (4*n) * w * h batch∗(4∗n)∗w∗h的偏移信息。这里的4,请参考FCOS模型的定义。想在把第二个的分布信息乘到了第一个类别分的预测上。
总结:GFL模型把目标框中的点全当做正例计算,不过对于这些正样本框,有的分高,有的分低,使用预测分布来得到参数预测,类似把原本一个数分解成了多个,似乎增强了稳定性,v2中把这种分布预测反哺到置信度的预测中,加快收敛提高精度。
2.代码
由于我要用在********中,所以基本自己换了种写法。
- dfl的积分:
从分布恢复预测的数值。
自己写的,和原实现做了些改变,主要主要reshape时,保证通道数据的连续性。
原模型预测4个参数,把reshape中2改成4即可,这里的127是我的输出特征宽度。
以下同。
但是解析分布向量时以 hwhwhw…的形式比hhh…www…似乎mAP能高一些,可以多试试。
def forward(self, x):
"""
输入dfl的n * (x*length) * h * w 积分出 n * x * h * w
"""
n,c,h,w = x.shape
# x = x.reshape(n,c//2,2, h, w)
x = x.reshape(n,2,c//2, h, w)# 一定要主语通道的拆分,如果直接拆成(n,2,h,w,c//2), 模型可能不会收敛,原因是reshape
x = x.permute(0,1,3,4,2)
x = F.softmax(x, dim=4)
x = F.linear(x, self.project.type_as(x)) * 127 / (lenght-1) # length就是划分成几个,比如8格,最多加权出7,区间[0,length-1]
return x
2.qfloss
def qfloss(preds, target, beta=2.0):
scale_factor = (preds - target).abs().pow(beta)
# binary_cross_entropy_with_logits = sigmoid(preds) + binary_cross_entropy()
loss = F.binary_cross_entropy_with_logits(preds, target, reduction='none') * scale_factor
nums = (target==1).sum()
if nums == 0:
loss = loss.sum()
else:
loss = loss.sum()/ (1e-4 + nums)
return loss
分析如下的qfloss公式计算(其中右边括号内由binary_cross_entropy_with_logits直接计算得到):
−
∣
g
−
p
∣
2
∗
(
g
∗
l
o
g
(
p
)
+
(
1
−
g
)
l
o
g
(
1
−
p
)
)
-|g-p|^2*(g*log(p) + (1-g)log(1-p))
−∣g−p∣2∗(g∗log(p)+(1−g)log(1−p))
如果gt是小数的,那它将同时被左边的正例loss和右边的负例loss计算,对比focalloss这种0-1的硬计算,这里就像把原本右边负例的一部分loss,挪给了左边正例。
收敛慢了,mAP没看出效果,多试试吧。
- dfloss
def dfloss(pred, gt):
n,c,h,w= pred.shape
pred = pred.reshape(n,2,c//2, h, w)
pred = pred.permute(0,1,3,4,2) #
mask = (gt>0)
pos_gt = gt[mask]
pos_gt = pos_gt * (pred.shape[-1]-1) / 127 # 0-1 -> 0-8 #8得减一 127是特征图最大坐标而非128
pos_pred = pred[mask]
id_left = pos_gt.long()
id_right = id_left + 1
w_left = id_right.float() - pos_gt
w_right = pos_gt - id_left.float()
# cross_entropy = softmax + log + cross entropy
loss_dfl = F.cross_entropy(pos_pred, id_left, reduction='none') * w_left + w_right * F.cross_entropy(pos_pred, id_right, reduction='none')
loss_dfl = loss_dfl.mean()
return loss_dfl
- v2中的融合模块
这一块自己还没用过,可以参考源码实现
取topk的分布预测数值加上它们的mean,过两个卷积通道变成1,直接乘上框质量预测分支,注意是sigmoid以后的。
update:
配合dfl还不错,在小模型上提高收敛速度效果明显;
在大模型上似乎没用,训练完去掉融合分支也没影响…
### 定义这个轻量的融合模块
conf_vector = [nn.Conv2d(4 * self.total_dim, self.reg_channels, 1)]
conf_vector += [self.relu]
conf_vector += [nn.Conv2d(self.reg_channels, 1, 1), nn.Sigmoid()]
self.reg_conf = nn.Sequential(*conf_vector)
###
...
###
...
###
# 取topk的分布预测数值加上它们的mean,过两个卷积通道变成1
if self.add_mean:
stat = torch.cat([prob_topk, prob_topk.mean(dim=2, keepdim=True)],dim=2)
else:
stat = prob_topk
quality_score = self.reg_conf(stat.reshape(N, -1, H, W))
cls_score = self.gfl_cls(cls_feat).sigmoid() * quality_score
###
...
###