文章地址:https://arxiv.org/abs/1708.02002
代码: https://github.com/facebookresearch/Detectron
0 摘要
迄今为止最高精度的目标检测器基于由R-CNN推广的两阶段方法,将分类器应用于稀疏的候选目标位置集。相比之下,在可能的位置进行规则、密集采样的单阶段检测器具有更快和更简单的可能性,但迄今为止其精度落后于两阶段检测器的精度。本文中,我们研究了出现这种情况的原因。我们发现密集检测器训练过程中前景区域和背景区域的极度不均衡是造成这种情况的主要原因。我们提出修改标准的交叉熵损失,降低对较好分类的样本的权重,以解决样本数量不一致的问题。我们提出的focal loss目的是在训练过程减少大量的easy nagative样本,专注于真正的难例样本。为了验证我们提出的损失函数的有效性,我们设计和训练了一个叫做RetinaNet的简单的密集检测器。实验结果表明,使用focal loss的RetinaNet,具有一阶段检测器的速度,同时准确率超过了所有的两阶段检测器。代码为:https://github.com/facebookresearch/Detectron。
1 简介
当前的SOTA的检测器主要基于两阶段的、prposal驱动的机制。正如R-CNN的做法,第一阶段生成候选目标位置的稀疏集,第二阶段使用卷积神经网络判断每一个候选框是前景还是背景。通过一系列的改进,这种两阶段的检测器在coco目标检测任务上取得了最好的效果。
尽管两阶段检测器取得了很好的成功,一个问题是:一个简单的一阶段检测器能否取得同样的准确率?一阶段检测器应用于正规的、密集的目标位置、尺度和长宽比。最近的一阶段检测器,如YOLO、SSD在准确率达到目前SOTA 两阶段检测器10% - 40%的同时取得了更快的检测速度。
本文首次通过应用新的损失函数,使一阶段检测器达到了和目前最先进的两阶段检测器(FPN、Mask-RCNN)相近的检测准确率。我们发现一阶段检测器训练过程中样本类别不均衡是准确率无法令人满意的主要原因,通过使用本文提出的focal loss消除了这种障碍。
在两阶段的R-CNN族检测器中,提出proposal的阶段(selective search,EdgeBoxes,DeepMask,RPN)将候选proposal数量降低到了一个很小的数量(1 - 2k),剔除了大量的背景样本。在这种两阶段检测器中,启发式采样,例如将前景:背景样本的比例限定在1:3,或是通过OHEM实现了背景和前景样本的相对平衡。
作为对比,一阶段检测器必须处理采样自输入图像的更大的候选proposal集合。通常,根据不同的位置、尺度和长宽比,大约会得到100k个proposal。虽然可以通过相似的启发式策略进行处理,但是仍然不足够高效。
本文提出了一个用于解决类别不均衡的损失函数。该损失函数是交叉熵损失的缩放,当正确分类的概率上升时,该损失函数可以下降到0,如图1所示。直观上来说,提出的损失函数在训练过程中会自动降低容易分类的样本的权重并专注于难例样本的学习。实验表明,使用focal loss的一阶段检测器可以轻松超越使用启发式采样/难例挖掘的一阶段检测器。我们还证明,focal loss的形式是不固定的,很多其他的形式也可以达到相近的效果。
我们设计了一个叫做RetinaNet的一阶段检测器用于显示focal loss的有效性。
2 Focal Loss
二分类
y
∈
{
−
1
,
+
1
}
,
p
∈
[
0
,
1
]
y \in \{-1,+1\},p \in [0,1]
y∈{−1,+1},p∈[0,1]是样本为正样本的概率,交叉熵:
C
E
(
p
,
y
)
=
{
−
log
(
p
)
i
f
y
=
1
−
log
(
1
−
p
)
o
t
h
e
r
w
i
s
e
CE(p,y) = \begin{cases} -\log(p) ~~~if~y = 1 \\ -\log(1-p) ~~ otherwise\end{cases}
CE(p,y)={−log(p) if y=1−log(1−p) otherwise
交叉熵的物理含义是,对于正样本,预测其为正样本的概率越大损失越小,对于负样本,预测其为正样本的概率越小损失越小。因此最小化交叉熵损失的目的就是使正样本被预测为正样本的概率尽可能大,负样本被预测为正样本的概率尽可能小,也就是对所有的样本都尽可能正确的进行分类。
定义:
p
t
=
{
p
i
f
y
=
1
1
−
p
o
t
h
e
r
w
i
s
e
p_t = \begin{cases} p~~if~y=1 \\ 1-p~~otherwise\end{cases}
pt={p if y=11−p otherwise
因此,
C
E
(
p
,
y
)
=
C
E
(
p
t
)
=
−
log
(
p
t
)
CE(p,y)=CE(p_t)=-\log(p_t)
CE(p,y)=CE(pt)=−log(pt)。
图1中最上面的蓝色线表示交叉上的曲线。一个值得注意的特性是,即便对于容易分类的样本( p t > > 0.5 p_t >> 0.5 pt>>0.5),其损失值也不是特别小。当训练集中包含很多的正样本时,这些难以训练的难例样本会淹没在大量的正样本中。
2.1 平衡交叉熵
解决样本类别不均衡的做法是使用一个权重系数
α
∈
[
0
,
1
]
\alpha \in [0,1]
α∈[0,1]。一般这个值是样本出现频率的倒数或者是一个交叉验证得到的超参数。定义:
α
t
=
{
α
i
f
y
=
1
1
−
α
o
t
h
e
r
w
i
s
e
\alpha_t = \begin{cases} \alpha~~if~y=1\\ 1-\alpha~otherwise\end{cases}
αt={α if y=11−α otherwise
定义
α
\alpha
α平衡的交叉熵损失为:
C
E
(
p
t
)
=
−
α
t
log
(
p
t
)
=
{
−
α
log
(
p
)
i
f
y
=
1
−
(
1
−
α
)
log
(
1
−
p
)
o
t
h
e
r
w
i
s
e
CE(p_t) = -\alpha_t \log(p_t) = \begin{cases} -\alpha \log(p) ~~ if~y = 1 \\ -(1- \alpha)\log(1-p)~otherwise\end {cases}
CE(pt)=−αtlog(pt)={−αlog(p) if y=1−(1−α)log(1−p) otherwise
α
\alpha
α可以实现对正负样本数量不均衡的平衡。对于数量较多的样本类,可以令其
α
\alpha
α值较小。同理,对于样本数量较少的样本类,可以令其
α
\alpha
α值较大。但
α
\alpha
α没有解决easy和hard negative的不均衡。
2.2 focal loss
F L ( p t ) = − ( 1 − p t ) γ log ( p t ) = { ( 1 − p ) γ log ( p ) i f y = 1 − p γ log ( 1 − p ) o t h e r w i s e , γ > 0 FL(p_t) = -(1 - p_t)^{\gamma}\log(p_t) = \begin{cases} {(1-p)}^{\gamma}\log(p) ~~ if~y = 1 \\ -p^{\gamma}\log(1-p)~otherwise\end {cases},\gamma > 0 FL(pt)=−(1−pt)γlog(pt)={(1−p)γlog(p) if y=1−pγlog(1−p) otherwise,γ>0
当 p t → 1 p_t \rightarrow 1 pt→1(正样本被预测为正样本的概率很大,负样本被预测为正样本的概率很小,表示容易区分的样本)时, 1 − p t → 0 1-p_t \rightarrow 0 1−pt→0,由于 γ > 1 \gamma > 1 γ>1, ( 1 − p t ) γ (1-p_t)^{\gamma} (1−pt)γ很小,所以容易被正确分类的样本的损失很小。当 p t → 0 p_t \rightarrow 0 pt→0(正样本被预测为正样本的概率很小,负样本被预测为正样本的概率很大,表示难例样本)时, ( 1 − p t ) → 1 (1 - p_t) \rightarrow 1 (1−pt)→1,由于 γ > 1 \gamma > 1 γ>1, ( 1 − p t ) γ (1-p_t)^{\gamma} (1−pt)γ很大,所以难以被正确分类的样本造成的损失很大。所以focal loss起到了增大难以分类的样本的损失影响,减弱易于正确分类的样本的损失影响。总结起来,focal loss解决了easy和hard negative的不均衡。
γ \gamma γ值越大,引入的调整参数的影响越大。实验中发现 γ = 2 \gamma = 2 γ=2效果最好。 γ = 2 \gamma = 2 γ=2时,一个 p t p_t pt = 0.9的样本,其focal loss相对于交叉熵loss减少了100倍,一个 p t p_t pt = 0.968的样本,其focal loss相对于交叉熵loss减少了1000倍。同样对容易误分类的样本 p t ≤ 0.5 , γ = 2 p_t \leq 0.5,\gamma = 2 pt≤0.5,γ=2时,其损失被增大了4倍以上。
实际实验中,使用focal loss的
α
\alpha
α平衡版本:
F
L
(
p
t
)
=
−
α
t
(
1
−
p
t
)
γ
log
(
p
t
)
=
{
−
α
(
1
−
p
)
γ
log
(
p
)
i
f
y
=
1
−
(
1
−
α
)
p
γ
log
(
1
−
p
)
o
t
h
e
r
w
i
s
e
FL(p_t) = -\alpha_t(1-p_t)^{\gamma}\log(p_t)=\begin{cases} -\alpha {(1-p)}^{\gamma}\log(p) ~~ if~y = 1 \\ -(1- \alpha)p^{\gamma}\log(1-p)~otherwise\end {cases}
FL(pt)=−αt(1−pt)γlog(pt)={−α(1−p)γlog(p) if y=1−(1−α)pγlog(1−p) otherwise
之所以使用这种形式是因为相比不使用
α
\alpha
α平衡的focal loss准确率略微上升。同时也提升了数值计算的稳定性。物理意义是同时解决了各类别样本数量不均衡和easy和hard negative的不均衡两个问题。
实现:
单分类(单标签)时的实现
实现总体参考:https://zhuanlan.zhihu.com/p/308290543
The softmax version of focal loss is: FL(p_t) = -alpha * (1 - p_t)**gamma * log(p_t),
where p_i = exp(s_i) / sum_j exp(s_j), t is the target (ground truth) class, and s_j is the unnormalized score for class j.
本代码中只考虑了正类别的损失,即只有上式中的y=1
部分。
# encoding: utf-8
"""
@author: xingyu liao
@contact: sherlockliao01@gmail.com
"""
import torch
import torch.nn.functional as F
# based on:
# https://github.com/kornia/kornia/blob/master/kornia/losses/focal.py
def focal_loss(
input: torch.Tensor,
target: torch.Tensor,
alpha: float,
gamma: float = 2.0,
reduction: str = 'mean') -> torch.Tensor:
r"""Criterion that computes Focal loss.
See :class:`fastreid.modeling.losses.FocalLoss` for details.
According to [1], the Focal loss is computed as follows:
.. math::
\text{FL}(p_t) = -\alpha_t (1 - p_t)^{\gamma} \, \text{log}(p_t)
where:
- :math:`p_t` is the model's estimated probability for each class.
Arguments:
alpha (float): Weighting factor :math:`\alpha \in [0, 1]`.
gamma (float): Focusing parameter :math:`\gamma >= 0`.
reduction (str, optional): Specifies the reduction to apply to the
output: ‘none’ | ‘mean’ | ‘sum’. ‘none’: no reduction will be applied,
‘mean’: the sum of the output will be divided by the number of elements
in the output, ‘sum’: the output will be summed. Default: ‘none’.
Shape:
- Input: :math:`(N, C, *)` where C = number of classes.
- Target: :math:`(N, *)` where each value is
:math:`0 ≤ targets[i] ≤ C−1`.
Examples:
>>> N = 5 # num_classes
>>> loss = FocalLoss(cfg)
>>> input = torch.randn(1, N, 3, 5, requires_grad=True)
>>> target = torch.empty(1, 3, 5, dtype=torch.long).random_(N)
>>> output = loss(input, target)
>>> output.backward()
References:
[1] https://arxiv.org/abs/1708.02002
"""
if not torch.is_tensor(input):
raise TypeError("Input type is not a torch.Tensor. Got {}"
.format(type(input)))
if not len(input.shape) >= 2:
raise ValueError("Invalid input shape, we expect BxCx*. Got: {}"
.format(input.shape))
if input.size(0) != target.size(0):
raise ValueError('Expected input batch_size ({}) to match target batch_size ({}).'
.format(input.size(0), target.size(0)))
n = input.size(0)
out_size = (n,) + input.size()[2:]
if target.size()[1:] != input.size()[2:]:
raise ValueError('Expected target size {}, got {}'.format(
out_size, target.size()))
if not input.device == target.device:
raise ValueError(
"input and target must be in the same device. Got: {}".format(
input.device, target.device))
# compute softmax over the classes axis
input_soft = F.softmax(input, dim=1)
# create the labels one hot tensor
target_one_hot = F.one_hot(target, num_classes=input.shape[1])
# compute the actual focal loss
weight = torch.pow(-input_soft + 1., gamma)
focal = -alpha * weight * torch.log(input_soft)
loss_tmp = torch.sum(target_one_hot * focal, dim=1)
if reduction == 'none':
loss = loss_tmp
elif reduction == 'mean':
loss = torch.mean(loss_tmp)
elif reduction == 'sum':
loss = torch.sum(loss_tmp)
else:
raise NotImplementedError("Invalid reduction mode: {}"
.format(reduction))
return loss
多个二分类(多标签)时的实现
The binary form of focal loss is: FL(p_t) = -alpha * (1 - p_t)**gamma * log(p_t),
where p = sigmoid(x), p_t = p or 1 - p depending on if the label is 1 or 0,respectively.
下面的代码既考虑了正类别的损失,也考虑了负类别的损失。
class FocalLoss(nn.Module):
# Wraps focal loss around existing loss_fcn(), i.e. criteria = FocalLoss(nn.BCEWithLogitsLoss(), gamma=1.5)
def __init__(self, loss_fcn, gamma=1.5, alpha=0.25):
super(FocalLoss, self).__init__()
self.loss_fcn = loss_fcn # must be nn.BCEWithLogitsLoss()
self.gamma = gamma
self.alpha = alpha
self.reduction = loss_fcn.reduction
self.loss_fcn.reduction = 'none' # required to apply FL to each element
def forward(self, pred, true):
loss = self.loss_fcn(pred, true)
# p_t = torch.exp(-loss)
# loss *= self.alpha * (1.000001 - p_t) ** self.gamma # non-zero power for gradient stability
# TF implementation https://github.com/tensorflow/addons/blob/v0.7.1/tensorflow_addons/losses/focal_loss.py
pred_prob = torch.sigmoid(pred) # prob from logits
p_t = true * pred_prob + (1 - true) * (1 - pred_prob)
alpha_factor = true * self.alpha + (1 - true) * (1 - self.alpha)
modulating_factor = (1.0 - p_t) ** self.gamma
loss *= alpha_factor * modulating_factor
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else: # 'none'
return loss
2.3 类别不均衡和模型初始化
二分类默认初始化成得到y=-1和y=1的概率相等。基于这种初始化,在类别不均衡的情况下,总损失取决于经常出现的样本类别并且会造成训练早期的不稳定。为了解决这个问题,在模型训练早期引入一个基于稀少样本的概率 p p p的先验参数。假如 p = 0.01 p = 0.01 p=0.01,设置该先验为 π \pi π。这是3.1节用到的模型初始化。我们发现在类别严重不均衡的情况下,这种初始化可以增强交叉熵损失/focal loss的训练稳定性。
2.4 类别不均衡和两阶段检测器
前面的两阶段检测器一般都是使用交叉熵损失。它们通过两种机制解决类别不均衡问题:(1)两阶段级联(2)minibatch样本重采样。第一阶段的proposal提出机制可以将几近无穷的proposal数量减少到1 ~ 2k个。更重要的是,这些留下的proposal也不是随机选取的,而是和真实的目标位置有较大关联的proposal,这已经移除了大量易于分类的负样本。在第二阶段的训练过程中,往往会对minibatch进行重新采样,例如使正负样本的比例接近1:3。这相当于通过采样实现 α \alpha α平衡。本文相当于通过focal loss解决一阶段目标检测系统中的样本类别不均衡问题。
3 RetinaNet检测器
RetinaNet由一个骨干网络和两个子网络组成,骨干网络计算整个输入图像的卷积特征。第一个子网络用于进行目标分类,第二个子网络用于边界框回归。整个网络如图3所示。
FPN骨架:FPN是在卷积网络的输出上添加从上到下和横向的连接,从而能够基于单幅输入图像构建出丰富的、多尺度的特征金字塔。特征金字塔的每一层都用于在不同的尺度进行目标检测。
我们在ResNet基础架构的基础上使用FPN,我们构建了一个从P3到P7的金字塔, l l l表示金字塔的层数( P l P_l Pl的分辨率比输入图像小 2 l 2^l 2l倍)。所有的金字塔层都具有256个channel。
Anchors:P3到P7对应的anchor大小分别为 3 2 2 32^2 322到 51 2 2 512^2 5122。每一层使用的anchor的长宽比均为 { 1 : 2 , 1 : 1 , 2 : 1 } \{1:2,1:1,2:1\} {1:2,1:1,2:1},作者对每一层的每一个长宽比的anchor集设置尺度大小为原始大小 { 2 0 , 2 1 3 , 2 2 3 } \{2^0,2^{\frac{1}{3}},2^{\frac{2}{3}}\} {20,231,232}的anchor,这样对每一层有9个不同的anchor。
如果一个anchor和真实框之间的IOU大于等于0.5,设该anchor为正例。如果一个anchor和真实框之间的IOU介于0到0.4之间,则该anchor为反例。IOU介于0.4到0.5之间的anchor在训练过程中被忽略。
卷积子网络:分类子网络在每一个空间位置预测A个anchor包含K类目标的概率。该子网络是一个应用于FCN全部层的小的FCN,各层之间参数共享。设计原则很简单。给定一个金字塔具有C个channel的输入特征映射,先经过通道数同样为C的3 * 3卷积,然后使用Relu激活,最后使用KA个大小为3 * 3的卷积核进行卷积操作。最后使用KA个sigmoid激活输入每一个anchor包含目标的概率。在大部分实验中,C = 256,A=9。
目标回归子网络:目标回归子网络使用FCN用于估计一个anchor和它附近真实框之间的偏移量。回归子网络和分类子网络设计相似,除了输出大小为4A。预测的四个值同样是宽高的比值和中心点的偏移量。
3.1 推理和训练
推理:推理时每一个FPN层先按照阈值0.05过滤检测结果,然后按照置信度从高到低选择至多1000个检测框。最后所有层的预测框合并并使用0.5的阈值进行NMS操作得到最终的检测结果。
Focal Loss:分类子网络使用focal loss。实验中发现设置 γ = 2 \gamma = 2 γ=2可以取得最好的效果。在训练RetinaNet的时候,每一个输入图像大约100k个anchor应用focal loss。这和原来很多网络使用RPN或OHEM只保留很少的anchor不同。最终一个图像的focal loss是大约100k个anchor的focal loss的和除以和真实框匹配了的anchor的数量。这里之所以没有除以所有anchor的数量是因为大多数的anchor都是easy nagetive样本,使用focal loss时这些样本的对损失的贡献很有限。当 γ \gamma γ增大时, α \alpha α的值应该减小,在实验中发现 γ = 2 , α = 0.25 \gamma = 2,\alpha = 0.25 γ=2,α=0.25的效果最好。
初始化:本文实验中使用了ResNet-50-FPN和ResNet-101-FPN,基础的ResNet-50和ResNet-101网络都是在ImageNet1K数据集上训练的。对FPN新添加的层,除最后一层之外的其他卷积层偏置项b初始化为0,权重初始化标准差为0.01的高斯分布。分类子网络的最后一个卷积层,偏置 b = − log ( ( 1 − π ) / π ) b =-\log((1-\pi)/\pi) b=−log((1−π)/π),其中 π \pi π表示训练开始前每一个anchor被标记为前景目标的概率。在所有的实验中,作者设置 π = 0.01 \pi = 0.01 π=0.01。这样初始化可以防止大量的负样本在训练的第一次迭代过程中产生一个大的、不稳定的损失值。
最优化:使用SGD优化算法,在8个GPU上,每个minibatch包含16幅图像。
训练过程中的整体损失是分类自网络的focal loss和回归子网络的smooth L1损失。
4 实验效果
使用focal loss的效果要比使用OHEM好很多。