🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
现在我们知道如何从头开始构建几乎任何东西,让我们使用这些知识来创建全新的(并且非常有用!)功能:类激活映射。它让我们深入了解 CNN 为什么做出这样的预测。
在此过程中,我们将了解 PyTorch 的一个我们以前从未见过的方便功能,即hook,我们将应用本书其余部分介绍的许多概念。如果你真的想测试你对本书材料的理解,在你读完本章后,试着把它放在一边,自己从头开始重新创造这些想法(不要偷看!)。
类激活图(CAM) 由 Bolei Zhou 等人介绍。在 “为判别性本地化学习深层特征”中。它使用最后的输出 卷积层(就在平均池化层之前)与预测一起为我们提供了模型做出决定的原因的热图可视化。这是一个有用的解释工具。
更准确地说,在最后一个卷积层的每个位置,我们有与最后一个线性层一样多的过滤器。因此,我们可以计算这些激活与最终权重的点积,以获得特征图上每个位置的用于做出决定的特征的分数。
我们将需要一种方法来在模型训练时访问模型内部的激活。在 PyTorch 中,这可以通过hook来完成。Hooks 相当于 PyTorch 的 fastai 的回调。Learner
然而,钩子允许您将代码注入前向和后向计算本身,而不是让您像 fastai 回调一样将代码注入训练循环。我们可以将一个钩子附加到模型的任何层,它会在我们计算输出(前向钩子)或 反向传播(后向钩子)期间执行。前向钩子是一个接受三样东西的函数——一个模块、它的输入和它的输出——它可以执行你想要的任何行为。(fastai 还提供了一个方便 HookCallback
的,我们不会在这里介绍,但看看 fastai 文档;它使使用钩子更容易一些。)
为了说明,我们将使用我们在第 1 章中训练的相同猫狗模型 :
- path = untar_data(URLs.PETS)/'images'
- def is_cat(x): return x[0].isupper()
- dls = ImageDataLoaders.from_name_func(
- path, get_image_files(path), valid_pct=0.2, seed=21,
- label_func=is_cat, item_tfms=Resize(224))
- learn = cnn_learner(dls, resnet34, metrics=error_rate)
- learn.fine_tune(1)
epoch | train_loss | vaild_loss | error_rate | time |
---|---|---|---|---|
0 | 0.141987 | 0.018823 | 0.007442 | 00:16 |
epoch | train_loss | vaild_loss | error_rate | time |
---|---|---|---|---|
0 | 0.050934 | 0.015366 | 0.006766 | 00:21 |
首先,我们将获取一张猫图片和一批数据:
- img = PILImage.create(image_cat())
- x, = first(dls.test_dl([img]))
对于 CAM,我们要存储最后一个卷积层的激活值。我们把我们的钩子函数放在一个类中,这样它就有一个我们可以稍后访问的状态,并且只存储输出的副本:
- class Hook():
- def hook_func(self, m, i, o): self.stored = o.detach().clone()
然后我们可以实例化 一个Hook
并将其附加到我们想要的层,即 CNN 主体的最后一层:
- hook_output = Hook()
- hook = learn.model[0].register_forward_hook(hook_output.hook_func)
现在我们可以抓取一批并通过我们的模型提供它:
with torch.no_grad(): output = learn.model.eval()(x)
我们可以访问我们存储的激活:
act = hook_output.stored[0]
让我们也仔细检查我们的预测:
F.softmax(output, dim=-1)
tensor([[7.3566e-07, 1.0000e+00]], device='cuda:0')
我们知道0
(for False
) 是“狗”,因为类在 fastai 中自动排序,但我们仍然可以通过查看来仔细检查dls.vocab
:
dls.vocab
(#2) [False,True]
所以,我们的模型非常有信心这是一张猫的照片。
为了将我们的权重矩阵(2 乘以激活数)与激活(批量大小按行按列)进行点积,我们使用自定义einsum
:
act.shape
torch.Size([512, 7, 7])
- cam_map = torch.einsum('ck,kij->cij', learn.model[1][-1].weight, act)
- cam_map.shape
torch.Size([2, 7, 7])
对于我们批次中的每张图像,对于每个类别,我们都会得到一个 7×7 的特征图,告诉我们哪里的激活值更高,哪里的激活值更低。这将使我们看到图片的哪些区域影响了模型的决定。
例如,我们可以找出哪些区域使模型决定这只动物是一只猫(请注意,DataLoader
我们需要输入decode
,x
因为它已经被索引时不维护类型TensorImage
——这可能会在您阅读本文时修复):
- x_dec = TensorImage(dls.train.decode((x,))[0][0])
- _,ax = plt.subplots()
- x_dec.show(ctx=ax)
- ax.imshow(cam_map[1].detach().cpu(), alpha=0.6, extent=(0,224,224,0),
- interpolation='bilinear', cmap='magma');
在这种情况下,亮黄色区域对应高激活,紫色区域对应低激活。在这种情况下,我们可以看到头部和前爪是让模型确定这是一张猫图片的两个主要区域。
一旦你完成了你的钩子,你应该删除它,否则它可能会泄漏一些内存:
hook.remove()
这就是为什么让 Hook
类成为上下文管理器通常是个好主意,在您输入时注册挂钩 它并在您退出时将其删除。上下文管理器是一种 Python 结构,__enter__
当对象在with
子句中创建时以及__exit__
在子句末尾调用with
。例如,这就是 Python 处理with open(...) as f:
结构的方式,您经常会在打开文件时看到这种结构,而无需close(f)
在末尾显式显示。
如果我们定义Hook
如下
- class Hook():
- def __init__(self, m):
- self.hook = m.register_forward_hook(self.hook_func)
- def hook_func(self, m, i, o): self.stored = o.detach().clone()
- def __enter__(self, *args): return self
- def __exit__(self, *args): self.hook.remove()
我们可以这样安全地使用它:
- with Hook(learn.model[0]) as hook:
- with torch.no_grad(): output = learn.model.eval()(x.cuda())
- act = hook.stored
fastaiHook
为您提供了这个类,以及一些其他方便的类,使使用钩子更容易。
我们刚刚看到的方法让我们只计算具有最后激活的热图,因为一旦我们有了我们的特征,我们就必须将它们相乘 通过最后的权重矩阵。这不适用于网络的内层。2016 年论文 “Grad-CAM:你为什么这么说?”中介绍的变体 由 Ramprasaath R. Selvaraju 等人撰写。使用所需类别的最终激活的梯度。如果你还记得一点关于反向传递的知识,最后一层输出相对于该层输入的梯度等于层权重,因为它是一个线性层。
对于更深的层,我们仍然需要梯度,但它们不再只等于权重。我们必须计算它们。PyTorch 在向后传递期间为我们计算了每一层的梯度 ,但它们没有存储(张量除外,其中requires_grad
is True
)。但是,我们可以在向后传递上注册一个钩子,PyTorch 会将梯度作为参数提供给它,因此我们可以将它们存储在那里。为此,我们将使用一个类似于 的HookBwd
类Hook
,但拦截和存储梯度而不是激活:
- class HookBwd():
- def __init__(self, m):
- self.hook = m.register_backward_hook(self.hook_func)
- def hook_func(self, m, gi, go): self.stored = go[0].detach().clone()
- def __enter__(self, *args): return self
- def __exit__(self, *args): self.hook.remove()
然后对于类索引1
(对于True
,即“猫”),我们像以前一样截取最后一个卷积层的特征,并计算我们类的输出激活的梯度。我们不能只调用 output.backward
,因为梯度仅对标量(通常是我们的损失)有意义,并且output
是 2 阶张量。但是,如果我们选择一个图像(我们将使用0
)和一个类别(我们将使用1
),我们可以计算我们喜欢的任何权重或激活的梯度,相对于该单个值,使用output[0,cls].backward
。我们的钩子拦截我们将用作权重的梯度:
- cls = 1
- with HookBwd(learn.model[0]) as hookg:
- with Hook(learn.model[0]) as hook:
- output = learn.model.eval()(x.cuda())
- act = hook.stored
- output[0,cls].backward()
- grad = hookg.stored
Grad-CAM 的权重由特征图上的梯度平均值给出。然后就和之前一模一样了:
- w = grad[0].mean(dim=[1,2], keepdim=True)
- cam_map = (w * act[0]).sum(0)
- _,ax = plt.subplots()
- x_dec.show(ctx=ax)
- ax.imshow(cam_map.detach().cpu(), alpha=0.6, extent=(0,224,224,0),
- interpolation='bilinear', cmap='magma');
Grad-CAM 的新颖之处在于我们可以在任何层上使用它。例如,这里我们在倒数第二个 ResNet 组的输出上使用它:
- with HookBwd(learn.model[0][-2]) as hookg:
- with Hook(learn.model[0][-2]) as hook:
- output = learn.model.eval()(x.cuda())
- act = hook.stored
- output[0,cls].backward()
- grad = hookg.stored
- w = grad[0].mean(dim=[1,2], keepdim=True)
- cam_map = (w * act[0]).sum(0)
我们现在可以查看该层的激活图:
- _,ax = plt.subplots()
- x_dec.show(ctx=ax)
- ax.imshow(cam_map.detach().cpu(), alpha=0.6, extent=(0,224,224,0),
- interpolation='bilinear', cmap='magma');