【detectron】FPN模型构建

1.FPN的原理

FPN的原理示意图如下,上述包括一个自底向上的线路,一个自顶向下的线路横向连接(lateral connection),图中放大的区域就是横向连接。

自底向上的路径:自下而上的路径是卷积网络的前馈计算,在前向过程中,feature map的大小在经过某些层后会改变,而在经过其他一些层的时候不会改变,作者将不改变feature map大小的层归为一个stage,因此每次抽取的特征都是每个stage的最后一个层输出,这样就能构成特征金字塔。 具体而言,对于ResNets,通过这个表格我们可以知道,conv2,conv3,conv4和conv5就是一个stage,我们使用每个阶段的最后一个residual block输出的特征激活输出。 对于conv2,conv3,conv4和conv5输出,我们将这些最后residual block的输出表示为{C2,C3,C4,C5},并且它们相对于输入图像具有{4, 8, 16, 32} 的步长(也就是相对于输入的图像缩小了4,8,16,32倍)。 

自顶向下的路径:自顶向下的路径通过对在空间上更抽象但语义更强高层特征图进行上采样来幻化高分辨率的特征。也就是将低分辨率的特征图做2倍上采样(为了简单起见,使用最近邻上采样)。横向连接是将上采样的结果和自底向上生成的相同大小的feature map进行融合。这个过程是迭代的,直到生成最终的分辨率图。 

也就是说:

(1)down-top就是每个residual block(C1去掉了,太大太耗内存了),scale缩小2,C2,C3,C4,C5(1/4, 1/8, 1/16, 1/32)。 
(2)top-down就是把高层的低分辨强语义的feature 最近邻上采样2x 
(3)lateral conn 就是把C2通过1x1卷积,使其的channel和top-down过来的一样,然后两者直接相加 ,通过上述操作一直迭代到生成最好分辨率的feature:P2。

这里举一个例子:

(1)利用256个1*1的卷积核对C5进行卷积,生成分辨率最低但语义最强的feature P5,开始迭代 。
(2)然后P5上采样放大2倍,C4经过一个1*1*256的卷积后与放大后P5尺寸相加,也就是融合。

         经过上述两个步骤问为什么是256个卷积核,由于金字塔的所有层次都像传统的特征化图像金字塔一样使用共享分类器/回归器,因此我们在所有特征图中固定特征维度(通道数,记为d)。我们在本文中设置d = 256,因此所有额外的卷积层都有256个通道的输出。
(3)以此迭代下去到P2结束 
(4)对于生成的每一个Pk,后面还加一个3*3的卷积(原文说reduce the aliasing effect of upsampling) ,其目的是消除上采样的混叠效应(aliasing effect)。

(5)最终生成的feature map结果是P2,P3,P4,P5,和原来自底向上的卷积结果C2,C3,C4,C5一一对应。

另外还将FPN用在了RPN中,原来的RPN网络是以主网络的某个卷积层输出的feature map作为输入,简单讲就是只用这一个尺度的feature map。但是现在要将FPN嵌在RPN网络中,生成不同尺度特征并融合作为RPN网络的输入。在每一个scale层,都定义了不同大小的anchor,对于P2,P3,P4,P5,P6这些层,定义anchor的大小为32^2,64^2,128^2,256^2,512^2,另外每个scale层都有3个长宽对比度:1:2,1:1,2:1。所以整个特征金字塔有15种anchor。

对上述的理解总的来说可以用下面这一张图概括。

还有一个问题,RPN生成roi后对应feature时在哪个level上取呢? 
k0是faster rcnn时在哪取feature map呢?例如resnet那篇文章是在C4取的,k0=4(C5相当于fc,也有在C5取的,在后面再多添加fc),比如roi是w/2,h/2,那么k=k0-1=4-1=3 。这里224是ImageNet的标准输入。

还有个问题,从不同level取feature做roipooling后需要分类和回归,这些各个level需要共享吗?本文的做法是共享,还有一点不同的是resnet论文中是把C5作为fc来用的,本文由于C5已经用到前面feature了,所以采用在后面加fc6 fc7,注意这样是比把C5弄到后面快一点。 

2.detectron中的参数

理解了原理,看这些参数就容易多了

# --------------------------------------------------------------------------- #
# FPN 参数
# --------------------------------------------------------------------------- #
__C.FPN = AttrDict()

