EfficientDet ---最细节的论文解析

EfficientDet: Scalable and Efficient Object Detection

论文地址:EfficientDet
源码地址:torch版本的代码,官方的是tensorflow版本的
本文做的目标检测使用的backbone依旧是谷歌所提出的EfficientNet,关于EfficientNet的论文和代码及讲解我这里不展开细讲,推荐一个博主霹雳巴拉雷,之前也是看他在B站以及CSDN的代码教学,学到了很多,这里表示感谢。

PS: 这是本人的第一篇博客,现在研一在读,水平有限,如有不正确的地方,请多包含>0<
也希望可以私信我或者评论留言,一起讨论

1. 摘要
模型效率和模型准确度对目标检测任务来说是非常重要的,本文设计了新的网络结构来做检测任务,并且提出了一些新的优化方法来提升模型效率。
主要包括两点:
1)提出加权双向的多尺度的特征融合网络(bi-directional feature pyramid network,BiFPN)
2)使用compound scaling method同时对resolution,depth,width(这三个方面是针对backbone的);
特征金字塔网络,以及最后的class/box predictions network进行缩放。

称这个网络为EfficientDet,相比之前的网络结构实现了最好的精度和效率,在EfficientDet-D7在COCO2017的test-dev数据集上获得目前为止最高的AP值55.1,相比之前的检测网络的参数量少了4-9倍以及FLOPs少了13-42倍。

EfficientDet ---最细节的论文解析_第1张图片
关于Introduction和Related work部分我就不讲了,自己可以读读论文。主要讲解两个创新点

2. Network Architecture----BiFPN
2.1 Cross-Scale Connection
EfficientDet ---最细节的论文解析_第2张图片
上图为3中之前的FPN结构和论文中提出的BiFPN,图a表示传统的FPN结构,只有一个top-down分支。这里的P3-P7表示输入图像下采样的比例,对于P3而言,特征图的大小=原图/2^3, P4-P7同理,传统的做法是将5个预测特征图先各自做一次11的卷积操作(包括BN+relu);然后将P7经过上采样得到与P6相同size的特征图,进行feature fusion,即直接相加,在经过一次33的卷积操作(包括BN+relu)得到P6 level的预测特征图的输出;同理可得到P5-P3 level的特征图。注意这里的P7 level就是P7先经过11卷积再经过33卷积直接输出的,由于该节点只有一个输入,没有经过feature fusion。
EfficientDet ---最细节的论文解析_第3张图片
首先引入多尺度融合问题,对于一个多尺度特征图序列Pin = {Pin1,Pin2,…},目标就是找到一个变换f可以有效的聚合来自不同尺度的特征图,再输出一个新的聚合之后的特征Pout = f(Pin).

以右图中的a传统的FPN为例,输入的特征图有Pin={P3in,P4in,
P5in,P6in,P7in},因此传统的FPN聚合特征方式是以top-down的形式的,如上面公式

到这里,那么传统FPN的缺点是什么呢?很明显,传统的FPN仅仅有一个top-down分支,信息流单一,feature fusion比较有局限性。因此在此基础上加上一条bottom-up分支,就成了如图b所示的PANet,解决了图a中的问题,虽然精度超过传统FPN以及NAS-FPN,但是是以更多的参数量以及FLPOs为代价的。对于图c,使用Neural Architecture Search网络结构搜索技术来自动的去寻找特征网络拓扑,在精度和计算量上比较平衡,但是需要GPU花费大量的时间去找到这么一个拓扑图,并且这种拓扑图往往是无规则的,可解释性比较差,很难修改。

因此,为解决accuracy和efficiency的权衡问题,论文中提出图D的做法,关于如何设计出这样的拓扑图的,有以下三个主要步骤
1)移除只有一个输入的节点,如上图b中的P7 level的中间一个节点和P3 level中的第三个节点,主要是为了简化网络,更好的进行feature fusion;
2) 增加一条额外的边,将原始的输入与bottom-up中的节点直接相连,相当于是一个残差连接,也就是图d中的P4-P6中的跳跃的边。主要是为了在不增加计算代价的前提下,可以融合更多更复杂的特征;
3) 不同于传统FPN和PANet,只将feature fusion部分做一次,而是将一个top-down分支和一个bottom-up分支作为一个block或者说一个特征融合层,然后重复这个block多次,为了融合更多high-level的特征。

