CNN可视化Convolutional Features

  • A+
所属分类:深度学习
摘要这篇文章介绍一下关于CNN中卷积层可视化的一种方法。主要的思想是找一张图片,使得某个layer的filter的激活值最大,这张图片就是能被这个filter所检测的对象。文章中有详细的解释。

简介

有的时候,我们想要了解卷积层的某个filter是在检测什么特征,于是想要对其进行一些可视化的解释。这里会讲的一种方法,主要的思想是找一张图片,使得某个layer的filter的激活值最大,这张图片就是能被这个filter所检测的对象。

特别说明:这里的初始对象,一般会有两种方式,如下:

  • 输入一堆图片,找出其中一张,使得某个神经元激活最大
  • 随机给一张图片,通过梯度下降,修改这张图片的像素,是某个神经元的激活最大

接下来会讲的是第二种方法。

关于filter的激活值

关于filter的激活值,我们可以使用下面的定义,其实相当于给定了一个loss函数,可以进行梯度下降,进行反复迭代。

  • 如现在filter的输出为11*11的一个矩阵(经过一个卷积层的输出)
  • 下图a_ij_k表示第k个filter的第i行j列;
CNN可视化Convolutional Features
  • 对输出值进行求和(这里的定义可以根据需要进行改变);
CNN可视化Convolutional Features
  • 最终的目标是使得a_k最大;
  • 最终,使用梯度下降法进行求解;

最后,十分建议阅读下面的参考链接,原文给出了更加详细的解释。我也是模仿的他的进行的实验。

参考链接How to visualize convolutional features in 40 lines of code

一些其他的CNN可视化的链接

方法简单介绍

其实根据上面的如何计算filter的激活值,我们就可以大致知道步骤是什么了。这里简单叙述一下,因为在实际的使用过程中,会有一些小的tricks,会关系到最后图像生成的效果的好坏。

  1. 初始化一张图片, 56*56
  2. 使用预训练好的VGG16网络,固定网络参数
  3. 若想可视化第40层layer的第k个filter的conv, 我们设置loss函数为 (-1*神经元激活值);
  4. 梯度下降, 对初始图片进行更新;
  5. 对得到的图片*1.2, 得到新的图片,重复上面的步骤;

其中第五步比较关键,我们可以看到初始化的图片不是很大,只有56*56. 这是因为原文作者在实际做的时候发现,若初始图片较大,得到的特征的频率会较高,即没有现在这么好的显示效果。

就像下图所展示,图像的size小的时候,pattern更加明显。所以在实际的操作中,会有第五步,就是希望初始化的图片从较小的开始,逐渐变大,使得最后的特征不会频率很高,导致无法看清。

CNN可视化Convolutional Features

下面我们先看一下结果的分析,关于具体的实现会在最后一部分进行说明。

简单分析

随着卷积层的加深, pattern在变得复杂

下面从左到右,分别是layer7, layer14和layer40画出的效果图, 随着layer的增加,卷积层所能检测的pattern在变得复杂。

CNN可视化Convolutional Features

验证猜想

比如对于layer 40, filter 64,绘制出的结果如下图所示。我们可以看出包含羽毛,鸟的脚部与头部,我们想要验证一下这个filter是否在输入鸟的图片时,可以得到比较大的激活值(这是验证的方法)

CNN可视化Convolutional Features

于是,我们准备一张鸟的图片,输入网络,图片如下所示:

CNN可视化Convolutional Features

于是我们可以得到layer40上,每一个filter被激活的程度。可以看到在64个filter上激活值还是很大的。同时,在其他的几层上,也有较大的激活值,我们把那些位置的filter对于的图片均画出.

CNN可视化Convolutional Features

其中,有些是具有可解释性的,我们仍能看到一些鸟的特征,如下所示:

CNN可视化Convolutional Features

但是,有些filter不具有可解释性,作者在原文是这样解释的,即可能这些pattern与背景有关,或是一些网络需要一起用来判断鸟类。

Regarding the bottom row, however, I have no clue. Maybe those patterns are associated with the background of the image or simply represent something the network needs for detecting birds that I don’t understand. I suppose this will remain part of the black box for now…

