在2017年底,Face++發了一篇論文ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices討論了一個極有效率且能夠運行在手機等移動設備上的網絡結構——ShuffleNet。這個英文名我更願意翻譯成「重組通道網絡」,ShuffleNet經過分組卷積與\(1 \times 1\)的卷積核來下降計算量,經過重組通道來豐富各個通道的信息。這個論文的mxnet源碼的開源地址爲:MXShuffleNet。html
論文說中到「We propose using pointwise group convolutions to reduce computation complexity of 1 × 1 convolutions」,那麼爲何用分組卷積與小的卷積核會減小計算的複雜度呢?先來看看卷積在編程中是如何實現的,Caffe與mxnet的CPU版本都是用差很少的方法實現的,但Caffe的計算代碼會更加簡潔。python
在不分組與輸入的樣本量爲1(batch_size=1)的條件下,輸出一個通道上的一個點是卷積核會與全部的通道卷積之積,如圖1所示:git
在Caffe的計算方法中,先要將輸入張量爲\(n \times C_{in} \times H_{in} \times W_{in}\)(n是batch_size)轉化爲一個$ \left(C_{in} \times K_h \times K_w\right) \times \left(H_{in} \times W_{in}\right)\(的矩陣,這個過程叫**im2col**。最後獲得的輸出張量爲\)n \times C_{out} \times H_{in} \times W_{in}$。github
獲得的兩個矩陣Feature與Filter相乘獲得輸出矩陣Output,再Reshape成\(C_{out} \times C_{in} \times H_{out} \times W_{out}\)張量:
\[ Filter_{C_{out} \times \left( C_{in} \times K_h \times K_w \right)} \times Feature_{\left(C_{in} \times K_h \times K_w\right) \times \left(H_{out} \times W_{out}\right)} = Output_{C_{out} \times (H_{out} \times W_{out})} \tag{1.1} \]編程
如今的計算技術中,對方長度爲\(n\)的方陣,計算量能從\(n^3\)代碼到\(n^{2.376}\),最小的複雜度如今仍然未知,本文爲了方便計算量就以\(n^3\)爲基準。因此式(1.1)的矩陣計算最普通的計算量\(Computation\)是:
\[ Computation=C_{out} \times H_{out} \times W_{out} \times \left( C_{in} \times K_h \times K_w \right)^2 \tag{1.2} \]
從式(1.2)中能夠看出來,卷積核的大小對計算量影響是很大的,\(3 \times 3\)的卷積核比\(1 \times 1\)的計算量要大\(3^4=81\)倍。網絡
什麼叫作分組,就是將輸入與輸出的通道分紅幾組,好比輸出與輸入的通道數都是4個且分紅2組,那第一、2通道的輸出只使用第一、2通道的輸入,一樣那第三、4通道的輸出只使用第一、2通道的輸入。也就是說,不一樣組的輸出與輸入沒有關係了,減小聯繫必然會使計算量減少,但同時也會致使信息的丟失。ide
當分紅g組後,一層參數量的大小由\(Filter_{C_{out} \times \left( C_{in} \times K_h \times K_w \right)}\)變成\(Filter_{C_{out} \times \left( C_{in} \times K_h \times K_w / g \right)}\)。Feature Matrix的大小雖然沒發生變化,可是每一組的使用量是原來的$1/g,Filter也只用到全部參數的\(1/g\)\(。而後再循環計算\)g$次(同時FeatureMatrix與FilterMatrix要有地址偏移),那麼計算公式與計算量的大小爲:
\[ Filter_{C_{out}/g \times \left( C_{in} \times K_h \times K_w /g \right)} \times Feature_{\left(C_{in} \times K_h \times K_w /g\right) \times \left(H_{out} \times W_{out}\right)} = Output_{C_{out}/g \times (H_{out} \times W_{out})} \tag{1.3} \]
\[ Computation=C_{out} \times H_{out} \times W_{out} \times \left( C_{in} \times K_h \times K_w /g \right)^2 \tag{1.4} \]函數
因此,分紅\(g\)組可使參數量變成原來的\(1/g\),計算量是原來的\(1/g^2\)。優化
爲了節省內存,多個樣本輸入的時候,上述的全部過程都不會改變,而是每個樣本都運行一次上述的過程。this
以上只是最簡單、粗略的分析,實際上計算效率的提高並不會有上述這麼多,一方面由於im2col會消耗與矩陣運算差很少的時間,另外一方面由於現代的blas庫優化了矩陣運算,複雜度並無上述分析的那麼多,還有計算過程for循環是比較耗時的指令,即便用openmp也不能優化卷積的計算過程。
在上面我提到過,分組會致使信息的丟失,那麼有沒有辦法來解決這個問題呢?這個論文給出的方法就是交換通道,由於在同一組中不一樣的通道蘊含的信息多是相同的,若是在不一樣的組以後交換一些通道,那麼就能交換信息,使得各個組的信息更豐富,能提取到的特徵天然就更多,這樣是有利於獲得更好的結果。
ShuffleUnit的設計參考了ResNet,總有兩個基本單元,兩人個基本單元功能不同,將他們組合起來就能夠獲得ShuffleNet。這樣的設計能夠在增長網絡的深度(比mobilenet深約一倍)的同時,減小參數總量與計算量(本人運行Cifar10時,速度大約是molibenet的10倍)。
def combine(residual, data, combine): if combine == 'add': return residual + data elif combine == 'concat': return mx.sym.concat(residual, data, dim=1) return None
add是表明圖6中的單元b),concat是表明圖6中的單元c)。
def channel_shuffle(data, groups): data = mx.sym.reshape(data, shape=(0, -4, groups, -1, -2)) data = mx.sym.swapaxes(data, 1, 2) data = mx.sym.reshape(data, shape=(0, -3, -2)) return data
這個函數就是交換通道的函數,函數的第一行data = mx.sym.reshape(data, shape=(0, -4, groups, -1, -2))是將輸入爲\(n \times C_{in} \times H_{in} \times W_{in}\)reshape成\(n \times (C_{in}/g) \times g\times H_{in} \times W_{in}\),要注意的是mxnet中reshape不會改變張量在內存中的排列順序。至於要mxnet中的0,-1,-2,-3,-4的具體意義能夠這樣看到:
import mxnet as mx print(help(mx.sym.reshape))
能夠看到輸出如下(只提取出一小部分,其他的可用上述方法查看),這裏有各個參數的具體意義:
- ``0`` copy this dimension from the input to the output shape. - ``-1`` infers the dimension of the output shape by using the remainder of the input dimensions - ``-2`` copy all/remainder of the input dimensions to the output shape. - ``-3`` use the product of two consecutive dimensions of the input shape as the output dimension. - ``-4`` split one dimension of the input into two dimensions passed subsequent to -4 in shape (can contain -1).
函數的第二行是交換第一與第二個維度,那麼如今這個symbol的符號的shape就變成了\(n \times g \times (C_{in}/g) \times H_{in} \times W_{in}\)。這裏的第零個維度是\(n\)。要注意的是交換維度改變了張量在內存中的排列順序,改變了內存中的順序實現上就是完成了圖5c)中的Channel Shuffle操做,不一樣的顏色代碼數據在原來內存中的位置。
函數的最後一行合併了第一與第二個維度,輸出的張量與輸入的張量shape都是\(n \times C_{in} \times H_{in} \times W_{in}\)。
def shuffleUnit(residual, in_channels, out_channels, combine_type, groups=3, grouped_conv=True): if combine_type == 'add': DWConv_stride = 1 elif combine_type == 'concat': DWConv_stride = 2 out_channels -= in_channels first_groups = groups if grouped_conv else 1 bottleneck_channels = out_channels // 4 data = mx.sym.Convolution(data=residual, num_filter=bottleneck_channels, kernel=(1, 1), stride=(1, 1), num_group=first_groups) data = mx.sym.BatchNorm(data=data) data = mx.sym.Activation(data=data, act_type='relu') data = channel_shuffle(data, groups) data = mx.sym.Convolution(data=data, num_filter=bottleneck_channels, kernel=(3, 3), pad=(1, 1), stride=(DWConv_stride, DWConv_stride), num_group=groups) data = mx.sym.BatchNorm(data=data) data = mx.sym.Convolution(data=data, num_filter=out_channels, kernel=(1, 1), stride=(1, 1), num_group=groups) data = mx.sym.BatchNorm(data=data) if combine_type == 'concat': residual = mx.sym.Pooling(data=residual, kernel=(3, 3), pool_type='avg', stride=(2, 2), pad=(1, 1)) data = combine(residual, data, combine_type) return data
ShuffleUnit這個函數實現上是實現圖6的b)與c),add對應成b),comcat對應於c)。
def make_stage(data, stage, groups=3): stage_repeats = [3, 7, 3] grouped_conv = stage > 2 if groups == 1: out_channels = [-1, 24, 144, 288, 567] elif groups == 2: out_channels = [-1, 24, 200, 400, 800] elif groups == 3: out_channels = [-1, 24, 240, 480, 960] elif groups == 4: out_channels = [-1, 24, 272, 544, 1088] elif groups == 8: out_channels = [-1, 24, 384, 768, 1536] data = shuffleUnit(data, out_channels[stage - 1], out_channels[stage], 'concat', groups, grouped_conv) for i in range(stage_repeats[stage - 2]): data = shuffleUnit(data, out_channels[stage], out_channels[stage], 'add', groups, True) return data def get_shufflenet(num_classes=10): data = mx.sym.var('data') data = mx.sym.Convolution(data=data, num_filter=24, kernel=(3, 3), stride=(2, 2), pad=(1, 1)) data = mx.sym.Pooling(data=data, kernel=(3, 3), pool_type='max', stride=(2, 2), pad=(1, 1)) data = make_stage(data, 2) data = make_stage(data, 3) data = make_stage(data, 4) data = mx.sym.Pooling(data=data, kernel=(1, 1), global_pool=True, pool_type='avg') data = mx.sym.flatten(data=data) data = mx.sym.FullyConnected(data=data, num_hidden=num_classes) out = mx.sym.SoftmaxOutput(data=data, name='softmax') return out
這兩個函數能夠直接獲得做者在論文中的表:
論文後面用了種實驗證實這兩個技術的有效性,且證明了ShuffleNet的優秀,這裏就不細說,看論文後面的表就能一目瞭然。
【防止爬蟲轉載而致使的格式問題——連接】:
http://www.cnblogs.com/heguanyou/p/8087422.html