神经网络为什么觉得你的猫是猫:神奇的类激活图CAM技术(附Pytorch实现代码)

1 引言

对于一般的分类而言,我们将数据导入神经网络,神经网络告诉我们这是哪一类,而对于一些必要情况下,我们更需要知道神经网络如何做出判断,输入数据的哪些参数影响着神经网络的判断。于是,Bolei Zhou等人发表的论文Learning Deep Features for Discriminative Localization提出了类激活图技术,告诉我们为什么神经网络觉得你的猫是一只猫。

2 类激活图的原理

原论文中的解释为:

请添加图片描述

我们对卷积特征图执行全局平均池化,并将其用作产生所需输出(分类或其他)的全连接层的特征。给定这种简单的连通性结构,我们可以通过将输出层的权重映射到卷积特征来识别图像区域的重要性,这种技术我们称为类激活映射。

如图所示,全局平均汇集输出最后一个卷积层的每个单元的特征图的空间平均值。这些值的加权和用于生成最终输出。类似地,我们计算最后一个卷积层的特征图的加权和,以获得我们的类激活图。

2.1 公式计算

现在有一张图片,我们使用 f k ( x , y ) f_k(x,y) fk(x,y)表示在最后一个卷积层中,位置 ( x , y ) (x,y) (x,y)在第 k k k个激活图的特征,然后执行全局平均池化GAP我们知。道,对于 c ∗ n ∗ n c*n*n cnn的特征图(每张特征图 n ∗ n n*n nn,共有 c c c张),执行平均池化后只剩下 c ∗ 1 ∗ 1 c*1*1 c11,相当于把每张特征图中所有的点求和后取平均。因此,在GAP后我们得到 F k = ∑ x , y f k ( x , y ) F^k = \sum_{x,y}f_k(x,y) Fk=x,yfk(x,y) (注意论文中该公式没有取平均)。对于一个给定的类 c c c,在最后进行 s o f t m a x softmax softmax激活之前是输入为 S c = ∑ k w k c F k S_c = \sum_kw_k^cF_k Sc=kwkcFk w k c w_k^c wkc是一组权重代表对于一个类c而言,每个特征图 k k k的重要程度,也就是最后全连接层的权重。最后使用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) 。在论文中,最后一层没有使用 b i a s bias bias,因为对于分类的性能几乎没有影响。

总而言之,类别 c c c的输出为:
S c = ∑ k w k c ∑ x , y f k ( x , y ) = ∑ x , y ∑ k w k c f k ( x , y ) S_c = \displaystyle\sum_k w_k^c\displaystyle\sum_{x,y}f_k(x,y) = \sum_{x,y}\displaystyle\sum_k w_k^cf_k(x,y) Sc=kwkcx,yfk(x,y)=x,ykwkcfk(x,y)
对于类别c中而言,某个位置 ( x , y ) (x,y) (x,y)的输出为 M c ( x , y ) = ∑ k w k c f k ( x , y ) M_c(x,y) = \displaystyle\sum_kw_k^cf_k(x,y) Mc(x,y)=kwkcfk(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)

从这个公式来看,类激活图CAM就是将最后一层卷积层中每个特征图按照不同的权重叠加到一起,生成一个加权特征图。

2.2 通俗理解

在上述论文中,类激活图使用了GAP(全局平均池化),它省去了全连接层,减少了参数,还能防止过拟合。如下图所示,图源CSDN中的一篇博客

请添加图片描述

在一个卷积神经网络中,我们卷积到最后可能会有许多的特征图,GAP之前的卷积层告诉我们网络最后的每个特征图是什么样的。对于GAP之后的全连接层而言,它通过一组权重计算得出哪几个特征图对识别猫更有利,我们直接把属于猫(在多分类中可能会有n个类)的权重矩阵和所有的特征图拿出来进行相乘,就可以知道神经网络认为哪些特征图能帮助它识别一只猫,这边是CAM的原理所在。

我们最后的目的是将多个特征图按照权重叠加到一起,生成一个加权特征图,这个加权特征图是一张不同颜色深度的图片,然后将该特征图覆盖在原图上,用不同颜色表示每个像素点对于分类的重要程度,也表示神经网络的关注点所在。

举个例子,在猫狗分类中,我们现在有一张猫的图片,经过网络后再最后一层得到了512×7的特征图,也就是说有512个7*7的特征图,然后我们将这些特征图经过GAP后连接到最后带 s o f t m a x softmax softmax激活的全连接层中分类,这个全连接层的权重的维度为512×2,在这里,我们只去属于猫类的权重(论文中的类别c),即512个权重,与之前的特征图点乘得到7×7的特征图,这7×7的特征图代表了最后一层卷积层中所有特征图的加权和,然后我们将这个加权特征图使用opencv转化为热力图,叠加在原始图片上,即用不同深浅的颜色表示不同像素点对分类的贡献。

