金哥和你一起學模型壓縮——結構篇(1)
富強、民主、文明、和諧、自由、平等、公正、法治、愛國、敬業、誠信、友善!
我在Gammalab的時候工程組有個特別聰明的同事叫
金戈
,人特別聰明,時常會問我一些我答不上來的問題。比如今天他就問我:“旭峰,人臉檢測為什麼要用三通道來做呢?灰度圖應該夠用了吧?”
我想了半天不知道怎麼回答他,於是說:“產品經理問的?”
金哥回覆我說:“我就是在思考如果產品經理問我我該怎麼回答。”
我…
anyway,本篇新坑就以金哥為榜樣來學習一下模型壓縮。模型壓縮是工程化必不可少的技術,可以說AI越落地,人們就越會關心模型壓縮。模型壓縮有多種技術,包括結構,量化,剪枝,蒸餾及各種其他方法。本文先從最基本的模型結構開始。
經典的
模型結構
論文包括:
MobileNet_v1: MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
MobileNet_v2: MobileNetV2: Inverted Residuals and Linear Bottlenecks
MobileNet_v3: Searching for MobileNetV3
ShuffleNet_v1: ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices
ShuffleNet_v2: ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design
IGC_v1: Interleaved Group Convolutions for Deep Neural Networks
IGC_v2: IGCV2: Interleaved Structured Sparse Convolutional Neural Networks
IGC_v3: IGCV3: Interleaved Low-Rank Group Convolutions for Efficient Deep Neural Networks
SqueezeNet_v1: SQUEEZENET: ALEXNET-LEVEL ACCURACY WITH 50X FEWER PARAMETERS AND <0。5MB MODEL SIZE
SqueezeNext: SqueezeNext: Hardware-Aware Neural Network Design
本文我們會從基礎講起,從頭學習,作為一個入門的小綜述吧。
模型壓縮主要考察兩個指標,Params 和Flops,其中Params指的是模型的引數,因為我們訓練模型一般都是以float32為精度的,所以模型大小一般是引數的4倍。Flops全稱為floating point operations per second。意指每秒浮點運算速度。所以Params和Flops分別度量了模型的大小以及其算數。我們做模型壓縮,目標就是使Params和Flops儘可能地小。
金哥:但是我們工程當中…
我:你不要問這種我答不上來的問題,工程的環境是複雜的,我們這邊就單單討論Params和Flops。
好,我們繼續,既然制定了metrics,讓我們回憶一下標註卷積的Params和Flops吧。
如圖所示,在一個標準的conv當中,我們的Params和Flops應該為:
Params:(K_h * K_w * C_in)* C_out
其中K_h和K_w 代表了kernel的input_size, C_in是input feature map的channel數,C_out是output feature map的channel數
Flops: (K_h * K_w * C_in * C_out) * (H_out * W_out)
其中H_out和W_out 分別代表了output feature map的size
在全連線層中,對應的Params和Flops分別為:
Params:C_in * C_out
Flops:C_in * C_out
金哥:你是不是忘記把bias給加上了?
我:由於bias網上貌似本身也有些分歧,這裡暫時就先忽略不計。
金哥:為什麼卷積的params和Flops和全連線計算不同?
我:好問題,要不你思考一下?為什麼卷積的計算方式和全連線不同,以及這種不同會造成什麼樣的影響和最佳化方式呢?
介紹完
標準卷積
的計算方式,我們接下來介紹一下模型壓縮中的經典方法Group Conv, 可以說,理解了Group Conv,也就算對模型壓縮入門了。
所謂Group Conv,其實就是把input feature進行分組計算,為什麼要進行這步操作呢?因為早期大家都比較不富裕,
視訊記憶體
不夠,只好想歪招進行分組訓練。我們來看一下Group Conv的
Params和Flops
:
Param:G * (K_h * K_w * C_in / G) * (C_out / G) = K_h * K_w * C_in * C_out / G
Flops:K_h * K_w * C_in * C_out * H_out * H_in / G
其中G為group數。
透過公式我們可以發現,只要G足夠大,Params和Flops就足夠小。經典的MobileNet就是把Group Conv的一個特例,在MobileNet,G設定為了input feature map的channel數。
具體
group conv
怎麼用呢,也很簡單,我們看一下pytorch的引數:
其中groups就代表了分組數,解釋得很清楚了吧,金哥你有什麼問題嗎?
金哥:Group Conv有什麼理論依據嗎?還是僅僅為了減少引數而被應用的?
我:好問題,我們稍後就分析。還有嗎?
金哥:有,你有沒有覺得你的字實在是太難看了?
我:有。
金哥:那你還寫?
我:我只是以前看別人寫覺得很好看…。
好了我們不糾結字的問題了,迴歸主題,group conv究竟有沒有道理呢?我們透過一個經典的backbone來重新認識一下group conv——resnext。在resnext中,作者總結了一種神經網路的正規化:split-transform-merge,以經典的Inception為例,我們看一下Inception的經典結構:
可以看到,Inception在一個input上有不同的分支(也就是不同的group)來進行不同的非線性變換來獲取不同的feature特徵進行融合以獲取更好的特徵。他的操作方式為:1)split:先把feature轉換為一個low-dimension的
特徵向量
,在Incetion中,就是conv1*1;2)transform:運用不同的transform方式進行非線性變換;3)merge:融合特徵,獲得了更好的特徵表示。
Split-transform-merge
的核心在於(其實也就是Inception的核心),在計算複雜度比較低的情況下,獲取和那些密集複雜網路相同的特徵表示能力。但是Inception的問題在於,他人工設計的痕跡太重,因此在resnext中,作者把網路的blocks改成了:
其中a是resnext的網路結構,b是Xception的網路結構,c是原始的resnet。我們可以發現,其實Xception和Resnext非常像,兩者的思想也非常接近。
金哥:那你為什麼要用resnext舉例子?
我:你看看resnext最後一個作者,是我校友好嗎?
金哥:那你就是瞧不上google咯?
我:google爸爸要我我肯定真香啊。
金哥:你這人怎麼這麼沒用原則…
好吧,讓我們看一下resnext的效果:
在相似的引數條件下,resnext的效果要顯著好於resnet。
進一步思考,為什麼resnext這種結構更近優秀呢?首先,group conv最讓人覺得困惑的地方其實在於,在一般的CNN中,卷積的channel其實是相互作用的。Group conv,極端一點,把G設定為input feature的通道數的話,其實就完全孤立了各個通道,這顯然也是不符合常識的。那為什麼依舊使用group conv這種操作呢?一種可能性是,就如同卷積大獲成功的原因之一是,我們運用卷積能有效地抽取區域性特徵(因此代替了全連線)。那通道是否也有這樣的性質呢?顯然,不是每個通道都應該獲得同樣的關注。
2017ImageNet的冠軍SENet就提供了一種
通道關係
的解決方案:
SENet的block其實也非常簡單,對於一個input feature map 分成兩支,一支是先經過Global Average Pooling加一個全連線層過sigmoid轉換為一個
關係向量
,另一支直接過網路,然後把過sigmoid後的權值乘到channel上以控制channel的重要性。今天看來這個結構相當簡單,類似attention的思想也比較符合常識。當然,我們也需要意識到的是,這個結構的成功是基於ImageNet這樣大規模的資料集上的,普通小資料集用這樣的attention,很可能調參不成反被慘虐。當然,這個模型結構的成功一定程度上說明了某些channel是冗餘的,因此group conv還有一種可能不錯的解釋就是,一定程度上防止了過擬合?
金哥:你這麼說你心裡不慌呢?
我:不慌…
Ok,鋪墊了這麼多,讓我們來進入正題,看一看經典的小模型框架。我們從MobileNet_v1開始入手。
MobileNet_v1的核心在於兩點,Depthwise + Pointwise
如圖所示,左邊是經典的一個小block,右邊是MobileNet_v1的block。其中DepthWise其實就是Group Conv的一個特殊版。
Params:K_h * K_w * C_in
Flops: (K_h * K_w * C_in) * (H_out * W_out)
和標準的卷積相比,引數量少了C_out倍。正如前文所說,獨立的通道也是有問題的。因此MobileNet_v1巧妙地加上PointWise,也就是1*1的conv層,既可以把維度調整到和其他的backbone一致作為對比,也可以使打通通道,好操作。透過Depthwise及Pointwise的操作,引數量下降了:
嗯,這個倍數…。。其中D_k代表了kernel_size, D_F代表了output的feature size。
當然,良心的google還為廣大工程師提供了兩個超參,分別是Width Multiplier
寬度乘子
及Resolution Multiplier解析度乘子。其中寬度乘子主要控制input_channels和out_channels的數目,一般來說設定為[1, 0。75, 0。5],於是整體的引數就可以計算為:
來看一下效果:
可以看到,當
乘子
為0。75和0。5時,網路依舊能保持不錯的效能,amazing!
解析度因子
也很好理解,透過縮小圖片解析度減少計算量,我們也來看一下實驗效果:
由實驗可以看出,圖片解析度的降低並沒有顯著降低精度。這個現象符合常識嗎,特別是ImageNet這種有1000類標籤的大資料集?暫時沒有想到合理的解釋,也希望有大神推薦一下相關的paper。後面實驗各種nb的結果就不放了,大家有興趣可以自己去看一下。
OK,我們來總結一下MobileNet_v1, 巧妙地利用了Depthwise + Pointwise的結構,既降低了引數量,又使模型保持了不錯的資訊特徵。各種實驗也印證了這的確是個好backbone。現在問題來了,baseline這麼優秀,後面人怎麼吃飯呢?
事實證明總有大神能夠突破創新的,讓我們一起來看一下ShuffleNet_v1吧。講ShuffleNet前,我們不妨想想MobileNet還有什麼問題,我們從Params的角度來分析一下:
這是MobilNet_v1的引數分佈,可以看到,Conv1*1佔據了大量的引數量。因此,如何有效地降低Conv1*1是最大的問題。
根據我們前面的分析,當想減少引數的時候,Group Conv是種非常有效的手段,但是Group Conv的缺點在於,無法有效地使通道之間交換資訊,在MobileNet中是用Conv1*1解決了這個問題,但是我們本身的目的就是為了解決Conv1*1引數量過高的問題,怎麼辦呢?ShuffleNet提出了Channel Shuffle for Group Conv來解決這個問題。
首先,讓我們來看一下總體bottleneck上的改進:
可以看到,最左邊其實是MobileNet_v1的結構。ShuffleNet首先把conv1*1換成了Group Conv1*1,大大縮減了計算量,其次用Channel Shuffle的方法打破了Group之間的孤立狀態,之後再連上DWConv3*3以及GConv1*1,引數量我就不寫了,眼見的在各個環節上都有降低,並且實實在在解決了Conv1*1的問題。
接下來我們來分析一下神秘的
Channel Shuffl
e到底是怎麼一回事:
非常簡單的思路,從Group Conv得到feature進行concat後把所有feature map打亂shuffle傳遞到下一層以進行不同group的資訊溝通,好想法,非常簡單有效。但是這個shuffle規則是人工設定的,感覺還是並不合理。並且實現相當困難,在此給曠世的團隊鼓鼓掌,的確厲害。
感受一下這個恐怖的Flops:
看一下模型效果:
其中1,0。5代表了channels數scale的設定,越小則channel相應越小。可見在較小的模型中,channels越少則效果越差(顯然),group數越多反而效果越好。看一下論文的解釋:
ShuffleNet後文還有大量翔實的實驗,比較有興趣,這裡不多介紹了,大家可以去當個彩蛋看一下,非常有意思。
大概花了差不多一天不到的時間學習並整理模型壓縮的結構篇,說實話還是蠻累的。最缺的是實驗驗證以及程式碼的理解,這些以後有機會慢慢補上。噢,差點忘了金哥了,金哥你有啥要補充的。
金哥:那之前立下的MobileNet_v2, ShuffleNet_v2, Mobile Net_v3 這些flag呢?
我:em…。讓我們下次再好好討論吧!