CVPR2021全新Backbone | ReXNet在CV全任務以超低FLOPs達到SOTA水平

點擊下面卡片關注 AI算法與圖像處理 ”,選擇加"星標"或“置頂”

重磅乾貨,第一時間送達



本文主要是針對Representational Bottleneck問題進行的研究,並提出了一套可以顯著改善模型性能的設計原則。僅僅對Baseline網絡進行微小的改變,可以在ImageNet分類、COCO檢測以及遷移學習上實現顯著的性能提升。

1 簡介

本文主要是針對Representational Bottleneck問題進行的討論,並提出了一套可以顯著改善模型性能的設計原則。本文中作者認爲在傳統網絡的設計的中可能會存在Representational Bottleneck問題,並且該問題會導致模型性能的降低。

爲了研究Representational Bottleneck問題,本文作者研究了由上萬個隨機網絡產生的特徵矩陣的秩。爲了設計更精確的網絡結構,作者進一步研究了整個層的Channel配置。並在此基礎上提出了簡單而有效的設計原則來緩解Representational Bottleneck問題。

通過遵循這一原則對Baseline網絡進行微小的改變,可以在ImageNet分類上實現顯著的性能改進。此外,在COCO目標檢測結果和遷移學習結果的改善也爲該方法用來解決Representational Bottleneck問題和提高性能提供了依據。

本文主要貢獻

  1. 通過數學和實驗研究探討網絡中出現的Representational Bottleneck問題;
  2. 提出了用於改進網絡架構的新設計原則;
  3. 在ImageNet數據集上取得了SOTA結果,在COCO檢測和4種不同的細粒度分類上取得了顯著的遷移學習結果。

2 表徵瓶頸

2.1 特徵編碼

給定一個深度爲L層的網絡,通過 維的輸入 可以得到 個被編碼爲的特徵,其中 爲權重。

這裏稱 的層爲 層,稱 的層爲 層。 爲第 個點出的非線性函數,比如帶有BN層的ReLU層,每個fi(·)表示第i個點非線性,如帶有批歸一化(BN)層的ReLU, 爲Softmax函數。

當訓練模型的時候,每一次反向傳播都會通過輸入 得到的輸出 與Label矩陣( )之間的Gap來進行權重更新。

因此,這便意味着Gap的大小可能會直接影響特徵的編碼效果。這裏對CNN的公式做略微的改動爲;式中 分別爲卷積運算和第 個卷積層核的權值。用傳統的 重新排序來重寫每個卷積,其中 重新排序的特徵,這裏將第 個特徵寫成:

2.2 表徵瓶頸與特徵矩陣的秩

回顧Softmax bottleneck

這裏討論一下Softmax bottleneck,也是Representational bottleneck的一種,發生在Softmax層,以形式化表徵性瓶頸。

由2.1所提的卷積公式可知,交叉熵損失的輸出爲 ,其秩以 的秩爲界,即 。由於輸入維度 小於輸出維度 ,編碼後的特徵由於秩不足而不能完全表徵所有類別。這解釋了Softmax層的一個Softmax bottleneck實例。

爲了解決這一問題,相關工作表明,通過引入非線性函數來緩解Softmax層的秩不足,性能得到了很大的改善。

此外,如果將 增加到更接近 ,它會成爲另一種解決Representational bottleneck的解決方案嗎?

通過layer-wise rank expansion來減少Representational bottleneck

這裏從爲ImageNet分類任務而設計的網絡說起。網絡被設計成有多個下采樣塊的模型,同時留下其他層具有相同的輸出和輸入通道大小。作者推測,擴展channel大小的層(即 層),如下采樣塊,將有秩不足,並可能有Representational bottleneck。

而本文作者的目標是通過擴大權重矩陣 的秩來緩解中間層的Representational bottleneck問題。

給定某一層生成的第 個特徵 的閾值爲 (這裏假設 )。這裏 ,其中 表示與另一個函數 的點乘。在滿足不等式的條件下,特徵 的秩範圍爲:

因此,可以得出結論,秩範圍可以通過增加 的秩和用適當的用具有更大秩的函數 來替換展開,如使用swish或ELU激活函數,這與前面提到的非線性的解決方法類似。