用我不太精湛的画技大概是这样一个过程,图中的加权和表示使用opencv转化为热力图后的加权特征图,希望能帮助大家理解:
请添加图片描述

3 代码实现

首先,我们需要准备猫狗识别的数据集:百度网盘 ,提取码:fxvv

为了便于实现,我简单地调用了ResNet18来训练猫狗识别网络,而且没有使用GPU进行训练,如需使用请添加使用GPU代码。

from PIL import Image
import torch
import torch.nn as nn
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torchvision import models,transforms
from torch.nn import functional as F
import  pandas as pd
import  os

#数据处理
def data_process(train_path):
    img_list = os.listdir(train_path)
    train_df = pd.DataFrame()
    train_df['img'] = img_list
    train_df['img'] = train_df['img'].apply(lambda x : train_path+'/'+x)
    train_df['label'] = train_df['img'].apply(lambda x : 0 if x.split('/')[-1][:3] == 'cat' else 1)
    return train_df['img'].values ,train_df['label'].values

#转化为pytorch格式
class Cat_Dog_Data(Dataset):
    def __init__(self,img,label,transform):
        self.img = img
        self.label = label
        self.transform = transform

    def __getitem__(self, index):
        img = self.img[index]
        label = self.label[index]
        image = Image.open(img).convert('RGB')
        label = int(self.label[index])
        if self.transform is not None:
            image = self.transform(image)
        label = torch.tensor(label).long()
        return image, label

    def __len__(self):
        return len(self.label)

if __name__ == '__main__':
    train_path = r'./train/train'
    data_img,data_label = data_process(train_path)
    transform = transforms.Compose([
        transforms.Scale((224,224)),
        transforms.ToTensor(),
    ])
    train_data = Cat_Dog_Data(data_img,data_label,transform)
    train_data = DataLoader(train_data,batch_size = 64,shuffle=True)
    model = models.resnet18(pretrained=False)
    model.fc = nn.Linear(512,2)
    criterion = nn.CrossEntropyLoss()
    optim = torch.optim.Adam(params=model.parameters(), lr = 0.01)
    max_epoch = 1
    model.train()
    for epoch in range(max_epoch):
        for i,(img,label) in enumerate(train_data):
            out = model(img)
            loss = criterion(out,label)
            optim.zero_grad()
            loss.backward()
            optim.step()
            print('Epoch:{}/{} , batch:{}/{} , loss:{:.6f}'.format(epoch,max_epoch,i,len(train_data),loss))
        torch.save(model,'cat_dog_model.pth')

在训练好网络后,就可以进行提取网络中最后一层卷积层的特征图和全连接层的权重进行计算CAM了,代码参考了 CSDN ‘’鬼扯子‘同学’的一篇博客,稍加修改并添加了注释,具体过程:

  1. 读取网络
  2. 提取GAP之前的卷积特征
  3. 提取最后的全连接层权值
  4. 将卷积特征与权重点乘得到特征图加权和
  5. 将4中的加权和放缩为图片CAM
  6. 将CAM图与原图叠加到一起保存输出
from PIL import Image
import torch
import torch.nn as nn
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from torchvision import models,transforms
from torch.nn import functional as F
import  pandas as pd
import numpy as np
import cv2
import  os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" #不加这句话点乘会报错
#读取模型
model_ft = torch.load('cat_dog_model.pth', map_location=lambda  storage, loc:storage)

#获取GAP之前的特征以及全连接层的权重
model_features = nn.Sequential(*list(model_ft.children())[:-2])
#.children()返回网络外层元素,[:-2]即倒数第二层,也就是网络只到达最后一个卷积层
fc_weights = model_ft.state_dict()['fc.weight'].cpu().numpy()  # 维度[2,512],最后全连接层的权重
class_ = {0:'cat', 1:'dog'}
model_ft.eval()
model_features.eval()

#读取图片
img_path = r'C:\Users\lenovo\Desktop\CAM\train\train\dog.5966.jpg'
features_blobs = []
img = Image.open(img_path).convert('RGB')
transform = transforms.Compose([
    # transforms.Scale((224, 224)),
    transforms.ToTensor(),
])
img_tensor = transform(img).unsqueeze(0) #图片维度[1,3,224,224]

#图片导入网络
features = model_features(img_tensor).detach().cpu().numpy() #获取GAP之前的特征
logit = model_ft(img_tensor) #获取模型最后的输出概率
h_x = F.softmax(logit, dim=1).data.squeeze() #导入softmax获取概率大小
probs,idx = torch.max(h_x).numpy(),torch.argmax(h_x).numpy() #最大概率

#计算CAM
bs, c, h, w = features.shape #batch_size , channel ,height , width
features = features.reshape((c, h*w)) #512,49
cam = fc_weights[idx].dot(features) #
cam = cam.reshape(h, w)
cam_img = (cam - cam.min()) / (cam.max() - cam.min())  #将cam的值缩减到0-1之间
cam_img = np.uint8(255 * cam_img) #将cam的值放大到0-255

