SqueezeNet詳細解讀

CopperDong發表於2018-04-12

概要:SqueezeNet的工作為以下幾個方面:

  1. 提出了新的網路架構Fire Module,通過減少引數來進行模型壓縮
  2. 使用其他方法對提出的SqeezeNet模型進行進一步壓縮
  3. 對引數空間進行了探索,主要研究了壓縮比和333∗3卷積比例的影響

這篇文章是 SQUEEZENET: ALEXNET-LEVEL ACCURACY WITH 50X FEWER PARAMETERS AND <0.5MB MODEL SIZE 的解讀,在精簡部分內容的同時補充了相關的概念。如有錯誤,敬請指正。 
論文連結:http://arxiv.org/abs/1602.07360 
程式碼連結:https://github.com/DeepScale/SqueezeNet

ABSTRACT

近來深層卷積網路的主要研究方向集中在提高正確率。對於相同的正確率水平,更小的CNN架構可以提供如下的優勢: 
(1)在分散式訓練中,與伺服器通訊需求更小 
(2)引數更少,從雲端下載模型的資料量小 
(3)更適合在FPGA等記憶體受限的裝置上部署。 
基於這些優點,本文提出SqeezeNet。它在ImageNet上實現了和AlexNet相同的正確率,但是隻使用了1/50的引數。更進一步,使用模型壓縮技術,可以將SqueezeNet壓縮到0.5MB,這是AlexNet的1/510。

1 INTRODUCTION AND MOTIVATION

對於一個給定的正確率,通常可以找到多種CNN架構來實現與之相近的正確率。其中,引數數量更少的CNN架構有如下優勢: 
(1)更高效的分散式訓練 
伺服器間的通訊是分散式CNN訓練的重要限制因素。對於分散式 資料並行 訓練方式,通訊需求和模型引數數量正相關。小模型對通訊需求更低。 
(2)減小下載模型到客戶端的額外開銷 
比如在自動駕駛中,經常需要更新客戶端模型。更小的模型可以減少通訊的額外開銷,使得更新更加容易。 
(3)便於FPGA和嵌入式硬體上的部署

2 RELATED WORK

2.1 MODEL COMPRESSION

常用的模型壓縮技術有: 
(1)奇異值分解(singular value decomposition (SVD))1 
(2)網路剪枝(Network Pruning)2:使用網路剪枝和稀疏矩陣 
(3)深度壓縮(Deep compression)3:使用網路剪枝,數字化和huffman編碼 
(4)硬體加速器(hardware accelerator)4

2.2 CNN MICROARCHITECTURE

在設計深度網路架構的過程中,如果手動選擇每一層的濾波器顯得過於繁複。通常先構建由幾個卷積層組成的小模組,再將模組堆疊形成完整的網路。定義這種模組的網路為CNN microarchitecture。

2.3 CNN MACROARCHITECTURE

與模組相對應,定義完整的網路架構為CNN macroarchitecture。在完整的網路架構中,深度是一個重要的引數。

2.4 NEURAL NETWORK DESIGN SPACE EXPLORATION

由於超引數繁多,深度神經網路具有很大的設計空間(design space)。通常進行設計空間探索的方法有: 
(1)貝葉斯優化 
(2)模擬退火 
(3)隨機搜尋 
(4)遺傳演算法

3 SQUEEZENET: PRESERVING ACCURACY WITH FEW PARAMETERS

3.1 ARCHITECTURAL DESIGN STRATEGIES

使用以下三個策略來減少SqueezeNet設計引數 
(1)使用111∗1卷積代替333∗3 卷積:引數減少為原來的1/9 
(2)減少輸入通道數量:這一部分使用squeeze layers來實現 
(3)將欠取樣操作延後,可以給卷積層提供更大的啟用圖:更大的啟用圖保留了更多的資訊,可以提供更高的分類準確率 
其中,(1)和(2)可以顯著減少引數數量,(3)可以在引數數量受限的情況下提高準確率。

3.2 THE FIRE MODULE

