看懂這篇指南,包你掌握神經(jīng)網(wǎng)絡(luò)的“黑匣子”
“人類的神經(jīng)網(wǎng)絡(luò)是如何運(yùn)行的?”這個問題讓很多數(shù)據(jù)科學(xué)家感到困惑。解釋某個簡單神經(jīng)網(wǎng)絡(luò)的工作機(jī)制非常容易,但是當(dāng)某個計算機(jī)視覺項(xiàng)目中的層數(shù)增加1000倍時,該怎么辦呢?
終端用戶想要了解模型是如何得到最終結(jié)果時,僅用紙和筆來解釋深度神經(jīng)網(wǎng)絡(luò)的工作機(jī)制是行不通的。那么,如何讓神經(jīng)網(wǎng)絡(luò)不再像“黑匣子”一樣神秘?
可視化可以做到這一點(diǎn)——將神經(jīng)網(wǎng)絡(luò)的不同特征可視化能使一切變得清晰明了,也能更直觀地呈現(xiàn)卷積神經(jīng)網(wǎng)絡(luò)(CNN) 成千上萬的圖像訓(xùn)練結(jié)果。
本文將介紹卷積神經(jīng)網(wǎng)絡(luò)可視化的不同技術(shù)。此外,我們還將致力于從這些可視化中提取不同看法,以完善卷積神經(jīng)網(wǎng)絡(luò)模型。
注意:本文對神經(jīng)網(wǎng)絡(luò)和卷積神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)知識點(diǎn)將不再進(jìn)行討論。以下三篇文章可幫助你重溫或了解相關(guān)知識點(diǎn)。
- A Comprehensive Tutorial to learn Convolutional Neural Networks from Scratch (從零開始學(xué)習(xí)卷積神經(jīng)網(wǎng)絡(luò)的全面教程):https://www.analyticsvidhya.com/blog/2018/12/guide-convolutional-neural-network-cnn/?utm_source=blog&utm_medium=understanding-visualizing-neural-networks
- An Introductory Guide to Deep Learning and Neural Networks (深度學(xué)習(xí)與神經(jīng)網(wǎng)絡(luò)入門指南):https://www.analyticsvidhya.com/blog/2018/10/introduction-neural-networks-deep-learning/?utm_source=blog&utm_medium=understanding-visualizing-neural-networks
- Fundamentals of Deep Learning – Starting with Artificial Neural Network (深度學(xué)習(xí)的基礎(chǔ)——從人工神經(jīng)網(wǎng)絡(luò)開始):https://www.analyticsvidhya.com/blog/2016/03/introduction-deep-learning-fundamentals-neural-networks/?utm_source=blog&utm_medium=understanding-visualizing-neural-networks
為什么要用可視化解碼神經(jīng)網(wǎng)絡(luò)?
這是一個值得研究的問題。有很多方法可以幫助理解神經(jīng)網(wǎng)絡(luò)的工作原理,為何要轉(zhuǎn)向可視化這條非同尋常的路呢?
通過一個例子來回答這個問題。例如,某個項(xiàng)目需要對雪豹和阿拉伯豹等動物圖像進(jìn)行分類。從直覺上講,可以通過圖像的背景進(jìn)行區(qū)分。
這兩種動物的棲息地截然不同。大多數(shù)雪豹的圖片都以雪為背景,而大多數(shù)阿拉伯豹的圖片背景多為廣闊的沙漠。
那么問題來了:一旦模型開始對雪和沙漠的圖像進(jìn)行分類,如何確保模型已經(jīng)正確學(xué)習(xí)了如何區(qū)分這兩種豹的特征呢?答案就是可視化。
可視化幫助我們理解是什么特征可以引導(dǎo)模型以對圖像進(jìn)行分類。
有很多種方法可以將模型可視化,本文將介紹其中的幾種方法。
建立模型體系結(jié)構(gòu)
學(xué)習(xí)的最好方式是對概念進(jìn)行編碼。因此,本文將直接深入研究Python代碼,提供實(shí)用的編碼指南。
本文使用VGG16體系結(jié)構(gòu),并在ImageNet數(shù)據(jù)集上使用預(yù)先訓(xùn)練的權(quán)重。第一步,將模型導(dǎo)入程序并了解其體系結(jié)構(gòu)。
之后使用Keras中的‘model.summary()’函數(shù)將模型體系結(jié)構(gòu)可視化。這是在進(jìn)入模型構(gòu)建環(huán)節(jié)之前十分關(guān)鍵的一步。因?yàn)樾枰_保輸入和輸出的形狀與問題陳述相匹配,因此需要將模型概述可視化。
- #importing required modules
- from keras.applications import VGG16
- #loading the saved model
- #we are using the complete architecture thus include_top=True
- model = VGG16(weights='imagenet',include_top=True)
- #show the summary of model
- model.summary()
下表即為由上述代碼生成的模型概述。
該表記錄了模型的詳細(xì)架構(gòu)以及每一層可訓(xùn)練參數(shù)的數(shù)量。希望ni可以花一些時間閱讀以上內(nèi)容,并了解我們目前達(dá)到的水平。
只訓(xùn)練模型層的一個子集(特征提取)時,這一點(diǎn)尤為重要。通過生成模型概述,可以確保不可訓(xùn)練參數(shù)的數(shù)量與不想訓(xùn)練的層數(shù)相匹配。
此外,開發(fā)人員可以使用可訓(xùn)練參數(shù)總量來檢查GPU是否能夠分配足夠內(nèi)存來訓(xùn)練模型。對于使用電腦工作的大多數(shù)人來說,這項(xiàng)任務(wù)很常見,但也是一種挑戰(zhàn)。
認(rèn)識卷積神經(jīng)網(wǎng)絡(luò)各層
了解模型的整體架構(gòu)以后,就可以嘗試深入探究神經(jīng)網(wǎng)絡(luò)的每一層了。
事實(shí)上,訪問Keras模型的各層并提取每一層的相關(guān)參數(shù)是非常容易的,這包括權(quán)重和過濾器數(shù)量等其他信息。
首先,創(chuàng)建字典,并將層名稱映射到其相應(yīng)的特征和權(quán)重。
- #creating a mapping of layer name ot layer details
- #we will create a dictionary layers_info which maps a layer name to its charcteristics
- layers_info = {}
- for i in model.layers:
- layers_info[i.name] = i.get_config()
- #here the layer_weights dictionary will map every layer_name to its corresponding weights
- layer_weights = {}
- for i in model.layers:
- layer_weights[i.name] = i.get_weights()
- print(layers_info['block5_conv1'])
上述代碼的輸出結(jié)果如下所示,包含了block5_conv1層的不同參數(shù):
- {'name': 'block5_conv1',
- 'trainable': True,
- 'filters': 512,
- 'kernel_size': (3, 3),
- 'strides': (1, 1),
- 'padding': 'same',
- 'data_format': 'channels_last',
- 'dilation_rate': (1, 1),
- 'activation': 'relu',
- 'use_bias': True,
- 'kernel_initializer': {'class_name': 'VarianceScaling',
- 'config': {'scale': 1.0,
- 'mode': 'fan_avg',
- 'distribution': 'uniform',
- 'seed': None}},
- 'bias_initializer': {'class_name': 'Zeros', 'config': {}},
- 'kernel_regularizer': None,
- 'bias_regularizer': None,
- 'activity_regularizer': None,
- 'kernel_constraint': None,
- 'bias_constraint': None}
’block5_conv1’層的可訓(xùn)練參數(shù)值是真實(shí)的,這意味著之后可以通過進(jìn)一步模型訓(xùn)練來更新權(quán)重。
過濾器——卷積神經(jīng)網(wǎng)絡(luò)構(gòu)件的可視化
過濾器是卷積神經(jīng)網(wǎng)絡(luò)的基本組成部分。如下圖所示,不同的過濾器會從圖像中提取不同類型的特征:
如圖所示,每個卷積層都由多個過濾器組成?;仡櫳弦还?jié)中提到的‘block5_conv1’層的參數(shù)概要顯示了該層含有512個過濾器,確實(shí)是這個道理。
通過下列編碼,可以繪制每VGG16模塊的第一個卷積層的首個過濾器:
- layers = model.layers
- layer_ids = [1,4,7,11,15]
- #plot the filters
- fig,ax = plt.subplots(nrows=1,ncols=5)
- for i in range(5):
- ax[i].imshow(layers[layer_ids[i]].get_weights()[0][:,:,:,0][:,:,0],cmap='gray')
- ax[i].set_title('block'+str(i+1))
- ax[i].set_xticks([])
- ax[i].set_yticks([])
以上輸出結(jié)果即為不同層的過濾器。由于VGG16只使用3×3過濾器,因此所有過濾器形狀大小都相同。
激活最大化——將模型所期望的進(jìn)行可視化
通過下面的圖片來理解最大激活的概念:
在識別大象的過程中,哪些特征比較重要?
下面是一些較容易想到的特征。
- 獠牙
- 象鼻
- 耳朵
這就是人類憑直覺判別大象的方式。但是,使用卷積神經(jīng)網(wǎng)絡(luò)優(yōu)化隨機(jī)圖像,并嘗試將其歸類為大象時,會得到什么結(jié)果呢?
卷積神經(jīng)網(wǎng)絡(luò)中,每個卷積層都在前一層的輸出中尋找相似的模式。當(dāng)輸入包含其正在尋找的模式時,就能實(shí)現(xiàn)最大激活。
在激活最大化技術(shù)中,更新每一層的輸入,使該過程中的損失達(dá)到最小值。
應(yīng)該怎么做呢?首先需要計算激活損失相對于輸入的梯度,并據(jù)此更新輸入。
以下為所述方法的代碼:
- #importing the required modules
- from vis.visualization import visualize_activation
- from vis.utils import utils
- from keras import activations
- from keras import applications
- import matplotlib.pyplot as plt
- %matplotlib inline
- plt.rcParams['figure.figsize'] = (18,6)
- #creating a VGG16 model using fully connected layers also because then we can
- #visualize the patterns for individual category
- from keras.applications import VGG16
- model = VGG16(weights='imagenet',include_top=True)
- #finding out the layer index using layer name
- #the find_layer_idx function accepts the model and name of layer as parameters and return the index of respective layer
- layer_idx = utils.find_layer_idx(model,'predictions')
- #changing the activation of the layer to linear
- model.layers[layer_idx].activation = activations.linear
- #applying modifications to the model
- model = utils.apply_modifications(model)
- #Indian elephant
- img3 = visualize_activation(model,layer_idx,filter_indices=385,max_iter=5000,verbose=True)
- plt.imshow(img3)
示例模型使用對應(yīng)于印度大象類別的隨機(jī)輸入,輸出了以下內(nèi)容:
從圖像中可以看到,模型期望的結(jié)構(gòu)為象牙、大眼睛和象鼻。這些信息可以有效幫助檢查數(shù)據(jù)集的完整性。例如,假設(shè)該模型將關(guān)注的特征理解為背景中的樹木或草叢等其它物體,由于印度象的棲息地中往往含有大量的樹木或草叢,模型就可能產(chǎn)生錯誤。然后,通過最大激活,就會發(fā)現(xiàn)已有的數(shù)據(jù)集可能不足以完成任務(wù),因此需要將生活在不同棲息地的大象圖像添加到訓(xùn)練集中,實(shí)現(xiàn)大象特征的準(zhǔn)確辨別。
遮擋圖——將輸入過程的重要部分可視化
激活最大化主要用于將圖像中模型的期待可視化。而圖像遮擋可以找出圖像中對模型來說至關(guān)重要的部分。
現(xiàn)在,為了理解圖像遮擋的工作原理,我們設(shè)立了一個模型,它能夠根據(jù)豐田、奧迪等制造商對汽車進(jìn)行分類。
能夠判斷圖中汽車屬于哪家公司嗎?一定很難吧。因?yàn)楣緲?biāo)識所在的部分被遮擋了。顯然,圖像中被遮擋部分是辨別汽車所屬廠商時非常重要的線索。
同樣地,為了生成遮擋圖,我們遮擋了圖像中的某些部分,然后計算它屬于某一類的概率。如果概率降低,就意味著遮擋部分對于完成分類非常重要。否則,該部分就無足輕重了。
示例程序?qū)⒏怕释瑘D像每個部分的像素值聯(lián)系起來,對其進(jìn)行標(biāo)準(zhǔn)化后生成熱圖:
- import numpy as np
- from keras.utils import np_utils
- from keras.models import Sequential
- from keras.layers import Dense, Dropout, Flatten, Activation, Conv2D, MaxPooling2D
- from keras.optimizers import Adam
- from keras.callbacks import EarlyStopping, ModelCheckpoint
- from keras.preprocessing.image import ImageDataGenerator
- from keras.activations import relu
- %matplotlib inline
- import matplotlib.pyplot as plt
- def iter_occlusion(image, size=8):
- occlusion = np.full((size * 5, size * 5, 1), [0.5], np.float32)
- occlusion_center = np.full((size, size, 1), [0.5], np.float32)
- occlusion_padding = size * 2
- # print('padding…')
- image_padded = np.pad(image, ( \
- (occlusion_padding, occlusion_padding), (occlusion_padding, occlusion_padding), (0, 0) \
- ), 'constant', constant_values = 0.0)
- for y in range(occlusion_padding, image.shape[0] + occlusion_padding, size):
- for x in range(occlusion_padding, image.shape[1] + occlusion_padding, size):
- tmp = image_padded.copy()
- tmp[y - occlusion_padding:y + occlusion_center.shape[0] + occlusion_padding, \
- x - occlusion_padding:x + occlusion_center.shape[1] + occlusion_padding] \
- = occlusion
- tmp[y:y + occlusion_center.shape[0], x:x + occlusion_center.shape[1]] = occlusion_center
- yield x - occlusion_padding, y - occlusion_padding, \
- tmp[occlusion_padding:tmp.shape[0] - occlusion_padding, occlusion_padding:tmp.shape[1] - occlusion_padding]
上述代碼定義的函數(shù)iter_occlusion能夠生成具有不同遮擋部分的圖像。
現(xiàn)在可以導(dǎo)入圖像并對其進(jìn)行加工:
- from keras.preprocessing.image import load_img
- # load an image from file
- image = load_img('car.jpeg', target_size=(224, 224))
- plt.imshow(image)
- plt.title('ORIGINAL IMAGE')
一共分為三個步驟:
- 對圖像進(jìn)行預(yù)處理
- 計算不同遮擋部分的概率
- 繪制熱圖
- from keras.preprocessing.image import img_to_array
- from keras.applications.vgg16 import preprocess_input
- # convert the image pixels to a numpy array
- image = img_to_array(image)
- # reshape data for the model
- imageimage = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
- # prepare the image for the VGG model
- image = preprocess_input(image)
- # predict the probability across all output classes
- yhat = model.predict(image)
- temp = image[0]
- print(temp.shape)
- heatmap = np.zeros((224,224))
- correct_class = np.argmax(yhat)
- for n,(x,y,image) in enumerate(iter_occlusion(temp,14)):
- heatmap[x:x+14,y:y+14] = model.predict(image.reshape((1, image.shape[0], image.shape[1], image.shape[2])))[0][correct_class]
- print(x,y,n,' - ',image.shape)
- heatmapheatmap1 = heatmap/heatmap.max()
- plt.imshow(heatmap)
是不是很有趣呢?接著將使用標(biāo)準(zhǔn)化的熱圖概率來創(chuàng)建一個遮擋部分并進(jìn)行繪制:
- import skimage.io as io
- #creating mask from the standardised heatmap probabilities
- mask = heatmap1 < 0.85
- maskmask1 = mask *256
- maskmask = mask.astype(int)
- io.imshow(mask,cmap='gray')
最后,通過使用下述程序,對輸入圖像進(jìn)行遮擋:
- import cv2
- #read the image
- image = cv2.imread('car.jpeg')
- image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
- #resize image to appropriate dimensions
- image = cv2.resize(image,(224,224))
- maskmask = mask.astype('uint8')
- #apply the mask to the image
- final = cv2.bitwise_and(image,image,maskmask = mask)
- final = cv2.cvtColor(final,cv2.COLOR_BGR2RGB)
- #plot the final image
- plt.imshow(final)
猜猜為什么只能看到某些部分?沒錯——只有那些對輸出圖片類型的概率有顯著貢獻(xiàn)的部分是可見的。簡而言之,這就是遮擋圖的全部含義。
特征圖——將輸入特征的貢獻(xiàn)可視化
特征圖是另一種基于梯度的可視化技術(shù)。這類圖像在 Deep Inside Convolutional Networks:Visualising Image Classification Models and Saliency Maps.論文中有介紹。
特征圖計算出每個像素對模型輸出的影響,包括計算相對于輸入圖像每一像素而言輸出的梯度。
這也說明了在輸入圖像像素細(xì)微改變時輸出類別將如何產(chǎn)生變化。梯度的所有正值都表明,像素值的細(xì)微變化會增加輸出值:
這些梯度與圖像的形狀相同(梯度是針對每個像素計算的),對直觀感覺產(chǎn)生影響。
那么如何生成顯著圖呢?首先使用下述代碼讀取輸入圖像。
然后,通過VGG16模型生成顯著圖:
- # Utility to search for layer index by name.
- # Alternatively we can specify this as -1 since it corresponds to the last layer.
- layer_idx = utils.find_layer_idx(model, 'predictions')
- # Swap softmax with linear
- model.layers[layer_idx].activation = activations.linear
- model = utils.apply_modifications(model)
- #generating saliency map with unguided backprop
- grads1 = visualize_saliency(model, layer_idx,filter_indices=None,seed_input=image)
- #plotting the unguided saliency map
- plt.imshow(grads1,cmap='jet')
可以看到,模型更加關(guān)注狗的面部。下圖呈現(xiàn)了使用導(dǎo)向反向傳播后的結(jié)果:
- #generating saliency map with guided backprop
- grads2 = visualize_saliency(model, layer_idx,filter_indices=None,seed_input=image,backprop_modifier='guided')
- #plotting the saliency map as heatmap
- plt.imshow(grads2,cmap='jet')
導(dǎo)向反向傳播將所有的負(fù)梯度變?yōu)?,即只更新對類別概率有積極影響的像素。
CAM(Class Activation Maps)(梯度加權(quán))
CAM也是一種神經(jīng)網(wǎng)絡(luò)可視化技術(shù),基本原理是根據(jù)其梯度或?qū)敵龅呢暙I(xiàn)來權(quán)衡激活圖。
以下節(jié)選自Grad-CAM論文給出了該技術(shù)的要點(diǎn):
Grad-CAM可以使用任何目標(biāo)概念的梯度(例如“狗”的未歸一化概率或者簡單的說明文字),進(jìn)入最終的卷積層生成一個粗略的定位圖,最后突出顯示圖像中用于預(yù)測概念的重要區(qū)域。 |
本質(zhì)上,只需取用最后一個卷積層的特征映射,并使用相對于特征圖的輸出梯度對每個濾波器進(jìn)行加權(quán)(相乘),就能達(dá)到目的。生成加權(quán)梯度類激活圖的過程包括以下步驟:
- 利用最后一層卷積層輸出的特征圖。對于VGG16來說,該特征映射的大小為14x14x512。
- 計算輸出與特征圖相對應(yīng)的梯度。
- 進(jìn)行梯度全局平均池化。
- 將特征映射與相應(yīng)的池化梯度相乘。
可以看到輸入圖像及其對應(yīng)的類激活圖如下:
下圖為類激活圖。
將過程分層輸出可視化
卷積神經(jīng)網(wǎng)絡(luò)的起始層通常尋求邊緣(edge)等小的細(xì)節(jié)信息。隨著對模型的深入了解,其特征也會發(fā)生變化。
對于模型不同層輸出的可視化可以直觀地呈現(xiàn)圖像在相應(yīng)層上突出顯示的特性。為了針對后續(xù)問題進(jìn)行架構(gòu)微調(diào),可視化是非常重要的一步。因?yàn)槲覀兛梢钥吹讲煌瑢拥牟煌匦?,并決定模型中使用的具體層。
例如,在比較神經(jīng)風(fēng)格遷移問題中不同層的性能時,可視化輸出可以給予極大的助力。
下述程序展示了如何實(shí)現(xiàn)VGG16模型的不同層的輸出:
- #importing required libraries and functions
- from keras.models import Model
- #defining names of layers from which we will take the output
- layer_names = ['block1_conv1','block2_conv1','block3_conv1','block4_conv2']
- outputs = []
- imageimage = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
- #extracting the output and appending to outputs
- for layer_name in layer_names:
- intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
- intermediate_output = intermediate_layer_model.predict(image)
- outputs.append(intermediate_output)
- #plotting the outputs
- fig,ax = plt.subplots(nrows=4,ncols=5,figsize=(20,20))
- for i in range(4):
- for z in range(5):
- ax[i][z].imshow(outputs[i][0,:,:,z])
- ax[i][z].set_title(layer_names[i])
- ax[i][z].set_xticks([])
- ax[i][z].set_yticks([])
- plt.savefig('layerwise_output.jpg')
如圖所示,VGG16(除block5外)的每一層都從圖像中提取了不同特征。起始層對應(yīng)的是類似邊緣的低級特征,而后一層對應(yīng)的是車頂、排氣等特征。
結(jié)語
可視化總是讓人驚奇不已。固然理解一項(xiàng)技術(shù)工作原理的方法有很多,但是將其原理可視化會讓理解過程變得更加有趣。以下相關(guān)熱門話題值得關(guān)注:
- 神經(jīng)網(wǎng)絡(luò)的特征提取過程是一個極為熱門的研究領(lǐng)域,同時也已經(jīng)促進(jìn)了很多工具的開發(fā),如TensorSpace和“激活地圖集”(ActivationAtlases)。
- TensorSpace也是一個支持多種模型格式的神經(jīng)網(wǎng)絡(luò)可視化工具,可以加載模型并以交互的方式將其可視化。TensorSpace還有一個“游樂場”,用戶可以使用多個架構(gòu)進(jìn)行可視化,在瀏覽器中嘗試神經(jīng)網(wǎng)絡(luò)。