2.2 Weighted Feature Fusion
首先我们要知道,在做feature fusion的时候,一般都是将不同尺度的tensor先resize成统一尺寸,然后直接相加。先前的方法大多都是没有区别的对待不同level的特征图。但是,由于不同level的特征图拥有不同的分辨率,对最后的output feature所带来的贡献是有所不同的,所以不应该一视同仁的去看待所有level的特征图。因此,论文中对每个输入都增加了一个可学习的权重,使得网络可以学习到每个输入特征的重要性,基于此,论文中提出三种加权融合方式:
1) Unbounded fusion 无约束的融合:在这里插入图片描述
这里的权重w可以是一个标量scalar,针对每一个特征图而言的;也可以是一个向量vector,针对feature map中的一个channel而言的;也可以是一个多维的tensor或者将矩阵,针对每一个pixel而言的。这种方式的计算代价很低,但是由于权重w是没有限制的,潜在的造成训练的不稳定,因此,我们要让每个权值可以归一化到一个固定的范围。因此引入下面两种归一化的融合方式

2 )Softmax-based fusion 基于softmax归一化的融合:
在这里插入图片描述
使用softmax归一化可以讲每一个权重normalize到一个概率值(0-1之间),更加明显的表示每个输入特征的重要性程度。该方法可以解决方法1中融合带来的问题,但是,这种softamx归一化会slowdownGPU的使用,为了最小化softmax带来的推理延迟代价,引入下面一种融合方式。
3)Fast normalized fusion 更快的归一化融合:
在这里插入图片描述
这里的每个权重wi都需要经过一个Relu函数,确保每个wi的值都≥0,与Softmax norm类似,也可将每个权重归一化到0-1之间,优点在于:不仅与Softmax fusion的精度相差不大,而且比softmax fusion的模型效率更高,推理速度更快,延迟更低。epsilon设置为0.0001用来保证数值的稳定。
EfficientDet ---最细节的论文解析_第4张图片
上面的实验图表示的是使用softmax融合和fast融合所学到的权重分布是相差不大的,这也就证明了为什么fast融合与softmax融合的accuracy是差不多的。

3. Network Architecture
首先给出原论文的网络结构图,因为原论文对于BIFPN部分的操作并没有展开,我根据源码对网络框架图进行了补充,在后面的源码部分再给出详细的解析,先上图
EfficientDet ---最细节的论文解析_第5张图片

3.1 BiFPN
首先对于backbone部分,使用的是EfficientNet-B0到B7分别对应论文中的EfficientDet-D0到D7,由于EfficientNet中对原图的缩放只达到 ‘/32’ 这个level,也就是说backbone只能输出上图中的P1–P5,又由于浅层的特征图一般不具有high-level的特征,所有对于前三层的特征图并没有使用,只拿到EfficientNet中的P3,P4,P5这三个输出的特征图,所有先要将P5进行下采样两次得到P6,P7,如何采样呢?论文中也没细讲,只能去源码中找答案:
P5到P6经过一个DWconv(也就是论文中提到的可分离卷积) + BN + Maxpool,P6到P7只需要经过一次最大池化;
P6和P7可直接作为P6in和P7in;
而对于P3,P4和P5,都需要做一次1*1的卷积+BN操作得到P3in,P4in,P5in;
由于P4和P5需要做一次残差连接,所以源码中见给P4->P4in和P5->P5in做了两次,即得到两个相同的P4in和两个相同的P5in;

在获得P3in,P4in_1, P4in_2, P5in_1, P5in_2, P6in, P7in,
再将P7in进行上采样乘上权重w1与P6in*权值w2进行feature fusion得到P6up;之后再对P6up进行上采样乘上权值w1与P5in_1乘上权值w2做feature fusion得到P5up;
再对P5tup进行上采样乘上权值w1与P4in_1乘上权值w2做feature fusion得到P4up;
之后对P4up进行上采样再乘上权值w1与P3in乘上权值w2融合得到P3out;

注意,这里总共有4组初始化全为1的权值(size为2),也就是说每次融合的权重为一组,并且每个权值都经过relu激活函数,保证权值wi都≥0。并且在做特征融合的之前需要归一化再乘上各自的input feature,即(wi/w1+w2+epison)