#是否开启FPN,True:开启 FPN
__C.FPN.FPN_ON = False

# FPN 特征层的通道维度Channel dimension
__C.FPN.DIM = 256

# True,初始化侧向连接lateral connections 输出 0
__C.FPN.ZERO_INIT_LATERAL = False

# 最粗糙coarsest FPN 层的步长
# 用于将输入正确地补零,是需要的
__C.FPN.COARSEST_STRIDE = 32

#
# FPN 可以只是 RPN、或只是目标检测,或两者都用.
#

# True, 采用 FPN 用于目标检测 RoI 变换
__C.FPN.MULTILEVEL_ROIS = False
# RoI-to-FPN 层的映射启发式 超参数
__C.FPN.ROI_CANONICAL_SCALE = 224  # s0:相当于最后公式里的原图大小224
__C.FPN.ROI_CANONICAL_LEVEL = 4  # k0: where s0 maps to,相当于在C4取
# FPN 金字塔pyramid 的最粗糙层Coarsest level,即P5
__C.FPN.ROI_MAX_LEVEL = 5
# FPN 金字塔pyramid 的最精细层Finest level,即P2
__C.FPN.ROI_MIN_LEVEL = 2

# True,在 RPN 中使用 FPN
__C.FPN.MULTILEVEL_RPN = False
# FPN 金字塔pyramid应用于FPN的最粗糙层Coarsest level,即P6
__C.FPN.RPN_MAX_LEVEL = 6
# FPN 金字塔pyramid应用于FPN的最精细层Finest level,即P2
__C.FPN.RPN_MIN_LEVEL = 2
# FPN RPN anchor 长宽比aspect ratios
__C.FPN.RPN_ASPECT_RATIOS = (0.5, 1, 2)
# 在 RPN_MIN_LEVEL 上 RPN anchors 开始的尺寸
# RPN anchors start at this size on RPN_MIN_LEVEL
# The anchor size doubled each level after that
# With a default of 32 and levels 2 to 6, we get anchor sizes of 32 to 512
__C.FPN.RPN_ANCHOR_START_SIZE = 32  #如果检测小物体可以适当调小
# 使用额外的 FPN 层levels, as done in the RetinaNet paper
__C.FPN.EXTRA_CONV_LEVELS = False

3.detectron中的代码

def add_fpn(model, fpn_level_info):
    """Add FPN connections based on the model described in the FPN paper."""
    # FPN levels are built starting from the highest/coarest level of the
    # backbone (usually "conv5"). First we build down, recursively constructing
    # lower/finer resolution FPN levels. Then we build up, constructing levels
    # that are even higher/coarser than the starting level.
    """
    FPN levels 是从骨干backbone 网络的 highest/coarest level(通常为 conv5) 开始构建的. 
    首先向下,递归地(recursively)构建 lower/finer 分辨率的 FPN levels(P5,P4,P3,...); 
    然后向上,构建比起始 level higher/coarser 分辨率的 FPN levels(P6).
    """
    fpn_dim = cfg.FPN.DIM
    min_level, max_level = get_min_max_levels()
    # Count the number of backbone stages that we will generate FPN levels for
    # starting from the coarest backbone stage (usually the "conv5"-like level)
    # E.g., if the backbone level info defines stages 4 stages: "conv5",
    # "conv4", ... "conv2" and min_level=2, then we end up with 4 - (2 - 2) = 4
    # backbone stages to add FPN to.
    num_backbone_stages = (
        len(fpn_level_info.blobs) - (min_level - LOWEST_BACKBONE_LVL)
    )
    
    #这里将conv2_x到conv5_x的输出都称为lateral_input_blobs,可以称为横向链接的输入
    lateral_input_blobs = fpn_level_info.blobs[:num_backbone_stages]
    output_blobs = [
        'fpn_inner_{}'.format(s)
        for s in fpn_level_info.blobs[:num_backbone_stages]
    ]
    fpn_dim_lateral = fpn_level_info.dims  #(2048,1024,512,256)
    xavier_fill = ('XavierFill', {})

    # For the coarsest backbone level: 1x1 conv only seeds recursion
    if cfg.FPN.USE_GN:
        # use GroupNorm
        c = model.ConvGN(
            lateral_input_blobs[0],
            output_blobs[0],  # note: this is a prefix
            dim_in=fpn_dim_lateral[0],
            dim_out=fpn_dim,
            group_gn=get_group_gn(fpn_dim),
            kernel=1,
            pad=0,
            stride=1,
            weight_init=xavier_fill,
            bias_init=const_fill(0.0)
        )
        output_blobs[0] = c  # rename it
    else: #首先对conv5_x的输出进行卷积,卷积的大小为1×1×256,得到P5
        model.Conv(
            lateral_input_blobs[0],  #输入层:res5_2_sum
            output_blobs[0],         #输出层:fpn_inner_res5_2_sum
            dim_in=fpn_dim_lateral[0], #输入的特征图的大小:2048
            dim_out=fpn_dim,       #中间卷积核个数:256
            kernel=1,   #卷积核的大小
            pad=0,      
            stride=1,
            weight_init=xavier_fill,
            bias_init=const_fill(0.0)
        )

    #
    # Step 1: recursively build down starting from the coarsest backbone level
    #  从 coarest backbone level 开始,递归地向下构建 FPN levels

    # For other levels add top-down and lateral connections
    for i in range(num_backbone_stages - 1):
        add_topdown_lateral_module(
            model,
            output_blobs[i],             # top-down blob   P5,fpn_inner_res5_2_sum
            lateral_input_blobs[i + 1],  # lateral blob    P5上采样后要与conv4的输出横向链接,res4_5_sum
            output_blobs[i + 1],         # next output blob   将P5上采样后的与conv4横向链接,得到的结果为fpn_inner_res4_5_sum,也就是P4啦
            fpn_dim,                     # output dimension    256
            fpn_dim_lateral[i + 1]       # lateral input dimension  横向链接的输入的大小,也就是conv4的输入的大小,1024
        )

    # Post-hoc scale-specific 3x3 convs
    blobs_fpn = []
    spatial_scales = []
    for i in range(num_backbone_stages):
        if cfg.FPN.USE_GN:
            # use GroupNorm
            fpn_blob = model.ConvGN(
                output_blobs[i],
                'fpn_{}'.format(fpn_level_info.blobs[i]),
                dim_in=fpn_dim,
                dim_out=fpn_dim,
                group_gn=get_group_gn(fpn_dim),
                kernel=3,
                pad=1,
                stride=1,
                weight_init=xavier_fill,
                bias_init=const_fill(0.0)
            )
        else: #对于每一个Pk,增加3*3的卷积
            fpn_blob = model.Conv(
                output_blobs[i],
                'fpn_{}'.format(fpn_level_info.blobs[i]),
                dim_in=fpn_dim,  #输入:256
                dim_out=fpn_dim, #输出:256
                kernel=3,
                pad=1,
                stride=1,
                weight_init=xavier_fill,
                bias_init=const_fill(0.0)
            )
        blobs_fpn += [fpn_blob]  #这个blobs_fpn是最终的FPN每一层的
        spatial_scales += [fpn_level_info.spatial_scales[i]]

    #
    # Step 2: build up starting from the coarsest backbone level
    #

    # Check if we need the P6 feature map
    if not cfg.FPN.EXTRA_CONV_LEVELS and max_level == HIGHEST_BACKBONE_LVL + 1:
        # Original FPN P6 level implementation from our CVPR'17 FPN paper
        P6_blob_in = blobs_fpn[0] #P6的输入是P5的输出 gpu_0/fpn_res5_2_sum
        P6_name = P6_blob_in + '_subsampled_2x'  #gpu_0/fpn_res5_2_sum_subsampled_2x
        # Use max pooling to simulate stride 2 subsampling
        P6_blob = model.MaxPool(P6_blob_in, P6_name, kernel=1, pad=0, stride=2)   #gpu_0/fpn_res5_2_sum_subsampled_2x
        blobs_fpn.insert(0, P6_blob)   #增加P6_blob
        spatial_scales.insert(0, spatial_scales[0] * 0.5) 

    # Coarser FPN levels introduced for RetinaNet
    if cfg.FPN.EXTRA_CONV_LEVELS and max_level > HIGHEST_BACKBONE_LVL:
        fpn_blob = fpn_level_info.blobs[0]
        dim_in = fpn_level_info.dims[0]
        for i in range(HIGHEST_BACKBONE_LVL + 1, max_level + 1):
            fpn_blob_in = fpn_blob
            if i > HIGHEST_BACKBONE_LVL + 1:
                fpn_blob_in = model.Relu(fpn_blob, fpn_blob + '_relu')
            fpn_blob = model.Conv(
                fpn_blob_in,
                'fpn_' + str(i),
                dim_in=dim_in,
                dim_out=fpn_dim,
                kernel=3,
                pad=1,
                stride=2,
                weight_init=xavier_fill,
                bias_init=const_fill(0.0)
            )
            dim_in = fpn_dim
            blobs_fpn.insert(0, fpn_blob)
            spatial_scales.insert(0, spatial_scales[0] * 0.5)

    return blobs_fpn, fpn_dim, spatial_scales

最终生成的结构为:

P6gpu_0/fpn_res5_2_sum_subsampled_2x
P5gpu_0/fpn_res5_2_sum
P4gpu_0/fpn_res4_5_sum
P3gpu_0/fpn_res3_3_sum
P2gpu_0/fpn_res2_2_sum

进行rois分配的代码如下:

def map_rois_to_fpn_levels(rois, k_min, k_max):
    """Determine which FPN level each RoI in a set of RoIs should map to based
    on the heuristic in the FPN paper.
    """ 
    # Compute level ids
    s = np.sqrt(box_utils.boxes_area(rois))
    s0 = cfg.FPN.ROI_CANONICAL_SCALE  # default: 224
    lvl0 = cfg.FPN.ROI_CANONICAL_LEVEL  # default: 4

    # Eqn.(1) in FPN paper
    target_lvls = np.floor(lvl0 + np.log2(s / s0 + 1e-6))
    target_lvls = np.clip(target_lvls, k_min, k_max)
    return target_lvls

其中

max_level = cfg.FPN.ROI_MAX_LEVEL
min_level = cfg.FPN.ROI_MIN_LEVEL

从中可以看出,k的取值是向下取整的,例如当s0=224,s=4时,若边长为150,向下取值后,lvl0=3。

若target_lvls>max_level,则target_lvls = max_level;若target_lvls<min_level,则target_lvls = min_level。

4.如何融合不同层的特征

上面的代码默认融合的是conv2-conv5的特征(P6层实际上是P5向上采样得到的仅用来构建RPN产生候选区,并没有融合其他层的特征)实际上,我们都想知道针对不同的分类任务究竟融合哪几层才是最为有效的,那自然就要改变FPN的层数,这时候就出现下面四个非常重要的函数,该修改哪个呢?

  • C.FPN.ROI_MAX_LEVEL
  • C.FPN.ROI_MIN_LEVEL
  • C.FPN.RPN_MAX_LEVEL
  • C.FPN.RPN_MIN_LEVEL

在FPN网络中,ROI head所取的层数是与RPN的层数不一样的,也就是说我们可以添加P2-P6层,这些层都添加RPN,但是不同的ROI会根据大小分配到不同fpn层,就会采用不同层的特征,因此P2-P6层并不都用于ROI head(derectron中默认RPN是P2-P6,ROI是P2-P5)。按照我们1.中所讲,边长为32的rois在fast rcnn部分就会用P2层的特征,边长为64的rois就会用到P3层的特征,依次类推。如果我们规定了ROI head使用的层数,那么在fast rcnn部分就只会用这几层的特征,所以看上面rois分配的带代码,如果target_lvls>ROI_MAX_LEVEL,则target_lvls = ROI_MAX_LEVEL;若target_lvls<ROI_MIN_LEVEL,则target_lvls = ROI_MIN_LEVEL。

因此在设置上面四个参数时候,最先必须满足的条件如下,RPN_MAX_LEVEL必须大于等于ROI_MAX_LEVEL,这是我们改变上面4个参数的先决条件。

def _narrow_to_fpn_roi_levels(blobs, spatial_scales):
    """Return only the blobs and spatial scales that will be used for RoI heads.
    Inputs `blobs` and `spatial_scales` may include extra blobs and scales that
    are used for RPN proposals, but not for RoI heads.
    返回仅用于ROI head的fpn层,因为输入还包括那些用的生成RPN proposal的fpn层
    """
    # Code only supports case when RPN and ROI min levels are the same
    assert cfg.FPN.RPN_MIN_LEVEL == cfg.FPN.ROI_MIN_LEVEL
    # RPN max level can be >= to ROI max level
    assert cfg.FPN.RPN_MAX_LEVEL >= cfg.FPN.ROI_MAX_LEVEL
    # FPN RPN max level might be > FPN ROI max level in which case we
    # need to discard some leading conv blobs (blobs are ordered from
    # max/coarsest level to min/finest level)
    num_roi_levels = cfg.FPN.ROI_MAX_LEVEL - cfg.FPN.ROI_MIN_LEVEL + 1
    return blobs[-num_roi_levels:], spatial_scales[-num_roi_levels:]

明白之后,看2.中添加fpn的代码,代码开始就调用了如下函数。该函数就是确定要融合哪几层的特征。一般情况下,多尺度的RPN和多尺度的ROIS都为True,因此会进入到第三个if,这时候就可以发现,两者的最大值就是添加fpn的最高层(coarse,最粗糙层),两者的最小值就是添加fpn的最底层(finest,最精细层)

def get_min_max_levels():
    """The min and max FPN levels required for supporting RPN and/or RoI
    transform operations on multiple FPN levels.
    """
    min_level = LOWEST_BACKBONE_LVL  #2
    max_level = HIGHEST_BACKBONE_LVL  #5
    if cfg.FPN.MULTILEVEL_RPN and not cfg.FPN.MULTILEVEL_ROIS:  #如果使用MULTILEVEL_RPN,但是不使用MULTILEVEL_ROIS
        max_level = cfg.FPN.RPN_MAX_LEVEL
        min_level = cfg.FPN.RPN_MIN_LEVEL
    if not cfg.FPN.MULTILEVEL_RPN and cfg.FPN.MULTILEVEL_ROIS:  #如果使用MULTILEVEL_ROIS,但是不使用MULTILEVEL_RPN
        max_level = cfg.FPN.ROI_MAX_LEVEL
        min_level = cfg.FPN.ROI_MIN_LEVEL
    if cfg.FPN.MULTILEVEL_RPN and cfg.FPN.MULTILEVEL_ROIS:      #如果两者都使用
        max_level = max(cfg.FPN.RPN_MAX_LEVEL, cfg.FPN.ROI_MAX_LEVEL) #取两者之中的最大,和两者之中的最小
        min_level = min(cfg.FPN.RPN_MIN_LEVEL, cfg.FPN.ROI_MIN_LEVEL)
    return min_level, max_level

因此如果你要融合conv2-conv4,并鉴于RPN_MAX_LEVEL必须大于等于ROI_MAX_LEVEL,一般令RPN_MAX_LEVEL = 4即可,此时ROI_MAX_LEVEL的值不大于RPN_MAX_LEVEL即可。若你此时是检测小物体,因为物体小,P2,P3层产生的候选区大小就足以满足,并且P2,P3层相对于P4层也更精细一些,你就可以将ROI_MAX_LEVEL设置为3,那么在实际的训练和预测中,候选区只会用P2-P3的特征输入到fast rcnn部分训练和预测。此外也可能需要修改s0和s,也就是rois分配的方式。

当然除了修改上面4个参数还不够,还要在FPN.py开头中增加一个新的函数

def add_fpn_ResNet50_conv5_body_P4(model):
    return add_fpn_onto_conv_body(
        model, ResNet.add_ResNet50_conv5_body, fpn_level_info_ResNet50_conv4
    )

 其中第二个参数ResNet.add_ResNet50_conv5_body照常,因为是构建resnet模型我们无需改变。第三个参数fpn_level_info_ResNet50_conv4返回的是我们需要添加的fpn层的名称,我们要增加这个函数,如下。可见相比于默认融合conv5的fpn_level_info_ResNet50_conv5函数,少了P5层。

def fpn_level_info_ResNet50_conv4():
    return FpnLevelInfo(
        blobs=('res4_5_sum', 'res3_3_sum', 'res2_2_sum'),
        dims=(1024, 512, 256),
        spatial_scales=( 1. / 16., 1. / 8., 1. / 4.)
    )

至此只需要在yaml文件中,进行如下修改即可:

CONV_BODY: FPN.add_fpn_ResNet50_conv5_body_P4

如果你想融合的是conv3-conv5,只需要修改:RPN_MIN_LEVEL = ROIS_MIN_LEVEL = 3 即可,无需添加新的函数。

也就是说,只要融合的最高层不是Conv5,那么就需要添加上述的函数,例如融合2-4,3-4。只要融合的最高层有conv5,那么可以不用添加函数

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值