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 c∗n∗n的特征图(每张特征图 n ∗ n n*n n∗n,共有 c c c张),执行平均池化后只剩下 c ∗ 1 ∗ 1 c*1*1 c∗1∗1,相当于把每张特征图中所有的点求和后取平均。因此,在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=k∑wkcx,y∑fk(x,y)=x,y∑k∑wkcfk(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)=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)
从这个公式来看,类激活图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 ‘’鬼扯子‘同学’的一篇博客,稍加修改并添加了注释,具体过程:
- 读取网络
- 提取GAP之前的卷积特征
- 提取最后的全连接层权值
- 将卷积特征与权重点乘得到特征图加权和
- 将4中的加权和放缩为图片CAM
- 将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.