天天看點

輕量級網絡--ShuffleNet論文解讀ShuffleNet

ShuffleNet

ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices

原文位址: ShuffleNet

代碼:

  • TensorFlow
  • Caffe

Abstract

論文介紹一個效率極高的CNN架構ShuffleNet,專門應用于計算力受限的移動裝置。新的架構利用兩個操作:逐點群卷積(pointwise group convolution)和通道混洗(channel shuffle),與現有先進模型相比在類似的精度下大大降低計算量。在ImageNet和MS COCO上ShuffleNet表現出比其他先進模型的優越性能。

Introduction

現許多CNNs模型的發展方向是更大更深,這讓深度網絡模型難以運作在移動裝置上,針對這一問題,許多工作的重點放在對現有預訓練模型的修剪、壓縮或使用低精度資料表示。

論文中提出的ShuffleNet是探索一個可以滿足受限的條件的高效基礎架構。論文的Insight是現有的先進basic架構如 X c e p t i o n Xception Xception和 R e s N e X t ResNeXt ResNeXt在小型網絡模型中效率較低,因為大量的 1 × 1 1×1 1×1卷積耗費很多計算資源,論文提出了逐點群卷積(

pointwise group convolution

)幫助降低計算複雜度;但是使用逐點群卷積會有幅作用,故在此基礎上,論文提出通道混洗(

channel shuffle

)幫助資訊流通。基于這兩種技術,我們建構一個名為ShuffleNet的高效架構,相比于其他先進模型,對于給定的計算複雜度預算,ShuffleNet允許使用更多的特征映射通道,在小型網絡上有助于編碼更多資訊。

論文在ImageNet和MS COCO上做了相關實驗,展現出ShuffleNet設計原理的有效性和結構優越性。同時論文還探讨了在真實嵌入式裝置上運作效率。

Related Work

  • 高效模型設計: CNNs在CV任務中取得了極大的成功,在嵌入式裝置上運作高品質深度神經網絡需求越來越大,這也促進了對高效模型的探究。例如,與單純的堆疊卷積層,GoogleNet增加了網絡的寬度,複雜度降低很多;SqueezeNet在保持精度的同時大大減少參數和計算量;ResNet利用高效的bottleneck結構實作驚人的效果。Xception中提出深度可分卷積概括了Inception序列。MobileNet利用深度可分卷積建構的輕量級模型獲得了先進的成果;ShuffleNet的工作是推廣群卷積(group convolution)和深度可分卷積(depthwise separable convolution)。
  • 模型加速: 該方向旨在保持預訓練模型的精度同時加速推理過程。常見的工作有:通過修剪網絡連接配接或減少通道數減少模型中連接配接備援;量化和因式分解減少計算中備援;不修改參數的前提下,通過FFT和其他方法優化卷積計算消耗;蒸餾将大模型的知識轉化為小模型,是的小模型訓練更加容易;ShuffleNet的工作專注于設計更好的模型,直接提高性能,而不是加速或轉換現有模型。

Approch

針對群卷積的通道混洗(Channel Shuffle for Group Convolutions)

現代卷積神經網絡會包含多個重複子產品。其中,最先進的網絡例如Xception和ResNeXt将有效的深度可分離卷積或群卷積引入建構block中,在表示能力和計算消耗之間取得很好的折中。但是,我們注意到這兩個設計都沒有充分采用 1 × 1 1×1 1×1的逐點卷積,因為這需要很大的計算複雜度。例如,在ResNeXt中 3 × 3 3×3 3×3卷積配有群卷積,逐點卷積占了93.4%的multiplication-adds。

在小型網絡中,昂貴的逐點卷積造成有限的通道之間充滿限制,這會顯著的損失精度。為了解決這個問題,一個直接的方法是應用通道稀疏連接配接,例如組卷積(group convolutions)。通過確定每個卷積操作僅在對應的輸入通道組上,組卷積可以顯著的降低計算損失。然而,如果多個組卷積堆疊在一起,會有一個副作用: 某個通道輸出僅從一小部分輸入通道中導出,如下圖(a)所示,這樣的屬性降低了通道組之間的資訊流通,降低了資訊表示能力。