固定時,如果將特徵維數 調整到接近 ,則上式可以使得秩可以無限接近到特徵維數。對於一個由連續的1×1,3×3,1×1卷積組成的bottleneck塊,通過考慮bottleneck塊的輸入和輸出通道大小,用上式同樣可以展開秩的範圍。

2.3 實證研究

Layer-level秩分析

爲了進行Layer-level秩分析,作者生成一組由單一層組成的隨機網絡: 其中 隨機採樣, 則按比例進行調整。

特徵歸一化後的秩 是由每個網絡產生。爲了研究 而廣泛使用了非線性函數。對於每種標準化Channel大小,作者以通道比例 之間和每個非線性進行10,000個網絡的重複實驗。圖1a和1b中的標準化秩的展示。

通道配置研究

現在考慮如何設計一個分配整個層的通道大小的網絡。隨機生成具有expand層(即 )的L-depth網絡,以及使用少量的condense層的設計原則使得 ,這裏使用少量的condense層是因爲condense層直接降低了模型容量。在這裏作者將expand層數從0改變爲 ,並隨機生成網絡。

例如,一個expand層數爲0的網絡,所有層的通道大小都相同(除了stem層的通道大小)。作者對每個隨機生成的10,000個網絡重複實驗,並對歸一化秩求平均值。結果如圖1c和1d所示。

此外,還測試了採樣網絡的實際性能,每個配置有不同數量的expand層,有5個bottleneck,stem通道大小爲32。在CIFAR100數據集上訓練網絡,並在表1中給出了5個網絡的平均準確率。

觀察結果

從圖1a和圖1b中可以看到,選擇恰當的非線性函數與線性情況相比,可以在很大程度上擴大秩。

其次,無論是單層(圖1a)還是bottleneck塊(圖1b)情況下,歸一化的輸入通道大小 都與特徵的秩密切相關。

對於整個層的Channel配置,圖1c和1d表明,在網絡深度固定的情況下,可以使用更多的expand層來擴展秩。

這裏給出了擴展給定網絡秩的設計原則:

  1. 在一層上擴展輸入信道大小 ;
  2. 找到一個合適的非線性映射;
  3. 一個網絡應該設計多個expand層

3 改善網絡結構

3.1 表徵瓶頸發生在哪裏?

在網絡中哪一層可能出現表徵瓶頸呢?所有流行的深度網絡都有類似的架構,有許多擴展層將圖像輸入的通道從3通道輸入擴展到c通道然後輸出預測。

首先,對塊或層進行下采樣就像展開層一樣。其次,瓶頸模塊和反向瓶頸塊中的第一層也是一個擴展層。最後,還存在大量擴展輸出通道大小的倒數第2層。

本文作者認爲:表徵瓶頸將發生在這些擴展層和倒數第2層

3.2 網絡設計

中間卷積層

本文作者首先考慮了MobileNetV1。依次對接近倒數第2層的卷積做同樣的修改。通過:

  1. 擴大卷積層的輸入通道大小;
  2. 替換ReLU6s來細化每一層。

作者在MobileNetV1中做了類似MobileNetV2地更新。所有從末端到第1個的反向瓶頸都按照相同的原理依次修改。

在ResNet及其變體中,每個瓶頸塊在第3個卷積層之後不存在非線性,所以擴展輸入通道大小是唯一的補救辦法。

倒數第2個層

很多網絡架構在倒數第2層有一個輸出通道尺寸較大的卷積層。這是爲了防止最終分類器的表徵瓶頸,但是倒數第2層仍然受到這個問題的困擾。於是作者擴大了倒數第2層的輸入通道大小,並替換了ReLU6。

ReXNets

作者在這裏根據前面所說的規則設計了自己的模型,稱爲秩擴展網絡(ReXNets)。其中ReXNet-plain和ReXNet分別在MobileNetV1和MobileNetV2上進行了更新。

這裏設計模型是一個實例,它顯示了代表性瓶頸的減少是如何影響整體性能的,這將在實驗部分中顯示。爲了進行公平的比較,這裏的通道配置設計大致能滿足Baseline的整體參數和flops,如果通過適當的參數搜索方法,如NAS方法,還可以找到更好的網絡結構。

PyTorch實現如下:

import torch
import torch.nn as nn
from math import ceil

# Memory-efficient Siwsh using torch.jit.script borrowed from the code in (https://twitter.com/jeremyphoward/status/1188251041835315200)
# Currently use memory-efficient Swish as default:
USE_MEMORY_EFFICIENT_SWISH = True

if USE_MEMORY_EFFICIENT_SWISH:
    @torch.jit.script
    def swish_fwd(x):
        return x.mul(torch.sigmoid(x))


    @torch.jit.script
    def swish_bwd(x, grad_output):
        x_sigmoid = torch.sigmoid(x)
        return grad_output * (x_sigmoid * (1. + x * (1. - x_sigmoid)))


    class SwishJitImplementation(torch.autograd.Function):
        @staticmethod
        def forward(ctx, x):
            ctx.save_for_backward(x)
            return swish_fwd(x)

        @staticmethod
        def backward(ctx, grad_output):
            x = ctx.saved_tensors[0]
            return swish_bwd(x, grad_output)


    def swish(x, inplace=False):
        return SwishJitImplementation.apply(x)

else:
    def swish(x, inplace=False):
        return x.mul_(x.sigmoid()) if inplace else x.mul(x.sigmoid())


class Swish(nn.Module):
    def __init__(self, inplace=True):
        super(Swish, self).__init__()
        self.inplace = inplace

    def forward(self, x):
        return swish(x, self.inplace)


def ConvBNAct(out, in_channels, channels, kernel=1, stride=1, pad=0,
              num_group=1, active=True, relu6=False)
