CNN剖析:如果你願意一層一層剝開CNN的心
機器不學習 www.jqbxx.com : 深度聚合機器學習、深度學習演算法及技術實戰
如果你願意一層一層剝開CNN的心——你會明白它究竟在做什麼
一直以來,卷積神經網路對人們來說都是一個黑箱,我們只知道它識別圖片準確率很驚人,但是具體是怎麼做到的,它究竟使用了什麼特徵來分辨影象,我們一無所知。無數的學者、研究人員都想弄清楚CNN內部運作的機制,甚至試圖找到卷積神經網路和生物神經網路的聯絡。2013年,紐約大學的Matthew Zeiler和Rob Fergus的論文Visualizing and Understanding Convolutional Neural Networks用視覺化的方法揭示了CNN的每一層識別出了什麼特徵,也揭開了CNN內部的神祕面紗。之後,也有越來越多的學者使用各種方法將CNN的每一層的啟用值、filters等等視覺化,讓我們從各個方面瞭解到CNN內部的祕密。
今天這篇文章,將會帶大家從多個角度看看CNN各層的功能。
一、CNN每一層都輸出了什麼玩意兒
這個是最直接瞭解CNN每一層的方法,給一張圖片,經過每一個卷積層,圖片到底變成了啥。
這裡,我用Keras直接匯入VGG19這個網路,然後我自己上傳一張照片,讓這個照片從VGG中走一遭,同時記錄每一層的輸出,然後把這個輸出畫出來。
先引入必要的包:
import keras
from keras.applications.vgg19 import VGG19
from keras.preprocessing import image
from keras.applications.vgg19 import preprocess_input
from keras.models import Model
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
現在引入把我男神的圖片上傳一下,用keras的圖片處理工具把它處理成可以直接丟進網路的形式:
img_path = 'andrew.jpg'
img = image.load_img(img_path, target_size=(200, 300))
plt.imshow(img)
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
x.shape
我輸入的影象:
然後,我們匯入VGG模型,去掉FC層(就是把include_top設為FALSE),因為如果有FC層在的話,由於FC層神經元個數是固定的,所以網路的輸入形狀就有限制,就必須跟原來的網路的輸入一模一樣。但是卷積層不受輸入形狀的限制,因此我們只保留卷積層(和池化層)。
VGG19有19個CONV或FC層,但是如果我們打印出所有層的話,會包括POOL層,所以不止19個。這裡我取第2~20層的輸出,作為我們研究的物件:
base_model = VGG19(weights='imagenet',include_top=False)
# 獲取各層的輸出:
layer_outputs = [layer.output for layer in base_model.layers[2:20]]
# 獲取各層的名稱:
layer_names = []
for layer in base_model.layers[2:20]:
layer_names.append(layer.name)
print(layer_names)
注意,這裡的輸出還沒有實際的值!只是一個殼子,當我們把圖片輸入到模型中之後,它才有值。
然後我們組裝我們新的模型:輸入圖片,同時輸出各層的啟用值:
# 組裝模型:
model = Model(inputs=base_model.input, outputs=layer_outputs)
# 將前面的圖片資料x,輸入到model中,得到各層的啟用值activations:
activations = model.predict(x)
就這麼easy!(如果不太明白程式碼的含義,可以參見Keras文件。)
這個activations裡面,就裝好了各層的所有的啟用值。我們可以隨便找一層的activation打印出來它的形狀看看:
print(activations[0].shape)
#輸出:
#(1, 200, 300, 64)
什麼意思呢?
1,代表輸入圖片的個數,我們這裡只輸入了一個圖片,所以是1;
200,300,代表圖片的大小;
64,代表該層有多少個filters。 所以,相當於我們的這一層輸出了64張單通道圖片。
好了,我們可以將每一層啟用得到的圖片打印出來看看了。 我們將每一層所有filters對應的圖片拼在一起顯示,程式碼如下:
import math
for activation,layer_name in zip(activations,layer_names):
h = activation.shape[1]
w = activation.shape[2]
num_channels = activation.shape[3]
cols = 16
rows = math.ceil(num_channels/cols)
img_grid = np.zeros((h*rows,w*cols))
for c in range(num_channels):
f_r = math.ceil((c+1)/cols)
f_c = (c+1)if f_r==1 else (c+1-(f_r-1)*cols)
img_grid[(f_r-1)*h:f_r*h,(f_c-1)*w:f_c*w ] = activation[0,:,:,c]
plt.figure(figsize=(25,25))
plt.imshow(img_grid, aspect='equal',cmap='viridis')
plt.grid(False)
plt.title(layer_name,fontsize=16)
plt.show()
這個程式碼感覺寫的不大好。。。如果讀者有更好的方法,也請麻煩告知。
最後是輸出了18張大圖,由於版面限制,我這裡就挑其中的一些來展示:
這個是很靠前的一層(block1_conv2):
可以看到,裡面很多圖片都跟我們的輸入圖片很像。 如果我們放大仔細觀察的話,比如:
可以發現,很多圖片都是把原圖片的 邊緣勾勒了出來。因此,我們知道,該層主要的功能是邊緣檢測。
這裡再說一下我們分析的思路:
根據前面講解的CNN的原理,我們知道,當filter和我們的原影象的對應部分越像,它們卷積的結果就會越大,因此輸出的畫素點就越亮!因此,我們可以通過分析輸出圖片哪些部分比較亮來得知,該層的filters的作用。
所以,其實該層不光是“邊緣檢測”,還有一個功能——“顏色檢測”。因為我還發現了很多這樣的圖片:
這些圖中的 高亮部分,都對應於原圖片中的整塊的顏色,因此我們可以推斷 該層的部分filters具有檢測顏色的功能。
很有意思~
我們接著看中間的某一層(block2_conv2):
還是放大看一看:
這一層似乎複雜了很多,因為我們搞不清楚這些高亮的部分是一種什麼特徵,似乎是某種紋路。因此,和前面那個很淺的層相比,這一層提取的特徵就沒那麼直白了。
我們接著再看一個很深的層:
(圖太大,我擷取部分)
這大概是VGG的第十幾層吧,由於經過反覆的卷積,圖片大小會縮小,因此越來越“畫素化”,這個時候,我們可以把這些啟用圖片,跟原圖片去對比,看看原圖片哪些部分被激活了:
從這個圖可以看到,Andrew整個上半身都被激活了。 再看看這個:
Andrew的 手部被激活了。 更多的例子等大家自己去嘗試。 我們由此可以合理的推測,該層,已經可以將一些較複雜的東西作為特徵來識別了,比如“手”、“身體”等等。這些特徵比前面淺層的“邊緣”、“顏色”等特徵高階了不少。
為了讓大家更全面地看到各層的狀態, 我從每層中調了一張圖片排在一起打印出來:
綜上:
隨著CNN的層數增加,每一層的輸出影象越來越抽象,這意味著我們的filters在變得越來越複雜;我們可以很合理地推斷,隨著CNN的深入,網路層學得的特徵越來越高階和複雜。
二、CNN的每一層的filters到底識別啥特徵
在上面,我們已經知道了每一層的輸出是什麼樣子,並且由此推測每一層的filters越來越複雜。於是,我們就想進一步地探索一下,這些filters,到底在識別些什麼,到底長啥樣?
這裡就有一個大問題: 比如VGG,我們前面講過這是一個十分規則的網路,所有的filter大小都是3×3。這麼小的玩意兒,畫出來根本看不出任何貓膩。所有無法像我們上面畫出每一層的啟用值一樣來分析。
那麼怎麼辦呢? 我們依然可以用剛剛的思路來分析:
當輸入圖片與filter越像,他們的卷積輸出就會越大。因此,給CNN喂入大量的圖片,看看哪個的輸出最大。但這樣可行度不高,可以換個思路:我們可以直接輸入一個噪音圖片,用類似梯度下降的方法來不斷更新這個圖片,使得我們的輸出結果不斷增大,那麼這個圖片就一定程度上反映了filter的模樣。
這裡實際上不是用 梯度下降,而是用 梯度上升,因為我們要求的是一個極大值問題,而不是極小值問題。
梯度下降的更新引數w的過程,就是 w-->w-α·dw,其中α是學習率,dw是損失對w的梯度。
梯度上升是類似的,是更新輸入x,更新的方向變了: x-->x+s·dx,其中s代表步長,與α類似,dx是啟用值對x的梯度。
所以,我們可以仿照梯度下降法,來構造梯度上升演算法。 具體方法和程式碼可以參見keras的發明者Fchollet親自寫的教程: visualizing what convnets learn
這裡我展示一下從淺到深的5個卷積層的filters的模樣(注意,這個不是真的filters,而是輸入圖片,因為這個輸入圖片與filters的卷積結果最大化了,所以我們這裡用輸入圖片的模樣來代表filters的模樣):
【預警:圖片可能引起密恐者不適】
block1-conv3:
這一層,印證了我們之前的推斷:這個很靠近輸入的淺層的filters的功能,就是 “邊緣檢測”和“顏色檢測”。
可能還是有同學不大明白,畢竟這個問題我也想了好久,為什麼圖片會這麼密密麻麻的,看的讓人瘮得慌?因為這個不是真的filter!filter大小隻有3×3,而這些圖片的大小都是我們設定的輸入圖片大小150×150,加入我們的某個filter是檢測豎直邊緣,那麼輸入圖片要使卷積的結果最大,必然會到處各個角落都長滿豎直的條條,所以我們看到的圖片都是密密麻麻的某種圖案的堆積。
block2-conv3:
block3-conv3:
到了這一層,我開始看到各種較為 複雜的圖案了,比如螺旋、波浪、方塊、像眼睛一樣的形狀、像屋頂的磚瓦那樣的形狀······因缺思廳~
block4-conv3:
block5-conv3:
到了這個比較深的層,我們發現,圖片的圖案更加複雜了,似乎是 前面那些小圖案組成的大圖案,比如有類似 麻花的形狀,有類似 蜘蛛網的形狀,等等,我們直接說不出來,但是明顯這些filters識別的特徵更加高階了。由於我只選取了部分的filters視覺化,所以這裡看不到更多的圖案,也許把該層的幾百個filters都打印出來,我們可以找到一些像蟲子、手臂等東西的圖案。
同時我們發現,越到深層,圖片這種密密麻麻的程度就會降低,因為越到深層,filters對應於原影象的 視野就會越大,所以特徵圖案的範圍也會越大,因此不會那麼密集了。
另外,如果細心的話,我們可以注意到,越到深層,filters越稀疏,表現在圖中就是像這種失效圖片越來越多:
這些圖片就是純噪音,也就是根本沒有激活出什麼東西。具體原因我還不太清楚,等日後查清楚了再補充。但從另一個側面我們可以理解:越深層,filters的數目往往越多,比如我們這裡的block1-conv3,只有64個filters,但是最後一層block5-conv3有多達512個filters,所以有用的filters必然會更加稀疏一些。
綜上:
我們現在可以明白(剛剛是推斷),CNN的淺層的filters一般會檢測“邊緣”、“顏色”等最初級的特徵,之後,filters可以識別出各種“紋理紋路”,到深層的時候,filters可以檢測出類似“麻花”、“蜘蛛”等等由前面的基礎特徵組成的圖案。
三、更近一步,用Deconvnet視覺化filters
在CNNs視覺化中最有名的的論文當屬我們文首提到的: Matthew D. Zeiler and Rob Fergus:Visualizing and Understanding Convolutional Networks. 無論是吳恩達上深度學習還是李飛飛講計算機視覺,都會引用這個論文裡面的例子,有空推薦大家都去看看這個論文。
我看了好久不太懂,但是寫完上面的“第一部分”之後,我似乎理解了作者的思路。
我們回到我們在“一”中得到的某個深層的啟用值:
然後,我試著把原圖貼上去,看看它們哪些地方重合了:
當時,我們驚喜地發現,“上半身”、“手”、“Ng”被精準地激活了。
而上面那篇論文的作者,正是沿著 “將啟用值與輸入圖片對應”這種思路(我的猜測),利用 Deconvnet這種結構,將啟用值沿著CNN反向對映到輸入空間,並重構輸入影象,從而更加清晰明白地知道filters到底識別出了什麼。 可以說,這個思路,正式我們上面介紹的“一”、“二”的結合!
我畫一個草圖來說明:
我這個草圖相當地“草”,只是示意一下。 具體的方法,其實是將原來的CNN的順序完全反過來,但是元件不變(即filters、POOL等等都不變),
如 原來的順序是: input-->Conv-->relu-->Pool-->Activation
現在就變成了: Activation-->UnPool-->relu-->DeConv-->input
這裡的UnPool和DeConv,是對原來的Pool和conv的逆操作,這裡面的細節請翻閱原論文,對於DeConv這個操作,我還推薦看這個: https://arxiv.org/abs/1603.07285
其實說白了,Conv基本上是把一個大圖(input)通過filter變成了小圖(activation),DeConv就反過來,從小圖(activation)通過filter的轉置再變回大圖(input):
於是,我們把每一層的啟用值中挑選最大的啟用值,通過Deconvnet傳回去,對映到輸入空間重構輸入影象。這裡,我直接把論文中的結論搬出來給大家看看:
左邊這些灰色的圖案就是我們啟用值通過DeConvnet反向輸出的,右邊的是跟左邊圖案對應的原圖案的區域。
我們可以看出,第一層,filters識別出了各種邊緣和顏色, 第二層識別出了螺旋等各種紋路; 第三層開始識別出輪胎、人的上半身、一排字母等等; 第四層,已經開始識別出狗頭、鳥腿; 第五層城市直接識別出自行車、各種狗類等等完整的物體了!
其實我們發現這個跟我們在“二”中得到的似乎很像,但是 這裡得到的圖案是很具體的,而“二”中得到的各層的圖案很抽象。這是因為,在這裡,我們不是講所有的啟用值都映射回去,而是挑選最突出的某個啟用值來進行對映,而且,在“二”中,我們是從一個噪音影象來生成圖案使得啟用值最大(存在一個訓練的過程),而這裡是直接用某個具體圖片的啟用值傳回去重構圖片,因此是十分具體的。
綜上面的所有之上
CNNs的各層並不是黑箱,每一層都有其特定個功能,分工明確。從淺到深,CNN會逐步提取出邊緣、顏色、紋理、各種形狀的圖案,一直到提取出具體的物體。 也就是說,CNNs在訓練的過程中,自動的提取了我們的任務所需要的各種特徵:
這些特徵,越在淺層,越是普遍和通用;
越在深層,就越接近我們的實際任務場景。
因此,我們可以利用以及訓練好的CNNs來進行 遷移學習(transfer learning),也就是直接使用CNNs已經訓練好的那些filters(特徵提取器),來提取我們自己資料集的特徵,然後就可以很容易地實現分類、預測等等目的。
參考資料:
1.Matthew D. Zeiler and Rob Fergus:Visualizing and Understanding Convolutional Networks.
2.A guide to convolution arithmetic for deep learning
3.Visualizing what convnets learn