CNN可视化Convolutional Features

代码实现

导入相应的库

  1. import torch
  2. from torch.autograd import Variable
  3. from PIL import Image,ImageOps
  4. import torchvision.transforms as transforms
  5. import torchvision.models as models
  6. import numpy as np
  7. import cv2
  8. from cv2 import resize
  9. from matplotlib import pyplot as plt
  10. %matplotlib inline

设置device变量,用来控制是否使用cuda

  1. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

初始化图片

我们需要注意的是,这里我们要更新的是图片网络的weight是不变的

  1. # 生成随机的图片
  2. sz = 56
  3. img = np.uint8(np.random.uniform(150, 180, (3, sz, sz)))/255
  4. # img[None]可以增加一个维度
  5. img = torch.from_numpy(img[None]).float().to(device)
  6. img.shape
  7. # torch.Size([1, 3, 56, 56])

导入预训练好的网络

在这里使用带有batchnorm的VGG16。

  1. model_vgg16 = models.vgg16_bn(pretrained=True).features.to(device).eval()

使用hook类

在pytorch中,我们需要使用hook来得到网络中间层的输出。在这里我们先写好类,之后可以方便进行使用。方便得到某个layer的所有filter的输出。

  1. class SaveFeatures():
  2.     """注册hook和移除hook
  3.     """
  4.     def __init__(self, module):
  5.         self.hook = module.register_forward_hook(self.hook_fn)
  6.     def hook_fn(self, module, input, output):
  7.         # self.features = output.clone().detach().requires_grad_(True)
  8.         self.features = output.clone()
  9.     def close(self):
  10.         self.hook.remove()

接着,我们将其注册到layer40.

  1. layer = 40
  2. activations = SaveFeatures(list(model_vgg16.children())[layer])

 进行反向传播

我们首先先定义超参数,其中upscaling_steps就是上面讲方法介绍的第五步,逐步放大图像的次数,这里选择了13次。upscaling_factor就是每次放大的倍数。blur是每次放大后会有一个模糊的处理,也是为了让最后的效果变得更好。

  1. # 超参数
  2. lr = 0.1 # 学习率
  3. opt_steps = 25 # 迭代次数
  4. filters = 265 # 第265个filter,使其最大
  5. upscaling_steps = 13 # 图像放大次数
  6. blur=3
  7. upscaling_factor=1.2 # 把图像变粗

接着我们还需要设置一下均值和方差,这是由于VGG16网络会对输出进行normalization,所以我们需要自己对输入进行标准化。

  1. # 定义处理时的均值与方差
  2. cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).view(-1, 1, 1).to(device)
  3. cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).view(-1, 1, 1).to(device)

最后就是进行梯度下降即可。注意一定要移除hook在最后

  1. for epoch in range(upscaling_steps):  # scale the image up upscaling_steps times
  2.     # --------------------------------------------------------------------------------
  3.     # 因为原始的VGG网络对图片做了normalization, 所以这里对输入图片也要做normalization
  4.     # --------------------------------------------------------------------------------
  5.     img = (img - cnn_normalization_mean) / cnn_normalization_std
  6.     img
    =1
  7.     img
    =0
  8.     print('Imgshape1 : ',img.shape)
  9.     img_var = Variable(img, requires_grad=True)  # convert image to Variable that requires grad
  10.     # ----------
  11.     # 定义优化器
  12.     # ----------
  13.     optimizer = torch.optim.Adam([img_var], lr=lr, weight_decay=1e-6)
  14.     for n in range(opt_steps):  # optimize pixel values for opt_steps times
  15.         optimizer.zero_grad()
  16.         model_vgg16(img_var) # 正向传播
  17.         loss = -activations.features[0, filters].mean() # loss相当于最大该层的激活的值
  18.         loss.backward()
  19.         optimizer.step()
  20.     # ------------
  21.     # 图像进行还原
  22.     # ------------
  23.     print('Loss:',loss.cpu().detach().numpy())
  24.     img = img_var * cnn_normalization_std + cnn_normalization_mean # 这个使用img_var变换img
  25.     img
    =1
  26.     img
    =0
  27.     img = img.data.cpu().numpy()[0].transpose(1,2,0)
  28.     sz = int(upscaling_factor * sz)  # calculate new image size
  29.     img = cv2.resize(img, (sz, sz), interpolation = cv2.INTER_CUBIC)  # scale image up
  30.     if blur is not None: img = cv2.blur(img,(blur,blur))  # blur image to reduce high frequency patterns
  31.     print('Imgshape2 : ',img.shape)
  32.     img = torch.from_numpy(img.transpose(2,0,1)[None]).to(device)
  33.     print('Imgshape3 : ',img.shape)
  34.     print(str(epoch),',Finished')
  35.     print('=======')
  36. activations.close() # 移除hook

