機器不學習:如果你願意一層一層剝開CNN的心

機器不學習 www.jqbxx.com : 深度聚合機器學習、深度學習算法及技術實戰

如果你願意一層一層剝開CNN的心——你會明白它究竟在做什麼

機器不學習:如果你願意一層一層剝開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

我輸入的圖像:

機器不學習:如果你願意一層一層剝開CNN的心

然後,我們導入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的心

可以看到,裡面很多圖片都跟我們的輸入圖片很像。 如果我們放大仔細觀察的話,比如:

機器不學習:如果你願意一層一層剝開CNN的心

可以發現,很多圖片都是把原圖片的 邊緣勾勒了出來。因此,我們知道,該層主要的功能是邊緣檢測。

這裡再說一下我們分析的思路:

根據前面講解的CNN的原理,我們知道,當filter和我們的原圖像的對應部分越像,它們卷積的結果就會越大,因此輸出的像素點就越亮!因此,我們可以通過分析輸出圖片哪些部分比較亮來得知,該層的filters的作用。

所以,其實該層不光是“邊緣檢測”,還有一個功能——“顏色檢測”。因為我還發現了很多這樣的圖片:

機器不學習:如果你願意一層一層剝開CNN的心

這些圖中的 高亮部分,都對應於原圖片中的整塊的顏色,因此我們可以推斷 該層的部分filters具有檢測顏色的功能。

很有意思~

我們接著看中間的某一層(block2_conv2):

機器不學習:如果你願意一層一層剝開CNN的心

還是放大看一看:

機器不學習:如果你願意一層一層剝開CNN的心

這一層似乎複雜了很多,因為我們搞不清楚這些高亮的部分是一種什麼特徵,似乎是某種紋路。因此,和前面那個很淺的層相比,這一層提取的特徵就沒那麼直白了。

我們接著再看一個很深的層:

(圖太大,我截取部分)

機器不學習:如果你願意一層一層剝開CNN的心

這大概是VGG的第十幾層吧,由於經過反覆的卷積,圖片大小會縮小,因此越來越“像素化”,這個時候,我們可以把這些激活圖片,跟原圖片去對比,看看原圖片哪些部分被激活了:

機器不學習:如果你願意一層一層剝開CNN的心

從這個圖可以看到,Andrew整個上半身都被激活了。 再看看這個:

機器不學習:如果你願意一層一層剝開CNN的心

Andrew的 手部被激活了。 更多的例子等大家自己去嘗試。 我們由此可以合理的推測,該層,已經可以將一些較複雜的東西作為特徵來識別了,比如“手”、“身體”等等。這些特徵比前面淺層的“邊緣”、“顏色”等特徵高級了不少。

為了讓大家更全面地看到各層的狀態, 我從每層中調了一張圖片排在一起打印出來:

機器不學習:如果你願意一層一層剝開CNN的心

綜上:

隨著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:

機器不學習:如果你願意一層一層剝開CNN的心

這一層,印證了我們之前的推斷:這個很靠近輸入的淺層的filters的功能,就是 “邊緣檢測”和“顏色檢測”。

可能還是有同學不大明白,畢竟這個問題我也想了好久,為什麼圖片會這麼密密麻麻的,看的讓人瘮得慌?因為這個不是真的filter!filter大小隻有3×3,而這些圖片的大小都是我們設置的輸入圖片大小150×150,加入我們的某個filter是檢測豎直邊緣,那麼輸入圖片要使卷積的結果最大,必然會到處各個角落都長滿豎直的條條,所以我們看到的圖片都是密密麻麻的某種圖案的堆積。

block2-conv3:

機器不學習:如果你願意一層一層剝開CNN的心

block3-conv3:

機器不學習:如果你願意一層一層剝開CNN的心

到了這一層,我開始看到各種較為 複雜的圖案了,比如螺旋、波浪、方塊、像眼睛一樣的形狀、像屋頂的磚瓦那樣的形狀······因缺思廳~

block4-conv3:

機器不學習:如果你願意一層一層剝開CNN的心

block5-conv3:

機器不學習:如果你願意一層一層剝開CNN的心

到了這個比較深的層,我們發現,圖片的圖案更加複雜了,似乎是 前面那些小圖案組成的大圖案,比如有類似 麻花的形狀,有類似 蜘蛛網的形狀,等等,我們直接說不出來,但是明顯這些filters識別的特徵更加高級了。由於我只選取了部分的filters可視化,所以這裡看不到更多的圖案,也許把該層的幾百個filters都打印出來,我們可以找到一些像蟲子、手臂等東西的圖案。

同時我們發現,越到深層,圖片這種密密麻麻的程度就會降低,因為越到深層,filters對應於原圖像的 視野就會越大,所以特徵圖案的範圍也會越大,因此不會那麼密集了。

另外,如果細心的話,我們可以注意到,越到深層,filters越稀疏,表現在圖中就是像這種失效圖片越來越多:

機器不學習:如果你願意一層一層剝開CNN的心

這些圖片就是純噪音,也就是根本沒有激活出什麼東西。具體原因我還不太清楚,等日後查清楚了再補充。但從另一個側面我們可以理解:越深層,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. 無論是吳恩達上深度學習還是李飛飛講計算機視覺,都會引用這個論文裡面的例子,有空推薦大家都去看看這個論文。

我看了好久不太懂,但是寫完上面的“第一部分”之後,我似乎理解了作者的思路。

我們回到我們在“一”中得到的某個深層的激活值:

機器不學習:如果你願意一層一層剝開CNN的心

然後,我試著把原圖貼上去,看看它們哪些地方重合了:

機器不學習:如果你願意一層一層剝開CNN的心

當時,我們驚喜地發現,“上半身”、“手”、“Ng”被精準地激活了。

而上面那篇論文的作者,正是沿著 “將激活值與輸入圖片對應”這種思路(我的猜測),利用 Deconvnet這種結構,將激活值沿著CNN反向映射到輸入空間,並重構輸入圖像,從而更加清晰明白地知道filters到底識別出了什麼。 可以說,這個思路,正式我們上面介紹的“一”、“二”的結合!

我畫一個草圖來說明:

機器不學習:如果你願意一層一層剝開CNN的心

我這個草圖相當地“草”,只是示意一下。 具體的方法,其實是將原來的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):

機器不學習:如果你願意一層一層剝開CNN的心

於是,我們把每一層的激活值中挑選最大的激活值,通過Deconvnet傳回去,映射到輸入空間重構輸入圖像。這裡,我直接把論文中的結論搬出來給大家看看:

機器不學習:如果你願意一層一層剝開CNN的心

機器不學習:如果你願意一層一層剝開CNN的心

機器不學習:如果你願意一層一層剝開CNN的心

左邊這些灰色的圖案就是我們激活值通過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

轉自:https://zhuanlan.zhihu.com/p/42904109


分享到:


相關文章: