ShuffleNetV2:輕量級CNN網(wǎng)絡中的桂冠

前言


近來状囱,深度CNN網(wǎng)絡如ResNet和DenseNet署海,已經(jīng)極大地提高了圖像分類的準確度氛琢。但是除了準確度外,計算復雜度也是CNN網(wǎng)絡要考慮的重要指標疤剑,過復雜的網(wǎng)絡可能速度很慢滑绒,一些特定場景如無人車領域需要低延遲。另外移動端設備也需要既準確又快的小模型隘膘。為了滿足這些需求疑故,一些輕量級的CNN網(wǎng)絡如MobileNet和ShuffleNet被提出,它們在速度和準確度之間做了很好地平衡弯菊。今天我們要講的是ShuffleNetv2纵势,它是曠視最近提出的ShuffleNet升級版本,并被ECCV2018收錄管钳。在同等復雜度下吨悍,ShuffleNetv2比ShuffleNet和MobileNetv2更準確。


圖1:ShuffleNetv2與其它算法在不同平臺下的復雜度蹋嵌、速度以及準確度對比

設計理念

目前衡量模型復雜度的一個通用指標是FLOPs育瓜,具體指的是multiply-add數(shù)量,但是這卻是一個間接指標栽烂,因為它不完全等同于速度躏仇。如圖1中的(c)和(d)恋脚,可以看到相同F(xiàn)LOPs的兩個模型,其速度卻存在差異焰手。這種不一致主要歸結為兩個原因糟描,首先影響速度的不僅僅是FLOPs,如內存使用量(memory access cost, MAC)书妻,這不能忽略船响,對于GPUs來說可能會是瓶頸。另外模型的并行程度也影響速度躲履,并行度高的模型速度相對更快见间。另外一個原因,模型在不同平臺上的運行速度是有差異的工猜,如GPU和ARM米诉,而且采用不同的庫也會有影響。


圖2:不同模型的運行時間分解

據(jù)此篷帅,作者在特定的平臺下研究ShuffleNetv1和MobileNetv2的運行時間史侣,并結合理論與實驗得到了4條實用的指導原則:

(G1)同等通道大小最小化內存訪問量
對于輕量級CNN網(wǎng)絡,常采用深度可分割卷積(depthwise separable convolutions)魏身,其中點卷積( pointwise convolution)即1x1卷積復雜度最大惊橱。這里假定輸入和輸出特征的通道數(shù)分別為c_{1}c_{2},特征圖的空間大小為h\times w箭昵,那么1x1卷積的FLOPs為B=hwc_1c_2税朴。對應的MAC為hw(c_1+c_2)+c_1c_2(這里假定內存足夠),根據(jù)均值不等式宙枷,固定B時,MAC存在下限(令c_2=\frac{B}{hwc_1}):
MAC \geq 2\sqrt{hwB}+\frac{B}{hw} 僅當c_1=c_2$時茧跋,MAC取最小值慰丛,這個理論分析也通過實驗得到證實,如表1所示瘾杭,通道比為1:1時速度更快诅病。

表1:G1的實驗驗證


(G2)過量使用組卷積會增加MAC
組卷積(group convolution)是常用的設計組件,因為它可以減少復雜度卻不損失模型容量粥烁。但是這里發(fā)現(xiàn)贤笆,分組過多會增加MAC。對于組卷積讨阻,F(xiàn)LOPs為B=hwc_1c_2/g(其中g是組數(shù))芥永,而對應的MAC為hwc_1c_2+c_1c_2/g。如果固定輸入c_1\times h \times w以及B钝吮,那么MAC為:
MAC = hwc_1 + Bg/c_1 + B/hw
可以看到埋涧,當g增加時板辽,MAC會同時增加。這點也通過實驗證實棘催,所以明智之舉是不要使用太大g的組卷積劲弦。
(G3)網(wǎng)絡碎片化會降低并行度
一些網(wǎng)絡如Inception,以及Auto ML自動產生的網(wǎng)絡NASNET-A醇坝,它們傾向于采用“多路”結構邑跪,即存在一個lock中很多不同的小卷積或者pooling,這很容易造成網(wǎng)絡碎片化呼猪,減低模型的并行度画畅,相應速度會慢,這也可以通過實驗得到證明郑叠。
(G4)不能忽略元素級操作
對于元素級(element-wise operators)比如ReLU和Add夜赵,雖然它們的FLOPs較小,但是卻需要較大的MAC乡革。這里實驗發(fā)現(xiàn)如果將ResNet中殘差單元中的ReLU和shortcut移除的話寇僧,速度有20%的提升。

上面4條指導準則總結如下:

  • 1x1卷積進行平衡輸入和輸出的通道大蟹邪妗嘁傀;
  • 組卷積要謹慎使用,注意分組數(shù)视粮;
  • 避免網(wǎng)絡的碎片化细办;
  • 減少元素級運算。

網(wǎng)絡結構

根據(jù)前面的4條準則蕾殴,作者分析了ShuffleNetv1設計的不足笑撞,并在此基礎上改進得到了ShuffleNetv2,兩者模塊上的對比如圖3所示:


圖3:ShuffleNet兩個版本結構上的對比

在ShuffleNetv1的模塊中钓觉,大量使用了1x1組卷積茴肥,這違背了G2原則,另外v1采用了類似ResNet中的瓶頸層(bottleneck layer)荡灾,輸入和輸出通道數(shù)不同瓤狐,這違背了G1原則。同時使用過多的組批幌,也違背了G3原則础锐。短路連接中存在大量的元素級Add運算,這違背了G4原則荧缘。

為了改善v1的缺陷皆警,v2版本引入了一種新的運算:channel split。具體來說截粗,在開始時先將輸入特征圖在通道維度分成兩個分支:通道數(shù)分別為c'c-c'耀怜,實際實現(xiàn)時c'=c/2恢着。左邊分支做同等映射,右邊的分支包含3個連續(xù)的卷積财破,并且輸入和輸出通道相同掰派,這符合G1。而且兩個1x1卷積不再是組卷積左痢,這符合G2靡羡,另外兩個分支相當于已經(jīng)分成兩組。兩個分支的輸出不再是Add元素俊性,而是concat在一起略步,緊接著是對兩個分支concat結果進行channle shuffle,以保證兩個分支信息交流定页。其實concat和channel shuffle可以和下一個模塊單元的channel split合成一個元素級運算趟薄,這符合原則G4

對于下采樣模塊典徊,不再有channel split杭煎,而是每個分支都是直接copy一份輸入,每個分支都有stride=2的下采樣卒落,最后concat在一起后羡铲,特征圖空間大小減半,但是通道數(shù)翻倍儡毕。

ShuffleNetv2的整體結構如表2所示也切,基本與v1類似,其中設定每個block的channel數(shù)腰湾,如0.5x雷恃,1x,可以調整模型的復雜度费坊。

表2:ShuffleNetv2的整體結構


值得注意的一點是倒槐,v2在全局pooling之前增加了個conv5卷積,這是與v1的一個區(qū)別葵萎。最終的模型在ImageNet上的分類效果如表3所示:

表3: ShuffleNetv2在ImageNet上分類效果


可以看到导犹,在同等條件下唱凯,ShuffleNetv2相比其他模型速度稍快羡忘,而且準確度也稍好一點。同時作者還設計了大的ShuffleNetv2網(wǎng)絡磕昼,相比ResNet結構卷雕,其效果照樣具有競爭力。

從一定程度上說票从,ShuffleNetv2借鑒了DenseNet網(wǎng)絡漫雕,把shortcut結構從Add換成了Concat滨嘱,這實現(xiàn)了特征重用。但是不同于DenseNet浸间,v2并不是密集地concat太雨,而且concat之后有channel shuffle以混合特征,這或許是v2即快又好的一個重要原因魁蒜。

TensorFlow上的實現(xiàn)

目前ShuffleNetv2沒有看到官方開源實現(xiàn)囊扳,這里參考tensorpack中的復現(xiàn)(其中Top1 acc基本接近paper),給出v2在TensorFlow上實現(xiàn)兜看。我們使用TensorFlow中[tf.keras.Model來實現(xiàn)ShuffleNetv2锥咸。

首先我們先定義網(wǎng)絡中最基本的單元:Conv2D->BN->ReLU和DepthwiseConv2D->BN:

class Conv2D_BN_ReLU(tf.keras.Model):
    """Conv2D -> BN -> ReLU"""
    def __init__(self, channel, kernel_size=1, stride=1):
        super(Conv2D_BN_ReLU, self).__init__()

        self.conv = Conv2D(channel, kernel_size, strides=stride,
                            padding="SAME", use_bias=False)
        self.bn = BatchNormalization(axis=-1, momentum=0.9, epsilon=1e-5)
        self.relu = Activation("relu")

    def call(self, inputs, training=True):
        x = self.conv(inputs)
        x = self.bn(x, training=training)
        x = self.relu(x)
        return x

class DepthwiseConv2D_BN(tf.keras.Model):
    """DepthwiseConv2D -> BN"""
    def __init__(self, kernel_size=3, stride=1):
        super(DepthwiseConv2D_BN, self).__init__()

        self.dconv = DepthwiseConv2D(kernel_size, strides=stride,
                                     depth_multiplier=1,
                                     padding="SAME", use_bias=False)
        self.bn = BatchNormalization(axis=-1, momentum=0.9, epsilon=1e-5)

    def call(self, inputs, training=True):
        x = self.dconv(inputs)
        x = self.bn(x, training=training)

對于channel shuffle,只需要通過reshape操作即可:

def channle_shuffle(inputs, group):
    """Shuffle the channel
    Args:
        inputs: 4D Tensor
        group: int, number of groups
    Returns:
        Shuffled 4D Tensor
    """
    in_shape = inputs.get_shape().as_list()
    h, w, in_channel = in_shape[1:]
    assert in_channel % group == 0
    l = tf.reshape(inputs, [-1, h, w, in_channel // group, group])
    l = tf.transpose(l, [0, 1, 2, 4, 3])
    l = tf.reshape(l, [-1, h, w, in_channel])

    return l

下面细移,定義v2中的基本模塊搏予,先定義stride=1的模塊:

class ShufflenetUnit1(tf.keras.Model):
    def __init__(self, out_channel):
        """The unit of shufflenetv2 for stride=1
        Args:
            out_channel: int, number of channels
        """
        super(ShufflenetUnit1, self).__init__()

        assert out_channel % 2 == 0
        self.out_channel = out_channel

        self.conv1_bn_relu = Conv2D_BN_ReLU(out_channel // 2, 1, 1)
        self.dconv_bn = DepthwiseConv2D_BN(3, 1)
        self.conv2_bn_relu = Conv2D_BN_ReLU(out_channel // 2, 1, 1)

    def call(self, inputs, training=False):
        # split the channel
        shortcut, x = tf.split(inputs, 2, axis=3)

        x = self.conv1_bn_relu(x, training=training)
        x = self.dconv_bn(x, training=training)
        x = self.conv2_bn_relu(x, training=training)

        x = tf.concat([shortcut, x], axis=3)
        x = channle_shuffle(x, 2)
        return x

對于stride=2的下采樣模塊,與上面模塊略有不同:

class ShufflenetUnit2(tf.keras.Model):
    """The unit of shufflenetv2 for stride=2"""
    def __init__(self, in_channel, out_channel):
        super(ShufflenetUnit2, self).__init__()

        assert out_channel % 2 == 0
        self.in_channel = in_channel
        self.out_channel = out_channel

        self.conv1_bn_relu = Conv2D_BN_ReLU(out_channel // 2, 1, 1)
        self.dconv_bn = DepthwiseConv2D_BN(3, 2)
        self.conv2_bn_relu = Conv2D_BN_ReLU(out_channel - in_channel, 1, 1)

        # for shortcut
        self.shortcut_dconv_bn = DepthwiseConv2D_BN(3, 2)
        self.shortcut_conv_bn_relu = Conv2D_BN_ReLU(in_channel, 1, 1)

    def call(self, inputs, training=False):
        shortcut, x = inputs, inputs

        x = self.conv1_bn_relu(x, training=training)
        x = self.dconv_bn(x, training=training)
        x = self.conv2_bn_relu(x, training=training)

        shortcut = self.shortcut_dconv_bn(shortcut, training=training)
        shortcut = self.shortcut_conv_bn_relu(shortcut, training=training)

        x = tf.concat([shortcut, x], axis=3)
        x = channle_shuffle(x, 2)
        return x

根據(jù)定義的兩個模塊弧轧,我們可以實現(xiàn)stage的整合:

class ShufflenetStage(tf.keras.Model):
    """The stage of shufflenet"""
    def __init__(self, in_channel, out_channel, num_blocks):
        super(ShufflenetStage, self).__init__()

        self.in_channel = in_channel
        self.out_channel = out_channel

        self.ops = []
        for i in range(num_blocks):
            if i == 0:
                op = ShufflenetUnit2(in_channel, out_channel)
            else:
                op = ShufflenetUnit1(out_channel)
            self.ops.append(op)

    def call(self, inputs, training=False):
        x = inputs
        for op in self.ops:
            x = op(x, training=training)
        return x

建立所有準備模塊后雪侥,我們可以很快遞地實現(xiàn)ShuffleNetv2,這里實現(xiàn)1x模型:

class ShuffleNetv2(tf.keras.Model):
    """Shufflenetv2"""
    def __init__(self, num_classes, first_channel=24, channels_per_stage=(116, 232, 464)):
        super(ShuffleNetv2, self).__init__()

        self.num_classes = num_classes

        self.conv1_bn_relu = Conv2D_BN_ReLU(first_channel, 3, 2)
        self.pool1 = MaxPool2D(3, strides=2, padding="SAME")
        self.stage2 = ShufflenetStage(first_channel, channels_per_stage[0], 4)
        self.stage3 = ShufflenetStage(channels_per_stage[0], channels_per_stage[1], 8)
        self.stage4 = ShufflenetStage(channels_per_stage[1], channels_per_stage[2], 4)
        self.conv5_bn_relu = Conv2D_BN_ReLU(1024, 1, 1)
        self.gap = GlobalAveragePooling2D()
        self.linear = Dense(num_classes)

    def call(self, inputs, training=False):
        x = self.conv1_bn_relu(inputs, training=training)
        x = self.pool1(x)
        x = self.stage2(x, training=training)
        x = self.stage3(x, training=training)
        x = self.stage4(x, training=training)
        x = self.conv5_bn_relu(x, training=training)
        x = self.gap(x)
        x = self.linear(x)
        return x

我從tensorpack已訓練好的權重文件遷移到上面實現(xiàn)的模型劣针,然后就可以測試模型效果:

from tensorflow.keras.preprocessing import image
    from tensorflow.keras.applications.densenet import preprocess_input, decode_predictions

    img_path = './images/cat.jpg'
    img = image.load_img(img_path, target_size=(224, 224))
    x = image.img_to_array(img)
    x = np.expand_dims(x, axis=0)
    x = preprocess_input(x)

    inputs = tf.placeholder(tf.float32, [None, 224, 224, 3])
    model = ShuffleNetv2(1000)
    outputs = model(inputs, training=False)
    outputs = tf.nn.softmax(outputs)

    saver = tf.train.Saver()
    with tf.Session() as sess:
        saver.restore(sess, "./models/shufflene_v2_1.0.ckpt")
        preds = sess.run(outputs, feed_dict={inputs: x})
        print(decode_predictions(preds, top=3)[0])

感興趣的話校镐,可以訪問我的GitHub-xiaohu2015/DeepLearning_tutorials,歡迎star捺典。

參考

  1. ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design.
  2. tensorpack/examples
  3. pretrained weights
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末鸟廓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子襟己,更是在濱河造成了極大的恐慌引谜,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擎浴,死亡現(xiàn)場離奇詭異员咽,居然都是意外死亡,警方通過查閱死者的電腦和手機贮预,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門贝室,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人仿吞,你說我怎么就攤上這事滑频。” “怎么了唤冈?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵峡迷,是天一觀的道長。 經(jīng)常有香客問我,道長绘搞,這世上最難降的妖魔是什么彤避? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮夯辖,結果婚禮上琉预,老公的妹妹穿的比我還像新娘。我一直安慰自己蒿褂,他們只是感情好模孩,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贮缅,像睡著了一般榨咐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谴供,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天块茁,我揣著相機與錄音,去河邊找鬼桂肌。 笑死数焊,一個胖子當著我的面吹牛,可吹牛的內容都是我干的崎场。 我是一名探鬼主播佩耳,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谭跨!你這毒婦竟也來了干厚?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤螃宙,失蹤者是張志新(化名)和其女友劉穎蛮瞄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谆扎,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡挂捅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了堂湖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闲先。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖无蜂,靈堂內的尸體忽然破棺而出伺糠,到底是詐尸還是另有隱情,我是刑警寧澤酱讶,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布退盯,位于F島的核電站彼乌,受9級特大地震影響泻肯,放射性物質發(fā)生泄漏渊迁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一灶挟、第九天 我趴在偏房一處隱蔽的房頂上張望琉朽。 院中可真熱鬧,春花似錦稚铣、人聲如沸箱叁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耕漱。三九已至,卻和暖如春抬伺,著一層夾襖步出監(jiān)牢的瞬間螟够,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工峡钓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留妓笙,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓能岩,卻偏偏與公主長得像寞宫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拉鹃,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內容