查看原文
其他

RegNet:设计网络设计空间

AI小将 机器学习算法工程师 2021-12-31

点蓝色字关注“机器学习算法工程师

设为星标,干货直达!


网络设计空间介绍了网络设计空间的概念以及如何用统计学方法来评估网络设计空间,那么有没有可能对网络设计空间进行优化呢,这就是Designing Network Design Spaces这篇论文所做的工作:设计网络设计空间。如图所示,所谓设计网络设计空间就是不断地对设计空间优化,每一次迭代后设计空间变得更简单和更好,这里的设计空间质量评估就是采用前面说过的分布估计:从设计空间随机抽样一定量模型,然后得到EDF。可以看到从A到B再C,设计空间容量逐渐缩小(更简单),而EDF也得到提升(更好)。对于网络设计,早期的人工设计已经逐步被自动化的NAS来替代,NAS是从一个设定的搜索空间中自动找到一个好的模型,这个搜索往往会限定条件比如复杂度。NAS存在的问题是搜索的模型比较难理解背后的设计原则,而且如果是限定条件的设计也比较难泛化到其它设定下(比如不同的flops,解决方案是先用NAS搜索一个base网络,然后采用缩放策略得到不同flops的网络,如EfficientNet)。与NAS不同,设计网络空间最后的目标是得到一个更好的网络设计空间,而不是一个具体的网络,这里的设计会注重发现通用的设计原则,因为泛化性更好。设计网络空间也是采用半自动化流程,但与人工设计网络有点类似,每一个迭代是通过分布估计来发现能产生好网络的设计空间。

论文以ImageNet数据集做实验,为了提升效率,采用 low-compute和low-epoch训练策略,具体地,从设计空间中随机抽取500个flops约为400MF(10^6)的模型,在ImageNet上训练10个epoch得到error,并画出EDF来评估设计空间质量,这里用min error和mean error(EDF曲线上面积)来定量评估设计空间,另外采用empirical bootstrap来估计一些参数的分布。下图最左边展示了AnyNetX这个设计空间的EDF,这里的min error和mean error分别是39.0和49.0,另外两个图给出了stage4的depth和width的分布范围。

论文选择的初始设计空间为AnyNet,AnyNet包括3个部分:stem,body和head。其中stem是固定的:采用stride=2的3x3卷积层,输出channels为32。而head包括一个global avg pooling和用来分类的fc层。AnyNet设计空间的自由度就只在body里了,body包括4个stages,每个stage采用固定的block(比如residual bottleneck block),每个stage开始会采用stride=2的block来降低输入大小。可以看到AnyNet采用了CNN最常用的范式,设计空间的超参数就是各个stage的block数量,block的width(channels),以及其它的block内部参数(如采用residual bottleneck block还需要定义bottleneck ratio)。虽然AnyNet结构简单,但是这个设计空间的模型数是无穷的。

进一步地,这里限定block采用带group conv的residual bottlenecks block,这个block称为X block,对应的设计空间称为AnyNetX,可以看成AnyNet的一个子空间。X block的结构如下所示,包括1x1 conv ->3x3 group conv -> 1x1 conv以及残差连接,这里的1x1卷积用来改变channels,每个conv后跟BN+ReLU。对于stride=2的block,设定3x3 group conv的stride=2。X block共有3个参数:width ,bottleneck ratio 和 group width ,对于一个stage来说,除了这3个参数,还有block的数量。所以AnyNetX共有4x4=16个自由度(超参数),注意这里固定模型的图像输入为224。由于抽样的模型的flops限定在400MF左右,为了得到有效的模型,这里超参数限定 (整除8,共128个值),。这样,AnyNetX共包含个可能的网络,这个量是巨大的,我们只能用EDF来近似评估:从设计空间中随机抽样500个模型(flops在360MF和400MF之间),然后每个模型训练10个epoch来得到EDF。这里AnyNetX作为设计网络空间的起点,记为AnyNetX****A,后面我们将逐渐简化设计空间,并提高设计空间的质量。