輕量級網絡--ShuffleNet論文解讀ShuffleNet

如果我們允許組卷積能夠得到不同組的輸入資料,即上圖(b)所示效果,那麼輸入和輸出通道會是全關聯的。具體來說,對于上一層輸出的通道,我們可做一個混洗(Shuffle)操作,如上圖©所示,再分成幾個組,feed到下一層。

對于這個混洗操作,有一個有效高雅(efficiently and elegantly)的實作:

對于一個卷積層分為 g g g組,

  • 1.有 g × n g×n g×n個輸出通道
  • 2.reshape為 ( g , n ) (g,n) (g,n)
  • 3.再轉置為 ( n , g ) (n,g) (n,g)
  • 4.平坦化,再分回 g g g組作為下一層的輸入

示意圖如下:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

這樣操作有點在于是可微的,模型可以保持end-to-end訓練.

Shuffle unit

前面我們講了通道混洗的好處了,在實際過程中我們建構了一個ShuffleNet unit,便于建構實際模型。

輕量級網絡--ShuffleNet論文解讀ShuffleNet
  • 圖(a)是一個殘差子產品。對于主分支部分,我們可将其中标準卷積 3 × 3 3×3 3×3拆分成深度分離卷積(可參考我的

    MobileNet

    筆記)。我們将第一個 1 × 1 1×1 1×1卷積替換為逐點組卷積,再作通道混洗(即(b))。
  • 圖(b)即ShuffleNet unit,主分支最後的 1 × 1 C o n v 1×1 Conv 1×1Conv改為 1 × 1 G C o n v 1×1 GConv 1×1GConv,為了适配和恒等映射做通道融合。配合BN層和ReLU激活函數構成基本單元.
  • 圖©即是做降采樣的ShuffleNet unit,這主要做了兩點修改:
    • 在輔分支加入步長為2的

      3×3

      平均池化
    • 原本做元素相加的操作轉為了通道級聯,這擴大了通道次元,增加的計算成本卻很少

歸功于逐點群卷積和通道混洗,ShuffleNet unit可以高效的計算。相比于其他先進的單元,在相同設定下複雜度較低。

例如:給定輸入大小 h × w × c h×w×c h×w×c,通道數為 c c c。對于的bottleneck通道為 m m m:

  • ResNet unit需要 h w ( 2 c m + 9 m 2 ) F L O P S hw(2cm+9m^2)FLOPS hw(2cm+9m2)FLOPS計算量。
  • ResNeXt需要 h w ( 2 c m + 9 m 2 / g ) hw(2cm+9m^2/g) hw(2cm+9m2/g)FLOPS
  • 而ShuffleNet unit隻需要 h w ( 2 c m / g + 9 m ) hw(2cm/g+9m) hw(2cm/g+9m)FLOPS.

其中 g g g代表組卷積數目。即表示**:給定一個計算限制,ShuffleNet可以使用更寬的特征映射。我們發現這對小型網絡很重要,因為小型網絡沒有足夠的通道傳遞資訊**。

**需要注意的是:**雖然深度卷積可以減少計算量和參數量,但在低功耗裝置上,與密集的操作相比,計算/存儲通路的效率更差。故在ShuffleNet上我們隻在bottleneck上使用深度卷積,盡可能的減少開銷.

NetWork Architecture

在上面的基本單元基礎上,我們提出了ShuffleNet的整體架構:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

主要分為三個階段:

  • 每個階段的第一個block的步長為2,下一階段的通道翻倍
  • 每個階段内的除步長其他超參數保持不變
  • 每個ShuffleNet unit的bottleneck通道數為輸出的1/4(和ResNet設定一緻)