Fire Module是SqueezeNet中的基礎構建模組,如下定義 Fire Module :


 

  1. squeeze convolution layer:只使用111∗1 卷積 filter,即以上提到的策略(1)
  2. expand layer:使用111∗1 和333∗3 卷積 filter的組合
  3. Fire module中使用3ge可調的超引數:s1x1s1x1(squeeze convolution layer中111∗1 filter的個數)、e1x1e1x1(expand layer中111∗1 filter的個數)、e3x3e3x3(expand layer中333∗3 filter的個數)
  4. 使用Fire module的過程中,令s1x1s1x1 < e1x1e1x1 + e3x3e3x3,這樣squeeze layer可以限制輸入通道數量,即以上提到的策略(2)

3.3 THE SQUEEZENET ARCHITECTURE

SqueezeNet以卷積層(conv1)開始,接著使用8個Fire modules (fire2-9),最後以卷積層(conv10)結束。每個fire module中的filter數量逐漸增加,並且在conv1, fire4, fire8, 和 conv10這幾層之後使用步長為2的max-pooling,即將池化層放在相對靠後的位置,這使用了以上的策略(3)


如上圖,左邊為原始的SqueezeNet,中間為包含simple bypass的改進版本,最右側為使用complex bypass的改進版本。在下表中給出了更多的細節。




因為這是一篇講解網路壓縮的文章,這裡順便提一下引數計算的方法。以上表中的fire2模組為例:maxpool1層的輸出為55559655∗55∗96,一共有96個通道。之後緊接著的Squeeze層有16個11961∗1∗96的卷積filter,注意這裡是多通道卷積,為了避免與二維卷積混淆,在卷積尺寸末尾寫上了通道數。這一層的輸出尺寸為55551655∗55∗16,之後將輸出分別送到expand層中的11161∗1∗16(64個)和33163∗3∗16(64個)進行處理,注意這裡不對16個通道進行切分。為了得到大小相同的輸出,對33163∗3∗16的卷積輸入進行尺寸為1的zero padding。分別得到55556455∗55∗6455556455∗55∗64大小相同的兩個feature map。將這兩個feature map連線到一起得到555512855∗55∗128大小的feature map。考慮到bias引數,這裡的引數總數為: 
(1196+1)16+(1116+1)64+(3316+1)64(1552+1088+9280)=11920(1∗1∗96+1)∗16+(1∗1∗16+1)∗64+(3∗3∗16+1)∗64=(1552+1088+9280)=11920

可以看出,Squeeze層由於使用111∗1卷積極大地壓縮了引數數量,並且進行了降維操作,但是對應的代價是輸出特徵圖的通道數(維數)也大大減少。之後的expand層使用不同尺寸的卷積模板來提取特徵,同時將兩個輸出連線到一起,又將維度升高。但是33163∗3∗16的卷積模板引數較多,遠超111∗1卷積的引數,對減少引數十分不利,所以作者又針對33163∗3∗16卷積進行了剪枝操作以減少引數數量。從網路整體來看,feature map的尺寸不斷減小,通道數不斷增加,最後使用平均池化將輸出轉換成1110001∗1∗1000完成分類任務。

3.3.1 OTHER SQUEEZENET DETAILS

以下是網路設計中的一些要點: 
(1)為了使 111∗1 和 333∗3 filter輸出的結果又相同的尺寸,在expand modules中,給333∗3 filter的原始輸入新增一個畫素的邊界(zero-padding)。 
(2)squeeze 和 expand layers中都是用ReLU作為啟用函式 
(3)在fire9 module之後,使用Dropout,比例取50% 
(4)注意到SqueezeNet中沒有全連線層,這借鑑了Network in network的思想 
(5)訓練過程中,初始學習率設定為0.04,,在訓練過程中線性降低學習率。更多的細節參見本專案在github中的配置檔案。 
(6)由於Caffee中不支援使用兩個不同尺寸的filter,在expand layer中實際上是使用了兩個單獨的卷積層(111∗1filter 和 333∗3 filter),最後將這兩層的輸出連線在一起,這在數值上等價於使用單層但是包含兩個不同尺寸的filter。 
在github上還有SqueezeNet在其他框架下的實現:MXNet、Chainer、Keras、Torch。