最后将图像进行保存即可。

  1. # 保存图片
  2. image = img.cpu().clone()
  3. image = image.squeeze(0)
  4. unloader = transforms.ToPILImage()
  5. image = unloader(image)
  6. image = cv2.cvtColor(np.asarray(image),cv2.COLOR_RGB2BGR)
  7. cv2.imwrite('res1.jpg',image)
  8. torch.cuda.empty_cache()

看一下最终的效果。可以看到图中包含链条的形状,我们接下来进行验证结果。

CNN可视化Convolutional Features

验证结果

首先导入验证的图片。

  1. size = (224, 224)
  2. picture = Image.open("./Broad_chain_closeup.jpg")
  3. # picture = Image.open("./Wattledcranethumb.jpg")
  4. picture = ImageOps.fit(picture, size, Image.ANTIALIAS)
  5. picture
CNN可视化Convolutional Features
  1. # 转换为tensor
  2. loader = transforms.ToTensor()
  3. picture = loader(picture).to(device)
  4. # 将图片做标准化, 转换为vgg网络的输入
  5. cnn_normalization_mean = torch.tensor([0.485, 0.456, 0.406]).view(-1, 1, 1).to(device)
  6. cnn_normalization_std = torch.tensor([0.229, 0.224, 0.225]).view(-1, 1, 1).to(device)
  7. # 减均值. 除方差
  8. picture = (picture - cnn_normalization_mean) / cnn_normalization_std
  9. # 查看转换之后的效果
  10. # unload = transforms.ToPILImage()
  11. # unload(picture.cpu())

接着,使用这张图片在网络中进行传播,并得到layer42的输出。这里使用layer42是因为layer42是经过ReLU之后,可以均为正数。写成layer40也是可以的。

  1. # 定义网络
  2. model_vgg16 = models.vgg16_bn(pretrained=True).features.to(device).eval()
  3. # 对网络进行hook,hook住指定层的output的值
  4. layer = 42 # 这里是ReLU之后, 经过激活之后也应该较大
  5. filters = 265
  6. activations = SaveFeatures(list(model_vgg16.children())[layer])
  7. # 网络前向传播
  8. with torch.no_grad():
  9.     picture_var = Variable(picture[None])
  10.     model_vgg16(picture_var)
  11. activations.close() # 移除hook

最后绘制出所有filter的均值即可。

  1. # 画出每个filter的平均值
  2. mean_act = [activations.features[0,i].mean().item() for i in range(512)] # 计算平均值
  3. plt.figure(figsize=(7,5))
  4. act = plt.plot(mean_act,linewidth=2.) # 画出折线图
  5. extraticks=[filters] # 增加的一个坐标
  6. ax = act[0].axes
  7. ax.set_xlim(0,500)
  8. plt.axvline(x=filters, color='grey', linestyle='--') # 绘制虚线
  9. ax.set_xlabel("feature map")
  10. ax.set_ylabel("mean activation")
  11. ax.set_xticks([0,200,400] + extraticks)
  12. plt.show()
CNN可视化Convolutional Features

可以看到filter=265时候,激活的值是比较大的。

结语

以上就是整个详细的流程。详细的代码可以参考下面的链接。我已上传了完整的代码。

CNN可视化的一些解释-Github仓库地址

关于feature map的绘制,在Pytorch forum上也是有提及的,地址如下,Visualize feature map

  • 微信公众号
  • 关注微信公众号
  • weinxin
  • QQ群
  • 我们的QQ群号
  • weinxin
王 茂南

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: