CAM原文:Learning Deep Features for Discriminative Localization
最近的一项工作表明,CNNs在没有监督对象位置的情况下,实际表现为对象检测器。尽管卷积层有这种非凡的能力,但使用全连接层时这种能力就会丧失。最近提出的网络如Network in Network(NIN)和GoogleNet都在避免使用全连接层,在最小化参数量的同时保持不错的性能。
在训练过程中,GAP作为结构正则器,防止过拟合。在作者的实验中,他们发现对GAP不仅能作为正则器,对其稍加调整,甚至网络最初并没有被训练过的情况下,GAP还能在一次前向传播过程中很容易的识别出鉴别性图像区域,如下图1所示:
尽管他们的方法看起来简单,但在ILSVRC的若监督对象定位上,他们的最佳网络达到了了37.1%top-5测试误差,与全监督的AlexNet相近。
此外他们还证明了,他们的方法所提取到的深层特征的定位能力可以很容易的应用在其他数据集上,用于一般分类、定位和概念发现上。
CNNs在各种视觉识别任务上的表现非常出色,最近的一项工作表明,虽然是用图像级标签进行训练,但CNNs仍然据偶显著的对象定位能力。在他们的工作中,他们展示了通过使用合适的架构,就可以将这种能力推广,不仅仅能够定位对象,还可识别图像种哪些区域被作为CNN分类的判别依据。
在这里,他们就与该文章最相关的两个领域展开讨论:弱监督的对象定位和可视化CNN的内部表示。
在这一节中,作者描述了在CNNs中使用GAP生成类激活图(class activation maps,CAM)的详细过程。一个特定类别的类激活图表明了哪些判别图像区域被CNN用来鉴别图像类别,如下图2所示:
作者使用的网络结构类似于Network in Network和GoogleNet,这些网络主要由卷积层组成,在最终的输出层(在分类任务下为Softmax)前,他们将GAP应用到卷积特征图上,并将其用作产生所需输出的全连接层的特征。
如上图所示,GAP的输出是最后一个卷积层每个单元的特征图的空间平均值,这些值的加权和生成最终输出。类似的,通过计算最后一个卷积层的特征图的的加权和来获得CAM。在下面将对于softmax的情况进行更为详细的描述。这种技术同样可以应用到回归和其他损失函数中。
给定一幅图像,设 f k ( x , y ) f_k(x,y) fk(x,y) 表示最后一个卷积层中单元 k k k 在空间位置 ( x , y ) (x,y) (x,y) 的激活,对于单元 k k k ,GAP的输出结果为:
F k = ∑ x , y f k ( x , y ) F^k=\sum_{x,y}f_k(x,y) Fk=x,y∑fk(x,y)
因此,给定类别 c c c ,softmax的输入(类别评分函数,class score)为:
S c = ∑ k w k c F k S_c=\sum_kw^{c}_kF_k Sc=k∑wkcFk
这里 w k c w_{k}^c wkc 表示单元 k k k 对于类别 c c c 的权重贡献,实际上 w k c w_{k}^c wkc 表明了 F k F_k Fk 对于类别 c c c 的重要程度。
最终,对于类别 c c c ,softmax的输出为:
P c = e x p ( S c ) ∑ c e x p ( S c ) P_c=\frac{exp(S_c)}{\sum_c{exp(S_c)}} Pc=∑cexp(Sc)exp(Sc)
这里作者忽略了偏置项,他们明确地将softmax的输入偏置设置为0,因此这对分类性能没有影响。
将 F k = ∑ x , y f k ( x , y ) F^k=\sum_{x,y}f_k(x,y) Fk=x,y∑fk(x,y) 代入 S c S_c Sc (class score)中,得到:
S c = ∑ k w k c ∑ x , y f k ( x , y ) = ∑ x , y ∑ k w k c f k ( x , y ) S_c=\sum_kw^{c}_k\sum_{x,y}f_k(x,y)=\sum_{x,y}\sum_k{w_k^{c}f_k(x,y)} Sc=k∑wkcx,y∑fk(x,y)=x,y∑k∑wkcfk(x,y)
对于类别 c c c ,作者定义 M c M_c Mc 为类别激活图(CAM),其中CAM的每个空间元素由下式给出:
M c ( x , y ) = ∑ k w k c f k ( x , y ) M_c{(x,y)}=\sum_k{w_{k}^{c}f_k{(x,y)}} Mc(x,y)=k∑wkcfk(x,y)
因此,可以导出类别评分函数与类激活图的关系:
S c = ∑ x , y M c ( x , y ) S_{c}=\sum_{x,y}{M_c{(x,y)}} Sc=x,y∑Mc(x,y)
因此, M c M_c Mc 就直接反映了空间位置 ( x , y ) (x,y) (x,y) 处的激活对图像被归为 c c c 类的贡献(重要)程度。
基于以前的工作,作者期望每个单元被其感受野内的一些视觉模式激活。因此 f k f_k fk 就是表征这些视觉模式的图。类激活图(CAM)只是这些视觉模式在不同空间位置的线性加权和。通过简单的将CAM上采样到原始图像的大小,就可以识别与特定类别最相关的图像区域。
图2中展示了一些CAMs输出的例子,可以看到不同类别的图像的判别区域被突出显示。下图4展示了当使用不同的类别来生成CAMs时,单张图像的CAMs的差异,可以观察到,即使对于同一幅图像,被划分为不同类别时的判别区域也是不同的。
考虑到之前的工作是将GMP用于弱监督对象定位,作者认为强调GAP和GMP之间的直观区别是很重要的。他们相信GAP损失鼓励网络去识别对象的范围,而GMP则鼓励网络去仅识别一个判别部分。
这是因为,在对特征图做平均时,可以通过发现对象的所有判别部分去最大化该值,因为所有的低激活都会相应的减少特定特征图的输出。另一方面,对于GMP来说,由于只保留了最大值,故除了最具判别性的区域外,整幅图像的低激活区域(low scores)都在输出上得到体现。
作者在ILSVRC 2014上进行了实验验证,也证明了GMP与GAP在实现下你公司分类性能的情况下,GAP的定位能力表现优于GMP。
CNN的高层响应已经被证明是非常有效的通用特征,在各种图像数据集上具有先进的性能。在这一节中,作者表明他们的GAP CNNs所学习到的特征(不管是作为通用特征还是额外特征),尽管没有针对那些特定的任务进行训练,在分类判别图像区域时表现也都非常良好。
为了获得与原始softmax层相似的权重,只需在GAP层的输出上训练一个线性SVM即可。
之后的部分翻译意义不大,多为应用领域的拓展, 见原文即可
# simple implementation of CAM in PyTorch for the networks such as ResNet, DenseNet, SqueezeNet, Inception
import io
import requests
from PIL import Image
from torchvision import models, transforms
from torch.autograd import Variable
from torch.nn import functional as F
import numpy as np
import cv2
import pdb
# input image
LABELS_URL = 'https://s3.amazonaws.com/outcome-blog/imagenet/labels.json'
IMG_URL = 'http://media.mlive.com/news_impact/photo/9933031-large.jpg'
# 使用本地的图片和下载到本地的labels.json文件
# LABELS_PATH = "labels.json"
# networks such as googlenet, resnet, densenet already use global average pooling at the end, so CAM could be used directly.
model_id = 1
# 选择使用的网络
if model_id == 1:
net = models.squeezenet1_1(pretrained=True)
finalconv_name = 'features' # this is the last conv layer of the network
elif model_id == 2:
net = models.resnet18(pretrained=True)
finalconv_name = 'layer4'
elif model_id == 3:
net = models.densenet161(pretrained=True)
finalconv_name = 'features'
# 有固定参数的作用,如norm的参数
net.eval()
# 获取特定层的feature map
# hook the feature extractor
features_blobs = []
def hook_feature(module, input, output):
features_blobs.append(output.data.cpu().numpy())
net._modules.get(finalconv_name).register_forward_hook(hook_feature)
# 得到softmax weight
# get the softmax weight
params = list(net.parameters()) # 将参数变换为列表
weight_softmax = np.squeeze(params[-2].data.numpy())# 提取softmax层的参数
# 生成CAM图的函数,完成权重和feature相乘操作
def returnCAM(feature_conv, weight_softmax, class_idx):
# generate the class activation maps upsample to 256x256
size_upsample = (256, 256)
# 获取feature_conv特征的尺寸
bz, nc, h, w = feature_conv.shape
output_cam = []
# class_idx为预测分值较大的类别的数字表示的数组,一张图片中有N类物体则数组中N个元素
for idx in class_idx:
# weight_softmax中预测为第idx类的参数w乘以feature_map(为了相乘,故reshape了map的形状)
cam = weight_softmax[idx].dot(feature_conv.reshape((nc, h*w)))
# 将feature_map的形状reshape回去
cam = cam.reshape(h, w)
# 归一化操作(最小的值为0,最大的为1)
cam = cam - np.min(cam)
cam_img = cam / np.max(cam)
# 转换为图片的255的数据
cam_img = np.uint8(255 * cam_img)
# resize 图片尺寸与输入图片一致
output_cam.append(cv2.resize(cam_img, size_upsample))
return output_cam
# 数据处理,先缩放尺寸到(224*224),再变换数据类型为tensor,最后normalize
normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
preprocess = transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
normalize
])
# 通过requests库获取图片并保存,若是本地图片则只需要设置本地图片的保存地址,以便后来提取便好
img_path = "sample.jpg"
img_pil = Image.open(img_path)
# get the input image
# response = requests.get(IMG_URL)
# img_pil = Image.open(io.BytesIO(response.content))
# img_pil.save('test.jpg')
# 将图片数据处理成所需要的可用的数据
img_tensor = preprocess(img_pil)
# 处理图片为Variable数据
img_variable = Variable(img_tensor.unsqueeze(0))
# 将图片输入网络得到预测类别分值
logit = net(img_variable)
# download the imagenet category list
# 下载imageNet 分类标签列表,并存储在classes中(数字类别,类别名称)
classes = {
int(key):value for (key, value)
in requests.get(LABELS_URL).json().items()
}
# # 使用本地的 LABELS_PATH
# with open(LABELS_PATH) as f:
# data = json.load(f).items()
# classes = {int(key):value for (key, value) in data}
# 使用softmax打分
h_x = F.softmax(logit, dim=1).data.squeeze()
# 对分类的预测类别分值排序,输出预测值和在列表中的位置
probs, idx = h_x.sort(0, True)
# 转换数据类型
probs = probs.numpy()
idx = idx.numpy()
# 输出预测分值排名在前五的五个类别的预测分值和对应类别名称
# output the prediction
for i in range(0, 5):
print('{:.3f} -> {}'.format(probs[i], classes[idx[i]]))
# generate class activation mapping for the top1 prediction
# 输出与图片尺寸一致的CAM图片
CAMs = returnCAM(features_blobs[0], weight_softmax, [idx[0]])
# render the CAM and output
print('output CAM.jpg for the top1 prediction: %s'%classes[idx[0]])
# 将图片和CAM拼接在一起展示定位结果结果
img = cv2.imread('sample.jpg')
height, width, _ = img.shape
# 生成热度图
heatmap = cv2.applyColorMap(cv2.resize(CAMs[0],(width, height)), cv2.COLORMAP_JET)
result = heatmap * 0.3 + img * 0.5
cv2.imwrite('CAM.jpg', result)