卷積思想了解、Convolutional Neural Network(CNN)卷積神經網絡初探
1. 如何了解卷積
CNN卷積神經網絡的核心是卷積,當然CNN不僅僅隻有卷積,還有池化等其他技術,我們第一章先來一起讨論和了解下卷積的定義。
卷積是一個數學上的運算方法,在通信、機器學習、圖像處理等領域都有廣泛的應用。我們來一起從不同角度來看卷積,以求獲得一個全面的認知。
0x1: 從信号處理角度了解卷積
卷積是通信領域的一個很常用的概念和計算方法,以離散信号為例,連續信号同理。
已知一組離散沖擊信号,
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiMGRzUCR1UiMCVTJ4tyQyUiYrQ0MlsCR1USMCVTJ4tyQyUSYrQ0MlsCR1UCMCVTJ41DelR3Pu9Wa0FWdxV2Lc12bj5SdolGa65yd3d3Lc9CX6MHc0RHaiojIsJye.jpg)
,它們代表了一段時序上的3次采樣信号,如下圖
另一組沖擊響應信号,
,如圖
下面通過示範求
的過程,揭示卷積的實體意義。
1. 第一步
乘以
并平移到位置0(注意這裡随時間平移很重要)
這一步可以簡單了解為x的目前狀态值乘上了一個權重值y[0],接着移動到下一個時間響應點。
2. 第二步
并平移到位置1
3. 第三步
并平移到位置2
4. 第四步 - 累加
把上面三個圖疊加,就得到了
直覺上了解,一定是中間的時域的權重累乘和最多,而開始和結束階段的累積和較小。卷積的重要的實體意義是:
一個函數(如機關響應)在另一個函數(如輸入信号)上的權重疊加
這就是卷積的意義:權重疊加!
對于線性時不變系統,如果知道該系統的機關響應,那麼将機關響應和輸入信号求卷積,就相當于把輸入信号的各個時間點的機關響應權重疊加,就直接得到了輸出信号。
注意!這裡特别注意的一個概念是每個時間點輸入信号隻能輸入一次,相當于給目前系統一次激勵(權重乘),而這次激勵的響應在之後的整個時間周期中逐漸消減。
通俗的說:在輸入信号的每個位置,疊加一個機關響應,就得到了輸出信号。
0x2:用卷積核感覺野的視角來了解輸入信号輸出響應卷積問題
還是上面的例子,我們将Y信号看做是卷積核感覺野,卷積核的size為3。卷積核從從坐标[0]開始滑動(x[0]左邊一格),到坐标[3]停止(x[3]的位置)
卷積核滑動過程中,将感覺野内的輸入信号作為輸入,和卷積核進行卷積運算。總共滑動5次,得到5個數值結果,得到的結果和第一小節推導的是一樣的:
x[0] = ai;
x[1] = bi + aj;
x[2] = ci + bj + ak;
x[3] = cj + bk;
x[4] = ck;
筆者提醒:在圖像卷積中,大多數情況下,卷積核感覺野的起止位置是以輸入像素矩陣為邊界的,也即卷積後的特征圖和輸入矩陣同尺寸。
另外,這種卷積計算方式隐含了padding處理,是full pad的一種,具體細節我們後面會讨論。
0x3:從數學符号定義了解卷積
我們稱
為
的卷積
其連續的定義為:
其離散的定義為:
從公式上看,這兩個式子有一個共同的特征:
這個特征有什麼意義?
我們令
,那麼
就是下面這些直線:
如果周遊這些直線,就好比,把毛巾沿着角卷起來:
我們可以把這個毛巾想象成 f 和 g 在區間[a,b]的張量積,即:U(x,y)=f(x)g(y)
把它整張地毯卷起來以後,它變成了一個一維的函數,而且每點的函數值等于原本函數空間中所有函數卷起來後重合的點函數值之和,即累積和。
0x4:從一個離散卷積的例子:丢骰子來了解卷積
假設我們有兩枚骰子:
、
把這兩枚骰子都抛出去,然後求:兩粒骰子加起來等于4的機率是多少?
兩個骰子加起來要等于4,這本質上就是求卷積的問題。
我們先抛開數學公式的讨論,把骰子各個點數出現的情況逐一枚舉出來:
那麼,兩枚骰子點數加起來為4的情況有:
是以,兩枚骰子點數加起來為4的機率為:
符合卷積的定義,把它寫成标準的形式就是:
讀者朋友可以了解一下,這裡的”骰子加和等于4“,本質上就是最後的卷積和函數上的4這個點,上面求解的過程,就是在求解4這個點的卷積,即累加和。
0x5:從一個連續卷積的例子:做饅頭來了解卷積
樓下早點鋪子生意很好,供不應求,老闆買了一台機器,不斷的生産饅頭。
假設饅頭的生産速度是
,那麼一天後生産出來的饅頭總量為(積分結果):
每個饅頭生産出來之後,就會慢慢腐敗,假設腐敗函數為
,比如,10個饅頭,24小時會腐敗:
但是需要注意的是,饅頭的生産和饅頭的腐敗是同時進行的。很容易了解,第一個小時生産出來的饅頭,一天後會經曆24小時的腐敗,第二個小時生産出來的饅頭,一天後會經曆23小時的腐敗。
如此,我們可以知道,一天後,饅頭總共腐敗了:
這就是連續的卷積,同樣也符合上面讨論的卷積數學公式。
由此也可以再次了解到:卷積本質上展現了兩種信号的時移累加。
0x6:從函數映射(變換)角度了解卷積
從函數(或者說映射、變換)的角度了解。 卷積過程是在圖像每個位置進行線性變換映射成新值的過程,将卷積核看成權重,若拉成向量記為w,圖像對應位置的像素拉成向量記為x,則該位置卷積結果為y=w′x+b,即向量内積+偏置,将x變換為y。
從這個角度看,多層卷積是在進行逐層映射,整體構成一個複雜函數,訓練過程是在學習每個局部映射所需的權重,訓練過程可以看成是函數拟合的過程。
0x7:從圖像進行中的像素平滑問題了解卷積
1. 像素平滑原理
有這麼一副圖像,可以看到,圖像上有很多噪點:
高頻信号,就好像平地聳立的山峰,看起來很顯眼:
平滑這座山峰的辦法之一就是,把山峰刨掉一些土,填到山峰周圍的山谷裡去。
用數學的方式來說,就是把山峰和周圍的高度進行一下平均。
平滑後得到:
那具體如何能實作上圖的平滑效果的呢?實際上,圖像平滑的技術和理論非常多,我們本文的主題是卷積,是以我們接下來讨論下如何使用卷積的方法實作圖像平滑以及它的原理。
2. 基于卷積實作圖像平滑的原理
1)利用卷積核實作鄰域像素平均化
我們可以将上面那張有噪點的圖檔,轉化為數值型的像素矩陣:
接下來的問題是,我們可以把原始圖像像素點看成是輸入信号,但是卷積運算還需要一個“響應信号”,這個響應信号如何定義?
這裡就需要引入一個概念,即卷積核(convolutional filter)或者叫做感覺野(Sensing field)。
我們将下面這個平均矩陣,設定為原始像素輸入信号的相應信号。
為什麼要設定這種全部由平均數組成的矩陣呢?我們可以直覺地這麼了解:這個響應信号矩陣充當了一個很“遲鈍、中庸、打哈哈”性格的響應者,不管你輸入的信号如何充滿高頻變化,它永遠從每個信号的周圍各取8個信号,大家一起取一個平均,然後将結果傳回給你,這樣,最終的累加結果從整體上就會比較傾向于“平均主義”。
2)像素矩陣卷積運算過程 - 卷積運算與相關運算
我們來看一下計算過程,這個知識點是筆者在初次接觸CNN卷積概念時比較容易混淆的概念,這裡重點讨論下,希望能幫助到讀者朋友。
輸入矩陣和卷積核的運算是一一對應的内積運算,還是按照卷積定義的倒序運算呢?其實從計算機工程上來說,例如在tensorflow内部,會先将卷積核進行倒序翻轉,即旋轉180°,然後将翻轉後的卷積核和輸入矩陣進行一一對應的内積運算,這樣可以大大提高運算效率并且有利于分布式計算。隻是我們在寫代碼的時候要明白,卷積核(或者叫濾波器模版)和輸入矩陣本質上是在做卷積運算。
圖像卷積運算與相關運算(内積)的關系如下圖所示,其中F為濾波器,X為圖像,O為結果。
相關是将濾波器在圖像上滑動,對應位置相乘求和;
卷積則先将濾波器旋轉180度(行列均對稱翻轉),然後使用旋轉後的濾波器進行相關運算。
這也是為什麼卷積核也被稱為濾波器模版的原因之一,從效果上來講,即使将卷積核翻轉了180°,其本質也可以了解為一種模版。
比如我要平滑原始圖像中的
點,就在原始圖像像素矩陣中,取出
點附近的點組成矩陣
,和
進行卷積計算後,再将計算結果填回去:
讀者朋友可能會注意到,對于原始圖像邊緣的點,是無法完整圈出一個”附近矩陣“的,這個時候需要由我們人工指定插值方法,例如填0,或者填入均值等等。具體設定方式在TensorFlow/keras的API調用中通過參數指定。
對應關系如下動圖
寫成卷積公式就是:
這樣相當于實作了
這個矩陣在原來圖像上的滑動掃描,這其實也就是CNN神經網絡的卷積核“掃描”原始上一層輸入矩陣的原理。
筆者插入:這裡要注意區分單個感覺野内部的卷積運算和卷積核的滑動strip size的差別,它們是不同的概念,理論上說,卷積核可以和輸入矩陣一樣大,即不滑動,而卷積核感覺野内部的卷積運算可以照常進行。有可以一次跳躍多個step size,這取決于我們希望的掃描精度。
筆者思考:CNN強大的創新之處就在于将非常質樸的卷積原理,巧妙應用在了深度神經網絡的特征抽象和特征空間轉換上。從這裡可以感受到CNN創始者對數學規律了解的深刻程度,令人敬佩。
0x8:從不同圖像處理方式的差別角度了解卷積核以及不同卷積核參數背後代表的含義
上一個小節我們簡單讨論了利用”均值卷積“實作圖像平滑的目的,實際上卷積核的種類有非常多種,我們經常能看到的,平滑,模糊,去燥,銳化,邊緣提取等等工作,其實都可以通過卷積操作來完成,我們這個小節列舉幾種傳統機器學習和圖像進行中常用的卷積核。
1. 一個沒有任何作用的卷積核
将原像素中間像素值乘1,其餘全部乘0,顯然像素值不會發生任何變化。
2. 平滑均值濾波
該卷積核的作用在于取九個值的平均值代替中間像素值,是以起到的平滑的效果。
3. 高斯平滑
高斯平滑水準和垂直方向呈現高斯分布,在平滑的同時,更突出了中心點在像素平滑後的權重,相比于均值濾波而言,有着更好的平滑效果。
4. 圖像銳化
該卷積利用的其實是圖像中的邊緣資訊有着比周圍像素更高的對比度,而經過卷積之後進一步增強了這種對比度,進而使圖像顯得棱角分明、畫面清晰,起到銳化圖像的效果。
除了上述卷積核,邊緣銳化還可以選擇下面的卷積核,其本質思想是一樣的:
5. 梯度Prewitt
水準梯度卷積核:
垂直梯度卷積核:
梯度Prewitt卷積核與Soble卷積核的標明是類似的,都是對水準邊緣或垂直邊緣有比較好的檢測效果。
6. Soble邊緣檢測
Soble與上述卷積核不同之處在于,Soble更強調了和邊緣相鄰的像素點對邊緣的影響。
水準梯度:
垂直梯度:
7. 梯度Laplacian
卷積核:
Laplacian也是一種銳化方法,同時也可以做邊緣檢測,而且邊緣檢測的應用中并不局限于水準方向或垂直方向,這是Laplacian與soble的差別。下面這張圖可以很好的表征出二者的差別:
更本質上來說,不同的卷積核的差別在于内内部的權重參數,不同的權重參數賦予了卷積核不同的特征提取能力。
在CNN神經網絡出現之前,工程師和研究員們需要根據具體的研究對象和項目情況,人工設計特定的卷積核參數,或者逐個實驗傳統的知名卷積核(例如高斯卷積等),通過大量的實驗結果來選擇一個最合适的卷積核或者卷積核組合。但是神經網絡的強大之處在于其不是專家經驗驅動的,而是資料驅動的,通過向神經網絡輸入訓練樣本,網絡不斷調整直到得到最能表達(拟合)訓練樣本中規律的卷積核權重特征。
如果讀者朋友有興趣print出CNN卷積核的權重參數(需要稍微改動一些TensorFlow的核心代碼),可以看到在Minist手寫識别中,第一層卷積核的參數非常類似Soble邊緣檢測,當然不完全一樣,這也從一個側面展現了,卷積神經網絡是逐層抽象的,第一層往往側重提取圖像的邊緣特征。
筆者插入:讨論這一小節的目的是希望向讀者朋友展示卷積擅長在圖像處理領域發揮作用。但是CNN現在不僅僅在圖像處理領域被頻繁使用,在非圖像領域,例如NLP領域也在被大量使用。在筆者所從事的網絡安全領域,CNN被用來對日志文本進行特征提取和檢測,這種遷移是否具備合理性呢?目前似乎還沒有明确的理論證明說CNN隻能被用于某幾個或者某些領域,至少從結果上看,CNN在文本檢測領域表現還是很不錯的。但是有一點需要注意,我們在設計CNN網絡結構的時候,需要特别關注卷積核size、卷積核形狀(不一定是 N * N 的方陣),很多時候,文本檢測需要條帶狀的卷積核( width * 1),我們需要根據項目的實際情況,并且結合專家領域經驗設計合理的卷積核,并且通過實驗獲得效果的驗證。
0x9:從模版比對的角度了解卷積 - 卷積核被稱為濾波器的原因
我們知道,卷積與相關在計算上可以等價(雖然他們的概念是不同的),而相關運算常被用于模闆比對。
即我們可以認為卷積核定義了某種模式,卷積(相關)運算是在計算每個位置與該模式的相似程度,或者說每個位置具有該模式的分量有多少,目前位置與該模式越像,響應越強。
0x10:卷積可以提取圖像特征
假設一個業務場景,我們需要識别圖像中的”老鼠“這個目标,我們該怎麼利用卷積幫助我們完成任務呢?
現在我有一個老鼠的圖像(隻考慮灰階,是以隻有一層)
現在我們設計一個濾波器對這隻老鼠的圖像進行過濾,濾波器的大小為 7 * 7,我們這裡先不讨論卷積核翻轉180°的問題,其實在工程上是等價的,是以這個小節我們對濾波器和卷積核這2個詞彙不做區分:
依次将過濾器滑過原始圖像的某一個區域,并與該區域原始像素值進行點乘。現在假設我們将濾波器滑到了老鼠圖像的左上角,如下圖所示:
過濾器會與該原始圖像對應區域進行點乘後求和,結果如下圖所示:
我們發現,該區域本身存在一個弧度,且彎曲程度和過濾器的彎曲程度很相似,是以圖像的該區域像素與過濾器矩陣點乘求和後得到的數值很高。這個過程很形象地展示了”濾波“的這個概念。
接下來,我們繼續将過濾器移動到另外一個地方,靠近老鼠耳朵的地方,這時候我們發現該老鼠耳朵區域與該濾波器用來檢測的弧度很不相似,我們看看将用濾波器過濾該區域會發生什麼,如下圖所示:
我們發現,檢測弧度的濾波器在與老鼠耳朵這個區域的像素進行點乘後求和所得到的值為0。也就是說該區域與濾波器沒有對應關系。
卷積原理在CNN中發揮重要作用的核心原因,筆者認為可以總結為一句話:取其精華,去其糟粕。卷積通過濾波機制将不關注的部分濾波,而保留卷積核“關注”的圖譜特征,這也展現了神經網絡訓練的本質是資訊壓縮,即去除備援資訊,保留核心模式特征。
我們對整個原圖進行一次卷積,得到的結果中,在那個特定曲線和周邊區域,值就很高,在其他區域,值相對低。這就是一張激活圖。對應的高值區域就是我們所要檢測曲線的位置。
問題來了,我們如果需要完整地檢測老鼠這個物體概念,就需要設計多個不同的卷積核濾波器,可能分别是屁股,耳朵,胡子,嘴巴,身體等等,這個人工設計的過程就是基于專家經驗的特征工程。
但是新的問題馬上就來了,要是檢測的目标非常複雜怎麼辦?複雜多超出人能設計範疇或者工程量巨大,我們就不可能再使用專家經驗進行卷積核設計了,這個時候我們使用深度學習,進行資料驅動的模型訓練,建立一個包含非常多卷積核,甚至非常多層的卷積神經網絡,訓練的過程就是在調整各個卷積核對應的内部權重向量。
Relevant Link:
https://www.zhihu.com/question/22298352
https://www.zhihu.com/question/54677157/answer/14124529
https://www.zhihu.com/question/22298352
https://baijiahao.baidu.com/s?id=1613041733799043592&wfr=spider&for=p
https://blog.csdn.net/chaipp0607/article/details/72236892
1. CNN簡介
卷積網絡(convolutional network),也叫做卷積神經網絡(con- volutional neural network, CNN),是一種專門用來處理具有類似網格結構的資料的神經網絡。例如時間序列資料(可以認為是在時間軸上有規律地采樣形成的一維網格)、和圖像資料(可以看作是二維的像素網格)。
卷積網絡在諸多應用領域都表現優異。“卷積神經網絡’’ 一詞表明該網絡使用了卷積(convolution)這種數學運算。
卷積是一種特殊的線性運算。卷積網絡是指那些至少在網絡的一層中使用卷積運算來替代一般的矩陣乘法運算的神經網絡。
0x1:卷積運算
我們已經在第一章花了一些篇幅讨論了卷積的定義以及其運算過程,但是為了這章的完整性,我們針對卷積在卷積神經網絡中的運算再讨論一次。
在通常形式中,卷積是對兩個實變函數的一種數學運算,下式展示了卷積的數學公式:
這種運算就叫做卷積(convolution)。卷積運算通常用星号表示:
在卷積網絡的術語中,卷積的第一個參數(在這個例子中,函數 x)通常叫做輸入(input),第二個參數(函數 w)叫做核函數(kernel function)或者卷積核(filter)。輸出被稱作特征映射(feature map)。
在圖像處理領域,我們經常一次在多個次元上進行卷積運算,卷積運算可以很容易擴充到多元格式。例如,如果把一張二維的圖像 I 作為輸入,我們也許也想要使用一個二維的核 K:
一個 2 維卷積的例子:
0x2:卷積網絡的典型結構
卷積網絡中一個典型層包含三級,如下圖所示):
在第一級中,這一層并行地計算多個卷積産生一組線性激活響應;
在第二級中,每一個線性激活響應将會通過一個非線性的激活函數,例如整流線性激活函數。這一級有時也被稱為探測級(detector stage);
在第三級中,我們使用池化函數(pooling function)來進一步調整這一層的輸出;
之是以稱上圖為CNN卷積網絡的典型結構,在絕大多數數現有比較成功的項目中(包括resNet、googleNet等),都遵循了上述基本結構。
在一般情況下,卷積神經網絡真正使用時,還需要配合池化、激活函數等,以獲得更強的表達能力。
但是需要明白的是,模式蘊含在卷積核中,如果沒有非線性激活函數、沒有池化技術,網絡仍能學到模式,但表達能力會下降,由論文《Systematic evaluation of CNN advances on the ImageNet》,在ImageNet上,使用調整後的caffenet,不使用非線性激活函數相比使用ReLU的性能會下降約8個百分點,如下圖所示。通過池化和激活函數的配合,可以看到複現出的每層學到的特征是非常單純的,狗、人、物體是清晰的,少有其他其他元素的幹擾,可見網絡學到了待檢測對象差別于其他對象的模式。
筆者認為,可能你的項目并不一定需要例如池化這種技術,但在設計網絡結構的時候,需要明白,池化、dropout這種技術是用來增強卷積神經網絡對資料中隐藏的規律進行提取和捕獲的,同時池化可以讓網絡對輸入圖像中的特征具備一定的平移不變性,在很多時候,使用它們并不會帶來壞處。
0x3:卷積神經網絡的核心思想 - 使用卷積的動機
卷積神經網絡通過下面幾個重要的思想來幫助改進機器學習系統。
1. 稀疏互動(sparse interactions)- 局部感受野(local receptive fields)
傳統的神經網絡使用矩陣乘法來建立輸入與輸出的連接配接關系。其中,參數矩陣中每一個單獨的參數都描述了一個輸入單元與一個輸出單元間的互動。這意味着每一個輸出單元與每一個輸入單元都産生互動。
然而,卷積網絡具有稀疏互動(sparse interactions)(也叫做稀疏連接配接(sparse connectivity)或者稀疏權重(sparse weights))的特征。這裡的稀疏是相對full connected NN網絡而言的。
這是通過使核的大小遠小于輸入的大小來達到的。
舉個例子, 當處理一張圖像時,輸入的圖像可能包含成千上萬個像素點,但是我們可以通過隻占用幾十到上百個像素點的核來檢測一些小的有意義的特征,例如圖像的邊緣。
這意味着我們需要存儲的參數更少,不僅減少了模型的存儲需求,而且提高了它的統計效率。這也意味着為了得到輸出我們隻需要更少的計算量。
這些效率上的提高往往是很顯著的。如果有 m 個輸入和 n 個輸出,那麼傳統DNN網絡矩陣乘法需要 m × n 個參數并且相應算法的時間複雜度為 O(m × n)(對于每一個例子)。如果我們限制每一個輸 出擁有的連接配接數為 k,那麼稀疏的連接配接方法隻需要 k × n 個參數以及 O(k × n) 的運作時間。在很多實際應用中,隻需保持 k 比 m 小幾個數量級,就能在機器學習的任務中取得好的表現。
下圖展示了CNN稀疏互動和DNN全連接配接互動的差別:
上面的CNN稀疏連接配接架構,當 s 是由核寬度為 3 的卷積産生時,隻有三個輸出受到 x 的影響2;下面的DNN全連接配接架構,當 s 是由矩陣乘法産生時,連接配接不再是稀疏的,是以所有的輸出都會受到 x3 的影響。
另外一方面,在深度卷積網絡中,處在網絡深層的單元可能與絕大部分輸入是間接互動的,CNN的稀疏連接配接架構允許網絡可以通過隻描述稀疏互動的基石來高效地描述多個變量的複雜互動。這展現了一種層層遞進,逐層抽象的思想。
處于卷積網絡更深的層中的單元,它們的接受域要比處在淺層的單元的接受域更大。如果網絡還包含類似步幅卷積或者池化之類的結構特征,這種效應會加強。這意味着在卷積網絡中盡管直接連接配接都是很稀疏的,但處在更深的層中的單元可以間接地連接配接到全部或者大部分輸入圖像。
1)稀疏互動如何減少了神經網絡參數運算次數的原理說明
稀疏互動還有另外一種稱呼,叫局部感覺野。
回想一下傳統的全連接配接(full-connected) DNN,每一層的神經元都和上一層以及下一層的所有神經元建立了連接配接。
但是CNN不這樣做,我們以圖像為例,CNN隻會将隐藏層(CNN的hidden layer就是一層濾波器層/卷積層)和圖像上相同大小的一個區域進行連接配接。
這個輸入圖像的區域被稱為隐藏神經元的局部感受野,它是輸入像素上的一個小視窗。局部感受野中的每個神經元(個數由感受野大小而定),單獨學習一個權重w,同時整個卷積核也學習一個總的偏置b。
下圖展示了DNN網絡和CNN網絡在參數個數上的對比:
局部感受野像一台掃描器一樣,每次按照一定的跨距(可以是1、2..)從左到右,從上到下移動。
上圖左:全連接配接網絡。如果我們有1000 * 1000像素的圖像,有1百萬個隐層神經元,每個隐層神經元都連接配接圖像的每一個像素點,就有1000x1000x1000000=10^12個連接配接,也就是10^12個權值參數
上圖右:局部連接配接網絡,每一個節點與上層節點同位置附件10x10的視窗相連接配接,步距為1,則1百萬個隐層神經元就隻有((100w - 10) / 1 + 1) * (10 * 10),即10^8個參數。其權值連接配接個數比原來減少了四個數量級
筆者思考:CNN使用參數共享機制,這決定了CNN中每層中的一個卷積核隻能捕獲一種或者一類特征(因為卷積模版是固定的),是以一般情況下,CNN網絡的每層都需要設定多個卷積Filter。
2. 參數共享(parameter sharing) - 共享權重和偏置(shared weight)
我們把深度神經網絡看成是由大量簡單線性函數和非線性函數組合而成的複合函數,則參數共享(parameter sharing)是指在一個模型的多個函數中使用相同的參數。
在傳統的神經網絡中,當計算一層的輸出時,權重矩陣的每一個元素隻使用一次,當它乘以輸入的一個元素後就再也不會用到了。作為參數共享的同義詞,我們可以說一個網絡含有綁定的權重(tied weights),因為用于一個輸入的權重也會被綁定在其他的權重上。
在卷積神經網絡中,核的每一個元素都作用在輸入的每一位置上(是否考慮邊界像素取決于對邊界決策的設計)。卷積運算中的參數共享保證了我們隻需要學習一個參數集合,而不是對于每一位置都需要學習一個單獨的參數集合。
這雖 然沒有改變前向傳播的運作時間(仍然是 O(k × n)),但它顯著地把模型的存儲需求降低至 k 個參數,并且 k 通常要比 m 小很多個數量級。因為 m 和 n 通常有着大緻相同的大小(例如圖像方陣),k 在實際中相對于 m × n 是很小的。是以,卷積在存儲需求和統計效率方面極大地優于稠密矩陣的乘法運算。
共享權重和偏置指的是圖像的所有局部感受野都使用同一份權重和偏置。
例如對一張 28 * 28 的圖像,使用 5 * 5 的局部感受野,步距1,且該CNN隐層隻設定一個卷積核Filter。
則該CNN隐藏層總共由 (28 - 5 + 1)* (28 - 5 + 1) = 576 個感覺野隐藏神經元組成(不考慮邊緣padding,在實際項目中往往會加上邊緣padding,這樣卷積後張量次元不變),這很好了解,因為卷積核是逐行從左到右掃描過去的,當然。每個神經元都共享同一份w向量(5 * 5個像素點)和偏置b。
在不考慮邊緣padding的情況下,經過這樣的卷積後,原本 28 * 28 次元的輸入,就會得到 24 * 24 次元的輸出。我們接下來來看看每個元素的計算過程,也即每個感覺野神經元的計算公式。
1)單個感覺野隐藏神經元計算公式
以 5 * 5 感受野為例,對第(k,j)個感覺野隐藏神經元,輸出為
這裡σ是神經元的激活函數(例如可以是sigmoid或者ReLU);b是偏置的共享值;Wl,m是5*5的權重數組;a是和這個感覺野相同大小的輸入矩陣(對于第一層來說,就是像素強度值本身,如果不是第一層就是上一層激活函數的輸出)。
從公式中可以看到幾點:
1. 對于同一個卷積核Filter來說,所有(k,j)感覺野隐藏神經元都共享同一份參數w和偏置b;
2. 如果将卷積核的感覺野看成是”響應信号“,則每個感覺野神經元的計算累加公式可以看成是整個”輸入激發信号(這裡就是感覺野大小的輸入矩陣)“和”響應信号(卷積感覺野)“的累加結果,和信号裡的卷積概念上是一緻的。
在我們這個例子中,上面的公式,重複 576 次((28 - 5 + 1)* (28 - 5 + 1) = 576),就得到一個特征圖譜,即一個卷積核。
2)一個卷積濾波器就是一個特征映射
在CNN卷積網絡中,我們把從輸入層到隐藏層的映射稱為一個特征映射,每個卷積核可以生産一個特征映射。
我們把定義特征映射的權重稱為共享權重,把定義特征映射的偏置稱為共享偏置。共享權重和偏置合稱為卷積核或濾波器。
值得注意的是,一個特征映射代表了一種"視角"或者一種”模式“,為了完成圖像細節識别和提取需要多個特征映射:
在上圖中,有3個特征映射,每個特征映射定義為一個5*5共享權重和單個共享偏置的集合,其結果是網絡能夠檢測3種不同的特征。
筆者思考:由于采用了參數共享機制,CNN具備處理可變大小輸入的能力。這點上和RNN是類似的。但是所不同的是,RNN通過改進後的LSTM遺忘門來實作記憶模式的更新和丢棄,是以RNN更擅長處理序列資料,CNN則需要在每層網絡中使用多個卷積Filter來實作多模式的同時捕獲,所有CNN更擅長處理局部區塊特征模式。
3. 多卷積核組合
對于圖像識别來說,用一個卷積核操作隻能得到一部分特征,無法擷取全部特征。是以為了能夠得到圖像更多的特征資訊我們引入了多核卷積。
用多個卷積核來學習圖像更多的不同的特征(每個卷積核學習到不同的權重),一般來說在實際項目中,一層CNN卷積層會有多個卷積核,同時CNN網絡會有多個卷積層stacking,我們這個小節讨論一下CNN卷積層和多個卷積核之間的關系。
1)單個卷積層中使用多個卷積核
描述CNN網絡模型中某層的厚度,通常用名詞通道channel數或者特征圖feature map數。
我們通常把作為資料輸入的前層的厚度稱之為通道數(比如RGB三色圖層稱為輸入通道數為3),把作為卷積輸出的後層的厚度稱之為特征圖數。
卷積核(filter)一般是3D多層的,除了面積參數(比如3x3)之外, 還有厚度參數H(2D的視為厚度1)。
卷積核的厚度H, 一般等于前層厚度M(輸入通道數或feature map數),這是由于矩陣乘法的形式決定的;卷積核的個數N, 一般等于後層厚度(後層feature maps數)。因為每個卷積核就産生一個特征圖(feature map)。
從這個角度看,卷積核通常從屬于後層,為後層提供了各種檢視前層特征的視角:
1. 卷積核厚度等于1時為2D卷積,對應平面點相乘然後把結果加起來,相當于點積運算;
2. 卷積核厚度大于1時為3D卷積,每片分别平面點求卷積,然後把每片結果加起來,作為3D卷積結果;
歸納之,卷積的意思就是把一個區域,不管是一維線段,二維方陣,還是三維長方塊,全部按照卷積核的次元形狀,對應逐點相乘再求和,濃縮成一個标量值也就是降到零次元,作為下一層的一個feature map的一個點的值!不同的卷積核權重參數決定了點乘後的結果。
可以把單層卷積層的一個卷積核的計算過程比喻一群漁夫坐一個漁船撒網打魚,魚塘是多層水域,每層魚兒不同。
船每次移位一個stride到一個地方,每個漁夫撒一網,得到收獲,然後換一個距離stride再撒,如此重複直到周遊魚塘。
1. A漁夫盯着魚的品種特征,周遊魚塘後該漁夫描繪了魚塘的魚品種分布;
2. B漁夫盯着魚的重量特征,周遊魚塘後該漁夫描繪了魚塘的魚重量分布;
3. 還有N-2個漁夫,各自興趣各幹各的;
4. 最後得到N個特征圖,描述了魚塘的一切!
2D卷積表示漁夫的網就是帶一圈浮标的漁網,隻打上面一層水體的魚;3D卷積表示漁夫的網是多層嵌套的漁網,上中下層水體的魚兒都跑不掉;1x1卷積可以視為每次移位stride,甩鈎釣魚代替了撒網;
注意,如果CNN網絡中一層包含多個卷積核,這些卷積核經過卷積運算後得到的特征圖都是等尺寸的,然後會被疊加起來,得到一個最終的輸出。
2)多卷積核卷積結果疊加一起呢?
直接了解隐層中的卷積核卷積結果疊加可能不是那麼直覺,我們從輸入層這裡切入來了解。
我們知道,對于一副彩色圖像來說,是由RGB三個通道的像素矩陣疊加組成的。RGB像素的疊加最終組成了我們肉眼可見的色彩。
回到輸入層,RGB像素矩陣,我們可以了解為上一層(原始色彩像素矩陣層)的特征圖數為3,是以輸入層的卷積核厚度也需要為3。
将這個概念推廣到隐層中,在隐藏中,多個卷積核的卷積結果,我們也同樣将最後的結果累加起來,得到一個最終的數值矩陣,它代表的實體意義已經十分抽象,但其本質沒有改變,即代表了對上一層輸入的特征抽象。
3)多個卷積層stacking組合
為了直覺表述問題,我們先上圖,圖檔出自論文《Visualizing and Understanding Convolutional Networks》,作者可視化了卷積神經網絡每層學到的特征,當輸入給定圖檔時,每層學到的特征如下圖所示,注意,我們上面提到過每層得到的特征圖直接觀察是看不出什麼的,因為其中每個位置都代表了某種模式,需要在這個位置将模式複現出來才能形成人能夠了解的圖像,作者在文中将這個複現過程稱之為deconvolution,詳細檢視論文,其本質其實就是一個反向激活的過程,不難了解。
從上圖中可知,淺層layer學到的特征為簡單的邊緣、角點、紋理、幾何形狀、表面等,到深層layer學到的特征則更為複雜抽象,為狗、人臉、鍵盤等等,有幾點需要注意:
1. 卷積神經網絡每層的卷積核權重是由資料驅動學習得來,不是人工設計的,人工隻能勝任簡單磁碟區積核的設計,像邊緣,但描述複雜模式的卷積核則十分困難。
2. 資料驅動卷積神經網絡逐層學到由簡單到複雜的特征(模式),複雜模式是由簡單模式組合而成,比如Layer4的狗臉是由Layer3的幾何圖形組合而成,Layer3的幾何圖形是由Layer2的紋理組合而成,Layer2的紋理是由Layer1的邊緣組合而成,從特征圖上看的話,Layer4特征圖上一個點代表Layer3某種幾何圖形或表面的組合,Layer3特征圖上一個點代表Layer2某種紋理的組合,Layer2特征圖上一個點代表Layer1某種邊緣的組合。
3. 多層卷積層的多卷積核的這種組合是一種相對靈活的方式在進行,不同的邊緣->不同紋理->不同幾何圖形和表->不同的狗臉->不同的物體...,淺層模式的組合可以多種多樣,使深層可以描述的模式也可以多種多樣,是以具有很強的表達能力,不是“死闆”的模闆,而是“靈活”的模闆,泛化能力更強。
4. 平移等變表示
對于卷積,參數共享的特殊形式使得卷積神經網絡層具有對平移等變(equivariance) 的性質。
如果一個函數滿足輸入改變,輸出也以同樣的方式改變這一性質,我們就說它是等變(equivariant) 的。
特别地,如果函數 f(x) 與 g(x) 滿足 f(g(x)) = g(f(x)), 我們就說 f(x) 對于變換 g 具有等變性。
對于卷積來說,如果令 g 是輸入的任意平移函數,那麼卷積函數對于 g 具有等變性。為什麼這麼說?我們可以直覺地想象一下,卷積像一個掃描器逐漸掃描整個輸入空間,并且是參數共享的,是以對于平移來說,平移的距離并不改變最後卷積計算的結果。說的更具體一點就是,如果我們要檢測圖檔上的一隻貓,這隻貓不管在圖檔中取景在哪個位置都不影響卷積網絡提取它的圖像特征。
當處理時間序列資料時,這意味着通過卷積可以得到一個由輸入中出現不同特征的時刻所組成的時間軸。如果我們把輸入中的一個事件向後延時,在輸出中仍然會有完全相同的表示,隻是時間延後了。
圖像與之類似,卷積産生了一個 2 維映射來表明某些特征在輸入中出現的位置。如果我們移動輸入中的對象,它的表示也會在輸出中移動同樣的量。
當處理多個輸入位置時,一些作用在鄰居像素的函數是很有用的。例如在處理圖像時,在卷積網絡的第一層進行圖像的邊緣檢測是很有用的。相同的邊緣或多或少地散落在圖像的各處,是以應當對整個圖像進行參數共享。
但在某些情況下,我們并不希望對整幅圖進行參數共享。例如,在處理已經通過剪裁而使其居中的人臉圖像時,我們可能想要提取不同位置上的不同特征(處理人臉上部的部分網絡需要去搜尋眉毛,處理人臉下部的部分網絡就需要去搜尋下巴了)。
需要注意的是,卷積隻對平移變換具備等變的特征,對其他的一些變換并不是天然等變的,例如對于圖像的放縮或者旋轉變換。
5. 池化層(pooling)
1)什麼是池化
池化是一種非線性變換,池化函數和卷積核一樣,也有一個感覺野,在感覺野内,池化函數使用某一位置的相鄰輸出的總體統計特征來代替網絡在該位置的輸出。
卷積神經網絡通常會包含池化層(polling layer),混合層通常緊跟在卷積層之後使用,它要做的是簡化從卷積層輸出的資訊。一個池化層取得了從卷積層輸出的每一個特征映射并且産生一個"凝縮映射"。
2)池化的種類
池化本質上是一種通過統計特征代替原始特征向量的數學變換,它有很多種變換形式,我們這裡列舉幾種常見的:
1. 最大池化(max pooling)函數: 給出相鄰矩形區域内的最大值;
2. 平均池化(average pooling): 給出相鄰矩形區域内的平均值;
3. L2範數池化(l2 pooling):取區域中激活值的平方和的平方根,而不是最大激活值;
4. 基于據中心像素距離的權重平均函數;
3)池化的核心思想
1. 提高網絡對輸入的平移不變性
不管采用什麼樣的池化函數,當輸入作出少量平移時,池化能夠幫助輸入的表示近似不變(invariant)。
對于平移的不變性是指當我們對輸入進行少量平移時,經過池化函數後的大多數輸出并不會發生改變。下圖用了一個例子來說明這是如何實作的:
上面一幅圖中。下面一行顯示非線性的輸出。上面一行顯示最大池化的輸出,每個池的寬度為三個像素并且池化區域的步幅為一個像素。
下面一副圖中,對輸入右移了一個像素。下面一行的所有值都發生了改變,但上面一行隻有一半的值發生了改變,這是因為最大池化單元隻對周圍的最大值比較敏感,而不是對精确的位置。
使用池化可以看作是增加了一個無限強的先驗:這一層學得的函數必須具有對少量平移的不變性。當這個假設成立時,池化可以極大地提高卷積網絡的統計效率。
局部平移不變性是一個很有用的性質,尤其是當我們關心某個特征是否出現而不關心它出現的具體位置時。例如,當判定一張圖像中是否包含人臉時,我們并不需要知道眼睛的精确像素位置,我們隻需要知道有一隻眼睛在臉的左邊,有一隻在右邊就行了。
2. 資訊壓縮 - 保留圖像的整體結構資訊
以最大值池化函數為例,池化時,對鄰域内特征點取最大,這樣能更多的保留紋理特征。就像全國夜晚燈光分布圖,最大值,也就是最亮的地方,能夠更加反應整個圖像的全局結構資訊。如果被平均了,就反應不出最大值的特點,也就同樣不能反應整個圖像的全局結構。
對鄰域内特征點取平均,也有它的作用,主要是取最大值容易導緻各個區域之間的偏差增大。是以取最大和取平均各有優勢。
3. 減少網絡參數個數
因為池化綜合了全部鄰居的回報,這使得池化單元少于探測單元成為可能。
我們可以通過綜合池化區域的 k 個像素的統計特征而不是單個像素來實作。這種方法提高了網絡的計算效率,因為下一層少了約 k 倍的輸入。 當下一層的參數數目是關于那一層輸入大小的函數時(例如當下一層是全連接配接的基于矩陣乘法的網絡層時),這種對于輸入規模的減小也可以提高統計效率并且減少對于參數的存儲需求。
4. 解決不同size輸入圖像的處理問題
在很多任務中,池化對于處理不同大小的輸入具有重要作用。例如我們想對不 同大小的圖像進行分類時,分類層的輸入必須是固定的大小,而這通常通過動态調整池 化區域的size大小來實作,這樣分類層(通常是前饋網絡)總是能接收到相同數量的統計特征而不管最 初的輸入大小了。
相關技術細節可以google,"SPP-Net",這項技術最早被用于目标檢測中。
http://neuralnetworksanddeeplearning.com/chap6.html
https://yq.aliyun.com/articles/617314
https://www.julyedu.com/question/big/kp_id/26/ques_id/932
https://www.cnblogs.com/shine-lee/p/9932226.htm
https://www.zhihu.com/question/45873400
0x4:從機率分布的視角看卷積神經網絡 - 卷積與池化作為一種無限強的先驗
在學習機率統計機器學習過程中,我們認識到,神經網絡的模型權重參數,可以看做是一種模型參數的機率分布。而卷積神經網絡中的兩個核心思想:卷積和池化,從泛函的角度了解本質上可以了解為一種限制,而從機率分布的角度來了解,本質上可以了解為一種先驗機率,它刻畫了在我們看到資料之前我們認為什麼樣的模型是合理的信念。
先驗被認為是強或者弱取決于先驗中機率密度的集中程度:
弱先驗具有較高的熵值,例如方差很大的高斯分布。這樣的先驗允許資料對于參數的改變具有較大的自由性;
強先驗具有較低的熵值,例如方差很小的高斯分布。這樣的先驗在決定參數時,對最終取值時起着更大的決定性作用;
一個無限強的先驗需要對一些參數的機率置零并且完全禁止對這些參數指派, 無論資料對于這些參數的值給出了多大的支援。
我們可以把卷積網絡類比成全連接配接網絡,但對于這個全連接配接網絡的權重有一個 無限強的先驗。這個無限強的先驗是說一個隐藏單元的權重必須和它鄰居的權重相同,但可以在空間上移動。
這個先驗也要求除了那些處在隐藏單元的小的空間連續的接受域内的權重以外,其餘的權重都為零。
總之,我們可以把卷積的使用當作是對網絡中一層的參數引入了一個無限強的先驗機率分布。這個先驗說明了該層應該學得的函數隻包含局部連接配接關系并且對平移具有等變性。
類似的,使用池化也是一 個無限強的先驗:每一個單元都具有對少量平移的不變性。
當然,把卷積神經網絡當作一個具有無限強先驗的全連接配接網絡來實作會導緻極大的計算浪費。但把卷積神經網絡想成具有無限強先驗的全連接配接網絡可以幫助我們更好地洞察卷積神經網絡是如何工作的。
1. 其中一個關鍵的洞察是卷積和池化可能導緻欠拟合,這很容易了解,過多的先驗會減少模型搜尋的假設空間。與任何其他先驗類似,卷積和池化隻有當先驗的假設合理且正确時才有用。如果一項任務依賴于儲存精确的空間資訊,那麼在所有的特征上使用池化将會增大訓練誤差。
2. 另一個關鍵洞察是當我們比較卷積模型的統計學習表現時,隻能以基準中的其他卷積模型作為比較的對象。其他不使用卷積的模型即使我們把圖像中的所有像素點都置換後依然有可能進行學習。
《深度學習》 - 周志華
2. 基本卷積函數的變體 - 實際工程開發中對卷積函數的定制
當在卷積神經網絡的上下文中讨論卷積時,我們通常不是特指數學文獻中使用的那種标準的離散卷積運算。實際應用中的函數略有不同,這也很符合技術的應用原理,從數學原理到工程過程中,核心數學公式往往充當基石的作用,在基石之上進行各種定制和增加,得到符合工業場景的開發庫(例如TensorFlow)。本章我們詳細讨論一下這些差異,并且對神經網絡中用到的函數的一些重要性質進行重點說明。
0x1:多卷積并行
當我們提到神經網絡中的卷積時,我們通常是指由多個并行卷積組成的運算。這是因為具有單個核的卷積隻能提取一種類型的特征,盡管它作用在多個空間位置上。
我們通常希望網絡的每一層能夠在多個位置提取多種類型的特征。
0x2:多通道卷積核(3D卷積核張量)
輸入通常也不僅僅是實值的網格,而是由一系列觀測資料的向量構成的網格。例如,一幅彩色圖像在每一個像素點都會有紅綠藍三種顔色的亮度。RGB三種像素可以了解為3個輸入通道。
在多層的卷積網絡中,第二層的輸入是第一層的輸出,通常在每個位置包含多個不同卷積的累加輸出。
當處理圖像時,我們通常把卷積的輸入輸出都看作是 3 維的張量,其中一個索引用于标明不同的通道(例如紅綠藍),另外兩個索引标明在每個通道上的空間坐标。
0x3:全卷積函數輸出的下采樣 - 步幅(stride)
原始的離散信号卷積公式中,輸入信号和響應信号是逐個周遊相乘的。工程師們為了提高整體運算效率,通過引入采樣思想來降低計算的開銷,即跳過卷積核掃描路徑中的一些位置來,當然,相應的代價是提取特征的能力沒有先前那麼好了。這是由于采樣本身的失真缺陷帶來的。
我們可以把這一過程看作是對全卷積函數輸出的下采樣 (downsampling)。如果我們隻想在輸出的每個方向上每間隔 s 個像素進行采樣,那麼我們可以定義一個下采樣卷積函數 c 使得:
我們把 s 稱為下采樣卷積的步幅(stride)。當然也可以對每個移動方向定義不同的步幅。
0x4:奇數size卷積核
有幾種原因導緻我們普遍使用奇數size卷積核:
1. 奇數相對于偶數,有中心點,對邊沿、對線條更加敏感,可以更有效的提取邊沿資訊;
2. 如果卷積核size是奇數,就可以從圖像的兩邊對稱的padding;
3. 奇數size的卷積核,有central pixel 可以友善的确定position;
0x4:對輸入進行padding填充
在之前讨論CNN參數共享思想的時候已經提到一點,即卷積核在掃描輸入矩陣的時候,對輸入矩陣的邊界處理問題。在實際的工程項目中,95%時候都會用“零值0”對輸入 V 進行填充(pad),至于填充的方式我們接下來展開讨論。
對輸入進行零填充(padding)允許我們對核的寬度和輸出的大小進行獨立的控制。如果沒有零填充,我們就被迫面臨二選一的局面,要麼選擇網絡空間寬度的快速縮減,要麼選擇一個小型的核。
這兩種情境都會極大得限制網絡的表示能力。
筆者插入:使用零值進行填充是對原始輸入矩陣影響最小的插值,對于卷積運算來說,即使邊界處的padding部分為零,原始的輸入部分仍然能産生一定影響。但是如果插入均值或者max值等,會對原始輸入造成不可預期的影響。
padding填充的方式主要有以下幾類:
1. 有效(valid)卷積
valid是不使用零填充的極端情況,卷積核隻允許通路那些圖像中能夠完全包含整個核的位置。這種方式的結果就是,表示的寬度在每一層就會縮減。
如果輸入的圖像寬度是 m,核的寬度是 k,那麼輸出的寬度就會變成 m − k + 1。如果卷積核非常大的話縮減率會非常顯著。
因為每次卷積後都會導緻尺寸縮減,這限制了網絡中能夠包含的卷積層的層數。當層數增加時,網絡的空間次元最終會縮減到 1 × 1,這種情況下增加的層就不可能進行有意義的卷積了,是以在實際項目開發中,這種padding方式很少用到。
2. 相同(same)卷積
這種padding方式進行足夠的零填充來保持輸出和輸入具有相同的大小,卷積核從輸入矩陣的邊界開始掃描。
在這種情況下,隻要硬體支援,網絡就能包含任意多的卷積層,這是因為卷積運算不改變下一層的結構。
然而,輸入像素中靠近邊界的部分相比于中間部分對于輸出像素的影響更小。這可能會導緻邊界像素存在一定程度的欠表示。
3. 全(full)卷積
橙色部分為image, 藍色部分為filter。full模式的意思是,從filter和image剛相交開始做卷積,白色部分為填0。filter的運動範圍如圖所示。
full pad 進行了足夠多的零填充使得每個像素在每個方向上恰好被通路了 k 次,最終輸出圖像 的寬度為 m + k − 1。在這種情況下,輸出像素中靠近邊界的部分相比于中間部分是更少輸入像素的函數。
0x5:LocallyConnect(局部連接配接)
在一些情況下,我們并不是真的想使用卷積,而是想用一些局部連接配接的網絡層。
在這種情況下,我們的多層感覺機(卷積神經網絡)對應的鄰接矩陣(網絡結構)是相同的, 但每一個連接配接都有它自己的獨立權重,用一個 6 維的張量 W 來表示。
W 的索引分别是: 輸出的通道 i,輸出的行 j 和列 k,輸入的通道 l,輸入的行偏置 m 和列偏置 n。局部連接配接層的線性部分可以表示為:
這有時也被稱為非共享卷積(unshared convolution),因為它和具有一個小核的離散卷積運算很像,但并不橫跨位置來共享參數。
keras上的文檔如下:
The LocallyConnected1D layer works similarly to the Conv1D layer, except that weights are unshared, that is, a different set of filters is applied at each different patch of the input.
下圖比較了局部連接配接、卷積、和全連接配接的差別:
上圖(局部連接配接):
每一小片(接受域)有兩個像素的局部連接配接層。每條邊用唯一的字母标記,來顯示每條邊都有自身的權重參數。
中圖(小size卷積核):
核寬度為兩個像素的卷積層。該模型與局部連接配接層具有完全相同的連接配接。差別不在于哪些單元互相互動,而在于如何共享參數。 局部連接配接層沒有參數共享。正如用于标記每條邊的字母重複出現所訓示的,卷積層在整個輸入上重複使用相同的兩個權重。
下圖(全連接配接)
全連接配接層類似于局部連接配接層,它的每條邊都有其自身的參數。然而,它不具有局部連接配接層的連接配接受限(即在特定局部内輸入僅和特定輸入有關)的特征。
對少量通道間的連接配接進行模組化允許網絡使用更少的參數,這降低了存儲的消耗以及提高了統計效率,并且減少了前向和反向傳播所需要的計算量。但是這些目标的實作并沒有減少隐藏單元的數目。
從假設先驗的角度來看這個問題:全連接配接相當于無任何先驗假設;局部連接配接相當于弱假設先驗;卷積核是一種非常強的假設先驗,它極大限制了網絡中權重參數的機率分布。
0x6:平鋪卷積(tiled convolution)
平鋪卷積可以了解為對卷積層和局部連接配接層進行了折衷。
平鋪卷積學習一組核使得當我們在空間移動時它們可以循環利用。這意味着在近鄰的位置上擁有不同的過濾器,就像局部連接配接層一樣,但是對于這些參數的存儲需求僅僅 會增長常數倍,這個常數就是核的集合的大小,而不是整個輸出的特征映射的大小。
下圖對局部連接配接層、平鋪卷積、和标準卷積進行了比較。
上圖(局部連接配接):
局部連接配接層根本沒有共享參數。我們對每個連接配接使用唯一的字母标記,來表明每個連接配接都有它自身的權重。
中圖(平鋪卷積):
平鋪卷積有 t 個不同的核。這裡我們說明 t = 2 的情況。其中一個 核具有标記為 “a’’ 和 “b’’ 的邊,而另一個具有标記為 “c’’ 和 “d’’的邊。每當我們在輸出中右移一 個像素後,我們使用一個不同的核。這意味着,與局部連接配接層類似,輸出中的相鄰單元具有不同的參數。與局部連接配接層不同的是,在我們周遊所有可用的 t 個核之後,我們循環回到了第一個核。如果兩個輸出單元間隔 t 個步長的倍數,則它們共享參數。
下圖(标準卷積):
傳統卷積等效于 t = 1 的平鋪卷積,它是一個特殊的平鋪卷積。 它隻有一個核,并且被應用到各個地方,我們在圖中表示為在各處使用具有标記為 “a’’ 和 “b’’ 的 邊的核。
對于局部連接配接層、平鋪卷積和标準卷積這三種卷積方法來說。當使用相同大小的核時,這三種方法在單元之間具有相同的連接配接,三種方法之間的差別在于它們如何共享參數。或者說對标準卷積網絡施加的先驗假設有多強。從這個角度來看,卷積核的本質也是一種正則化懲罰技術。
筆者插入:局部連接配接層與平鋪卷積層都和最大池化有一些有趣的關聯,這些層的探測單元都是由不同的過濾器驅動的。如果這些過濾器能夠學會探測相同隐含特征的不同變換形式,那麼最大池化的單元對于學得的變換就具有不變性(例如對minist手寫識别中不同方向的數字)。卷積層對于平移具有内置的不變性。
https://blog.csdn.net/leviopku/article/details/80327478
https://keras.io/layers/local/
3. CNN的訓練運算過程
卷積網絡在本質上是一種輸入到輸出的映射,它能夠學習大量的輸入與輸出之間的映射關系,而不需要任何輸入和輸出之間的精确的數學表達式,隻要用已知的模式對卷積網絡加以訓練,網絡就具有輸入輸出對之間的映射能力。卷積網絡執行的是有導師訓練,是以其樣本集是由形如: (輸入向量,理想輸出向量)的向量對構成的。所有這些向量對,都應該是來源于網絡即将模拟的系統的實際"運作"結果。它們可以是從實際運作系統中采集來的。
我們在這個章節會針對卷積神經網絡訓練過程中,涉及到的技巧和計算公式展開讨論。
0x1:網絡權重一定要随機初始化 - 打破網絡結構對稱性
其實不單限于卷積神經網絡,所有深度神經網絡都有一樣的要求,即權重的初始化一定要随機化,不單是不能指派為0,即使不是0,全相同也不行。
以一個三層網絡為例,首先看下結構:
網絡最終輸出的激活函數表達式為:
1. 避免神經網絡退化為單神經元網絡
從公式中可以很清楚看到,如果神經網絡的權值被初始化為同樣,則在多層網絡中,從第二層開始,每一層的輸入值都是相同的。因為第二層的每個神經元都是基于第一層的輸入和目前神經元的權重計算得到。再往後傳遞,之後的隐層的計算結果也是相同的。
直覺上了解,整個神經網絡退化成了一個“由單神經元感覺機組成的層次網絡”,這種網絡的表達能力和邏輯回歸相比并沒有明顯的優勢。
2. 讓不同神經元能朝不同梯度方向優化權重
可能有人會說,神經網絡的訓練過程會不斷調整神經元的權重參數的!我們接下來看看将權重初始化為相同值,會對BP過程造成什麼影響。
BP過程中,偏置項和權重項的疊代的偏導數計算公式如:
梯度的計算公式:
把後兩個公式代入,可以看出所得到的梯度下降法的偏導相同,不停的疊代,不停的相同,不停的疊代,不停的相同,最後整個神經網絡就得到了相同的值(權重和截距)。
正确的随機初始化形式應該是,np.random.randn((i,k))*0.01,後面乘以一個很小的數很重要。我們希望随機初始化的數字非常小,但絕不能為0,這樣經過激活函數(例如tanh)後,會得到一個正常的值,如果w很大,那麼z=wx+b這個形式,會落在函數的兩端,這樣産生的後果就是梯度非常小,如果層比較深,那麼梯度下降就非常慢,這樣就會導緻學習收斂很慢。
3. 舉一個生活化的例子
想象一下,有人把你從直升機上扔到一個未知的山頂,而你卻被困在那裡。到處都是霧。你唯一知道的就是你應該以某種方式降低到海平面。你應該走哪個方向才能到達最低的可能點?
如果你找不到一條通往海平面的路,直升機會再次帶你去,然後把你送到同一個山頂。你将不得不再次采取同樣的方向,因為你是在“初始化”自己到相同的位置。然而,每次直升機把你随機抛到山上的某個地方,你就會采取不同的方向和步驟。是以,你有更好的機會達到最低的可能點。
這就是打破對稱性的意思。初始化是不對稱的,是以您可以找到相同問題的不同解決方案。
筆者插入:從某種程度上來說,權重随機初始化也是提高了卷積神經網絡泛化能力的一個因素,随機化帶來了對同一個對象不同感覺視角,也驅動卷積網絡具備平移不變性的的能力。
https://www.jianshu.com/p/f2b7db6ca08f
https://blog.csdn.net/ljp1919/article/details/79948742
https://baijiahao.baidu.com/s?id=1619021789208371948&wfr=spider&for=pc
https://blog.csdn.net/marsggbo/article/details/77771497
https://www.zhihu.com/question/36068411?sort=created
http://wemedia.ifeng.com/72622468/wemedia.shtml
https://cloud.tencent.com/developer/ask/111843
0x2: CNN的反向傳播BP計算
要将BP反向傳播算法到CNN,有幾個問題需要解決
1. 池化層沒有激活函數,我們可以令池化層的激活函數為σ(z)=z,即激活後就是自己本身。這樣池化層激活函數的導數為1;
2. 池化層在前向傳播的時候,對輸入進行了壓縮,那麼我們現在需要向前反向推導δl−1,這個推導方法和DNN完全不同;
3. 卷積層是通過張量卷積,或者說若幹個矩陣(每個特征映射對應一個卷積矩陣)卷積求和而得的目前層的輸出,這和DNN很不相同,DNN的全連接配接層是直接進行矩陣乘法得到目前層的輸出。這樣在卷積層反向傳播的時候,上一層的δl−1遞推計算方法肯定有所不同;
4. 對于卷積層,由于W使用的運算是卷積,那麼從δl推導出該層的所有卷積核的W,b的方式也不同;
在研究過程中,需要注意的是,由于卷積層可以有多個卷積核,各個卷積核的處理方法是完全相同且獨立的,為了簡化算法公式的複雜度,我們下面提到卷積核都是卷積層中若幹卷積核中的一個。
1. 已知池化層的 δl,推導上一隐藏層的 δl−1
在前向傳播算法時,池化層一般我們會用Max Pooling或者Average Poolig對輸入進行池化,池化的區域大小已知。現在我們反過來,要從縮小後的誤差δl,還原前一次較大區域對應的誤差。
在反向傳播時,我們首先會把δl的所有子矩陣矩陣大小還原成池化之前的大小:
1. 如果是Max Pooling,則把 δl 的所有子矩陣的各個池化局域的值放在之前做前向傳播算法得到最大值的位置(前向傳播時需要存儲池化前位置資訊);
2. 如果是Average Pooling,則把 δl 的所有子矩陣的各個池化局域的值取平均後放在還原後的子矩陣位置。這個過程一般叫做Upsample;
3. 其他池化技術也是類似的情況,值得注意的是,池化是一種資訊壓縮技術,被壓縮後的技術理論上是不可能100%還原回來的,是以針對池化的反向傳播也必然存在資訊丢失;
舉一個實際的例子來說明池化的反向傳播,假設我們的池化區域大小是2x2。δl 的第k個子矩陣為
由于池化區域為2x2,我們先講δlk做還原,即變成:
如果是MAX Pooling,假設我們之前在前向傳播時記錄的最大值位置分别是左上,右下,右上,左下,則轉換後的矩陣為
如果是Average,則進行平均:反推轉換後的矩陣為
這樣我們就得到了上一層
的值(針對目前層的誤差上采(誤差反向傳遞)樣到上一層的W和b對激活值a的影響)
,可以看到,從公式形式上,和普通DNN網絡的梯度導數乘以神經元權重值的形式是一樣的。
其中,upsample函數完成了池化誤差矩陣放大與誤差重新配置設定的邏輯(注意這裡和DNN不同,DNN是直接全連接配接了,不存在誤差重新配置設定的問題,DNN隻有誤差傳遞的問題)。
2. 已知卷積層的 δl,推導上一隐藏層的 δl−1(卷積核的反向傳播)
卷積層的前向傳播公式:
我們知道 δl−1 和 δl 的遞推關系為:
是以要推導出δl−1和δl的遞推關系,必須計算
的梯度表達式。注意到 zl 和 zl−1 的關系為:
是以我們有:
這裡的式子其實和DNN的類似(大體上都和激活函數的導數有關),差別在于對于含有卷積的式子求導時,卷積核被旋轉了180度。即式子中的rot180,翻轉180度的意思是上下翻轉一次,接着左右翻轉一次。
我們從卷積矩陣的視角來解釋一下這個公式的由來,假設我們 l−1 層的輸出 al−1 是一個3x3矩陣,第 l 層的卷積核 Wl 是一個2x2矩陣,采用1像素的步幅,則輸出zl是一個3x3的矩陣。我們簡化bl都是0,則有
我們列出a,W,za,W,z的矩陣表達式如下:
利用卷積的定義,很容易得出:
接着我們模拟反向求導:
從上式可以看出,對于 al−1 的梯度誤差 ∇al−1,等于第 l 層的梯度誤差乘以 ∂zl,而 ∂zl 對應上面的例子中相關聯的w的值。
假設我們的z矩陣對應的反向傳播誤差是【δ11,δ12,δ21,δ22】組成的2x2矩陣,則利用上面梯度的式子和4個等式,我們可以分别寫出∇al−1的9個标量的梯度。
比如對于a11的梯度,由于在4個等式中a11隻和z11有乘積關系,進而我們有:
對于a12的梯度,由于在4個等式中a12和z12,z11有乘積關系,進而我們有:
同樣的道理我們得到:
這上面9個式子其實可以用一個矩陣卷積的形式表示,即:
為了符合梯度計算,我們在誤差矩陣周圍填充了一圈0,此時我們将卷積核翻轉後和反向傳播的梯度誤差進行卷積,就得到了前一次的梯度誤差。
這個例子直覺的介紹了為什麼對含有卷積的式子求導時,卷積核要翻轉180度的原因。即卷積運算的BP計算也可以用卷積運算。
3. 已知卷積層的 δl,推導該層的 W,b 的梯度
得到了每一層的梯度誤差δl後,接下要需要求卷積核内部的權重 w,b 的權重梯度和誤差。
注意到卷積層 z 和 W,b 的關系為:
而對于b,則稍微有些特殊,因為 δl 是三維張量,而 b 隻是一個向量,不能像DNN那樣直接和 δl 相等。通常的做法是将 δl 的各個子矩陣的項分别求和,得到一個誤差向量,即為b的梯度
0x3: CNN BP算法過程
輸入:
1. m個圖檔樣本;
2. CNN模型的層數L和所有隐藏層的類型;
3. 定義卷積核的大小K,卷積核子矩陣的次元F,填充大小P,步幅S;
4. 定義激活函數(輸出層除外)和各層的神經元個數;
5. 定義梯度疊代參數疊代步長α,最大疊代次數MAX與停止疊代門檻值ϵ;
輸出:CNN模型各隐藏層與輸出層的W,b:
http://cogprints.org/5869/1/cnn_tutorial.pdf
http://www.cnblogs.com/nsnow/p/4562363.html
http://www.cnblogs.com/tornadomeet/p/3468450.html
http://www.cnblogs.com/pinard/p/6494810.html
http://www.cnblogs.com/pinard/p/6494810.html
4. CNN結構化輸出
卷積神經網絡的輸出可以是預測分類任務的類标簽、回歸任務的實數值、或者是高維的結構化對象。這取決于網絡的最後一層如何設計。
0x1:輸出預測分類任務的類标簽或類機率
例如, 模型可以産生張量 S,其中 Si,j,k 是網絡的輸入像素 (j, k) 屬于類 i 的機率。這允許模型标記圖像中的每個像素,并繪制沿着單個對象輪廓的精确掩模。這可以通過sigmoid實作。
如果我們希望更進一步,直接讓網絡輸出唯一的label判斷結果,可以使用softmax作為最後一層的激活函數。
0x2:輸出回歸任務的實數值
0x3:輸出高維結構化對象
所謂高維結構化對象,可以簡單了解為傳統CNN網絡去掉最後的softmax/sigmoid層,直接輸出中間的隐層的權重向量。通常這個對象隻是一個張量,由标準卷積層産生。
5. 反卷積(deconvolution)
簡答來說,逆卷積相對于卷積在神經網絡結構的正向和反向傳播中做相反的運算。
反卷積的應用非常廣泛,涉及到visualization、pixel-wiseprediction、unsupervised learning、image generation、GAN中的Generative圖檔生成都會用到deconv的結構。
0x1:反卷積運算數學公式
假設4x4的輸入,卷積Kernel為3x3, 沒有Padding / Stride, 則輸出為2x2。
輸入矩陣可展開為16維向量,記作
;
輸出矩陣可展開為4維向量,記作
卷積運算可表示為
我們知道,卷積本質是一種線性運算,是以可以表示成矩陣乘法的形式,我們将卷積核的掃描過程展開為一個稀疏矩陣的形式,就可以借助矩陣的數學計算工具來計算卷積網絡的前饋和BP過程。
神經網絡中的正向傳播轉換成如下矩陣運算:
接下來讨論反卷積,即網絡的損失對輸入x的梯度:
可以看到,所謂逆卷積其實就是正向時左乘
,而反向時左乘
,即
的運算。
0x2: 可視化卷積網絡的濾波器權重參數
我們将利用Keras觀察CNN到底在學些什麼,它是如何了解我們送入的訓練圖檔的。我們将使用Keras來對濾波器的激活值進行可視化。本文使用的神經網絡是VGG-16,資料集為ImageNet
1. 在Keras中定義VGG網絡的結構
from keras.models import Sequential
from keras.layers import Convolution2D, ZeroPadding2D, MaxPooling2D
img_width, img_height = 128, 128
# build the VGG16 network
model = Sequential()
model.add(ZeroPadding2D((1, 1), batch_input_shape=(1, 3, img_width, img_height)))
first_layer = model.layers[-1]
# this is a placeholder tensor that will contain our generated images
input_img = first_layer.input
# build the rest of the network
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(64, 3, 3, activation='relu', name='conv1_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(128, 3, 3, activation='relu', name='conv2_2'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(256, 3, 3, activation='relu', name='conv3_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv4_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_1'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_2'))
model.add(ZeroPadding2D((1, 1)))
model.add(Convolution2D(512, 3, 3, activation='relu', name='conv5_3'))
model.add(MaxPooling2D((2, 2), strides=(2, 2)))
# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers])
注意我們不需要全連接配接層,是以網絡就定義到最後一個卷積層為止。
2. 将預訓練好的權重載入模型
這麼做的目的是讓我們更直覺地觀察到卷積網絡的模版,因為随着CNN的訓練逐漸拟合,CNN各層的w和b會使得各層的神經元逐漸有意義,即表現出逐層提取圖像特征的特性,跳過訓練過程,直接觀察一個已經訓練好的模型,能夠讓我們從直覺上更加了解CNN的卷積神經元究竟在學習什麼。
# -*- coding: utf-8 -*-
from __future__ import print_function
from scipy.misc import imsave
import numpy as np
import time
from keras.applications import vgg16
from keras import backend as K
# dimensions of the generated pictures for each filter.
img_width = 128
img_height = 128
# the name of the layer we want to visualize
# (see model definition at keras/applications/vgg16.py)
layer_name = 'block5_conv1'
# util function to convert a tensor into a valid image
def deprocess_image(x):
# normalize tensor: center on 0., ensure std is 0.1
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1
# clip to [0, 1]
x += 0.5
x = np.clip(x, 0, 1)
# convert to RGB array
x *= 255
if K.image_data_format() == 'channels_first':
x = x.transpose((1, 2, 0))
x = np.clip(x, 0, 255).astype('uint8')
return x
# build the VGG16 network with ImageNet weights
model = vgg16.VGG16(weights='imagenet', include_top=False)
print('Model loaded.')
model.summary()
# this is the placeholder for the input images
input_img = model.input
# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers[1:]])
def normalize(x):
# utility function to normalize a tensor by its L2 norm
return x / (K.sqrt(K.mean(K.square(x))) + 1e-5)
kept_filters = []
for filter_index in range(0, 200):
# we only scan through the first 200 filters,
# but there are actually 512 of them
print('Processing filter %d' % filter_index)
start_time = time.time()
# we build a loss function that maximizes the activation
# of the nth filter of the layer considered
# 定義一個損失函數,這個損失函數将用于最大化某個指定濾波器的激活值。以該函數為優化目标優化後,我們可以真正看一下使得這個濾波器激活的究竟是些什麼東西
layer_output = layer_dict[layer_name].output
if K.image_data_format() == 'channels_first':
loss = K.mean(layer_output[:, filter_index, :, :])
else:
loss = K.mean(layer_output[:, :, :, filter_index])
# we compute the gradient of the input picture wrt this loss
grads = K.gradients(loss, input_img)[0]
# normalization trick: we normalize the gradient
# 計算出來的梯度進行了正規化,使得梯度不會過小或過大。這種正規化能夠使梯度上升的過程平滑進行
grads = normalize(grads)
# this function returns the loss and grads given the input picture
iterate = K.function([input_img], [loss, grads])
# step size for gradient ascent
step = 1.
# we start from a gray image with some random noise
if K.image_data_format() == 'channels_first':
input_img_data = np.random.random((1, 3, img_width, img_height))
else:
input_img_data = np.random.random((1, img_width, img_height, 3))
input_img_data = (input_img_data - 0.5) * 20 + 128
# we run gradient ascent for 20 steps
# 根據剛剛定義的函數,現在可以對某個濾波器的激活值進行梯度上升,這裡是梯度下降的逆向應用,即将目前圖像像素點朝着梯度的方向去"增強",讓圖像的像素點反過來和梯度方向去拟合
for i in range(20):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
print('Current loss value:', loss_value)
if loss_value <= 0.:
# some filters get stuck to 0, we can skip them
break
# decode the resulting input image
if loss_value > 0:
img = deprocess_image(input_img_data[0])
kept_filters.append((img, loss_value))
end_time = time.time()
print('Filter %d processed in %ds' % (filter_index, end_time - start_time))
# we will stich the best 64 filters on a 8 x 8 grid.
n = 8
# the filters that have the highest loss are assumed to be better-looking.
# we will only keep the top 64 filters.
kept_filters.sort(key=lambda x: x[1], reverse=True)
kept_filters = kept_filters[:n * n]
# build a black picture with enough space for
# our 8 x 8 filters of size 128 x 128, with a 5px margin in between
margin = 5
width = n * img_width + (n - 1) * margin
height = n * img_height + (n - 1) * margin
stitched_filters = np.zeros((width, height, 3))
# fill the picture with our saved filters
for i in range(n):
for j in range(n):
img, loss = kept_filters[i * n + j]
stitched_filters[(img_width + margin) * i: (img_width + margin) * i + img_width,
(img_height + margin) * j: (img_height + margin) * j + img_height, :] = img
# save the result to disk
imsave('stitched_filters_%dx%d.png' % (n, n), stitched_filters)
3. 可視化每一層的濾波器
下面我們可視化一下各個層的各個濾波器結果,看看CNN是如何對輸入進行逐層分解的
1. 第一層的濾波器主要完成方向、顔色的編碼,這些顔色和方向與基本的紋理組合,逐漸生成複雜的形狀
2. 可以觀察到,很多濾波器的内容其實是一樣的,隻不過旋轉了一個随機的的角度(如90度)而已。這意味着我們可以通過使得卷積濾波器具有旋轉不變性而顯著減少濾波器的數目
3. 這種旋轉的性質在高層的濾波器中仍然可以被觀察到。如Conv4_1
可以将每層的濾波器想為基向量,這些基向量一般是過完備的。基向量可以将層的輸入緊湊的編碼出來。濾波器随着其利用的空域資訊的拓寬而更加精細和複雜,每一層的CNN隐藏層将上一層的輸出進行整合,輸出一種抽象度更高(更貼近實體圖像的模版),可以想象,越是後面的CNN層,越是能看到有具體意義的"模糊圖像"
0x3: Network Visualization
下面展示的例子截圖可以讓我們更直覺的了解,濾波器的”模版特性“。
1. 1-layer:
input (32x32x3)
conv (32x32x16)
pool (16x16x16)
可以看到,第一層的卷積層(conv+relu)的激活函數輸出值對圖像生成了最抽象的一層"模版",将船的大緻輪廓勾勒了出來
2. 2-layer
conv (16x16x20)
relu (16x16x20)
pool (8x8x20)
可以看到如果不進行padding操作,conv越到後的層圖形被縮放的越小,同時特化也越來越明顯,卷積核開始更關注各自獨立的特征模版
從這個例子中可以看出,神經網絡了解了如何将輸入空間解耦為分層次的卷積濾波器組,同時,神經網絡了解了從一系列濾波器的組合到一系列特定标簽的機率映射。
https://gist.github.com/baraldilorenzo/07d7802847aaad0a35d3
http://cs.stanford.edu/people/karpathy/convnetjs/demo/cifar10.html
https://github.com/fchollet/keras/blob/master/examples/conv_filter_visualization.py
https://keras-cn.readthedocs.io/en/latest/blog/cnn_see_world/
https://www.zhihu.com/question/43609045