本文首发自【简书】作者【西北小生_】的博客,转载请私聊作者!

CAM的全称是Class Activation Mapping或Class Activation Map,即类激活映射或类激活图。
论文《Learning Deep Features for Discriminative Localization》发现了CNN分类模型的一个有趣的现象:
CNN的最后一层卷积输出的特征图,对其通道进行加权叠加后,其激活值(ReLU激活后的非零值)所在的区域,即为图像中的物体所在区域。而将这一叠加后的单通道特征图覆盖到输入图像上,即可高亮图像中物体所在位置区域。如图1中的输入图像和输出图像所示。
该文章作者将实现这一现象的方法命名为类激活映射,并将特征图叠加在原始输入图像上生成的新图片命名为类激活图。
CAM一般有两种用途:
第一种用途是最直接的用途,根据CAM高亮的图像区域,可以直观地解释CNN是如何区分不同类别的物体的。
对于第二种用途,一般的目标定位方法,都需要专门对图像中的物体位置区域进行标注,并将标注信息作为图像标签的一部分,然后通过训练带标签的图像和专门的目标定位模型才能实现定位,是一种强监督的方法。而CAM方法不需要物体在图像中的位置信息,仅仅依靠图像整体的类别标签训练分类模型,即可找到图像中物体所在的大致位置并高亮之,因此可以作为一种弱监督的目标定位方法。

表示特征图的每个输出通道首先被平均为一个值,
个通道得到
个值,然后这些值再被加权相加得到一个数,这个数就是第
类的置信分数,表征着输入图像的类别是
的可能性大小。
表示首先对特征图的每个通道进行加权求和(
),得到一个二维的特征图(通道维坍塌),然后再对这个二维特征图求平均值,得到第
类的置信分数。
由公式(1)的推导可知,先对特征图进行全局平均池化,再进行加权求和得到类别的置信分数,等价于先对特征图进行通道维度的加权求和,再进行全局平均池化。
经过这一等价变换,就突显了特征图通道加权和的重要性了:一方面,特征图的通道加权和直接编码了类别信息;另一方面,也是最重要的,特征图的通道加权和是二维的,还保留着图像的空间位置信息。我们可以通过可视化方法观察到图像中的相对位置信息与CNN编码的类别信息的关系。
这里的特征图的通道加权之和就叫做类别激活图。
本文以PyTorch自带的ResNet-18为例,分步骤讲解并用代码实现CAM的整个流程和细节。
首先导入需要用到的包:
import math
import torch
from torch import Tensor
from torch import nn
import torch.nn.functional as F
from typing import Optional, List
import torchvision.transforms as transforms
from PIL import Image
import torchvision.models as models
from torch import Tensor
from matplotlib import cm
from torchvision.transforms.functional import to_pil_image
定义输入图片路径,和保存输出的类激活图的路径:
img_path = '/home/dell/img/1.JPEG' # 输入图片的路径
save_path = '/home/dell/cam/CAM1.png' # 类激活图保存路径
定义输入图片预处理方式。由于本文用的输入图片来自ILSVRC-2012验证集,因此采用PyTorch官方文档提供的ImageNet验证集处理流程:
preprocess = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
本文选用的CNN模型是PyTorch自带的ResNet-18,首先导入预训练模型:
net = models.resnet18(pretrained=True).cuda() # 导入模型
由于特征图是模型前向传播时的中间变量,不能直接从模型中获取,需要用到PyTorch提供的hook工具,补课请参考我的这两篇博客:hook1,hook2。
通过输出模型(print(net))我们就能看到ResNet-18输出最后一层特征图的层为net.layer4(或者net.layer4[1]、net.layer4[1].bn2都可)。我们用hook工具注册这一层,以便获得它的输出特征图:
feature_map = [] # 建立列表容器,用于盛放输出特征图
def forward_hook(module, inp, outp): # 定义hook
feature_map.append(outp) # 把输出装入字典feature_map
net.layer4.register_forward_hook(forward_hook) # 对net.layer4这一层注册前向传播
做好了hook的定义和注册工作,现在只需要对输入图片进行预处理,然后执行一次模型前向传播即可获得CNN最后一层卷积层的输出特征图:
orign_img = Image.open(img_path).convert('RGB') # 打开图片并转换为RGB模型
img = preprocess(orign_img) # 图片预处理
img = torch.unsqueeze(img, 0) # 增加batch维度 [1, 3, 224, 224]
with torch.no_grad():
out = net(img.cuda()) # 前向传播
这时我们想要的特征图已经装在列表feature_map中了。我们输出尺寸来验证一下:
In [10]: print(feature_map[0].size())
torch.Size([1, 512, 7, 7])
CAM使用的权重是全连接输出层中,对应这张图像所属类别的权重。文字表述可能存在歧义或不清楚,直接看本文最上面的图中全连接层被着色的连接。可以看到,每个连接对应一个权重值,左边和特征图的每个通道(全局平均池化后)一一连接,右边全都连接着输出类别所对应的那个神经元。
由于我也不知道这张图的类别标签,这里假设模型对这张图像分类正确,我们来获得其输出类别所对应的权重:
cls = torch.argmax(out).item() # 获取预测类别编码
weights = net._modules.get('fc').weight.data[cls,:] # 获取类别对应的权重
cam = (weights.view(*weights.shape, 1, 1) * feature_map[0].squeeze(0)).sum(0)
这里的代码比较简单,扩充权重的维度([512, ][512, 1, 1])是为了使之在通道上与特征图相乘;去除特征图的batch维([1, 512, 7, 7]
[512, 7, 7])是为了使其维度和weights扩充后的维度相同以相乘。最后在第一维(通道维)上相加求和,得到一个
的类激活图。
这一步有两个细节需要注意:
我们首先定义归一化函数:
def _normalize(cams: Tensor) -> Tensor:
"""CAM normalization"""
cams.sub_(cams.flatten(start_dim=-2).min(-1).values.unsqueeze(-1).unsqueeze(-1))
cams.div_(cams.flatten(start_dim=-2).max(-1).values.unsqueeze(-1).unsqueeze(-1))
return cams
然后对类激活图执行ReLU激活和归一化,并利用PyTorch的 to_pil_image函数将其转换为PIL格式以便下步处理:
cam = _normalize(F.relu(cam, inplace=True)).cpu()
mask = to_pil_image(cam.detach().numpy(), mode='F')
将类激活图转换成PIL格式是为了方便下一步和输入图像融合,因为本例中我们选用的PIL库将输入图像打开,选用PIL库也是因为PyTorch处理图像时默认的图像格式是PIL格式的。
这一步也有很多细节需要注意:
我们将两个图像交叠融合的过程封装成了函数:
def overlay_mask(img: Image.Image, mask: Image.Image, colormap: str = 'jet', alpha: float = 0.6) -> Image.Image:
"""Overlay a colormapped mask on a background image
Args:
img: background image
mask: mask to be overlayed in grayscale
colormap: colormap to be applied on the mask
alpha: transparency of the background image
Returns:
overlayed image
"""
if not isinstance(img, Image.Image) or not isinstance(mask, Image.Image):
raise TypeError('img and mask arguments need to be PIL.Image')
if not isinstance(alpha, float) or alpha < 0 or alpha >= 1:
raise ValueError('alpha argument is expected to be of type float between 0 and 1')
cmap = cm.get_cmap(colormap)
# Resize mask and apply colormap
overlay = mask.resize(img.size, resample=Image.BICUBIC)
overlay = (255 * cmap(np.asarray(overlay) ** 2)[:, :, 1:]).astype(np.uint8)
# Overlay the image with the mask
overlayed_img = Image.fromarray((alpha * np.asarray(img) + (1 - alpha) * overlay).astype(np.uint8))
return overlayed_img
接下来就是激动人心的时刻了!!!将类激活图作为掩码,以一定的比例覆盖到原始输入图像上,生成类激活图:
result = overlay_mask(orign_img, mask)
这里的变量result已经是有着PIL图片格式的类激活图了,我们可以通过:
result.show()
可视化输出,也可以通过:
result.save(save_path)
将图片保存在本地查看。我们在这里展示一下输入图像和输出定位图像的对比:

关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby
在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has
这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru
我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD