打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
如何在Python代码中可视化卷积特征

本文的结构如下:首先,我将向您展示VGG-16网络中若干层卷积特征的可视化,然后我们会试着理解其中的一些可视化,我将向你们展示如何快速测试某个过滤器可能检测到的模式的假设。最后,我将解释创建本文中提供的模式所必需的Python代码。

特征可视化

下面您将看到VGG-16网络中几个层的过滤器的特征可视化。在研究它们的同时,我希望您观察生成的模式的复杂性是如何随着更深的网络而增加的。

Layer 7:Conv2d(64,128)

filters 12, 16, 86, 110

Layer 14: Conv2d(128, 256)

filters 3, 34, 39, 55, 62, 105, 115, 181, 231

Layer 30: Conv2d(512, 512)

filters 54, 62, 67, 92, 123, 141, 150, 172, 180, 2

Layer 40: Conv2d(512, 512) — top of the network

模式识别

让我们试着解释一些可视化的特征!

从这个开始,这会让你想起什么吗?

layer 40, filter 286

这张照片立刻让我想起了你在教堂里发现的拱形天花板的圆形拱门。

那么我们如何检验这个假设呢?图像是通过最大化第40层第286个feature maps的平均激活量而得到的。因此,我们简单地将神网络应用于图像,并绘制第40层中feature maps的平均激活。

我们看到了什么?如所期望的,feature maps286有强烈的尖峰!这是否意味着第40层的Filter 286负责检测拱形天花板呢?这里我要小心一点。Filter 286显然对图像中的拱形结构有响应,但请记住,这种拱形结构可能在几个不同的类别中扮演重要角色。

注意:虽然我使用层40(卷积层)来生成我们当前正在查看的图像,但我使用了第42层来生成显示每个feature map的平均激活的图。层41和42是 batch-norm和 ReLU。ReLU激活函数删除所有负值,这是选择42层而不是40的原因,否则,该图将显示大量负噪声,这使得我们很难看到我们感兴趣的正峰值。

到下一个例子。我可以看到是鸡头(或至少是鸟头)!你看到尖尖的喙和黑眼睛了吗?

layer 40, filter 256

测试照片:

feature map 256 显示了强烈的峰值。

下一个:

layer 40, filter 462

可能是filter 462 响应羽毛吗?

是的,filter 462响应了羽毛:

filter 265的猜测呢?

layer 40, filter 265

也许是链?

是的,似乎是对的!

然而,还有一些其他的峰值!让我们来看看分别为两个过滤器生成的特征可视化:

layer 40, filters 95, 303

当快速扫描第40层的512个过滤器生成的模式时,这两张图片都没有引起注意。但是现在网络可以说:也许有点连锁。

这很酷:

layer 40, filter 64

我相信会看到很多像羽毛一样的结构,让我想起鸟腿,左下方可能会有类似鸟头的东西,黑眼圈和长嘴。

好的,在feature map 64上有一个峰值,但是还有更多甚至更大的峰值!让我们来看看为其他四个过滤器生成的模式,它们的feature map显示峰值:

顶部有更多的鸟腿和更多的眼睛和喙?然而,对于下图,我不知道。也许这些模式与图像的背景相关联,或者只是代表网络检测我不理解的鸟类所需的东西。我想现在这仍然是黑匣子的一部分......

最后一个,然后我们查看Python代码:

我认出了一只猫的耳朵!

是的,feature map 277有一个峰值,但是什么原因导致了强烈的峰值直接到它的右边呢?

让我们快速生成一张图片,使层40中feature map 281的最大化平均激活:

layer 40, filter 281

可能是猫的毛?

事实是,即使在最终的卷积层中,大多数滤波器对我来说都是绝对抽象的。

一种更严格的方法是将网络应用于许多不同类型图像的整个数据集,并跟踪在特定层中最能激发特定过滤器的图像。

还有一件事我觉得很有趣。在浏览生成的模式时,我发现许多模式似乎以不同的方向出现(有时甚至是相同的方向)。

这是有道理的!卷积在平移上是不变的,因为filters水平和垂直地在图像上滑动。但它们不是旋转不变的因为filters不旋转。因此,网络似乎需要几个不同方向的类似filters来检测不同方向的对象和模式。

Python代码

这个想法如下:我们从包含随机像素的图片开始。我们将评估模式中的网络应用于该随机图像,计算特定层中某个feature map的平均激活,然后我们从该图中计算相对于输入图像像素值的梯度。知道像素值的梯度后,我们继续以最大化所选feature map的平均激活的方式更新像素值。

让我们再次解释它:网络权重是固定的,网络不会被训练,我们试图找到一个图像,通过执行梯度下降来最大化某个feature map的平均激活对像素值进行优化。

该技术也可用于神经风格转移。

为了实现这一点,我们需要:

  1. 从随机图像开始
  2. 评估模式下的预训练神经网络
  3. 用一种很好的方式获取我们感兴趣的隐藏层的结果
  4. 用于计算梯度的损失函数和用于更新像素值的优化器

让我们从生成噪声图像作为输入开始。我们可以这样做,即以下方式:img = np.uint8(np.random.uniform(150, 180, (sz, sz, 3)))/255,其中sz是图像的高度和宽度,3是颜色通道的数量,我们除以255,因为它是类型的变量uint8可以存储的最大值。如果您想要更多或更少的噪音,请使用数字150和180。然后我们将其转换为需要使用渐变的PyTorch变量img_var = V(img[None], requires_grad=True)(这是fastai语法)。像素值需要gradients ,因为我们希望使用反向传播来优化它们。

接下来,我们需要在评估模式下预训练的网络(这意味着权重是固定的)。这可以用model = vgg16(pre=True).eval()和set_trainable(model, False)完成。

现在,我们需要一种方法来访问其中一个隐藏层的特征。我们可以在我们感兴趣的隐藏层之后截断网络,以便它成为输出层。但是,有一种更好的方法可以在PyTorch中解决这个问题,称为钩子,它可以在PyTorch Module或Tensor上注册。要理解这一点,你必须知道:

  1. Pytorch Module是所有神经网络模块的基类。
  2. 我们的神经网络中的每一层都是一个Module。
  3. 每个Module都有一个forward方法,用于计算给定输入的Module的输出。

当我们将网络应用于噪声图像时,第一层的forward方法将图像作为输入并计算其输出。这个输出是第二层forward方法的输入,以此类推。当您在某一层注册forward钩子时,该钩子在调用该层的forward方法时执行。即:当你把你的网络应用到一个输入图像时,第一层计算它的输出,然后是第二层,以此类推。当我们到达为其注册了钩子的层时,它不仅计算它的输出,还执行钩子。

这有什么用呢?假设我们对层i的feature maps感兴趣,我们在层i上注册一个forward hook,一旦调用层i的forward方法,就会将层i的feature保存在一个变量中。

下面的类是这样做的:

from fastai.conv_learner import *from cv2 import resize%matplotlib inlineclass SaveFeatures(): def __init__(self, module): self.hook = module.register_forward_hook(self.hook_fn) def hook_fn(self, module, input, output): self.features = torch.tensor(output,requires_grad=True).cuda() def close(self): self.hook.remove()

当钩子执行时,它调用hook_fn方法(参见构造函数)。hook_fn方法将层输出保存在self.features中。注意,这个张量需要梯度,因为我们想对像素值进行反向传播。

如何使用SaveFeatures对象呢?

使用activations = SaveFeatures(list(self.model.children())[i])注册层i的钩子,在将模型应用到带有model(img_var)的图像之后,可以访问activations.features中钩子为我们保存的特征。记住调用clo1se方法来释放使用的内存。