注意这里的P3in,P4in以及P5in,P6in都是采用普通的1*1卷积,主要是为了调整channel数,由于P7in仅仅是P6in经过maxpool之后得到的,因此channel不改变,只将H和W缩减为P6in的一半。而在特征融合的时候,由于P7经过最近邻插值上采样得到与P6in相同的size,将两个相同尺寸的特征图乘上各自的权值再直接相加,之后经过一个swish激活函数,再经过一个DW卷积和一次BN操作,才得到P6_up; 同理得到P5up,P4up和P3out。
再将P4in,P4up和P3out乘上各自的权值,经过swish激活函数(x * sigmoid),再通过一个DWconv+BN操作得到P4out;同理得到P5out,P6out以及P7out,而对于P7out只有两个输入,P4out-P6out都是3个输入。

针对EfficientDetB0来说,P3.shape=(batchsize, 40, 64, 64), P4.shape = (batchsize, 112,32,32),
P5.shape = (batchsize, 320,16,16)经过1x1的普通卷积操作(输出channel均为64)+BN操作得到P3in,P4in和P5in,再将P5经过一次1x1的卷积(输出channel为64)+BN+maxpool得到P6in,
P6in.shape=(batchsize,64,8,8),进而将P6in经过一次maxpool得到P7in,且P7in.shape=(batch,64, 4,4)。并且这里一共有8组权重,因为每一个BiFPN模块有8次feature fusion的过程,对应论文中的fast norm fusion,在融合的时候使用。
注意这里的每组权重都被初始化为全1,并且每个权重都经过relu

将第一次得到的P3out-P7out作为第二个BIFPN模块的输入即对应于第二个BIFPN模块的P3in-P7in,后面的操作同第一个BIFPN模块。

3.2 预测部分

将BIFPN的输出给分类网络和回归网络,对于5个预测特征图的输入,相互独立的进入到BoxNet和ClassNet。对于ClassNet,5个预测图首先都要进行3次输入输出channel(对于EfficientDet-D0而言channel=64)都相等的可分离卷积即DW + PW卷积,之后进行一个卷积对该特征图上所有anchor进行类别预测,输入channel=64,输出channel= num_anchors * num_classes,num_anchors表示特征图上每个cell预测的anchors数目=9,num_classes=表示每个anchors要预测的类别数,其中的3次卷积之后都需要在后面跟上一个BN和一个Swish激活函数。再将一个batch下的所有图片在5个预测层上的anchors预测结果concat,输出的tensor.size=(batch_size,一张图片上的所有anchors数,num_classes)。最后将预测结果进行一个sigmoid函数得到每个anchors的预测类别分数。
分类loss使用的是Focal loss
EfficientDet ---最细节的论文解析_第6张图片

class FocalLoss(nn.Module):
    def __init__(self):
        super(FocalLoss, self).__init__()
    # 传递进来的参数为:class,regression参数,生成的anchors信息以及GT信息,如(4,14,5)这里的4为batchsize,14为GT数目,5表示每一个GT有4个坐标信息+1个类别索引
    def forward(self, classifications, regressions, anchors, annotations, **kwargs):
        alpha = 0.25 # focal loss中的α参数,u用来平衡正负样本的超参数
        gamma = 2.0  # focal loss中的β参数,用来降低easy-examples对loss的贡献
        batch_size = classifications.shape[0] # 获取batchsize = 4
        classification_losses = [] 
        regression_losses = []
        anchor = anchors[0, :, :]  # 获取anchors第一个维度的值即 (49104, 4)
        dtype = anchors.dtype
        # anchor: (y1, x1, y2, x2)---> (x,y,w,h)
        anchor_widths = anchor[:, 3] - anchor[:, 1] # w = x2 - x1
        anchor_heights = anchor[:, 2] - anchor[:, 0] # h = y2 -y1
        anchor_ctr_x = anchor[:, 1] + 0.5 * anchor_widths # x = x1 + 0.5*w
        anchor_ctr_y = anchor[:, 0] + 0.5 * anchor_heights # y = y1 + 0.5*h
        # 遍历一个batch下的所有图片
        for j in range(batch_size):

            classification = classifications[j, :, :] # 获取第j张图片的类别分数
            regression = regressions[j, :, :]         # 获取第j张图片的回归参数
 
            bbox_annotation = annotations[j]          # 获取第j张图片的GT信息,size = (GT个数, 5)
            bbox_annotation = bbox_annotation[bbox_annotation[:, 4] != -1] # 判断第二个维度的最后一个值是否等于-1,等于-1表示背景则丢弃,保留前景GT

            classification = torch.clamp(classification, 1e-4, 1.0 - 1e-4) # 将类比分数参数的最小值设置为0.0001,最大值设置为0.9999
            
            if bbox_annotation.shape[0] == 0: # 如果GT的第一维度的值为0,表明该image的GT中不存在目标
                if torch.cuda.is_available():
                    
                    alpha_factor = torch.ones_like(classification) * alpha
                    alpha_factor = alpha_factor.cuda()
                    alpha_factor = 1. - alpha_factor
                    focal_weight = classification
                    focal_weight = alpha_factor * torch.pow(focal_weight, gamma)
                    
                    bce = -(torch.log(1.0 - classification))
                    
                    cls_loss = focal_weight * bce
                    
                    regression_losses.append(torch.tensor(0).to(dtype).cuda())
                    classification_losses.append(cls_loss.sum())
               else:
                    
                    alpha_factor = torch.ones_like(classification) * alpha
                    alpha_factor = 1. - alpha_factor
                    focal_weight = classification
                    focal_weight = alpha_factor * torch.pow(focal_weight, gamma)
                    
                    bce = -(torch.log(1.0 - classification))
                    
                    cls_loss = focal_weight * bce
                    
                    regression_losses.append(torch.tensor(0).to(dtype))
                    classification_losses.append(cls_loss.sum())

                continue
            # GT中存在目标,执行下面语句  
            IoU = calc_iou(anchor[:, :], bbox_annotation[:, :4]) # 计算anchors与GT(取前四个为坐标信息)的IOU值,size = (49104, GT数目)

            IoU_max, IoU_argmax = torch.max(IoU, dim=1) # 得到每个anchor与哪一个GT的IOU的最大值,IOU_max保存的是anchor与GT的最大iou值,
            # IOU_argmax保存的是当前anchor对应哪一个GT的iou值最大,记录的是GT的索引

            # compute the loss for classification
            targets = torch.ones_like(classification) * -1 # 生成一个与classification相同size值全为-1的mask  size = (49104,90)
            if torch.cuda.is_available():
                targets = targets.cuda()
            # torch.lt,torch.ge都是逐个元素的比较,size = (49104, 90)
            targets[torch.lt(IoU_max, 0.4), :] = 0 # iou值小于0.4的都为True即为负样本,否则为False,将targets中为True的anchor那一行数值全部置为0
            # x = [i for i in targets if i > -1]
            # targets_negative = len(x) # 统计负样本的个数
            # size = (49104)
            positive_indices = torch.ge(IoU_max, 0.5) # IOU_max中≥0.5的返回True即为正样本,否则返回false,对应true的位置的anchor为正例

            num_positive_anchors = positive_indices.sum() # 将positive_indices中的数值求和,得到正样本个数

            assigned_annotations = bbox_annotation[IoU_argmax, :] # 扩充GT的维度,size = (1, 5)--->(49104, 5)            
            targets[positive_indices, :] = 0  # 将正样本索引位置的anchor信息置为0  (49104, 90)
          
            targets[positive_indices, assigned_annotations[positive_indices, 4].long()] = 1
            # y = [i for i in targets if i > 0]
            # targets_positive = len(y)  # 统计正样本的个数
            
            alpha_factor = torch.ones_like(targets) * alpha # 生成一个与targets size相同的α参数mask,值全为0.25
            if torch.cuda.is_available():
                alpha_factor = alpha_factor.cuda()
            # 若targets中某个元素值为1,则为正样本,正样本的α参数为0.25,反之为负样本,负样本的α参数为0.75
            alpha_factor = torch.where(torch.eq(targets, 1.), alpha_factor, 1. - alpha_factor)
            # 若targets中某个元素值为1,则为正样本,正样本的类别权重为(1 - P),反之为负样本,负样本的类别权重为P
            focal_weight = torch.where(torch.eq(targets, 1.), 1. - classification, classification)
            # 得到focal loss的权重,对于正样本,loss = α * (1-p)^γ;对于负样本,loss = (1-α) * p^γ
            focal_weight = alpha_factor * torch.pow(focal_weight, gamma)
            # 分类的crossentropy:-(plogp + (1-p)log(1-p))
            bce = -(targets * torch.log(classification) + (1.0 - targets) * torch.log(1.0 - classification))
            # 分类的crossentropy乘上focal loss的权重因子得到focal loss     size = (49104, 90)
            cls_loss = focal_weight * bce 

            zeros = torch.zeros_like(cls_loss) #生成一个与cls_loss相同size的tensor,并令其中的值全为0
            if torch.cuda.is_available():
                zeros = zeros.cuda() 
            # 如果targets中的元素不等于-1,表示为前景的分类loss,则返回cls_loss,否则为背景,对于背景没有分类loss,直接返回0
            cls_loss = torch.where(torch.ne(targets, -1.0), cls_loss, zeros)
            # 求得全部正样本的分类loss/总的正样本数------class loss
            classification_losses.append(cls_loss.sum() / torch.clamp(num_positive_anchors.to(dtype), min=1.0))

            # 如果正样本个数大于0,此时才有bbox的回归loss
            if positive_indices.sum() > 0:
                assigned_annotations = assigned_annotations[positive_indices, :] # 获取正样本的GT信息

                anchor_widths_pi = anchor_widths[positive_indices]   # 获取anchors中正样本的w
                anchor_heights_pi = anchor_heights[positive_indices] # 获取anchors中正样本的h
                anchor_ctr_x_pi = anchor_ctr_x[positive_indices]     # 获取anchors中正样本的x
                anchor_ctr_y_pi = anchor_ctr_y[positive_indices]     # 获取anchors中正样本的y
                # GT :(x1, y1, x2, y2)
                gt_widths = assigned_annotations[:, 2] - assigned_annotations[:, 0]  # 获取正样本对应GT的w
                gt_heights = assigned_annotations[:, 3] - assigned_annotations[:, 1] # 获取GT的h
                gt_ctr_x = assigned_annotations[:, 0] + 0.5 * gt_widths              # 获取GT的x
                gt_ctr_y = assigned_annotations[:, 1] + 0.5 * gt_heights             # 获取GT的y
 
                # efficientdet style 将GT的w和h参数的最小值设置为1
                gt_widths = torch.clamp(gt_widths, min=1)    
                gt_heights = torch.clamp(gt_heights, min=1) 
                # 将anchor相对于GT的坐标encode之后得到target,因为并不是用regressio去拟合GT而是拿regression取拟合target,再送入后面的SmoothL1 loss
                targets_dx = (gt_ctr_x - anchor_ctr_x_pi) / anchor_widths_pi    # x = (GT_x - anchor_x)/anchor_w
                targets_dy = (gt_ctr_y - anchor_ctr_y_pi) / anchor_heights_pi   # y = (GT_y - anchor_y)/anchor_h
                targets_dw = torch.log(gt_widths / anchor_widths_pi)            # w = log (GT_w / anchor_wa)
                targets_dh = torch.log(gt_heights / anchor_heights_pi)          # h = log (GT_h / anchor_ha)

                targets = torch.stack((targets_dy, targets_dx, targets_dh, targets_dw))
                targets = targets.t()
                # 将decoder后的坐标信息与GT做差之后再取绝对值
                regression_diff = torch.abs(targets - regression[positive_indices, :])  
                # SmoothL1 loss----regression 使用smoothL1即光滑的L1 loss,由于普通的L1 loss有折点,不光滑,导致BP时不稳定,采用Smooth之后
                # 并且使用分段函数的设置,相对于普通的L2 loss,对离群点鲁棒性更好,异常值不敏感,不会导致训练结果跑飞的情况
                regression_loss = torch.where(
                    torch.le(regression_diff, 1.0 / 9.0),  # 如果|pred-GT|<1/9,那么就使用1/2*9*regression_diff的平方
                    0.5 * 9.0 * torch.pow(regression_diff, 2), 
                    regression_diff - 0.5 / 9.0     #否则loss=|pred-GT| - 0.5/9
                )
                regression_losses.append(regression_loss.mean())
            else:
                if torch.cuda.is_available():
                    regression_losses.append(torch.tensor(0).to(dtype).cuda())
                else:
                    regression_losses.append(torch.tensor(0).to(dtype))

