这里所说的可视化是指对卷积神经网络中间层的输出特征图进行可视化,比如将网络第八层的输出特征图保存为图像显示出来。那么,我们实际上要做的事情非常简单,分为如下两步:
【1】搭建网络模型,并将数据输入到网络之中;
【2】提取想可视化层的输出特征图,并将其按每个channel都保存为一张图像的方式进行可视化,其原因在于有几个channel就代表了该层输出有几张特征图(也代表了该层的卷积核数量)。
下面提供了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
# 该函数创建保存特征图的文件目录,以网络层号命名文件夹,如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时定义了想要的中间层输出,直接索引该属性即可得到期望可视化的特征图