#将图片转化为热力图
img = cv2.imread(img_path)
height, width, _ = img.shape  #读取输入图片的尺寸
heatmap = cv2.applyColorMap(cv2.resize(cam_img, (width, height)), cv2.COLORMAP_JET)  #CAM resize match input image size

result = heatmap * 0.5 + img * 0.5    #两张图片相加的比例
text = '%s %.2f%%' % (class_[int(idx)], probs*100) 				
cv2.putText(result, text, (0, 50), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.9,
            color=(123, 222, 238), thickness=2, lineType=cv2.LINE_AA)

CAM_RESULT_PATH = r'./CAM_Photo//'   #把结果存下来
if not os.path.exists(CAM_RESULT_PATH):
    os.mkdir(CAM_RESULT_PATH)
image_name_ = img_path.split('\\')[-1].split('.')[0] + img_path.split('\\')[-1].split('.')[1]
cv2.imwrite(CAM_RESULT_PATH + image_name_ + '_' + 'pred_' + class_[int(idx)] + '.jpg', result)  #存储

4.结果

正确的结果:

请添加图片描述

请添加图片描述

错误的结果:

请添加图片描述

请添加图片描述

结果分析:由于猫狗在毛发,形态等有相似之处,所以神经网络的分类结果并不是太理想,对于猫而言,神经网络分类较为正确,CAM也基本上能关注到猫本身,而对于狗而言,经常会与猫混淆,而且CAM会关注到一些背景,如草地,笼子等。

5 结语与联系方式

CAM我是在人体骨架动作识别领域了解到的(参考文献2),作者通过将人体骨架节点和时间作为x和y来推断神经网络在训练过程中着重关注哪些关节点(或者说哪些关节点对于人体骨架动作识别更有利),从而使用多个流,每个流都只输入上一流没有关注到的节点,即CAM分数低的节点,来迫使神经网络关注到更多的信息。可见CAM技术不止局限于图像领域,本文主要是我对于CAM的一些理解以及实现,能力有限,如有不对欢迎评论区或者私信向我指出:1759412770@qq.com,zn1759412770@163.com

6 参考文献

[1] Zhou B , Khosla A , Lapedriza A , et al. Learning Deep Features for Discriminative Localization[C]// CVPR. IEEE Computer Society, 2016.

[2] Song Y F , Zhang Z , Shan C , et al. Richly Activated Graph Convolutional Network for Robust Skeleton-based Action Recognition[J]. IEEE Transactions on Circuits and Systems for Video Technology, 2020, PP(99):1-1.

  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是基于自己创建的CNN回归模型的Grad-CAM可视化激活的热力PyTorch实现代码: ```python import torch import torch.nn.functional as F import cv2 import numpy as np class GradCAM: def __init__(self, model, target_layer): self.model = model self.target_layer = target_layer self.feature_maps = None self.gradient = None self.activation_maps = None def forward(self, x): self.feature_maps = [] self.gradient = [] for name, module in self.model.named_modules(): x = module(x) if name == self.target_layer: x.register_hook(self.gradient_hook) self.feature_maps.append(x) elif "conv" in name: self.feature_maps.append(x) self.activation_maps = x return self.activation_maps def gradient_hook(self, grad): self.gradient.append(grad) def backward(self): gradient = self.gradient[0] feature_maps = self.feature_maps[-1] batch_size, channel, height, width = feature_maps.shape weights = F.adaptive_avg_pool2d(gradient, (1, 1)) weights = weights.view(batch_size, channel) activation_maps = feature_maps.view(batch_size, channel, -1) weights = weights.unsqueeze(-1) cam = torch.bmm(activation_maps, weights) cam = F.relu(cam) cam = cam.view(batch_size, 1, height, width) cam = F.interpolate(cam, size=(224, 224), mode='bilinear', align_corners=False) cam = cam.squeeze() return cam.detach().cpu().numpy() model = YourCNNModel() gradcam = GradCAM(model, 'conv2') # target_layer is the layer you want to visualize # input image img = cv2.imread('image.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (224, 224)) img = np.transpose(img, (2, 0, 1)) img = np.expand_dims(img, axis=0) img = torch.from_numpy(img).float() # forward pass and backward pass output = model(img) cam = gradcam.forward(img) cam = gradcam.backward() # plot the heat map plt.imshow(img.squeeze().permute(1, 2, 0)) plt.imshow(cam, alpha=0.5, cmap='jet') plt.show() ``` 其中,`GradCAM`实现了Grad-CAM算法的前向传播和反向传播,`model`是你自己创建的CNN回归模型,`target_layer`是你想要可视化的卷积层名称。在使用时,需要将输入像转换为PyTorch张量,然后调用`gradcam.forward()`得到卷积层的特征和梯度,再调用`gradcam.backward()`得到激活热力。最后,将输入像和激活热力叠加在一起即可得到可视化结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锌a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值