【经典视觉算法推荐】Focal Loss 论文公式推导及主要贡献

在这里插入图片描述

论文:《Focal Loss for Dense Object Detection》
作者提出了focal loss来减小易分样本的loss在总体loss中的权重,使得模型专注于优化难分样本。

PDF论文链接:https://arxiv.org/pdf/1708.02002.pdf
文献公式推导参考:https://www.aiuai.cn/aifarm636.html
PyTorch代码: PyTorch-Networks

该论文首先总结了目标检测的两个主要方向:

  1. two-stage 区域候选型检测器;
  2. one-stage 密集采样每一个可能的位置。
    Two-stage 分为两段,第一段专注于 proposals 的提取,第二段专注于对第一段提取出的 proposals 进行分类和边框回归。One-stage 摒弃了提取 proposals 的过程,对图片分格后,对每个格子预测一定数量的边框。相应的,这两种网络也各有相应的问题。Two-stage 网络需要单独对提取出的 proposals 进行分类和回归,这将带来速度的问题;One-stage 网络没有提取 proposals,这将引入前景背景类别不平衡的问题,给精确度带来影响;

论文引进的 focal loss,就是为了解决 one-stage 所丢失的精确度问题。其参考了 two-stage 提取 proposal 带来的影响,给背景anchor 和 前景anchor对应的权重,以解决样本类别不平衡问题。

在这里插入图片描述
上图可以看到在原有的基础上增加了一个预测概率p和超参数r,其中p的存在就是如果这个样本预测的已经很好了(也就是p->1)那么这个样本产生的loss就接近于0,γ的作用是对这个接近的速度做控制。此外,可以看出γ越大,预测越正确的样本loss下降的越快,也就是这部分样本对于loss的贡献就越小。其实这个工作原理到此就差不多了,作者起了个挺好听的名字叫RetinaNet,下面是一些实现细节:
1. class subnet和regression subnet用了更深的3*3 conv层
2. focal loss初始化的时候加了bias: b = − log((1 − π)/π),其中π = 0.01,用来防止最开始的几轮迭代时梯度不稳定
实验的对比结果如下表:
在这里插入图片描述
进一步解释loss下降的数学机制:
在这里插入图片描述
从上面一段话,我们可以看出,当某一个类别分类预测概率p越来越高(分类准确的情况下),那么loss会因为q权重因子
在这里插入图片描述

存在而被大大降低(指数函数底数小于1单调递减的函数,随自变量的增加而衰减。)!这也是我们所希望的!!!当一个类别预测概率小于0.5,也就是基本上被判断到了别的类别去了!分错了!loss相对于“分对了”的情况而言最多只能减少4倍!!!loss减少量是“分对情况”下的“扩大”几十倍,百倍,千倍!

论文的上图这里进一步解释:
可以看出即使是容易分类的样本p大于0.5,其loss也没有小到可以忽略的程度,如果这部分样本数量非常多,即便难分样本的loss大,但难分样本的数量很少,这样累加后也会导致易分样本对总体loss的贡献远大于难分样本,从而使得模型偏向于优化数量众多的易分样本而忽略真正该优化的难分样本。

进一步解释超参数:α,γ
在Focal Loss中α对应的是正样本(占比少的类)的权重,且一般值较小。
在Focal Loss中γ 占主导地位。随着γ 的增大,α要相应的减小
在这里插入图片描述
基于此,作者也在论文中做了解释:
在这里插入图片描述

为了更好的理解loss带来的贡献,这里通过样本数量的计算来加深印象:

假设我们模型

负样本10000笔资料probability(pt) = 0.95(简单样本), 这边可以理解为easy-example

正样本10笔资料, probability(pt) = 0.05(困难样本),

CE的值:

负样本 : log(p_t) * 样本数(100000) = 0.02227 * 100000 = 2227
正样本 : log(p_t) * 样本数(10) = 1.30102 * 10 = 13.0102
total loss = 2227+13.0102 = 2240
正样本占比:13.0102 / 2240 = 0.0058

