摘要:在本論文中揭示了這樣一種現象:一層內的許多特徵圖共用相似但不相同的模式。
本文分享自華為雲社群《Split to Be Slim: 論文復現》,作者: 李長安 。
Split to Be Slim: An Overlooked Redundancy in Vanilla Convolution 論文復現
已經提出了許多有效的解決方案來減少推理加速模型的冗餘。然而,常見的方法主要集中在消除不太重要的過濾器或構建有效的操作,同時忽略特徵圖中的模式冗餘。
在本論文中揭示了這樣一種現象:一層內的許多特徵圖共用相似但不相同的模式。但是,很難確定具有相似模式的特徵是否是冗餘的或包含基本細節。因此,論文作者不是直接去除不確定的冗餘特徵,而是提出了一種基於分割的折積操作,即 SPConv,以容忍具有相似模式但需要較少計算的特徵。
具體來說,論文將輸入特徵圖分為Representative部分和不Uncertain冗餘部分,其中通過相對繁重的計算從代表性部分中提取內在資訊,而對不確定冗餘部分中的微小隱藏細節進行一些輕量級處理手術。為了重新校準和融合這兩組處理過的特徵,我們提出了一個無引數特徵融合模組。此外,我們的 SPConv 被制定為以隨插即用的方式替換 vanilla 折積。在沒有任何花裡胡哨的情況下,基準測試結果表明,配備 SPConv 的網路在 GPU 上的準確性和推理時間上始終優於最先進的基線,FLOPs 和引數急劇下降。
然而,如上圖所示,同一層的特徵中存在相似模式,也就是說存在特徵冗餘問題。但同時,並未存在完全相同的兩個通道特徵,進而導致無法直接剔除冗餘通道特徵。 因此,可以選擇一些有代表性的特徵圖來補充內在資訊,而剩餘的冗餘只需要補充微小的不同細節。
在現有的濾波器中,比如常規折積、GhostConv、OctConv、HetConv均在所有輸入通道上執行k*k折積。然而,如上圖所示,同一層的特徵中存在相似模式,也就是說存在特徵冗餘問題。但同時,並未存在完全相同的兩個通道特徵,進而導致無法直接剔除冗餘通道特徵。
受此現象啟發,作者提出將所有輸入特徵按比例拆分為兩部分:
因此該過程可以描述為(見SPConv的左側部分),公式如下圖所示:
在將所有輸入通道分成兩個主要部分後,代表部分之間可能存在冗餘。換句話說,代表通道可以分為幾個部分,每個部分代表一個主要類別的特徵,例如顏色和紋理。因此,我們在代表性通道上採用組折積以進一步減少冗餘,如圖 2 的中間部分所示。我們可以將組折積視為具有稀疏塊對角折積核的普通折積,其中每個塊對應於通道,並且分割區之間沒有連線。這意味著,在組折積之後,我們進一步減少了代表性部分之間的冗餘,同時我們還切斷了可能不可避免地有用的跨通道連線。我們通過在所有代表性通道上新增逐點折積來彌補這種資訊丟失。與常用的組折積後點折積不同,我們在相同的代表性通道上進行 GWC 和 PWC。然後我們通過直接求和來融合這兩個結果特徵,因為它們具有相同的通道來源,從而獲得了額外的分數(這裡我們將組大小設定為 2)。所以方程2的代表部分可以表述為方程3:
到目前為止,我們已經將 vanilla 3×3 折積拆分為兩個操作:對於代表部分,我們進行 3×3 組折積和 1×1 逐點折積的直接求和融合,以抵消分組資訊丟失;對於冗餘部分,我們應用 1 × 1 核心來補充一些微小的有用細節。結果,我們得到了兩類特徵。因為這兩個特徵來自不同的輸入通道,所以需要一種融合方法來控制資訊流。與等式 2 的直接求和融合不同,我們為我們的 SP-Conv 設計了一個新穎的特徵融合模組,無需匯入額外的引數,有助於實現更好的效能。如圖 2 右側所示,
import paddle import paddle.nn as nn def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1): """3x3 convolution with padding""" return nn.Conv2D(in_planes, out_planes, kernel_size=3, stride=stride, padding=dilation, groups=groups, dilation=dilation) class SPConv_3x3(nn.Layer): def __init__(self, inplanes=32, outplanes=32, stride=1, ratio=0.5): super(SPConv_3x3, self).__init__() self.inplanes_3x3 = int(inplanes*ratio) self.inplanes_1x1 = inplanes - self.inplanes_3x3 self.outplanes_3x3 = int(outplanes*ratio) self.outplanes_1x1 = outplanes - self.outplanes_3x3 self.outplanes = outplanes self.stride = stride self.gwc = nn.Conv2D(self.inplanes_3x3, self.outplanes, kernel_size=3, stride=self.stride, padding=1, groups=2) self.pwc = nn.Conv2D(self.inplanes_3x3, self.outplanes, kernel_size=1) self.conv1x1 = nn.Conv2D(self.inplanes_1x1, self.outplanes,kernel_size=1) self.avgpool_s2_1 = nn.AvgPool2D(kernel_size=2,stride=2) self.avgpool_s2_3 = nn.AvgPool2D(kernel_size=2, stride=2) self.avgpool_add_1 = nn.AdaptiveAvgPool2D(1) self.avgpool_add_3 = nn.AdaptiveAvgPool2D(1) self.bn1 = nn.BatchNorm2D(self.outplanes) self.bn2 = nn.BatchNorm2D(self.outplanes) self.ratio = ratio self.groups = int(1/self.ratio) def forward(self, x): # print(x.shape) b, c, _, _ = x.shape x_3x3 = x[:,:int(c*self.ratio),:,:] x_1x1 = x[:,int(c*self.ratio):,:,:] out_3x3_gwc = self.gwc(x_3x3) if self.stride ==2: x_3x3 = self.avgpool_s2_3(x_3x3) out_3x3_pwc = self.pwc(x_3x3) out_3x3 = out_3x3_gwc + out_3x3_pwc out_3x3 = self.bn1(out_3x3) out_3x3_ratio = self.avgpool_add_3(out_3x3).squeeze(axis=3).squeeze(axis=2) # use avgpool first to reduce information lost if self.stride == 2: x_1x1 = self.avgpool_s2_1(x_1x1) out_1x1 = self.conv1x1(x_1x1) out_1x1 = self.bn2(out_1x1) out_1x1_ratio = self.avgpool_add_1(out_1x1).squeeze(axis=3).squeeze(axis=2) out_31_ratio = paddle.stack((out_3x3_ratio, out_1x1_ratio), 2) out_31_ratio = nn.Softmax(axis=2)(out_31_ratio) out = out_1x1 * (out_31_ratio[:,:,1].reshape([b, self.outplanes, 1, 1]).expand_as(out_1x1))\ + out_3x3 * (out_31_ratio[:,:,0].reshape([b, self.outplanes, 1, 1]).expand_as(out_3x3)) return out # paddle.summary(SPConv_3x3(), (1,32,224,224)) spconv = SPConv_3x3() tmp = paddle.randn([1, 32, 224, 224]) conv_out1 = spconv(tmp) print(conv_out1.shape) W0724 22:30:03.841145 13041 gpu_resources.cc:61] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1 W0724 22:30:03.845882 13041 gpu_resources.cc:91] device: 0, cuDNN Version: 7.6. [1, 32, 224, 224] /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/nn/layer/norm.py:654: UserWarning: When training, we now always track global mean and variance. "When training, we now always track global mean and variance.")
為驗證所提方法的有效性,設定SPConv中的折積核k=3,g=2,同時整個網路設定統一的全域性超引數(不同階段設定不同的會更優,但會過於精細)。
在小尺度資料集Cifar10、resnet18網路進行對比分析,為公平對比,所有實驗均在含1個NVIDIA Tesla V100GPU的伺服器上從頭開始訓練,且採用預設的資料增廣與訓練策略,不包含其他額外Tricks。
import paddle from paddle.metric import Accuracy from paddle.vision.transforms import Compose, Normalize, Resize, Transpose, ToTensor from sp_resnet import resnet18_sp callback = paddle.callbacks.VisualDL(log_dir='visualdl_log_res_sp') normalize = Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC') transform = Compose([ToTensor(), Normalize(), Resize(size=(224,224))]) cifar10_train = paddle.vision.datasets.Cifar10(mode='train', transform=transform) cifar10_test = paddle.vision.datasets.Cifar10(mode='test', transform=transform) # 構建訓練集資料載入器 train_loader = paddle.io.DataLoader(cifar10_train, batch_size=128, shuffle=True, drop_last=True) # 構建測試集資料載入器 test_loader = paddle.io.DataLoader(cifar10_test, batch_size=128, shuffle=True, drop_last=True) res_sp = paddle.Model(resnet18_sp(num_classes=10)) optim = paddle.optimizer.Adam(learning_rate=3e-4, parameters=res_sp.parameters()) res_sp.prepare( optim, paddle.nn.CrossEntropyLoss(), Accuracy() ) res_sp.fit(train_data=train_loader, eval_data=test_loader, epochs=10, callbacks=callback, verbose=1 ) import paddle from paddle.metric import Accuracy from paddle.vision.transforms import Compose, Normalize, Resize, Transpose, ToTensor from paddle.vision.models import resnet18 callback = paddle.callbacks.VisualDL(log_dir='visualdl_log_res_18') normalize = Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5], data_format='HWC') transform = Compose([ToTensor(), Normalize(), Resize(size=(224,224))]) cifar10_train = paddle.vision.datasets.Cifar10(mode='train', transform=transform) cifar10_test = paddle.vision.datasets.Cifar10(mode='test', transform=transform) # 構建訓練集資料載入器 train_loader = paddle.io.DataLoader(cifar10_train, batch_size=128, shuffle=True, drop_last=True) # 構建測試集資料載入器 test_loader = paddle.io.DataLoader(cifar10_test, batch_size=128, shuffle=True, drop_last=True) res_18 = paddle.Model(resnet18(num_classes=10)) optim = paddle.optimizer.Adam(learning_rate=3e-4, parameters=res_18.parameters()) res_18.prepare( optim, paddle.nn.CrossEntropyLoss(), Accuracy() ) res_18.fit(train_data=train_loader, eval_data=test_loader, epochs=10, callbacks=callback, verbose=1 )
最後,我們再來看一下消融實驗結果,見下圖。可以看到:
在原作中,作者給出了ResNet20、VGG16在資料集Cifar10上的對比結果,原因也可能在於本實驗中模型迭代次數不夠,但是相比來看,特徵圖在進行了去冗餘操作之後(類似於剪枝),精度下降似乎是正確的。
在該文中,作者重新對常規折積中的資訊冗餘問題進行了重思考,為緩解該問題,作者提出了一種新穎的SPConv,它將輸入特徵拆分為兩組不同特徵並進行不同的處理,最後採用簡化版SK進行融合。最後作者通過充分的實驗分析說明了所提方法的有效性,在具有更高精度的時候具有更快的推理速度、更少的FLOPs與引數量。
所提SPConv是一種「隨插即用」型單元,它可以輕易與其他網路架構相結合,同時與當前主流模型壓縮方法互補,如能精心組合設計,有可能得到更輕量型的模型。
隨插即用!北郵&南開大學開源SPConv:精度更高、速度更快的折積
Split to Be Slim: An Overlooked Redundancy in Vanilla Convolution