现在我们可以访问图层i的feature maps了!feature maps可以是这样的形状[1,512,7,7],其中1是批处理维度,512是filters/feature maps的数量,7是feature maps的高度和宽度。目标是最大化所选feature map j的平均激活。因此,我们定义以下损失函数:[loss = -activations.features[0, j].mean()以及optimizer = torch.optim.Adam([img_var], lr=lr, weight_decay=1e-6) 优化像素值的优化器。优化器在默认情况下最小化损失,因此我们不告诉优化器最大化损失,而是简单地将平均激活乘以-1。使用optimizer.zero_grad()重置梯度,使用loss.back()计算像素值的梯度,使用optimizer.step()改变像素值。

让我们来看一个例子:

layer 40, filter 265

接下来,我改变了噪声输入图像的大小。

你能观察到“链状图案”的频率似乎随着图像尺寸的增加而增加吗?我知道可能很难看出我的意思。然而,生成的图案的频率随着图像尺寸的增加而增加是有意义的,因为卷积filters具有固定的尺寸,但是它们与图像的相对大小随着图像分辨率的增加而减小。换句话说:假设所创建的模式的像素大小总是大致相同。如果我们增加图像的尺寸,生成的图案的相对尺寸会减小,图案的频率会增加。

如果我的假设是正确的,我们想要的是低分辨率示例的低频模式(甚至比上面显示的还要低),但是具有高分辨率。这有意义吗?我们怎么做呢?

我尝试从一个非常低分辨率的图像开始,即56×56像素,对像素值进行几个步骤的优化,然后将图像的大小增加一定的因子。在放大图像之后,我对像素值进行了进一步的优化,然后再次放大图像。

这样做效果更好:

layer 40, filter 265

我们现在有一个低频模式,分辨率更高,而且没有太多噪音。为什么这样做?当我们从低分辨率开始时,我们得到一个低频模式。在升级之后,如果我们使用随机图像以较大的图像大小开始,则放大的模式具有比优化器生成的频率更低的频率。因此,当在下一次迭代中优化像素值时,我们处于更好的起点并且似乎避免了较差的局部最小值。这有意义吗?为了进一步减少高频模式,我在升频后稍微模糊了图像,这比低频模式更能影响高频模式。

我发现按比例增加12倍可以得到很好的效果。

看看下面的Python代码。您会发现我们已经讨论了最重要的行,例如创建随机图像,注册钩子,定义优化器和损失以及优化像素值。

class FilterVisualizer(): def __init__(self, size=56, upscaling_steps=12, upscaling_factor=1.2): self.size, self.upscaling_steps, self.upscaling_factor = size, upscaling_steps, upscaling_factor self.model = vgg16(pre=True).cuda().eval() set_trainable(self.model, False) def visualize(self, layer, filter, lr=0.1, opt_steps=20, blur=None): sz = self.size img = np.uint8(np.random.uniform(150, 180, (sz, sz, 3)))/255 # generate random image activations = SaveFeatures(list(self.model.children())[layer]) # register hook for _ in range(self.upscaling_steps): # scale the image up upscaling_steps times train_tfms, val_tfms = tfms_from_model(vgg16, sz) img_var = V(val_tfms(img)[None], requires_grad=True) # convert image to Variable that requires grad optimizer = torch.optim.Adam([img_var], lr=lr, weight_decay=1e-6) for n in range(opt_steps): # optimize pixel values for opt_steps times optimizer.zero_grad() self.model(img_var) loss = -activations.features[0, filter].mean() loss.backward() optimizer.step() img = val_tfms.denorm(img_var.data.cpu().numpy()[0].transpose(1,2,0)) self.output = img sz = int(self.upscaling_factor * sz) # calculate new image size img = cv2.resize(img, (sz, sz), interpolation = cv2.INTER_CUBIC) # scale image up if blur is not None: img = cv2.blur(img,(blur,blur)) # blur image to reduce high frequency patterns self.save(layer, filter) activations.close()  def save(self, layer, filter): plt.imsave('layer_'+str(layer)+'_filter_'+str(filter)+'.jpg', np.clip(self.output, 0, 1))

使用FilterVisualizer可参考以下Python代码:

layer = 40filter = 265FV = FilterVisualizer(size=56, upscaling_steps=12, upscaling_factor=1.2)FV.visualize(layer, filter, blur=5)img = PIL.Image.open('layer_'+str(layer)+'_filter_'+str(filter)+'.jpg')plt.figure(figsize=(7,7))plt.imshow(img)

该Python代码假设你有一个Nvidia GPU。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
手把手带你走进卷积神经网络!
万字长文,用代码的思想讲解 Yolo3 算法实现原理,Pytorch 训练 Yolo 模型
手磕实现 CNN卷积神经网络!- 《深度学习入门:基于Python的理论与实现》系列之三
DL之CNN:利用自定义DeepConvNet【7+1】算法对mnist数据集训练实现手写数字识别并预测(超过99%)
pytorch实现YoloV3模型
手把手教你用 PyTorch 快速准确地建立神经网络
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服