4 EVALUATION OF SQUEEZENET

在表2中,以AlexNet為標準來比較不同壓縮方法的效果。



SVD方法能將預訓練的AlexNet模型壓縮為原先的1/5,top1正確率略微降低。網路剪枝的方法能將模型壓縮到原來的1/9,top1和top5正確率幾乎保持不變。深度壓縮能將模型壓縮到原先的1/35,正確率基本不變。SqeezeNet的壓縮倍率可以達到50以上,並且正確率還能有 略微的提升。注意到幾時使用未進行壓縮的32位數值精度來表示模型,SqeezeNet也比壓縮率最高的模型更小,同時表現也更好。 
如果將深度壓縮(Deep Compression)的方法用在SqeezeNet上,使用33%的稀疏表示和8位精度,會得到一個僅有0.66MB的模型。進一步,如果使用6位精度,會得到僅有0.47MB的模型,同時正確率不變。 
此外,結果表明深度壓縮不僅對包含龐大引數數量的CNN網路作用,對於較小的網路,比如SqueezeNet,也是有用的。將SqueezeNet的網路架構創新和深度壓縮結合起來可以將原模型壓縮到1/510。

5 CNN MICROARCHITECTURE DESIGN SPACE EXPLORATION

5.1 CNN MICROARCHITECTURE METAPARAMETERS

在SqueezeNet中,每一個Fire module有3個維度的超引數,即s1x1s1x1 、 e1x1e1x1 和 e3x3e3x3。SqueezeNet一共有8個Fire modules,即一共24個超引數。下面討論其中一些重要的超引數的影響。為方便研究,定義如下引數:

  1. baseebasee:Fire module中expand filter的個數
  2. freqfreq:Fire module的個數
  3. increeincree:在每freqfreq個Fire module之後增加的expand filter個數
  4. eiei:第ii 個Fire module中,expand filters的個數
  5. SRSR:壓縮比,即the squeeze ratio ,為squeeze layer中filter個數除以Fire module中filter總個數得到的一個比例
  6. pct3x3pct3x3:在expand layer有111∗1333∗3兩種卷積,這裡定義的引數是333∗3卷積個佔卷積總個數的比例

下圖為實驗結果:

5.2 SQUEEZE RATIO

Figure 3 中左圖給出了壓縮比(SR)的影響。壓縮比小於0.25時,正確率開始顯著下降。

5.3 TRADING OFF 1X1 AND 3X3 FILTERS

Figure 3 中右圖給出了333∗3卷積比例的影響,在比例小於25%時,正確率開始顯著下降,此時模型大小約為原先的44%。超過50%後,模型大小顯著增加,但是正確率不再上升。

6 CNN MACROARCHITECTURE DESIGN SPACE EXPLORATION

受ResNet啟發,這裡探究旁路連線(bypass conection)的影響。在Figure 2中展示了三種不同的網路架構。下表給出了實驗結果:




注意到使用旁路連線後正確率確實有一定提高。

7 Conclusions

在SqueezeNet提出後,Dense-Sparse-Dense (DSD)5使用了新的方法來進行壓縮同時提高了精度。

8 Implementation

在這裡我們分析SqeezeNet的PyTorch實現,以加深對網路架構的理解。原始碼可見:https://github.com/pytorch/vision/blob/master/torchvision/models/squeezenet.py

為方便分析,除去其中不必要的程式碼。

8.1 Fire module的實現

首先Fire類是對torch.nn.Modules類進行擴充得到的,需要繼承Modules類,並實現__init__()方法,以及forward()方法。其中,__init__()方法用於定義一些新的屬性,這些屬性可以包括Modules的例項,如一個torch.nn.Conv2d,nn.ReLU等。即建立該網路中的子網路,在建立這些子網路時,這些網路的引數也被初始化。接著使用super(Fire, self).__init__()呼叫基類初始化函式對基類進行初始化。

