前言
深度学习是一个“黑盒系统”,它通过“end-end”的方式来工作,图像数据作为输入,输出类别标签、回归值等信息,中间过程不可见。如何才能打开“黑盒”,一探究竟,让“黑盒”变成“灰盒”,甚至“白盒”?于是有了“深度学习可解释性”这一研究领域,而 CAM 技术就是其中之一,其利用“特征可视化”来探究深度卷积神经网络的工作机制和判断依据。
CAM的概念
CAM(Class Activation Mapping,类别激活映射图),亦称为类别热力图或显著性图。它的大小与原图一致,像素值表示原始图片的对应区域对预测输出的影响程度,值越大贡献越大。
像素值的取值范围从0到1,一般也用0~255的灰度图表示。示例:
cam_gray = np.uint8(255 * cam) # 转换为0~255的灰度图
为了更直观的表达,可更进一步将灰度图转换为彩色图。示例:
cam_color = cv2.applyColorMap(cam_gray, cv2.COLORMAP_HSV) # 转换成伪彩色图
一般用热力图和原图叠加的形式进行呈现,如下所示:
CAM 的作用
CAM 的作用:
- 1)有助于理解和分析神经网络的工作原理及决策过程,进而更好地选择或设计网络。
不同模型对同一张图的 CAM 是有差异的,同一个模型的不同训练过程的 CAM 也是有差异的。参考 CAM 我们可以对设计的网络提出更高的要求:不但关注预测准确率,还可关注网络是否提取到我们需要的特征。例如对于两个模型 A、B,若它们的 accuracy 一致,但从 CAM 上看到 A 相对 B 提取更多所需的特征(高亮区域更集中在目标附近),那么我们判断模型 A 更好一些。 - 2)利用可视化的信息引导网络更好的学习。
例如可利用 CAM 信息通过"擦除"或"裁剪"的方式对数据进行增强。 - 3)利用 CAM 作为原始的种子,进行弱监督语义分割或弱监督定位。
由于 CAM 能够覆盖到目标物体,因此仅利用分类标注也可用来完成语义分割或目标检测任务,这极大程度降低了标注的工作量。当然,对分类网络的 CAM 精度的要求很高,不然误差相对较大。
CAM 的获取步骤
总结 CAM 的获取步骤:
- 1)提取需要可视化的特征图,例如尺寸为512*7*7的张量;
- 2)获取该张量的每个 channel 的权重,即长度为512的向量;
- 3)通过线性融合的方式,将该张量在 channel 维度上加权求和,获取尺寸为7*7的 map;
- 4)对该 map 进行归一化,并通过插值的方式 resize 成和原图一样的尺寸。
利用 GAP 获取 CAM
GAP(Global Average Pooling,全局平均池化操作),可直接将 C*W*H 的特征图转换成 C*1*1。分别选用 resnet18、resnet50、densenet121 三种不同的模型,结合 hook 机制获取 CAM:
import numpy as np
from torchvision import models, transforms
import cv2
from PIL import Image
from torch.nn import functional as F
# 定义预训练模型: resnet18、resnet50、densenet121
resnet18 = models.resnet18(pretrained=True)
resnet50 = models.resnet50(pretrained=True)
densenet121 = models.densenet121(pretrained=True)
resnet18.eval()
resnet50.eval()
densenet121.eval()
# 图片数据转换
image_transform = transforms.Compose([
# 将输入图片resize成统一尺寸
transforms.Resize([224, 224]),
# 将PIL Image或numpy.ndarray转换为tensor,并除255归一化到[0,1]之间
transforms.ToTensor(),
# 标准化处理-->转换为标准正太分布,使模型更容易收敛
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
# =====注册hook start=====
feature_data = []
def feature_hook(model, input, output):
feature_data.append(output.data.numpy())
resnet18._modules.get('layer4').register_forward_hook(feature_hook)
resnet50._modules.get('layer4').register_forward_hook(feature_hook)
densenet121._modules.get('features').register_forward_hook(feature_hook)
# =====注册hook end=====
# 获取fc层的权重
fc_weights_resnet18 = resnet18._modules.get('fc').weight.data.numpy()
fc_weights_resnet50 = resnet50._modules.get('fc').weight.data.numpy()
fc_weights_densenet121 = densenet121._modules.get('classifier').weight.data.numpy()
# 获取预测类别id
image = image_transform(Image.open("cat.jpg")).unsqueeze(0)
out_resnet18 = resnet18(image)
out_resnet50 = resnet50(image)
out_densenet121 = densenet121(image)
predict_classes_id_resnet18 = np.argmax(F.softmax(out_resnet18, dim=1).data.numpy())
predict_classes_id_resnet50 = np.argmax(F.softmax(out_resnet50, dim=1).data.numpy())
predict_classes_id_densenet121 = np.argmax(F.softmax(out_densenet121, dim=1).data.numpy())
# =====获取CAM start=====
def makeCAM(feature, weights, classes_id):
print(feature.shape, weights.shape, classes_id)
# batchsize, C, h, w
bz, nc, h, w = feature.shape
# (512,) @ (512, 7*7) = (49,)
cam = weights[classes_id].dot(feature.reshape(nc, h * w))
cam = cam.reshape(h, w) # (7, 7)
# 归一化到[0, 1]之间
cam = (cam - cam.min()) / (cam.max() - cam.min())
# 转换为0~255的灰度图
cam_gray = np.uint8(255 * cam)
# 最后,上采样操作,与网络输入的尺寸一致,并返回
return cv2.resize(cam_gray, (224, 224))
cam_gray_resnet18 = makeCAM(feature_data[0], fc_weights_resnet18, predict_classes_id_resnet18)
cam_gray_resnet50 = makeCAM(feature_data[1], fc_weights_resnet50, predict_classes_id_resnet50)
cam_gray_densenet121 = makeCAM(feature_data[2], fc_weights_densenet121, predict_classes_id_densenet121)
# =====获取CAM start=====
# =====叠加CAM和原图,并保存图片=====
# 1)读取原图
src_image = cv2.imread("cat.jpg")
h, w, _ = src_image.shape
# 2)cam转换成与原图大小一致的彩色度(cv2.COLORMAP_HSV为彩色图的其中一种类型)
cam_color_resnet18 = cv2.applyColorMap(cv2.resize(cam_gray_resnet18, (w, h)),
cv2.COLORMAP_HSV)
cam_color_resnet50 = cv2.applyColorMap(cv2.resize(cam_gray_resnet50, (w, h)),
cv2.COLORMAP_HSV)
cam_color_densenet121 = cv2.applyColorMap(cv2.resize(cam_gray_densenet121, (w, h)),
cv2.COLORMAP_HSV)
# 3)合并cam和原图,并保存
cam_resnet18 = src_image * 0.5 + cam_color_resnet18 * 0.5
cam_resnet50 = src_image * 0.5 + cam_color_resnet50 * 0.5
cam_densenet121 = src_image * 0.5 + cam_color_densenet121 * 0.5
cam_hstack = np.hstack((src_image, cam_resnet18, cam_resnet50, cam_densenet121))
cv2.imwrite("cam_hstack.jpg", cam_hstack)
# 可视化
Image.open("cam_hstack.jpg").show()
最终的可视化效果如下图所示(从左到右依次是原图、cam_resnet18、cam_resnet50、cam_densenet121):
可见,不同的模型得到的 CAM 效果不同,从覆盖区域来看,densenet121 的效果相对更好一些。
更通用的获取 CAM 的方法——Grad-CAM
利用 GAP 获取 CAM 的方式有它的局限性:
- 1)要求模型必须有 GAP 层;
- 2)只能提取最后一层特征图的热力图。
Grad-CAM 是为了克服上面的缺陷而提出的,Grad-CAM 可适用于非 GAP 的网络结构,并且可提取任意层的热力图。
网友评论