卷积神经网络的特征图可视化秘籍——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]
或是[0,1]
。在保存单个channel的特征图时,保存函数又需要输出特征图像素分布为[0,255]
或是[0,1]
。那么在这里,采用了最大最小比例放缩的方法将输出特征图的像素值分布区间转化到了[0,1]
,而没有像上述链接一样使用Sigmoid来将像素值分布区间转化为[0,1]
。笔者认为采用最大最小比例放缩的方法更加合理,另外需要注意添加一个1e-5
来防止分母为0的情况。
import cv2
import numpy as np
import torch
from torch.autograd import Variable
from torchvision import models
import os
def mkdir(path):
isExists = os.path.exists(path)
if not isExists:
os.makedirs(path)
return True
else:
return False
def preprocess_image(cv2im, resize_im=True):
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
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)
for channel, _ in enumerate(im_as_arr):
im_as_arr[channel] /= 255
im_as_arr[channel] -= mean[channel]
im_as_arr[channel] /= std[channel]
im_as_ten = torch.from_numpy(im_as_arr).float()
im_as_ten = im_as_ten.unsqueeze_(0)
return im_as_ten
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
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)
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()
for i in range(features.shape[1]):
feature = features[:, i, :, :]
feature = feature.view(feature.shape[1], feature.shape[2])
feature = feature.data.numpy()
feature = (feature - np.amin(feature))/(np.amax(feature) - np.amin(feature) + 1e-5)
feature = np.round(feature * 255)
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)
if __name__=='__main__':
for k in range(1):
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
...
x = input
for index, layer in enumerate(self.pretrained_model):
x = layer(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
class ClassNet(nn.Module):
def __init__(self):
nn.Module.__init__(self)
self.features = []
self.net_1 = nn.Linear(in_features=4, out_features=128)
self.features.append(self.net_1)
self.net_2 = nn.Linear(in_features=128, out_features=64)
self.features.append(self.net_2)
self.net_3 = nn.Linear(in_features=64, out_features=32)
self.features.append(self.net_3)
self.net_4 = nn.Linear(in_features=32, out_features=2)
self.features.append(self.net_4)
def forward(self, x):
y_1 = self.net_1(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)
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
class ClassNet(nn.Module):
def __init__(self):
nn.Module.__init__(self)
self.features = []
self.net_1 = nn.Linear(in_features=4, out_features=128)
self.features.append(self.net_1)
self.net_2 = nn.Linear(in_features=128, out_features=64)
self.features.append(self.net_2)
self.net_3 = nn.Linear(in_features=64, out_features=32)
self.features.append(self.net_3)
self.net_4 = nn.Linear(in_features=32, out_features=2)
self.features.append(self.net_4)
def forward(self, x):
self.y_1 = self.net_1(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)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)