首先,在Fire類的__init__()函式中,定義瞭如下幾個新增的屬性: 
1. inplanes:輸入向量 
2. squeeze:sqeeze layer,由二維111∗1卷積組成。其中,參考PyTorch文件,torch.nn.Conv2d 的定義為class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True),程式碼中inplanes為輸入通道,squeeze_planes為輸出通道,卷積模板尺寸為111∗1
3. expand1x1:expand layer中的111∗1卷積。 
4. expand3x3:expand layer中的333∗3卷積。注意,為了使 111∗1 和 333∗3 filter輸出的結果有相同的尺寸,在expand modules中,給3∗3 filter的原始輸入新增一個畫素的邊界(zero-padding) 
5. 所有的啟用函式都選擇ReLU。注意inplace=True引數可以在原始的輸入上直接進行操作,不會再為輸出分配額外的記憶體,可以節省一部分記憶體,但同時也會破壞原始的輸入。

之後實現foward方法,整個流程如下: 
首先,將輸入x經過squeeze layer進行卷積操作,再經過 squeeze_activation() 進行啟用,然後將輸出分別送到expand1x1expand1x1expand3x3expand3x3中進行卷積和啟用操作,最後,使用torch.cat()可以將expand1x1_activation和 expand3x3_activation這兩個維度相同的輸出張量連線在一起。注意這裡的dim=1,即按照列連線,最終得到若干行。

class Fire(nn.Module):

    def __init__(self, inplanes, squeeze_planes,
                 expand1x1_planes, expand3x3_planes):
        super(Fire, self).__init__()
        self.inplanes = inplanes
        self.squeeze = nn.Conv2d(inplanes, squeeze_planes, kernel_size=1)
        self.squeeze_activation = nn.ReLU(inplace=True)
        self.expand1x1 = nn.Conv2d(squeeze_planes, expand1x1_planes,
                                   kernel_size=1)
        self.expand1x1_activation = nn.ReLU(inplace=True)
        self.expand3x3 = nn.Conv2d(squeeze_planes, expand3x3_planes,
                                   kernel_size=3, padding=1)
        self.expand3x3_activation = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.squeeze_activation(self.squeeze(x))
        return torch.cat([
            self.expand1x1_activation(self.expand1x1(x)),
            self.expand3x3_activation(self.expand3x3(x))
        ], 1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

以上實現了SqeezeNet中最重要的Fire module,為搭建整體網路做好了準備。接下來定義SqueezeNet類,它同樣繼承自nn.Module,這裡實現了version=1.0和version=1.1兩個SqueezeNet版本。區別在於1.0只有 AlexNet的1/50的引數,而1.1在1.0的基礎上進一步壓縮,引數略微減少,計算量降低為1.0的40%左右。SqueezeNet類定義瞭如下屬性: 
1. num_classes:分類的類別個數 
2. self.features:定義了主要的網路層,nn.Sequential()是PyTorch中的序列容器(sequential container),可以按照順序將Modules新增到其中,這也是網路巨集觀架構實現的重要步驟。要理解這一部分程式碼,最好結合表1中的細節,並且自己計算一遍卷積操作後的feature map的尺寸。注意表1中的輸入圖片尺寸是227227227∗227而不是224224224∗224,否則跟之後的輸出尺寸對不上。 
3. 注意nn.MaxPool2d()函式中ceil_mode=True會對池化結果進行向上取整而不是向下取整。 
4. 最後一個卷積層的初始化方法與其他層不同,在接下來的for迴圈中,定義了不同層的初始化方法,可以看到,最後一層使用了均值為0,方差為0.01的正太分佈初始化方法,其餘層使用He Kaiming論文中的均勻分佈初始化方法。同時這裡使用了num_classes引數,可以調整分類類別數目。並且所有的bias初始化為零。 
5. self.classifier:定義了網路末尾的分類器模組,注意其中使用了nn.Dropout() 
6. forward()方法:可以看到由features模組和classifier模組構成。

class SqueezeNet(nn.Module):

    def __init__(self, version=1.0, num_classes=1000):
        super(SqueezeNet, self).__init__()
        if version not in [1.0, 1.1]:
            raise ValueError("Unsupported SqueezeNet version {version}:"
                             "1.0 or 1.1 expected".format(version=version))
        self.num_classes = num_classes
        if version == 1.0:
            self.features = nn.Sequential(
                nn.Conv2d(3, 96, kernel_size=7, stride=2),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(96, 16, 64, 64),
                Fire(128, 16, 64, 64),
                Fire(128, 32, 128, 128),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(256, 32, 128, 128),
                Fire(256, 48, 192, 192),
                Fire(384, 48, 192, 192),
                Fire(384, 64, 256, 256),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(512, 64, 256, 256),
            )
        else:
            self.features = nn.Sequential(
                nn.Conv2d(3, 64, kernel_size=3, stride=2),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(64, 16, 64, 64),
                Fire(128, 16, 64, 64),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(128, 32, 128, 128),
                Fire(256, 32, 128, 128),
                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True),
                Fire(256, 48, 192, 192),
                Fire(384, 48, 192, 192),
                Fire(384, 64, 256, 256),
                Fire(512, 64, 256, 256),
            )
        # Final convolution is initialized differently form the rest
        final_conv = nn.Conv2d(512, self.num_classes, kernel_size=1)
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            final_conv,
            nn.ReLU(inplace=True),
            nn.AvgPool2d(13)
        )

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                if m is final_conv:
                    init.normal(m.weight.data, mean=0.0, std=0.01)
                else:
                    init.kaiming_uniform(m.weight.data)
                if m.bias is not None:
                    m.bias.data.zero_()

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x.view(x.size(0), self.num_classes)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

在之後的程式碼中定義了完整的squeezenet1_0squeezenet1_1,如果需要的話,可以使用PyTorch的預訓練模型。

def squeezenet1_0(pretrained=False, **kwargs):
    r"""SqueezeNet model architecture from the `"SqueezeNet: AlexNet-level
    accuracy with 50x fewer parameters and <0.5MB model size"
    <https://arxiv.org/abs/1602.07360>`_ paper.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = SqueezeNet(version=1.0, **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['squeezenet1_0']))
    return model


def squeezenet1_1(pretrained=False, **kwargs):
    r"""SqueezeNet 1.1 model from the `official SqueezeNet repo
    <https://github.com/DeepScale/SqueezeNet/tree/master/SqueezeNet_v1.1>`_.
    SqueezeNet 1.1 has 2.4x less computation and slightly fewer parameters
    than SqueezeNet 1.0, without sacrificing accuracy.

    Args:
        pretrained (bool): If True, returns a model pre-trained on ImageNet
    """
    model = SqueezeNet(version=1.1, **kwargs)
    if pretrained:
        model.load_state_dict(model_zoo.load_url(model_urls['squeezenet1_1']))
    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

Refference


  1. E.L Denton, W. Zaremba, J. Bruna, Y. LeCun, and R. Fergus. Exploiting linear structure within 
    convolutional networks for efficient evaluation. In NIPS, 2014. 
  2. S. Han, J. Pool, J. Tran, and W. Dally. Learning both weights and connections for efficient neural 
    networks. In NIPS, 2015b. 
  3. S. Han, H. Mao, and W. Dally. Deep compression: Compressing DNNs with pruning, trained 
    quantization and huffman coding. arxiv:1510.00149v3, 2015a. 
  4. Song Han, Xingyu Liu, Huizi Mao, Jing Pu, Ardavan Pedram, Mark A Horowitz, and William J 
    Dally. Eie: Efficient inference engine on compressed deep neural network. International Symposium 
    on Computer Architecture (ISCA), 2016a. 
  5. Song Han, Jeff Pool, Sharan Narang, Huizi Mao, Shijian Tang, Erich Elsen, Bryan Catanzaro, John 
    Tran, and William J. Dally. Dsd: Regularizing deep neural networks with dense-sparse-dense 
    training flow. arXiv:1607.04381, 2016b. 

相關文章