這裡主要是給出一個baseline。在ShuffleNet Unit中,參數 g g g控制逐點卷積的連接配接稀疏性(即分組數),對于給定的限制下,越大的 g g g會有越多的輸出通道,這幫助我們編碼資訊。

定制模型需要滿足指定的預算,我們可以簡單的使用放縮因子 s s s控制通道數,ShuffleNet s × s× s×即表示通道數放縮到 s s s倍。

Experiment

實驗在ImageNet的分類集上做評估,大多數遵循ResNeXt的設定,除了兩點:

  • 權重衰減從1e-4降低到了4e-5
  • 資料增強使用較少的aggressive scale 增強

這樣做的原因是小型網絡在訓練過程通常會遇到欠拟合而不是過拟合問題。

On the Importance of Pointwise Group Convolutions

為了評估逐點卷積的重要性,比較相同複雜度下組數從1到8的ShuffleNet模型,同時我們通過放縮因子 s s s控制網絡寬度,擴充為3種:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

從結果來看,有組卷積的一緻比沒有組卷積( g = 1 g=1 g=1)的效果要好。注意組卷積可允許獲得更多通道的資訊,我們假設性能的收益源于更寬的特征映射,這幫助我們編碼更多資訊。并且,較小的模型的特征映射通道更少,這意味着能多的從特征映射上擷取收益。

表2還顯示,對于一些模型,随着 g g g增大,性能上有所下降。意味組數增加,每個卷積濾波器的輸入通道越來越少,損害了模型表示能力。

值得注意的是,對于小型的ShuffleNet 0.25×,組數越大性能越好,這表明對于小模型更寬的特征映射更有效。受此啟發,在原結構的階段3删除兩個單元,即表2中的

arch2

結構,放寬對應的特征映射,明顯新的架構效果要好很多。

Channel Shuffle vs. No Shuffle

Shuffle操作是為了實作多個組之間資訊交流,下表表現了有無Shuffle操作的性能差異:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

在三個不同複雜度下帶Shuffle的都表現出更優異的性能,尤其是當組更大(arch2, g = 8 g=8 g=8),具有shuffle操作性能提升較多,這表現出Shuffle操作的重要性。

Comparison with Other Structure Units

我們對比不同unit之間的性能差異,使用表1的結構,用各個unit控制階段2-4之間的Shuffle unit,調整通道數保證複雜度類似。

輕量級網絡--ShuffleNet論文解讀ShuffleNet

可以看到ShuffleNet的表現是比較出色的。有趣的是,我們發現特征映射通道和精度之間是存在直覺上的關系,以38MFLOPs為例,VGG-like, ResNet, ResNeXt, Xception-like, ShuffleNet模型在階段4上的輸出通道為50, 192, 192, 288, 576,這是和精度的變化趨勢是一緻的。我們可以在給定的預算中使用更多的通道,通常可以獲得更好的性能。

上述的模型不包括GoogleNet或Inception結構,因為Inception涉及到太多超參數了,做為參考,我們采用了一個類似的輕量級網絡PVANET。結果如下:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

ShuffleNet模型效果要好點

Comparison with MobileNets and Other Frameworks

與MobileNet和其他模型相比:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

相比于不同深度的模型對比,可以看到我們的模型要比MobileNet的效果要好,這表明ShuffleNet的有效性主要來源于高效的結構設計,而不是深度。

Generalization Ability

我們在MS COCO目标檢測任務上測試ShuffleNet的泛化和遷移學習能力,以Faster RCNN為例:

輕量級網絡--ShuffleNet論文解讀ShuffleNet

ShuffleNet的效果要比同等條件下的MobileNet效果要好,我們認為收益源于ShuffleNet的設計。

Actual Speedup Evaluation

評估ShuffleNet在ARM平台的移動裝置上的推斷速度。

輕量級網絡--ShuffleNet論文解讀ShuffleNet

三種分辨率輸入做測試,由于記憶體通路和其他開銷,原本理論上4倍的加速降低到了2.6倍左右。

Conclusion

論文針對現多數有效模型采用的逐點卷積存在的問題,提出了

組卷積

通道混洗

的處理方法,并在此基礎上提出了一個ShuffleNet unit,後續對該單元做了一系列的實驗驗證,證明ShuffleNet的結構有效性。

代碼分析

這裡分析的github-MG2033的代碼。

層定義

先看layers.py檔案,這部分定義了模型中使用的層。

# 建構的shufflenet unit
def shufflenet_unit(name, x, w=None, num_groups=1, group_conv_bottleneck=True, num_filters=16, stride=(1, 1),
                    l2_strength=0.0, bias=0.0, batchnorm_enabled=True, is_training=True, fusion='add'):
    # Paper parameters. If you want to change them feel free to pass them as method parameters.
    activation = tf.nn.relu

    with tf.variable_scope(name) as scope:
        residual = x
        bottleneck_filters = (num_filters // 4) if fusion == 'add' else (num_filters - residual.get_shape()[
            3].value) // 4

        if group_conv_bottleneck:
            # 先1x1卷積
            bottleneck = grouped_conv2d('Gbottleneck', x=x, w=None, num_filters=bottleneck_filters, kernel_size=(1, 1),
                                        padding='VALID',
                                        num_groups=num_groups, l2_strength=l2_strength, bias=bias,
                                        activation=activation,
                                        batchnorm_enabled=batchnorm_enabled, is_training=is_training)
            # 通道混洗
            shuffled = channel_shuffle('channel_shuffle', bottleneck, num_groups) 
        else:
            bottleneck = conv2d('bottleneck', x=x, w=None, num_filters=bottleneck_filters, kernel_size=(1, 1),
                                padding='VALID', l2_strength=l2_strength, bias=bias, activation=activation,
                                batchnorm_enabled=batchnorm_enabled, is_training=is_training)
            shuffled = bottleneck
        # 3x3深度分離卷積
        padded = tf.pad(shuffled, [[0, 0], [1, 1], [1, 1], [0, 0]], "CONSTANT")
        depthwise = depthwise_conv2d('depthwise', x=padded, w=None, stride=stride, l2_strength=l2_strength,
                                     padding='VALID', bias=bias,
                                     activation=None, batchnorm_enabled=batchnorm_enabled, is_training=is_training)
        # 如果步長為2,則下采樣
        if stride == (2, 2):
            residual_pooled = avg_pool_2d(residual, size=(3, 3), stride=stride, padding='SAME')
        else:
            residual_pooled = residual

        # 如果是通道連接配接
        if fusion == 'concat':
            group_conv1x1 = grouped_conv2d('Gconv1x1', x=depthwise, w=None,
                                           num_filters=num_filters - residual.get_shape()[3].value,
                                           kernel_size=(1, 1),
                                           padding='VALID',
                                           num_groups=num_groups, l2_strength=l2_strength, bias=bias,
                                           activation=None,
                                           batchnorm_enabled=batchnorm_enabled, is_training=is_training)
            return activation(tf.concat([residual_pooled, group_conv1x1], axis=-1))
        # 最後像素加
        elif fusion == 'add':
            group_conv1x1 = grouped_conv2d('Gconv1x1', x=depthwise, w=None,
                                           num_filters=num_filters,
                                           kernel_size=(1, 1),
                                           padding='VALID',
                                           num_groups=num_groups, l2_strength=l2_strength, bias=bias,
                                           activation=None,
                                           batchnorm_enabled=batchnorm_enabled, is_training=is_training)
            residual_match = residual_pooled
            # This is used if the number of filters of the residual block is different from that
            # of the group convolution.
            if num_filters != residual_pooled.get_shape()[3].value:
                residual_match = conv2d('residual_match', x=residual_pooled, w=None, num_filters=num_filters,
                                        kernel_size=(1, 1),
                                        padding='VALID', l2_strength=l2_strength, bias=bias, activation=None,
                                        batchnorm_enabled=batchnorm_enabled, is_training=is_training)
            return activation(group_conv1x1 + residual_match)
        else:
            raise ValueError("Specify whether the fusion is \'concat\' or \'add\'")


# 通道混洗
def channel_shuffle(name, x, num_groups):
    with tf.variable_scope(name) as scope:
        n, h, w, c = x.shape.as_list()
        x_reshaped = tf.reshape(x, [-1, h, w, num_groups, c // num_groups]) # 先合并重組
        x_transposed = tf.transpose(x_reshaped, [0, 1, 2, 4, 3]) # 轉置
        output = tf.reshape(x_transposed, [-1, h, w, c]) # 攤平
        return output



           

模型定義

直接看model.py檔案,有了上面的層定義,這代碼看起來就整潔很多了。

import tensorflow as tf
from layers import shufflenet_unit, conv2d, max_pool_2d, avg_pool_2d, dense, flatten


class ShuffleNet:
    """ShuffleNet is implemented here!"""
    MEAN = [103.94, 116.78, 123.68]
    NORMALIZER = 0.017

    def __init__(self, args):
        self.args = args
        self.X = None
        self.y = None
        self.logits = None
        self.is_training = None
        self.loss = None
        self.regularization_loss = None
        self.cross_entropy_loss = None
        self.train_op = None
        self.accuracy = None
        self.y_out_argmax = None
        self.summaries_merged = None

        # A number stands for the num_groups
        # Output channels for conv1 layer
        self.output_channels = {'1': [144, 288, 576], '2': [200, 400, 800], '3': [240, 480, 960], '4': [272, 544, 1088],
                                '8': [384, 768, 1536], 'conv1': 24}

        self.__build()

    def __init_input(self):
        batch_size = self.args.batch_size if self.args.train_or_test == 'train' else 1
        with tf.variable_scope('input'):
            # Input images
            self.X = tf.placeholder(tf.float32,
                                    [batch_size, self.args.img_height, self.args.img_width,
                                     self.args.num_channels])
            # Classification supervision, it's an argmax. Feel free to change it to one-hot,
            # but don't forget to change the loss from sparse as well
            self.y = tf.placeholder(tf.int32, [batch_size])
            # is_training is for batch normalization and dropout, if they exist
            self.is_training = tf.placeholder(tf.bool)

    def __resize(self, x):
        return tf.image.resize_bicubic(x, [224, 224])

    def __stage(self, x, stage=2, repeat=3):
        if 2 <= stage <= 4:
            stage_layer = shufflenet_unit('stage' + str(stage) + '_0', x=x, w=None,
                                          num_groups=self.args.num_groups,
                                          group_conv_bottleneck=not (stage == 2),
                                          num_filters=
                                          self.output_channels[str(self.args.num_groups)][
                                              stage - 2],
                                          stride=(2, 2),
                                          fusion='concat', l2_strength=self.args.l2_strength,
                                          bias=self.args.bias,
                                          batchnorm_enabled=self.args.batchnorm_enabled,
                                          is_training=self.is_training)
            for i in range(1, repeat + 1):
                stage_layer = shufflenet_unit('stage' + str(stage) + '_' + str(i),
                                              x=stage_layer, w=None,
                                              num_groups=self.args.num_groups,
                                              group_conv_bottleneck=True,
                                              num_filters=self.output_channels[
                                                  str(self.args.num_groups)][stage - 2],
                                              stride=(1, 1),
                                              fusion='add',
                                              l2_strength=self.args.l2_strength,
                                              bias=self.args.bias,
                                              batchnorm_enabled=self.args.batchnorm_enabled,
                                              is_training=self.is_training)
            return stage_layer
        else:
            raise ValueError("Stage should be from 2 -> 4")

    def __init_output(self):
        with tf.variable_scope('output'):
            # Losses
            self.regularization_loss = tf.reduce_sum(tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES))
            self.cross_entropy_loss = tf.reduce_mean(
                tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits, labels=self.y, name='loss'))
            self.loss = self.regularization_loss + self.cross_entropy_loss

            # Optimizer
            update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
            with tf.control_dependencies(update_ops):
                self.optimizer = tf.train.AdamOptimizer(learning_rate=self.args.learning_rate)
                self.train_op = self.optimizer.minimize(self.loss)
                # This is for debugging NaNs. Check TensorFlow documentation.
                self.check_op = tf.add_check_numerics_ops()

            # Output and Metrics
            self.y_out_softmax = tf.nn.softmax(self.logits)
            self.y_out_argmax = tf.argmax(self.y_out_softmax, axis=-1, output_type=tf.int32)
            self.accuracy = tf.reduce_mean(tf.cast(tf.equal(self.y, self.y_out_argmax), tf.float32))

        with tf.name_scope('train-summary-per-iteration'):
            tf.summary.scalar('loss', self.loss)
            tf.summary.scalar('acc', self.accuracy)
            self.summaries_merged = tf.summary.merge_all()

    def __build(self):
        self.__init_global_epoch()
        self.__init_global_step()
        self.__init_input()

        with tf.name_scope('Preprocessing'):
            red, green, blue = tf.split(self.X, num_or_size_splits=3, axis=3)
            preprocessed_input = tf.concat([
                tf.subtract(blue, ShuffleNet.MEAN[0]) * ShuffleNet.NORMALIZER,
                tf.subtract(green, ShuffleNet.MEAN[1]) * ShuffleNet.NORMALIZER,
                tf.subtract(red, ShuffleNet.MEAN[2]) * ShuffleNet.NORMALIZER,
            ], 3)
        x_padded = tf.pad(preprocessed_input, [[0, 0], [1, 1], [1, 1], [0, 0]], "CONSTANT")
        conv1 = conv2d('conv1', x=x_padded, w=None, num_filters=self.output_channels['conv1'], kernel_size=(3, 3),
                       stride=(2, 2), l2_strength=self.args.l2_strength, bias=self.args.bias,
                       batchnorm_enabled=self.args.batchnorm_enabled, is_training=self.is_training,
                       activation=tf.nn.relu, padding='VALID')
        padded = tf.pad(conv1, [[0, 0], [0, 1], [0, 1], [0, 0]], "CONSTANT")
        max_pool = max_pool_2d(padded, size=(3, 3), stride=(2, 2), name='max_pool')
        stage2 = self.__stage(max_pool, stage=2, repeat=3)
        stage3 = self.__stage(stage2, stage=3, repeat=7)
        stage4 = self.__stage(stage3, stage=4, repeat=3)
        global_pool = avg_pool_2d(stage4, size=(7, 7), stride=(1, 1), name='global_pool', padding='VALID')

        logits_unflattened = conv2d('fc', global_pool, w=None, num_filters=self.args.num_classes,
                                    kernel_size=(1, 1),
                                    l2_strength=self.args.l2_strength,
                                    bias=self.args.bias,
                                    is_training=self.is_training)
        self.logits = flatten(logits_unflattened)

        self.__init_output()

    def __init_global_epoch(self):
        """
        Create a global epoch tensor to totally save the process of the training
        :return:
        """
        with tf.variable_scope('global_epoch'):
            self.global_epoch_tensor = tf.Variable(-1, trainable=False, name='global_epoch')
            self.global_epoch_input = tf.placeholder('int32', None, name='global_epoch_input')
            self.global_epoch_assign_op = self.global_epoch_tensor.assign(self.global_epoch_input)

    def __init_global_step(self):
        """
        Create a global step variable to be a reference to the number of iterations
        :return:
        """
        with tf.variable_scope('global_step'):
            self.global_step_tensor = tf.Variable(0, trainable=False, name='global_step')
            self.global_step_input = tf.placeholder('int32', None, name='global_step_input')
            self.global_step_assign_op = self.global_step_tensor.assign(self.global_step_input)

           

ShuffleNet思路還是比較清晰的~

繼續閱讀