>>> x=2227+13.0102
>>> x
2240.0102
>>> 13.0102/x
0.005808098552408377

FL的值 :

a=0.25 y=2

负样本 : 0.75*(1-0.95)^2 * 0.02227 样本数(100000) = 0.00004176 * 100000 = 4.1756
正样本 : 0.25
(1-0.05)^2 * 1.30102 *样本数(10)= 0.29354264 * 10 = 2.935
total loss = 4.175 + 2.935 = 7.110
正样本占比:2.935/7.110 = 0.4127(与0.0058差距甚大)
参考来源这里

实现源码:PyTroch代码:

import torch
import torch.nn as nn
import torchvision

def Conv3x3ReLU(in_channels,out_channels):
    return nn.Sequential(
        nn.Conv2d(in_channels=in_channels,out_channels=out_channels,kernel_size=3,stride=1,padding=1),
        nn.ReLU6(inplace=True)
    )

def locLayer(in_channels,out_channels):
    return nn.Sequential(
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
            nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1),
        )

def confLayer(in_channels,out_channels):
    return nn.Sequential(
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        Conv3x3ReLU(in_channels=in_channels, out_channels=in_channels),
        nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=1, padding=1),
    )

class RetinaNet(nn.Module):
    def __init__(self, num_classes=80, num_anchores = 9):
        super(RetinaNet, self).__init__()
        self.num_classes = num_classes
        resnet = torchvision.models.resnet50()
        layers = list(resnet.children())

        self.layer1 = nn.Sequential(*layers[:5])
        self.layer2 = nn.Sequential(*layers[5])
        self.layer3 = nn.Sequential(*layers[6])
        self.layer4 = nn.Sequential(*layers[7])

        self.lateral5 = nn.Conv2d(in_channels=2048, out_channels=256, kernel_size=1)
        self.lateral4 = nn.Conv2d(in_channels=1024, out_channels=256, kernel_size=1)
        self.lateral3 = nn.Conv2d(in_channels=512, out_channels=256, kernel_size=1)

        self.upsample4 = nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=4, stride=2, padding=1)
        self.upsample3 = nn.ConvTranspose2d(in_channels=256, out_channels=256, kernel_size=4, stride=2, padding=1)

        self.downsample6 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=2, padding=1)
        self.downsample6_relu = nn.ReLU6(inplace=True)
        self.downsample5 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=2, padding=1)

        self.loc_layer3 = locLayer(in_channels=256,out_channels=4*num_anchores)
        self.conf_layer3 = confLayer(in_channels=256,out_channels=self.num_classes*num_anchores)

        self.loc_layer4 = locLayer(in_channels=256, out_channels=4*num_anchores)
        self.conf_layer4 = confLayer(in_channels=256, out_channels=self.num_classes*num_anchores)

        self.loc_layer5 = locLayer(in_channels=256, out_channels=4*num_anchores)
        self.conf_layer5 = confLayer(in_channels=256, out_channels=self.num_classes*num_anchores)

        self.loc_layer6 = locLayer(in_channels=256, out_channels=4*num_anchores)
        self.conf_layer6 = confLayer(in_channels=256, out_channels=self.num_classes*num_anchores)

        self.loc_layer7 = locLayer(in_channels=256, out_channels=4*num_anchores)
        self.conf_layer7 = confLayer(in_channels=256, out_channels=self.num_classes*num_anchores)

        self.init_params()

    def init_params(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        x = self.layer1(x)
        c3 =x = self.layer2(x)
        c4 =x = self.layer3(x)
        c5 = x = self.layer4(x)

        p5 = self.lateral5(c5)
        p4 = self.upsample4(p5) + self.lateral4(c4)
        p3 = self.upsample3(p4) + self.lateral3(c3)

        p6 = self.downsample5(p5)
        p7 = self.downsample6_relu(self.downsample6(p6))

        loc3 = self.loc_layer3(p3)
        conf3 = self.conf_layer3(p3)

        loc4 = self.loc_layer4(p4)
        conf4 = self.conf_layer4(p4)

        loc5 = self.loc_layer5(p5)
        conf5 = self.conf_layer5(p5)

        loc6 = self.loc_layer6(p6)
        conf6 = self.conf_layer6(p6)

        loc7 = self.loc_layer7(p7)
        conf7 = self.conf_layer7(p7)

        locs = torch.cat([loc3.permute(0, 2, 3, 1).contiguous().view(loc3.size(0), -1),
                    loc4.permute(0, 2, 3, 1).contiguous().view(loc4.size(0), -1),
                    loc5.permute(0, 2, 3, 1).contiguous().view(loc5.size(0), -1),
                    loc6.permute(0, 2, 3, 1).contiguous().view(loc6.size(0), -1),
                    loc7.permute(0, 2, 3, 1).contiguous().view(loc7.size(0), -1)],dim=1)

        confs = torch.cat([conf3.permute(0, 2, 3, 1).contiguous().view(conf3.size(0), -1),
                           conf4.permute(0, 2, 3, 1).contiguous().view(conf4.size(0), -1),
                           conf5.permute(0, 2, 3, 1).contiguous().view(conf5.size(0), -1),
                           conf6.permute(0, 2, 3, 1).contiguous().view(conf6.size(0), -1),
                           conf7.permute(0, 2, 3, 1).contiguous().view(conf7.size(0), -1),], dim=1)

        out = (locs, confs)
        return out

if __name__ == '__main__':
    model = RetinaNet()
    print(model)

    input = torch.randn(1, 3, 800, 800)
    out = model(input)
    print(out[0].shape)
    print(out[1].shape)


结语:
其实作者的思路很简单,作者想知道为什么One-stage的目标检测不如Two-stage的目标检测框架效果好,提出了造成这种差异的根本原因是类别不平衡(可以理解为前景类和背景类的不平衡,也可以理解为正负样本的不平衡)。这种类别不平衡在Two-stage中是用RPN来抑制的(因为RPN可以判断Anchor的类别,是前景还是背景,减少了Anchor的数量),而在One-stage中可以用加权的损失函数来解决这一问题,这也是Focal Loss提出的主要思路(给正样本更大的权重,给负样本更小的权重,使正样本在损失函数中贡献更大)。

准确来讲,focal loss是通过检测问题中正负样本数量差距过大的场景来解决分类问题中样本不均衡的问题,类推到其他场景如图像分类以及分割等多分类问题也会有很好的效果,而且整个损失函数的形式也不拘束于论文中的形式,一切可以平滑的按概率对损失进行不同程度的抑制或加强的方法都可以使用,而且论文中依然使用了α = 0.25 \alpha=0.25α=0.25这个参数来进一步抑制整体损失,这样可以让损失趋向于0的范围更广,可以根据样本不均衡的程度酌情考虑。参考来源这里

**数学解释:**对于这种问题的关键就是Focal Loss的思路是怎么样的,Focal Loss的思路就是将之前普通的交叉熵损失函数变成了平滑加权的交叉熵损失函数,那么重点就是如何变成平滑加权的,Focal Loss选择的是乘以一个
在这里插入图片描述

的系数,也就是说,我们只要选择任意一个函数(函数必须满足的条件:单调连续增函数,且在趋于0时取较小值,趋于1时取较大值,理想状态是趋于1时变化更快,即函数曲线类似上图)即可。

其他参考文献:
https://blog.csdn.net/zziahgf/article/details/83589973
https://zhuanlan.zhihu.com/p/32631517

论文源码参考:
https://blog.csdn.net/weixin_38145317
https://liumin.blog.csdn.net/article/details/102135318

https://blog.csdn.net/qq_19784349/article/details/86629968

https://blog.csdn.net/weixin_43624538/article/details/88885622

Focal Loss的理解以及在多分类任务上的使用(Pytorch)
https://blog.csdn.net/u014311125/

函数绘制参考:https://blog.csdn.net/zengbowengood/article/details/104369125

猜你喜欢

转载自blog.csdn.net/weixin_41194129/article/details/117693471
今日推荐