卷積神經網絡的特征圖可視化秘籍——PyTorch實作
- 可視化的定義及步驟
- PyTorch實作
-
- 以預訓練好的VGG16為例進行可視化
- 關鍵代碼剖析
- 如果是自行搭建的網絡,如何索引網絡層?
-
- 繼續使用序号索引
- 不使用序号,直接索引模型内部網絡層的屬性
可視化的定義及步驟
這裡所說的可視化是指對卷積神經網絡中間層的輸出特征圖進行可視化,比如将網絡第八層的輸出特征圖儲存為圖像顯示出來。那麼,我們實際上要做的事情非常簡單,分為如下兩步:
【1】搭建網絡模型,并将資料輸入到網絡之中;
【2】提取想可視化層的輸出特征圖,并将其按每個channel都儲存為一張圖像的方式進行可視化,其原因在于有幾個channel就代表了該層輸出有幾張特征圖(也代表了該層的卷積核數量)。
PyTorch實作
以預訓練好的VGG16為例進行可視化
下面提供了PyTorch實作的從在ImageNet上預訓練好的VGG16中可視化第一層輸出的代碼,該代碼參考了PyTorch|提取神經網絡中間層特征進行可視化的實作,實作思路及所做的修改如下:
- 在基于
預訓練的ImageNet
網絡上,處理單張圖像作為網絡的輸入,對該圖像進行的歸一化處理以VGG16
圖像的标準處理方式進行。ImageNet
- 根據給定的可視化層序号,擷取該層的輸出特征圖,注意傳回的是一個
的四維張量。[1,channels,width,height]
- 将每一個
的結果即channel
的二維張量都儲存為一張圖像,那麼該層的輸出特征圖一共有[width,height]
個channels
的灰階圖像(單通道圖像)。[width,height]
- 在對輸入圖像的處理上,采用了
圖像的歸一化方式,得到的圖像像素值分布區間為ImageNet
之間,而不是熟悉的[-2.7,2.1]
或是[-1,1]
。在儲存單個channel的特征圖時,儲存函數又需要輸出特征圖像素分布為[0,1]
或是[0,255]
。那麼在這裡,采用了最大最小比例放縮的方法将輸出特征圖的像素值分布區間轉化到了[0,1]
,而沒有像上述連結一樣使用Sigmoid來将像素值分布區間轉化為[0,1]
。筆者認為采用最大最小比例放縮的方法更加合理,另外需要注意添加一個[0,1]
來防止分母為0的情況。1e-5
import cv2
import numpy as np
import torch
from torch.autograd import Variable
from torchvision import models
import os
# 該函數建立儲存特征圖的檔案目錄,以網絡層号命名檔案夾,如feature\\1\\..檔案夾中儲存的是模型第二層的輸出特征圖
def mkdir(path):
isExists = os.path.exists(path) # 判斷路徑是否存在,若存在則傳回True,若不存在則傳回False
if not isExists: # 如果不存在則建立目錄
os.makedirs(path)
return True
else:
return False
# 圖像預處理函數,将圖像轉換成[224,224]大小,并進行Normalize,傳回[1,3,224,224]的四維張量
def preprocess_image(cv2im, resize_im=True):
# 在ImageNet100萬張圖像上計算得到的圖像的均值和标準差,它會使圖像像素值大小在[-2.7,2.1]之間,但是整體圖像像素值的分布會是标準正态分布(均值為0,方差為1)
# 之是以使用這種方法,是因為這是基于ImageNet的預訓練VGG16對輸入圖像的要求
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
# 改變圖像大小并進行Normalize
if resize_im:
cv2im = cv2.resize(cv2im, dsize=(224,224),interpolation=cv2.INTER_CUBIC)
im_as_arr = np.float32(cv2im)
im_as_arr = np.ascontiguousarray(im_as_arr[..., ::-1])
im_as_arr = im_as_arr.transpose(2, 0, 1) # 将[W,H,C]的次序改變為[C,W,H]
for channel, _ in enumerate(im_as_arr): # 進行在ImageNet上預訓練的VGG16要求的ImageNet輸入圖像的Normalize
im_as_arr[channel] /= 255
im_as_arr[channel] -= mean[channel]
im_as_arr[channel] /= std[channel]
# 轉變為三維Tensor,[C,W,H]
im_as_ten = torch.from_numpy(im_as_arr).float()
im_as_ten = im_as_ten.unsqueeze_(0) # 擴充為四維Tensor,變為[1,C,W,H]
return im_as_ten # 傳回處理好的[1,3,224,224]四維Tensor
class FeatureVisualization():
def __init__(self,img_path,selected_layer):
'''
:param img_path: 輸入圖像的路徑
:param selected_layer: 待可視化的網絡層的序号
'''
self.img_path = img_path
self.selected_layer = selected_layer
self.pretrained_model = models.vgg16(pretrained=True).features # 調用預訓練好的vgg16模型
def process_image(self):
img = cv2.imread(self.img_path)
img = preprocess_image(img)
return img
def get_feature(self):
input=self.process_image() # 讀取輸入圖像
# 以下是關鍵代碼:根據給定的層序号,傳回該層的輸出
x = input
for index, layer in enumerate(self.pretrained_model):
x = layer(x) # 将輸入給到模型各層,注意第一層的輸出要作為第二層的輸入,是以才會複用x
if (index == self.selected_layer): # 如果模型各層的索引序号等于期望可視化的標明層号
return x # 傳回模型目前層的輸出四維特征圖
def get_single_feature(self):
features = self.get_feature() # 得到期望模型層的輸出四維特征圖
return features
def save_feature_to_img(self):
features=self.get_single_feature() # 傳回一個指定層輸出的特征圖,屬于四維張量[batch,channel,width,height]
for i in range(features.shape[1]):
feature = features[:, i, :, :] # 在channel次元上,每個channel代表了一個卷積核的輸出特征圖,是以對每個channel的圖像分别進行處理和儲存
feature = feature.view(feature.shape[1], feature.shape[2]) # batch為1,是以可以直接view成二維張量
feature = feature.data.numpy() # 轉為numpy
# 根據圖像的像素值中最大最小值,将特征圖的像素值歸一化到了[0,1];
feature = (feature - np.amin(feature))/(np.amax(feature) - np.amin(feature) + 1e-5) # 注意要防止分母為0!
feature = np.round(feature * 255) # [0, 1]——[0, 255],為cv2.imwrite()函數而進行
mkdir('C:\\Users\\hu\\Desktop\\fea\\' + str(self.selected_layer)) # 建立儲存檔案夾,以標明可視化層的序号命名
cv2.imwrite('C:\\Users\\hu\\Desktop\\fea\\' + str(self.selected_layer) + '\\' + str(i) + '.jpg',feature) # 儲存目前層輸出的每個channel上的特征圖為一張圖像
if __name__=='__main__':
for k in range(1): # k代表標明的可視化的層的序号
myClass = FeatureVisualization('C:\\Users\\hu\\Desktop\\TRP.jpg', k) # 執行個體化類
print (myClass.pretrained_model)
myClass.save_feature_to_img() # 開始可視化,并将特征圖儲存成圖像
關鍵代碼剖析
self.pretrained_model = models.vgg16(pretrained=True).features # 調用預訓練好的vgg16模型
...
x = input
for index, layer in enumerate(self.pretrained_model):
x = layer(x) # 将輸入給到模型各層,注意第一層的輸出要作為第二層的輸入,是以才會複用x
if (index == self.selected_layer): # 如果模型各層的索引序号等于期望可視化的標明層号
return x # 傳回模型目前層的輸出四維特征圖
首先,
self.pretrained_model
實際上就是網絡層按順序排列組成的清單,展示了網絡層的順序排列,我們列印出
self.pretrained_model
來看:
for index, layer in enumerate(self.pretrained_model):
中的
enumerate
(與列印圖中的
(1)、(2)、(3)
等一緻),目的在于得知期望輸出的層在模型中的的序号
index
,這樣就可以将它與對應的標明序号相比對,進而将期望層的輸出特征圖傳回了。
另一個需要注意的地方是這裡複用了
x
,這是因為模型第二層的輸入要求是第一層的輸出,而不再是原始輸入了,是以需要複用
x
。
如果是自行搭建的網絡,如何索引網絡層?
在上面給出的示例中,基于預訓練的模型VGG16,通過
self.pretrained_model = models.vgg16(pretrained=True).features
就可以傳回一個包含所有網絡層的
Sequential
,然後通過
for index, layer in enumerate(self.pretrained_model):
中的
enumerate
來實作得知期望輸出層在模型中的序号。通過模型輸出層的序号與標明的序号是否比對,來判斷是否是我們想要的進行可視化的層。
那麼,對于我們自己定義和訓練的網絡來說,一般是沒有features這個屬性的,我們如何來索引想要的網絡層,進而得到它的輸出特征圖呢?在這裡介紹兩種方法。
繼續使用序号索引
既然自行搭建的網絡沒有features屬性,那我們就人為建構一個,反正它的本質是個清單或Sequential(Sequential本身也可以了解成一個清單,可以通過net[3]這種索引通路其中元素)。然後把網絡層依次添加到裡面,就可以照上述方法接着使用了,代碼如下:
import torch as t
from torch import nn
# 整個網絡由四層網絡搭建,神經元個數分别為4——128——64——32——2
class ClassNet(nn.Module):
def __init__(self):
# 初始化模型
nn.Module.__init__(self)
self.features = []
# 搭建多層感覺機網絡模型
self.net_1 = nn.Linear(in_features=4, out_features=128) # 網絡第一層:輸入神經元為4個,輸出神經元為128個
self.features.append(self.net_1)
self.net_2 = nn.Linear(in_features=128, out_features=64) # 網絡第二層:輸入神經元為128個,輸出神經元為64個
self.features.append(self.net_2)
self.net_3 = nn.Linear(in_features=64, out_features=32) # 網絡第三層:輸入神經元為64個,輸出神經元為32個
self.features.append(self.net_3)
self.net_4 = nn.Linear(in_features=32, out_features=2) # 網絡第四層:輸入神經元為32個,輸出神經元為2個
self.features.append(self.net_4)
def forward(self, x): # 取得網絡輸入x
y_1 = self.net_1(x) # 将輸入x輸入第一層,得到第一層的輸出結果
y_2 = self.net_2(y_1) # 将第一層的輸出結果作為第二層的輸入
y_3 = self.net_3(y_2) # 将第二層的輸出結果作為第三層的輸入
y_4 = self.net_4(y_3) # 将第三層的輸出結果作為第四層的輸入
return y_4 # 得到的第四層的輸出結果即為模型最終輸出,兩個神經元分别代表土壤流失量和徑流深的數值
input = t.zeros(4)
input = t.unsqueeze(input,dim=0)
net = ClassNet() # 搭模組化型
k = 0 # 可視化第一層的輸出特征圖
x = input
for index, layer in enumerate(net.features):
print(index,layer)
x = layer(x) # 複用x使得第一層的輸出作為第二層輸入
if index == k:
print(x)
不使用序号,直接索引模型内部網絡層的屬性
從本質上來說,使用序号的索引就是為了确定期望的中間層處于模型的什麼位置。那麼如果我們在構模組化型時定義了想要的模型中間層輸出(前提),那麼就可以直接通路該屬性:
比如在下面模型中,我們想要模型第二層的輸出結果可視化,由于我們在
forward()
函數中定義了第二層的輸出為
self.y_2
,那麼就隻需要通過通路
net.y_2
,就能直接将該輸出儲存出來,進行後面的逐channel儲存操作。
關于擷取模型中間層的輸出,這篇部落格Pytorch學習(十六)----擷取網絡的任意一層的輸出中關于【1】如何從Sequential中提取單獨網絡層以及通過Sequential在forward中定義中間層輸出;【2】通過hook函數(鈎子函數)擷取中間層輸出;這兩部分有點意思。
import torch as t
from torch import nn
# 整個網絡由四層網絡搭建,神經元個數分别為4——128——64——32——2
class ClassNet(nn.Module):
def __init__(self):
# 初始化模型
nn.Module.__init__(self)
self.features = []
# 搭建多層感覺機網絡模型
self.net_1 = nn.Linear(in_features=4, out_features=128) # 網絡第一層:輸入神經元為4個,輸出神經元為128個
self.features.append(self.net_1)
self.net_2 = nn.Linear(in_features=128, out_features=64) # 網絡第二層:輸入神經元為128個,輸出神經元為64個
self.features.append(self.net_2)
self.net_3 = nn.Linear(in_features=64, out_features=32) # 網絡第三層:輸入神經元為64個,輸出神經元為32個
self.features.append(self.net_3)
self.net_4 = nn.Linear(in_features=32, out_features=2) # 網絡第四層:輸入神經元為32個,輸出神經元為2個
self.features.append(self.net_4)
def forward(self, x): # 取得網絡輸入x
self.y_1 = self.net_1(x) # 将輸入x輸入第一層,得到第一層的輸出結果
self.y_2 = self.net_2(self.y_1) # 将第一層的輸出結果作為第二層的輸入
self.y_3 = self.net_3(self.y_2) # 将第二層的輸出結果作為第三層的輸入
self.y_4 = self.net_4(self.y_3) # 将第三層的輸出結果作為第四層的輸入
return self.y_4 # 得到的第四層的輸出結果即為模型最終輸出,兩個神經元分别代表土壤流失量和徑流深的數值
input = t.zeros(4)
input = t.unsqueeze(input,dim=0)
net = ClassNet() # 搭模組化型
output = net(input) # 将輸入給到模型
print(net.y_2) # 由于在forward時定義了想要的中間層輸出,直接索引該屬性即可得到期望可視化的特征圖