1. 回顧傳統 CNN 及它的不足之處
1.1 時間!時間!
傳統的卷積神經網絡已經在很多領域大顯身手,在許多機器學習項目中取得了巨大的成就。但是它仍然存在一個最致命的問題——花費過大。
花費過大,主要體現在兩種方面:
第一個方面是計算資源的消耗大
人們開玩笑說,深度學習到最後比的就是誰的孔方兄多,雖然有開玩笑的成分,但也不無道理。不過考慮到做項目的人往往都可以申請經費,最後花的是公家的錢,因此這個問題並不是最主要的問題——就算買不起豪華級別的顯卡,也可以租一臺雲服務器用來跑算法嘛!能用錢解決的問題都不是問題。
真正處於瓶頸位置的問題,其實是第2個方面——也就是時間消耗過大。君不見隨隨便便跑一個算法,兩三天都是好的,跑得慢一點的,甚至需要四五天、一個星期、半個月。也難怪人們把深度學習戲稱爲“煉丹術”,畢竟大家都沒有緋紅之王這種神器,跑算法的這段時間就只能像守着煉丹爐一樣熬着,到最後才能看看算法效果怎麼樣。
奇怪,卷積神經網絡不是號稱什麼“參數共享”“稀疏連接”,可以有效降低全連接網絡的消耗嗎?反正教學的時候各種聽起來高大上的詞給我們反覆灌輸CNN如何如何能減少參數,節省計算資源。聽起來很勇的樣子還是怎麼像彬彬一樣遜呢?
這個問題我們要從頭開始考慮。
1.2 回顧:從 Dense 連接到 Conv 連接
現在假設,我們不知道有 CNN 這種東西,而我們想要做一個簡單的圖像分類模型。輸入是 的圖片,我們首先得把它展開成一個 432 維的向量,然後後面跟上一個 64 維的 hidden layer。網絡的結構大致是下面這個樣子的:
這個結構需要多少參數呢?我們在 keras 裏面敲一敲:
input_layer = layers.Input(shape=(12, 12, 3))
flat = layers.Flatten()(input_layer)
hid = layers.Dense(64, use_bias=False)(flat)
model = keras.Model(input=input_layer, output=hid)
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 12, 12, 3) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 432) 0
_________________________________________________________________
dense_1 (Dense) (None, 64) 27648
=================================================================
Total params: 27,648
Trainable params: 27,648
Non-trainable params: 0
_________________________________________________________________
我去!這誰受得了?好在,後來我們知道了有卷積神經網絡這麼一個神奇的東西。於是我們把模型改成了這幅樣子:
input_layer = layers.Input(shape=(12, 12, 3))
conv_layer = layers.Conv2D(1, (5, 5), use_bias=False)(input_layer)
flat = layers.Flatten()(conv_layer)
model = keras.Model(input=input_layer, output=flat)
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 12, 12, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 8, 8, 1) 75
_________________________________________________________________
flatten_1 (Flatten) (None, 64) 0
=================================================================
Total params: 75
Trainable params: 75
Non-trainable params: 0
_________________________________________________________________
注意到了嗎?同樣的輸入,同樣的輸出,一個簡單的 Conv 網絡就把參數的數量從慘不忍睹的27648減到了75,“稀疏連接”恐怖如斯!由於神經網絡的運算本質就是矩陣相乘,所以更少的參數,意味着更少的乘法、更少的時間開銷。也正是因爲傳統的卷積神經網絡層具有如此卓越的削減運算開銷的功能,基於 CNN 的計算機視覺才能如火如荼的發展。
1.3 新時代、新挑戰
我們現在來定量地考慮一個問題:上面兩種網絡分別需要多少的運算量?
首先看看全連接網絡,Flatten 不算在計算中,所以核心的運算就是一個 的矩陣乘法,也就是 64 個 432 維向量乘以 432 維向量,也就是
Dense 選手的表現很差勁呢。
下面來看卷積網絡,同樣 Flatten 是不算在計算量中的,所以我們抽出核心的卷積部分來分析。
一個 (5, 5, 3)
的卷積核,本身要做 次的乘法操作,橫着移動 8 次,豎着移動 8 次,一共是 次的乘法操作。
如果要輸出 N 個通道,那麼就是 次乘法。
這個結果很好嘛?當然好了!把全連接和卷機兩個一對比,簡直就是數量級的差別,這是飛躍性的提升!
這個結果足夠好嗎?這就見仁見智了,如果是在10年前,我們會滿心歡喜的接受這樣的結果;但是10年後技術在不斷髮展,新的問題也在不斷湧現。這時候我們再來看傳統的卷積操作,就會有些不滿了。因爲處在新時代,我們處理的數據量越來越大、神經網絡的層數越來越多,帶來的結果是乘法運算的數量再次幾何倍數地正常。儘管傳統的CNN和單純的全連接相比有了質的提升,但是越來越無法滿足我們現在的需要。
現在我們再一次回到了 1.1 節,那一節的標題是“時間!時間!”。明白是什麼意思了嘛?
面對這樣的窘境,不少人開啓了新的思考:既然從全連接層到傳統的卷積層,我們削減了計算的開支;那我們能不能進一步改進網絡,讓它的計算量進一步的減少呢?
答案是——Separable Convolutions。
2. Separable Convolutions
Separable Convolutions,顧名思義,是“可分離”的卷積神經網絡。有的童鞋可能就納悶了:
幹嘛叫 Separable?
在正式講separable convolutions之前,我想談一談這個問題,因爲我覺得這個方法的所體現出來的哲理具有一定的普適性。
以下部分是我自己的思考(胡謅),算是從一個感性的方面來認識所謂的 Separable 這個詞。嫌我囉嗦的童鞋手動跳過。
我還記得在我小學的時候,有一天老師出了這樣一道題,已知從北京到上海有多少多少種走法,從上海到廣州又有多少多少種走法,那麼從北京到上海再到廣州有幾種走法呢?
那時傻乎乎的我把從北京到上海再從上海到廣州的路一條一條的列了出來,然而還沒等我在草稿紙上列完,同桌就已經搶先報出答案了,算法很簡單,把兩個數字乘起來就行了。
這雖然是一個簡單的小故事,但是其中卻蘊含了一個深刻的哲理。那就是合而慮之,不如分而治之。這個道理在我進入大學學習計算機以後愈加的凸顯——爲了降低系統的複雜度,我們進行了模塊化;爲了簡化問題的分析難度,我們使用了數學歸納法;爲了提高系統的安全性,我們選擇了去中心。
可以看到,“分”這個字簡直有一股奇怪的魔力,任何東西只要一分,馬上就不一樣了。古人云“三個臭皮匠,頂一個諸葛亮”。雖然三個臭皮匠加起來可能都不如諸葛亮,卻勝在普遍;昭烈皇帝三顧茅廬方得臥龍出山,而找三個臭皮匠卻何其簡單。
Separate,本質上就是把一個耦合嚴重的系統分離成多個高內聚低耦合的模塊,多個模塊組合在一起,比原來擰成一坨更精簡高效。
——我說的
我仔細思考了如何利用 Separate 來做優化,我認爲其中內在的道理和思路是這樣的:
- 給定一個任務
- 把 建模爲
- 想辦法變成
仔細觀察,你會發現 Separable Convolutions 完美符合這個思路。
好了,廢話完畢。現在我們來考慮怎麼分解卷積操作,首先要搞清楚的問題卷積操作的過程是什麼樣的?到底是個什麼東西?
- 卷積核放在image上
- 卷積覈對 channel 1、2、3分別做卷積
- 卷積核在 image 上移動
我們發現,卷積操作涉及到了兩個維度:
- 空間維度,卷積核是如何在一個 image 上跑來跑去的
- 深度維度,卷積核在不同深度的 channel 上依次做卷積操作
兩種維度,提供兩種視角,基於兩種不同的視角,人們提出了兩種不同的 Separable Convolution 方法。
- 基於空間視角,人們提出了 Spatial Separable Convolutions
- 基於深度視角,人們提出了 Depthwise Separable Convolutions
2.1 Spatial Separable Convolutions
在這個視角下,人們嘗試着 將卷積核分解爲若干個小卷積核。譬如,我們可以將卷積核分解爲兩個(或者若干個)向量的外積,如下圖所示:
怎麼用呢?如下圖所示,我們在原來的 image 上先用 kern1 做一次卷積,然後再在得到的結果上用 kern2 做一次卷積操作。
我們還是以之前 (12, 12, 3)
的圖像舉例,這次我們不用 (5, 5)
的卷積核,而是分別用兩個維度爲 5 的行列向量作爲卷積核,看看效果如何:
input_layer = layers.Input(shape=(12, 12, 3))
conv1 = layers.Conv2D(1, (5, 1), use_bias=False)(input_layer)
conv2 = layers.Conv2D(1, (1, 5), use_bias=False)(conv1)
flat = layers.Flatten()(conv2)
model = keras.Model(input=input_layer, output=flat)
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 12, 12, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 8, 12, 1) 15
_________________________________________________________________
conv2d_2 (Conv2D) (None, 8, 8, 1) 5
_________________________________________________________________
flatten_1 (Flatten) (None, 64) 0
=================================================================
Total params: 20
Trainable params: 20
Non-trainable params: 0
_________________________________________________________________
之前是 75,這一下變成了 20。輸入輸出的維度都沒變,參數數量卻只有原來的 1/3 !那麼乘法運算量變化如何?
顯然,第一次的卷積需要用到 ;第二次卷積需要 。所以總共需要 次。有了可觀的削減。
但是有個壞消息:從實際情況而言,並不是所有的卷積核都可以被有效地分解爲幾個更小的卷積核的。所以這種卷積方法用的也不多[1]。
2.2 Depthwise Separable Convolutions
同 Spatial 相比,Depthwise 的可分離卷積網絡着眼於 channel 這個維度。
現在假定一個完整的卷積操作可以把一個 的圖像轉變爲的輸出,其中 (H, W)
是圖像的尺寸, C
是輸入的通道,通常是 RGB 爲3; N
則是輸出的通道。DSC 把一個完整的卷積過程分成了兩步:
- Depthwise convolutions
- Pointwise convolutions
2.2.1 Depthwise convolutions
首先來回憶一下,傳統的卷積的做法是不是每個卷積核一次性處理所有通道?就像這樣:
Ok,DC 換了一種做法:一個卷積核只處理一個通道。
等等?一次只處理一個通道?這要怎麼搞?那麼我要使用多個卷積核使得輸出有多個通道的時候該去處理哪個輸入通道呢?
因爲這只是 DSC 的第一步啊!都說了 Separable 了嘛!所以第一步就只考慮 In-Channels ,至於 Out-Channels 是第二步要考慮的。
Depthwise convolutions 做的事情只有一個:用 C (channels)個卷積核分別作用在圖像的各個通道上:
keras裏面有 DepthwiseConv
的 API,我們可以看一下效果如何:
input_layer = layers.Input(shape=(12, 12, 3))
conv1 = layers.DepthwiseConv2D((5, 5), use_bias=False)(input_layer)
model = keras.Model(input=input_layer, output=conv1)
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 12, 12, 3) 0
_________________________________________________________________
depthwise_conv2d_1 (Depthwis (None, 8, 8, 3) 75
=================================================================
Total params: 75
Trainable params: 75
Non-trainable params: 0
_________________________________________________________________
3 個卷積核,每個都是 (5, 5)
,所以 75 個參數。
注意到:
DepthwiseConv2D
不需要指定filters
也就是卷積核的數量參數,因爲這個數量只會和輸入的通道數量相同- 輸出的 shape 爲
(8, 8, 3)
,顯然只改變了寬高並不改變通道
所以說,這一步完成了的操作。
2.2.2 Pointwise convolutions
DepthwiseConv 後就是 Pointwise Conv。其實,別看叫得那麼玄乎,這一步做的工作也很簡單,就是用 (1, 1)
的卷積核在上一步的輸出上做標準的卷積操作,就像這樣:
輸出的只有一個通道,想要多個通道怎麼辦?很簡單,用 N 個 1 x 1
卷積核就行了。
這一步完成了的操作。
2.2.3 把兩步加起來
把兩步加起來,就得到了 Depthwise Separable Convolutions。
在 keras 中有現成的 SeparableConv
API,完成了 DSC 的功能(然而沒有 Spatial……),我們再來試一次:
input_layer = layers.Input(shape=(12, 12, 3))
conv1 = layers.SeparableConv2D(1, (5, 5), use_bias=False)(input_layer)
model = keras.Model(input=input_layer, output=conv1)
model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 12, 12, 3) 0
_________________________________________________________________
separable_conv2d_1 (Separabl (None, 8, 8, 1) 78
=================================================================
Total params: 78
Trainable params: 78
Non-trainable params: 0
_________________________________________________________________
第一步 75 個參數;第二步中一個 (1, 1, 3)
的卷積核;一共 78 個參數。
那麼,需要用到多少次的乘法運算呢?
首先第一步中,一共是 次;第二步中,需要 次,所以一共是 次。
還記得原始的卷積神經網絡需要多少嗎?!我們成功地把 變成了 !
Nice Job!
然而不要太得意,我們要注意到,4800N 並不是永遠都比 4800 + 192N 大的,雖然二者在算法複雜度上確實有差距,但如果想讓優化後的差距產生明顯的效果,前提是 N 要足夠大!這就意味着,DSC 只有在網絡結構足夠複雜時才能大顯身手;如果網絡結構本身很簡單,那麼使用 DSC 反而會拖後腿,這就是需要權衡的了!
總結
Separable Convolutions makes CNN GREAT AGAIN!!!
Reference
[1]
Chi-Feng Wang, 《A Basic Introduction to Separable Convolutions》, 24-4月-2018. [在線]. 載於: https://towardsdatascience.com/a-basic-introduction-to-separable-convolutions-b99ec3102728.
[2]
Chris, 《Creating depthwise separable convolutions in Keras》, 24-9月-2019. [在線]. 載於: https://www.machinecurve.com/index.php/2019/09/24/creating-depthwise-separable-convolutions-in-keras/.
[3]
Chris, 《Understanding separable convolutions》, 23-9月-2019. [在線]. 載於: https://www.machinecurve.com/index.php/2019/09/23/understanding-separable-convolutions/.