同理,BoxNet部分类似ClassNet,也是将5个预测特征图首先经过3次DW卷积,输入输出channel都为64,即输入输出相同的DWconv+PWconv,再通过一个卷积,输入channel为64,输出channel=(num_anchors4),num_anchors表示特征图上一个cell需要预测num_anchors个anchor,4表示每个anchor需要预测的4个坐标信息。卷积之后调整维度信息,将原来的(batchsize,36,h,w)–>(batchsize,hw9,4),这样得到第一个特征图的所有回归参数。同理将剩下的4个预测特征图再做上面的操作,最后将5个预测特征层的输出在维度1上进行concat得到一个batch下每张图片的anchors回归参数(batchsize, h1w19+…+h5w5*9, 4).
回归loss使用的是SmoothL1 loss

3.3 Compound Scaling
同时对backbone,BiFPN,class/box Network 以及输入网络的图像分辨率同时做比例缩放
关于backbone就是使用不同的EfficientNet;
关于BiFPN部分,其中有Wbifpn表示BiFPN的宽度,即做DW卷积时使用的输出channel数,也就是得到P3in-P7in中的特征图的channel为Wbifpn,以及在feature fusion时的DW卷积输出channel为Wbifpn。

线性堆叠BiFPN模块,可增加网络的深度D_bifpn,而对于BiFPN的宽度W_bifpn,其中关于width的参数设置,论文是采用一系列的参数{1.2,1.25,1.3,1.35,1.4,1.45}再从中选取一个最佳的参数,以及class/box predictions netwrok,也是根据缩放系数φ来统一缩放,具体公式如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面给出EfficientDet-D0到D7x的网络配置信息表格:
EfficientDet ---最细节的论文解析_第7张图片
注意的是怎么计算Wbifpn,举个例子:对于D1版本,64*1.35^1 = 86.4,再将该数值调整到离他最近的8的整数倍即等于表格中的88;
而Dbifpn就是做多少次BiFPN模块操作;
Dbox和Dclass表示对每个预测特征图要进行多少次DW卷积+BN+Swish操作。
其他版本的配置信息同理可求得

4. Experiment

EfficientDet ---最细节的论文解析_第8张图片
在COCO上测试的,其中EfficientDet-D7x在当年达到了最高的mAP 值55.1以及AP50 (AP50就是IOU取0.5时测出来的AP,也就是VOC数据集常用的AP标准)74.3 相当的可观,非常的牛皮可以说,并且推理速度和延迟也并没有很高
下面的几个版本的延迟高是因为输入图像的分辨率太高了,作者在Appendix中指出,如果将D6的输入分辨率改为640x640,可以达到47.9的AP值以及34ms的推理延迟,可以做到实时检测

EfficientDet ---最细节的论文解析_第9张图片

消融实验有:
Disentangling backbone and BiFPN
EfficientDet ---最细节的论文解析_第10张图片
可以明显的看到,当替换backbone时AP值提高了3.3,在此基础上替换BiFPN又可以提高4.1个AP值,足以说明论文中的backbone和bifpn的设计是有效的

BiFPN Cross-Scale Connection
EfficientDet ---最细节的论文解析_第11张图片

Softmax & Fast Normalized Fusion
EfficientDet ---最细节的论文解析_第12张图片
我用预训练权重文件冻结了backbone和BIFPN模块,只微调ClassNet和BoxNet部分,训练了10个epoch,下面给出预测的效果图,训练的是EfficientDet-DO模型

个人感觉,这篇论文给我的启发还是很大的,在one-stage和two-stage的目标检测中,精度和效率一直是无法兼顾,就像是鱼和熊掌不可兼得,但是这篇论文给出的想法非常Nice,你可以根据对精度和效率的实际需求,来选择不同版本的检测框架。

后面有时间我会写一篇代码的详细解析,看了这么多论文,发现其实很多细节都是需要从源码中找到,有可能是作者希望我们多去拜读一下他的代码吧>0<

PS:如需使用本文中的观点,请标明转载,谢谢

你可能感兴趣的:(卷积,计算机视觉,神经网络,人工智能)