Split to Be Slim: 論文復現

華為雲開發者聯盟發表於2023-04-24
摘要:在本論文中揭示了這樣一種現象:一層內的許多特徵圖共享相似但不相同的模式。

本文分享自華為雲社群《Split to Be Slim: 論文復現》,作者: 李長安 。

Split to Be Slim: An Overlooked Redundancy in Vanilla Convolution 論文復現

1、問題切入

已經提出了許多有效的解決方案來減少推理加速模型的冗餘。然而,常見的方法主要集中在消除不太重要的過濾器或構建有效的操作,同時忽略特徵圖中的模式冗餘。

在本論文中揭示了這樣一種現象:一層內的許多特徵圖共享相似但不相同的模式。但是,很難確定具有相似模式的特徵是否是冗餘的或包含基本細節。因此,論文作者不是直接去除不確定的冗餘特徵,而是提出了一種基於分割的卷積操作,即 SPConv,以容忍具有相似模式但需要較少計算的特徵。

具體來說,論文將輸入特徵圖分為Representative部分和不Uncertain冗餘部分,其中透過相對繁重的計算從代表性部分中提取內在資訊,而對不確定冗餘部分中的微小隱藏細節進行一些輕量級處理手術。為了重新校準和融合這兩組處理過的特徵,我們提出了一個無引數特徵融合模組。此外,我們的 SPConv 被制定為以即插即用的方式替換 vanilla 卷積。在沒有任何花裡胡哨的情況下,基準測試結果表明,配備 SPConv 的網路在 GPU 上的準確性和推理時間上始終優於最先進的基線,FLOPs 和引數急劇下降。

2、特徵冗餘問題

Split to Be Slim: 論文復現

然而,如上圖所示,同一層的特徵中存在相似模式,也就是說存在特徵冗餘問題。但同時,並未存在完全相同的兩個通道特徵,進而導致無法直接剔除冗餘通道特徵。 因此,可以選擇一些有代表性的特徵圖來補充內在資訊,而剩餘的冗餘只需要補充微小的不同細節。

3、SPConv詳解

Split to Be Slim: 論文復現

在現有的濾波器中,比如常規卷積、GhostConv、OctConv、HetConv均在所有輸入通道上執行k*k卷積。然而,如上圖所示,同一層的特徵中存在相似模式,也就是說存在特徵冗餘問題。但同時,並未存在完全相同的兩個通道特徵,進而導致無法直接剔除冗餘通道特徵。

受此現象啟發,作者提出將所有輸入特徵按比例拆分為兩部分:

  1. Representative部分執行k*k卷積提取重要資訊;
  2. Uncertain部分執行1*1卷積補充隱含細節資訊。

因此該過程可以描述為(見SPConv的左側部分),公式如下圖所示:

Split to Be Slim: 論文復現

3.1 Further Reduction for Reprentative

在將所有輸入通道分成兩個主要部分後,代表部分之間可能存在冗餘。換句話說,代表通道可以分為幾個部分,每個部分代表一個主要類別的特徵,例如顏色和紋理。因此,我們在代表性通道上採用組卷積以進一步減少冗餘,如圖 2 的中間部分所示。我們可以將組卷積視為具有稀疏塊對角卷積核的普通卷積,其中每個塊對應於通道,並且分割槽之間沒有連線。這意味著,在組卷積之後,我們進一步減少了代表性部分之間的冗餘,同時我們還切斷了可能不可避免地有用的跨通道連線。我們透過在所有代表性通道上新增逐點卷積來彌補這種資訊丟失。與常用的組卷積後點卷積不同,我們在相同的代表性通道上進行 GWC 和 PWC。然後我們透過直接求和來融合這兩個結果特徵,因為它們具有相同的通道來源,從而獲得了額外的分數(這裡我們將組大小設定為 2)。所以方程2的代表部分可以表述為方程3:

Split to Be Slim: 論文復現

3.2 Parameter Free Feature Fusion Module

到目前為止,我們已經將 vanilla 3×3 卷積拆分為兩個操作:對於代表部分,我們進行 3×3 組卷積和 1×1 逐點卷積的直接求和融合,以抵消分組資訊丟失;對於冗餘部分,我們應用 1 × 1 核心來補充一些微小的有用細節。結果,我們得到了兩類特徵。因為這兩個特徵來自不同的輸入通道,所以需要一種融合方法來控制資訊流。與等式 2 的直接求和融合不同,我們為我們的 SP-Conv 設計了一個新穎的特徵融合模組,無需匯入額外的引數,有助於實現更好的效能。如圖 2 右側所示,

Split to Be Slim: 論文復現

3.3 程式碼復現

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.")

4、消融實驗

為驗證所提方法的有效性,設定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
 )

5、實驗結果分析

最後,我們再來看一下消融實驗結果,見下圖。可以看到:

  • 新增了SPConV模組的ResNet18效果反而不如原始的ResNet18

在原作中,作者給出了ResNet20、VGG16在資料集Cifar10上的對比結果,原因也可能在於本實驗中模型迭代次數不夠,但是相比來看,特徵圖在進行了去冗餘操作之後(類似於剪枝),精度下降似乎是正確的。

Split to Be Slim: 論文復現

6、總結

在該文中,作者重新對常規卷積中的資訊冗餘問題進行了重思考,為緩解該問題,作者提出了一種新穎的SPConv,它將輸入特徵拆分為兩組不同特徵並進行不同的處理,最後採用簡化版SK進行融合。最後作者透過充分的實驗分析說明了所提方法的有效性,在具有更高精度的時候具有更快的推理速度、更少的FLOPs與引數量。

所提SPConv是一種“即插即用”型單元,它可以輕易與其他網路架構相結合,同時與當前主流模型壓縮方法互補,如能精心組合設計,有可能得到更輕量型的模型。

7、參考資料

即插即用!北郵&南開大學開源SPConv:精度更高、速度更快的卷積

Split to Be Slim: An Overlooked Redundancy in Vanilla Convolution

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章