这里做的第一个简化是所有的stage采用相同的bottleneck ratio,简化的设计空间称为AnyNetX****B,从分布估计来看,AnyNetX****BAnyNetX****A两个设计空间EDF曲线基本一致,而且min error和mean error也近乎相同,但是AnyNetX****B更简单了,而且我们也可以对bottleneck ratio进行更多的分析,通过empirical bootstrap可以发现是较好的。

这里做的第2个简化是所有的stage采用相同的group width,简化的设计空间称为AnyNetX****C,从分布估计来看,AnyNetX****CAnyNetX****B两个设计空间EDF曲线也基本一致,这里也发现是最好的。相比AnyNetX****AAnyNetX****C少了6个自由度,设计空间变得更简单了,但是质量却没有下降。

进一步,对AnyNetX****C中的典型的好模型和差模型结构进行分析,如上图所示,可以发现好的模型其width随着stage是增加的,而差模型会违反这个规律,所以这里进一步限定,即后面的stage的width要不小于前面的stage,加上这种限制的设计空间称为AnyNetX****D中,从EDF也可以看出AnyNetX****D的mean error要小于AnyNetX****C,而相反的设置会恶化性能:同样地,分析也发现好模型的各个stage的block数量(或者称为stage depth,不等价于层数)也是倾向逐渐上升的(最后一个stage不一定满足),所以这里又进一步提出了加上AnyNetX****E,其mean error有进一步的下降。AnyNetX****DAnyNetX****E可以分别将设计空间的模型量降低4!倍。注意,从AnyNetX****DAnyNetX****E其限制是累积的,最终设计空间的模型量从 原来的降低到

进一步,论文分析了AnyNetX****E抽样中的最好的20个模型,发现它们的各个block下的width可以近似拟合成一个线性关系(下图左上图黑色实线,这里width是log坐标,所以显示为曲线):。这个线性函数近似揭示了网络的width随深度的变化规律。线性拟合意味着不同的block的width不同,但是设计空间限制每个stage的所有block采用相同的width,即一种量化的widths(分段常量函数)。所以需要一种方法将一个直线量化为一个分段常量函数,这里将介绍这种量化方法。首先,对block widths进行线性参数化:

这里共有3个参数:网络深度(depth),初始width 以及直线斜率。这样每个block的width是不同的,为了量化,首先通过下述约束来计算,这里是一个另外的参数。然后对四舍五入取整,记为,然后重新计算width:,这里将具有相同的width的block放在同一个stage,这样对于第i个stage,其width就是(对于第一个stage,其width为),其block的数量,即统计具有相同width的block数量。这样就实现了width的量化,对于一个给定的网络,可以根据网络的深度d通过网格搜索来确定这三个参数以使得预测的各个stage的width与实际值的误差最小。比如对于AnyNetX****E抽样中的两个最好模型,如上图所示,其拟合得到的量化结果(虚线)与实际值(实线)很贴近。论文中进一步对从AnyNetX****CAnyNetX****DAnyNetX****E三个设计空间的网络进行这种拟合,如上图所示,可以发现好的模型往往拟合误差较小,这说明设计的量化线性拟合能够很好地契合好模型的设计原则。那么,将这种限制施加在AnyNetX****E上,就能得到一种更好的设计空间,这个设计空间称为RegNet(也称RegNetX),共有6个超参数:除了bottleneck ratio 和 group width ,还有,有了这4个参数就可以通过前面说的量化方法来确定各个stage的width以及block数量。torchvision里RegNet实现给出了这部分的具体实现代码:

class BlockParams:
    def __init__(
        self,
        depths: List[int],
        widths: List[int],
        group_widths: List[int],
        bottleneck_multipliers: List[float],
        strides: List[int],
        se_ratio: Optional[float] = None,
    ) -> None:

        self.depths = depths
        self.widths = widths
        self.group_widths = group_widths
        self.bottleneck_multipliers = bottleneck_multipliers
        self.strides = strides
        self.se_ratio = se_ratio

    @classmethod
    def from_init_params(
        cls,
        depth: int,
        w_0: int,
        w_a: float,
        w_m: float,
        group_width: int,
        bottleneck_multiplier: float = 1.0,
        se_ratio: Optional[float] = None,
        **kwargs: Any,
    ) -> "BlockParams":

        """
        Programatically compute all the per-block settings,
        given the RegNet parameters.
        The first step is to compute the quantized linear block parameters,
        in log space. Key parameters are:
        - `w_a` is the width progression slope
        - `w_0` is the initial width
        - `w_m` is the width stepping in the log space
        In other terms
        `log(block_width) = log(w_0) + w_m * block_capacity`,
        with `bock_capacity` ramping up following the w_0 and w_a params.
        This block width is finally quantized to multiples of 8.
        The second step is to compute the parameters per stage,
        taking into account the skip connection and the final 1x1 convolutions.
        We use the fact that the output width is constant within a stage.
        """


        QUANT = 8
        STRIDE = 2

        if w_a < 0 or w_0 <= 0 or w_m <= 1 or w_0 % 8 != 0:
            raise ValueError("Invalid RegNet settings")
        # Compute the block widths. Each stage has one unique block width
        widths_cont = torch.arange(depth) * w_a + w_0 # 按照线性计算各个block的width
        # 计算s_j,实现量化
        block_capacity = torch.round(torch.log(widths_cont / w_0) / math.log(w_m))
        block_widths = (torch.round(torch.divide(w_0 * torch.pow(w_m, block_capacity), QUANT)) * QUANT).int().tolist()
        num_stages = len(set(block_widths))
  
        # Convert to per stage parameters
        split_helper = zip(
            block_widths + [0],
            [0] + block_widths,
            block_widths + [0],
            [0] + block_widths,
        )
        splits = [w != wp or r != rp for w, wp, r, rp in split_helper]

        stage_widths = [w for w, t in zip(block_widths, splits[:-1]) if t]
        stage_depths = torch.diff(torch.tensor([d for d, t in enumerate(splits) if t])).int().tolist()

        strides = [STRIDE] * num_stages
        bottleneck_multipliers = [bottleneck_multiplier] * num_stages
        group_widths = [group_width] * num_stages

        # Adjust the compatibility of stage widths and group widths
        stage_widths, group_widths = cls._adjust_widths_groups_compatibilty(
            stage_widths, bottleneck_multipliers, group_widths
        )

        return cls(
            depths=stage_depths,
            widths=stage_widths,
            group_widths=group_widths,
            bottleneck_multipliers=bottleneck_multipliers,
            strides=strides,
            se_ratio=se_ratio,
        )

    def _get_expanded_params(self):
        return zip(self.widths, self.strides, self.depths, self.group_widths, self.bottleneck_multipliers)

    @staticmethod
    def _adjust_widths_groups_compatibilty(
        stage_widths: List[int], bottleneck_ratios: List[float], group_widths: List[int]
    ) -> Tuple[List[int], List[int]]:

        """
        Adjusts the compatibility of widths and groups,
        depending on the bottleneck ratio.
        """

        # Compute all widths for the current settings
        widths = [int(w * b) for w, b in zip(stage_widths, bottleneck_ratios)]
        group_widths_min = [min(g, w_bot) for g, w_bot in zip(group_widths, widths)]

        # Compute the adjusted widths so that stage and group widths fit
        ws_bot = [_make_divisible(w_bot, g) for w_bot, g in zip(widths, group_widths_min)]
        stage_widths = [int(w_bot / b) for w_bot, b in zip(ws_bot, bottleneck_ratios)]
        return stage_widths, group_widths_min

对于RegNet设计空间,这里也限制了另外4个超参数的取值范围:

。下图为RegNetX和AnyNetX的EDF对比,可以看到RegNetX更好,可以取得更小的mean error。中间的图给的是对RegNetX施加两个不同的限制后()其EDF与原始RegNetX的对比,虽然从EDF上有稍微提升,但是为了不降低设计空间的网络多样性,并没有采用这两种限制。下表也给出了RegNetX和AnyNetX各个设计空间的网络容量对比,相比最初的AnyNetX,RegNetX空间大小已经减少到此外,论文还对RegNet设计空间的泛化性做了实验,毕竟RegNet设计时只考虑了单一的block类型,而且网络的flops也限制在400MF附近,训练epoch设定为10。当分别改变这些设置后,依然可以发现RegNet设计效果更好,这说明它具有很好的泛化性。


进一步,论文还对RegNet设计空间的模型进行了更加详细的分析,这里选择100个网络但是训练epoch增加至25以得到更加细粒度的分析,这些分析的结论有些是与之前的实践是相反的。首先是参数的趋势分析,对于最好的模型,主要有几点重要结论:

  • 不同的flops下网络depth是稳定的,大约是20(相当于60层),这意味增加flops倾向于使网络变宽而不是变深;
  • bottleneck ratio为1.0,这意味并不需要bottleneck结构了;
  • 约为2.5,而常规的模型一般是2.0;

其次是对网络复杂度的分析,这个最重要的结论是:相比flops,activations与网络的推理速度更密切相关,这里的activations定义为所有卷积层的输出tensor的大小。从下图可以明显看出,activations与网络的推理速度的线性关系更明显。下图也给出了activations,参数量,推理速度这三者与flops的关系图,这里也拟合了最好的模型的变化曲线。根据上述分析可以对RegNet空间做进一步的限制,首先设定,然后也对参数量和activations做了限制(根据拟合曲线,限定两者与flops的关联)。从EDF对比可以看出,进一步限制后,设计空间更好了:此外,论文也对 inverted bottleneck和 depthwise conv做了实验,发现两者会恶化性能。而对于输入图像大小,采用224效果是最好的。最后就是增加SE模块,这一新的设计空间称为RegNetY,从EDF对比可以发现,增加SE后,设计空间更好。

最后,基于上述优化的设计空间RegNetX和RegNetY确定了不同flops下的top模型,具体地,对于每个flops设置,从25个随机设置中选择最好的模型。然后在ImageNet上采用100-epoch训练5次计算出error,下表给出了不同flops下的网络具体参数和性能:对于模型性能,最好的对比就是与EfficientNet的对比,可以看到在低flops下,EfficientNet更好,但是在高flops下,RegNetY效果更好。RegNetY-8.0GF比EfficientNet-B5效果更好,而推理速度快了5倍,这主要是因为EfficientNet对图像输入也做了缩放,EfficientNet-B5的输入大小是456,而RegNetY-8.0GF的输入大小是224。注意,这里的对比在常规的训练策略下(100epoch,除了 weight decay无更多的正则化策略),如果EfficientNet采用原始的训练策略,其效果要远好于RegNetY。


小结

网络设计空间可以为网络结构优化提供另外一种思路,这种采用统计学的设计方法虽然看起来很tricky,但是能够揭示更多的设计原则。

参考

  1. On Network Design Spaces for Visual Recognition
  2. Designing Network Design Spaces
  3. facebookresearch/pycls
  4. pytorch/vision




推荐阅读

CPVT:一个卷积就可以隐式编码位置信息

SOTA模型Swin Transformer是如何炼成的!

谷歌AI用30亿数据训练了一个20亿参数Vision Transformer模型,在ImageNet上达到新的SOTA!

BatchNorm的避坑指南(上)

BatchNorm的避坑指南(下)

目标跟踪入门篇-相关滤波

SOTA模型Swin Transformer是如何炼成的!

MoCo V3:我并不是你想的那样!

Transformer在语义分割上的应用

"未来"的经典之作ViT:transformer is all you need!

PVT:可用于密集任务backbone的金字塔视觉transformer!

涨点神器FixRes:两次超越ImageNet数据集上的SOTA

Transformer为何能闯入CV界秒杀CNN?

不妨试试MoCo,来替换ImageNet上pretrain模型!


机器学习算法工程师


                                    一个用心的公众号

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存