本人最近在研究深度学习的目标检测网络,在研究代码的过程中,对提取图像特征这个黑盒子的内容比较好奇和感兴趣,就进行了进一步的研究,查询相关资料,实现了vgg16网络特征图的可视化。
首先贴出完整代码:

import torch
from torchvision import models, transforms
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import scipy.misc
import cv2
 
 
# 将输入图片转化为卷积运算格式tensor数据
def transfer_image(image_dir):
    # 以RGB格式打开图像
    # Pytorch DataLoader就是使用PIL所读取的图像格式
    # 建议就用这种方法读取图像,当读入灰度图像时convert('')
    image_info = Image.open(image_dir).convert('RGB')
    # 数据预处理方法
    image_transform = transforms.Compose([
        transforms.Resize(1024),#放缩图片大小至1024*1024
        transforms.CenterCrop(672),#从图象中间剪切672*672大小
        transforms.ToTensor(),#将图像数据转化为tensor
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#归一化处理
    ])
    image_info = image_transform(image_info)#更新载入图像的数据格式
    #下面这部分是tensor张量转化为图片的过程,opencv画图和PIL、torch画图方式
    array1 = image_info.numpy()
    maxvalue = array1.max()
   	array1 = array1*255/maxvalue
   	mat = np.uint8(array1)
    print('mat_shape',mat.shape)
    mat = mat.transpose(1,2,0)
    cv2.imwrite('convert_cocomi1.jpg',mat)
    mat=cv2.cvtColor(mat,cv2.COLOR_BGR2RGB)
    cv2.imwrite('convert_cocomi2.jpg',mat)

    image_info = image_info.unsqueeze(0)#增加tensor维度,便于卷积层进行运算
    return image_info
 
# 获取第k层的特征图
def get_k_layer_feature_map(feature_extractor, k, x):
    with torch.no_grad():
   		 #feature_extractor是特征提取层,后面可以具体看一下vgg16网络
        for index,layer in enumerate(feature_extractor):
            x = layer(x)#x是输入图像的张量数据,layer是该位置进行运算的卷积层,就是进行特征提取
            print('k values:',k)
            print('feature_extractor layer:',index)
            if k == index:#k代表想看第几层的特征图
          
                return x
 
#  可视化特征图
def show_feature_map(feature_map):
    feature_map = feature_map.squeeze(0)#squeeze(0)实现tensor降维,开始将数据转化为图像格式显示
    feature_map = feature_map.cpu().numpy()#进行卷积运算后转化为numpy格式
    feature_map_num = feature_map.shape[0]#特征图数量等于该层卷积运算后的特征图维度
    row_num = np.ceil(np.sqrt(feature_map_num))
    plt.figure()
    for index in range(1, feature_map_num+1):
        plt.subplot(row_num, row_num, index)
        plt.imshow(feature_map[index-1], cmap='gray')
        plt.axis('off')
        scipy.misc.imsave(str(index)+".png", feature_map[index-1])
    plt.show()
 
 
 
 
if __name__ ==  '__main__':
    # 初始化图像的路径
    image_dir = 'cocomi.jpg'
    # 定义提取第几层的feature map
    k = 5
    # 导入Pytorch封装的vgg16网络模型
    model = models.vgg16(pretrained=False)
    # 是否使用gpu运算
    use_gpu = torch.cuda.is_available()
    use_gpu =False
    # 读取图像信息
    image_info = transfer_image(image_dir)
    # 判断是否使用gpu
    if use_gpu:
        model = model.cuda()
        image_info = image_info.cuda()
    # vgg16包含features和classifier,但只有features部分有特征图
    # classifier部分的feature map是向量
    feature_extractor = model.features
    feature_map = get_k_layer_feature_map(feature_extractor, k, image_info)
    show_feature_map(feature_map)

运行代码后,结果如下:

tensorboard卷积核可视化 python pytorch卷积可视化_计算机视觉


可以看到,在k=5时生成了128张特征图,为什么是128张呢,看一下网络结构就明白了,对于初学者理解特征图维度也是有帮助的。

进行ipdb调试可以看到:

ipdb> model                                                                                                                                           
VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

上面就是完整的vgg16网络结构,分为features和classifier两部分,用于显示特征图的是features部分,从(0)-(30),分别是卷积层、激活函数层和最大池化层。我们的k=5对应的是(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))

输入维度是64,输出维度128,意思是输出128张特征图。

下面放几张具体的特张图,看一看提取效果:

tensorboard卷积核可视化 python pytorch卷积可视化_人工智能_02


tensorboard卷积核可视化 python pytorch卷积可视化_深度学习_03


从上述结果看起来就更直观一些。

至于图像卷积数据流向,如果你能仔细从代码入手一部分一部分调试,相信你也会有一些收获,认识上会更加具体,下面结合代码简单说一下:

首先是一张三通道的彩色图像进入卷积层,进行运算的时候,图像是分成三个通道单独进行卷积操作再相加得到特征图,经过第一层卷积层时,有64个卷积核(人为设置),与每一个卷积核运算都会产生一张特征图,这时,一张图像的输出就变成了64维的特征图,就是64张特征图,并以tensor的形式流动,后续的卷积池化都是这个原理。
有时一次处理多张图片时,tensor的形式又会有新变化,大家可以看这个:mini-batch批处理 进行卷积运算后,特征图仍然以张量的形式存在,要想特征图可视化,就要将tensor转化为numpy形式可以让cv库画出来。这些可以从贴出代码去理解,具体画图方式的区别如下:

opencv支持的图像数据是numpy格式,数据类型为uint8,像素值分布在[0,255], 但是tensor数据像素值并不是分布在[0,255],且数据类型为float32,所以需要做一下normalize和数据变换,将图像数据扩展到[0,255]。还有一点是tensor和numpy存储的数据维度顺序不同,需要transpose进行转化对应。

还有是opencv中的颜色通道顺序是BGR而PIL、torch里面的图像颜色通道是RGB,利用cvtColor对颜色通道进行转换。