:

    out.append(nn.Conv2d(in_channels, channels, kernel,
                         stride, pad, groups=num_group, bias=False))
    out.append(nn.BatchNorm2d(channels))
    if active:
        out.append(nn.ReLU6(inplace=Trueif relu6 else nn.ReLU(inplace=True))


def ConvBNSwish(out, in_channels, channels, kernel=1, stride=1, pad=0, num_group=1):
    out.append(nn.Conv2d(in_channels, channels, kernel,
                         stride, pad, groups=num_group, bias=False))
    out.append(nn.BatchNorm2d(channels))
    out.append(Swish())


class SE(nn.Module):
    def __init__(self, in_channels, channels, se_ratio=12):
        super(SE, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
            nn.Conv2d(in_channels, channels // se_ratio, kernel_size=1, padding=0),
            nn.BatchNorm2d(channels // se_ratio),
            nn.ReLU(inplace=True),
            nn.Conv2d(channels // se_ratio, channels, kernel_size=1, padding=0),
            nn.Sigmoid()
        )

    def forward(self, x):
        y = self.avg_pool(x)
        y = self.fc(y)
        return x * y


class LinearBottleneck(nn.Module):
    def __init__(self, in_channels, channels, t, stride, use_se=True, se_ratio=12,
                 **kwargs)
:

        super(LinearBottleneck, self).__init__(**kwargs)
        self.use_shortcut = stride == 1 and in_channels <= channels
        self.in_channels = in_channels
        self.out_channels = channels

        out = []
        if t != 1:
            dw_channels = in_channels * t
            ConvBNSwish(out, in_channels=in_channels, channels=dw_channels)
        else:
            dw_channels = in_channels

        ConvBNAct(out, in_channels=dw_channels, channels=dw_channels, kernel=3, stride=stride, pad=1,
                  num_group=dw_channels, active=False)

        if use_se:
            out.append(SE(dw_channels, dw_channels, se_ratio))

        out.append(nn.ReLU6())
        ConvBNAct(out, in_channels=dw_channels, channels=channels, active=False, relu6=True)
        self.out = nn.Sequential(*out)

    def forward(self, x):
        out = self.out(x)
        if self.use_shortcut:
            out[:, 0:self.in_channels] += x

        return out


class ReXNetV1(nn.Module):
    def __init__(self, input_ch=16, final_ch=180, width_mult=1.0, depth_mult=1.0, classes=1000,
                 use_se=True,
                 se_ratio=12,
                 dropout_ratio=0.2,
                 bn_momentum=0.9)
:

        super(ReXNetV1, self).__init__()

        layers = [122335]
        strides = [122212]
        use_ses = [FalseFalseTrueTrueTrueTrue]

        layers = [ceil(element * depth_mult) for element in layers]
        strides = sum([[element] + [1] * (layers[idx] - 1)
                       for idx, element in enumerate(strides)], [])
        if use_se:
            use_ses = sum([[element] * layers[idx] for idx, element in enumerate(use_ses)], [])
        else:
            use_ses = [False] * sum(layers[:])
        ts = [1] * layers[0] + [6] * sum(layers[1:])

        self.depth = sum(layers[:]) * 3
        stem_channel = 32 / width_mult if width_mult < 1.0 else 32
        inplanes = input_ch / width_mult if width_mult < 1.0 else input_ch

        features = []
        in_channels_group = []
        channels_group = []

        # The following channel configuration is a simple instance to make each layer become an expand layer.
        for i in range(self.depth // 3):
            if i == 0:
                in_channels_group.append(int(round(stem_channel * width_mult)))
                channels_group.append(int(round(inplanes * width_mult)))
            else:
                in_channels_group.append(int(round(inplanes * width_mult)))
                inplanes += final_ch / (self.depth // 3 * 1.0)
                channels_group.append(int(round(inplanes * width_mult)))

        ConvBNSwish(features, 3, int(round(stem_channel * width_mult)), kernel=3, stride=2, pad=1)

        for block_idx, (in_c, c, t, s, se) in enumerate(zip(in_channels_group, channels_group, ts, strides, use_ses)):
            features.append(LinearBottleneck(in_channels=in_c,
                                             channels=c,
                                             t=t,
                                             stride=s,
                                             use_se=se, se_ratio=se_ratio))

        pen_channels = int(1280 * width_mult)
        ConvBNSwish(features, c, pen_channels)

        features.append(nn.AdaptiveAvgPool2d(1))
        self.features = nn.Sequential(*features)
        self.output = nn.Sequential(
            nn.Dropout(dropout_ratio),
            nn.Conv2d(pen_channels, classes, 1, bias=True))

    def forward(self, x):
        x = self.features(x)
        x = self.output(x).squeeze()
        return x

4. 實驗

4.1 分類實驗

其實透過上表一斤可以看出該方法的優越性了,僅僅是MobileNet的FLOPs和參數兩就已經達到甚至超越了ResNet50的水平。

ReXNets的性能更是超越了EfficientNets

4.2 檢測實驗

可以看出ReXNet-1.3x+SSDLite僅僅用來十分之一的FLOPs和參數量就可以達到SSD的檢測水平。在遠低於yolos tiny系列的FLOPs和參數量的情況下更是玩爆yolo-v3-tiny、yolo-v4-tiny直逼yolo-v5s。

4.3 遷移學習

就不比比那麼多啦,就是好就對了,很香,很快,很好用!!!

5 參考

[1].ReXNet: Diminishing Representational Bottleneck on Convolutional Neural Network

[2].https://github.com/clovaai/rexnet/blob/master/rexnetv1.py

  
     
     
     

個人微信(如果沒有備註不拉羣!
請註明: 地區+學校/企業+研究方向+暱稱



下載1:何愷明頂會分享


AI算法與圖像處理」公衆號後臺回覆:何愷明,即可下載。總共有6份PDF,涉及 ResNet、Mask RCNN等經典工作的總結分析


下載2:終身受益的編程指南:Google編程風格指南


AI算法與圖像處理」公衆號後臺回覆:c++,即可下載。歷經十年考驗,最權威的編程規範!




   
   
   
下載3 CVPR2021

AI算法與圖像處公衆號後臺回覆: CVPR 即可下載1467篇CVPR 2020論文 和 CVPR 2021 最新論文

點亮 ,告訴大家你也在看




本文分享自微信公衆號 - AI算法與圖像處理(AI_study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章