CVPR 2021 | 全新Backbone!ReXNet:助力CV任務全面漲點
以下文章來源於AI人工智能初學者 ,作者ChaucerG

機器學習知識點總結、深度學習知識點總結以及相關垂直領域的跟進,比如CV,NLP等方面的知識。
點擊下方卡片,關注“CVer”公眾號
AI/CV重磅干貨,第一時間送達


ReXNet: Diminishing Representational Bottleneck on Convolutional Neural Network
paper: https://arxiv.org/abs/2007.00992
code: https://github.com/clovaai/rexnet
本文主要是針對Representational Bottleneck問題進行的研究,並提出了一套可以顯著改善模型性能的設計原則。僅僅對Baseline網絡進行微小的改變,可以在ImageNet分類、COCO檢測以及遷移學習上實現顯著的性能提升。
1 簡介
本文主要是針對Representational Bottleneck問題進行的討論,並提出了一套可以顯著改善模型性能的設計原則。本文中作者認為在傳統網絡的設計的中可能會存在Representational Bottleneck問題,並且該問題會導致模型性能的降低。
為了研究Representational Bottleneck問題,本文作者研究了由上萬個隨機網絡產生的特征矩陣的秩。為了設計更精確的網絡結構,作者進一步研究了整個層的Channel配置。並在此基礎上提出了簡單而有效的設計原則來緩解Representational Bottleneck問題。
通過遵循這一原則對Baseline網絡進行微小的改變,可以在ImageNet分類上實現顯著的性能改進。此外,在COCO目標檢測結果和遷移學習結果的改善也為該方法用來解決Representational Bottleneck問題和提高性能提供了依據。
本文主要貢獻
- 通過數學和實驗研究探討網絡中出現的Representational Bottleneck問題;
- 提出了用於改進網絡架構的新設計原則;
- 在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層來擴展秩。
這里給出了擴展給定網絡秩的設計原則:
- 在一層上擴展輸入信道大小;
- 找到一個合適的非線性映射;
- 一個網絡應該設計多個expand層。
3 改善網絡結構
3.1 表征瓶頸發生在哪里?
在網絡中哪一層可能出現表征瓶頸呢?所有流行的深度網絡都有類似的架構,有許多擴展層將圖像輸入的通道從3通道輸入擴展到c通道然后輸出預測。
首先,對塊或層進行下采樣就像展開層一樣。其次,瓶頸模塊和反向瓶頸塊中的第一層也是一個擴展層。最后,還存在大量擴展輸出通道大小的倒數第2層。
本文作者認為:表征瓶頸將發生在這些擴展層和倒數第2層。
3.2 網絡設計
中間卷積層
本文作者首先考慮了MobileNetV1。依次對接近倒數第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=True) if 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 = [1, 2, 2, 3, 3, 5]
strides = [1, 2, 2, 2, 1, 2]
use_ses = [False, False, True, True, True, True]
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 遷移學習
就不比比那么多啦,就是好就對了,很香,很快,很好用!!!
論文PDF下載
后台回復:ReXNet,即可下載論文PDF和代碼
下載2
后台回復:CVPR2021,即可下載代碼開源的論文合集