深度学习之图像分类(二)-- pytorch查看中间层特征矩阵以及卷积核参数

木卯 于 2021-08-26 发布

深度学习之图像分类(二)pytorch查看中间层特征矩阵以及卷积核参数

在开始学习深度学习图像分类模型Backbone理论知识之前,先看看如何在 pytorch 框架中查看中间层特征矩阵以及卷积核参数,学习视频源于 Bilibili

耳听为虚,眼见为实!可视化 feature maps 以及 kernel weights 在论文展示中非常重要,同时对于个人分析神经网络学习的特性也至关重要。本文学习的完整代码详见 此处

1. 可视化 feature maps

import torch
from alexnet_model import AlexNet
from resnet_model import resnet34
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from torchvision import transforms

"""
class AlexNet(nn.Module):
		...
		
		def forward(self, x):
				# 存储网络中间结果
        outputs = []
        for name, module in self.features.named_children():
            x = module(x)
            if name in ["0", "3", "6"]:
                outputs.append(x)
				
				# 网络正向传播过程返回的是中间结果
        return outputs
"""

# 与训练过程保持一致
data_transform = transforms.Compose(
    [transforms.Resize((224, 224)),
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# data_transform = transforms.Compose(
#     [transforms.Resize(256),
#      transforms.CenterCrop(224),
#      transforms.ToTensor(),
#      transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])

# create model
model = AlexNet(num_classes=5)
# model = resnet34(num_classes=5)
# load model weights
model_weight_path = "./AlexNet.pth"  # "./resNet34.pth"
model.load_state_dict(torch.load(model_weight_path))
print(model)

# load image
img = Image.open("../tulip.jpg")
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)		# 增加一个 batch 维度

# forward
out_put = model(img)
for feature_map in out_put:			# 使用 AlexNet 的话,out_put 是一个 list,有三个元素
    # [N, C, H, W] -> [C, H, W]
    im = np.squeeze(feature_map.detach().numpy())		# 只输入了一张图,squeeze 压缩掉 batch 维度,detach() 去除梯度信息
    # [C, H, W] -> [H, W, C]
    im = np.transpose(im, [1, 2, 0])

    # show top 12 feature maps
    plt.figure()
    for i in range(12):
        ax = plt.subplot(3, 4, i+1)
        # [H, W, C]
        # 我们特征矩阵每一个 channel 所对应的是一个二维的特征矩阵,就像灰度图一样,channel = 1
        # 如果不指定 cmap='gray' 默认是以蓝色和绿色替换黑色和白色
        plt.imshow(im[:, :, i], cmap='gray')
    plt.show()

以 AlexNet 为例,下图展示了测试的原始图片:

img0

下图打印了第一个卷积层计算得到的前 12 个通道的特征图,每个特征图的切片中可以通过明暗程度来理解卷积层 1 所关注的信息,其中越亮的地方就是卷积核越感兴趣的地方。通过对比原图发现,由于这是卷积层 1 输出的特征矩阵,所以基本还是能看出一些原始图的信息。

img1

卷积层 2 输出的信息如下所示,由于越往后,抽象程度越高,所以越来越不像所看到的花了。另外有些卷积核没有起到什么作用的,卷积之后得到的特征矩阵都是黑色的,说明根本就没有学到什么有用的信息。

img2

如果之前不增加 cmap='gray' 的话,图片如下所示:

img3

相比而言,使用 ResNet 得到的结果则更好,第一个卷积层输出结果可见它检测到了纹理信息,以及高亮部分展示了花朵等等。ResNet 的 layer 1 输出的特征图结果也比 AlexNet 很多全黑的要好。可能有两个原因造成这种情况,首先是 ResNet 本身比 AlexNet 要好;其次则是 ResNet 使用了迁移学习,用了 ImageNet 预训练的权重来训练的。

img4

img5

哦豁,如果想看全连接层的输出特征矩阵怎么办呢?

2. 可视化 kernel weights

同样以 AlexNet 和 ResNet 为例。

import torch
from alexnet_model import AlexNet
from resnet_model import resnet34
import matplotlib.pyplot as plt
import numpy as np


# create model
model = AlexNet(num_classes=5)
# model = resnet34(num_classes=5)
# load model weights
model_weight_path = "./AlexNet.pth"  # "resNet34.pth"
model.load_state_dict(torch.load(model_weight_path))
print(model)

# model.state_dict() 获取模型所有的可训练参数的字典;.keys() 获取名称
weights_keys = model.state_dict().keys()
for key in weights_keys:
    # remove num_batches_tracked para(in bn)
    if "num_batches_tracked" in key:
        continue
  
    # [kernel_number, kernel_channel, kernel_height, kernel_width]
    # 输出深度,输入深度,卷积核高,卷积核宽
    weight_t = model.state_dict()[key].numpy()

    # read a kernel information
    # k = weight_t[0, :, :, :]		# 读取第一个卷积核

    # calculate mean, std, min, max
    # 计算均值,标准差,最大值和最小值。
    weight_mean = weight_t.mean()
    weight_std = weight_t.std(ddof=1)
    weight_min = weight_t.min()
    weight_max = weight_t.max()
    print("mean is {}, std is {}, min is {}, max is {}".format(weight_mean,
                                                               weight_std,
                                                               weight_max,
                                                               weight_min))

    # plot hist image
    plt.close()
    weight_vec = np.reshape(weight_t, [-1])		# 卷积核权重展成一维的向量 --- 原始卷积核太小了就3x3
    plt.hist(weight_vec, bins=50)							# 统计卷积核权重值直方图的分布
    plt.title(key)
    plt.show()

下图展示了卷积层 1 的权重以及 bias 的分布。

img6

img7

(有时候能看到,很多卷积核参数都是 0…)