一、两种类型的分类模型
为了更好的解释CAM和Grad-cam,这里先介绍两种类型的分类模型。feature extraction+Flatten+softmax和feature extraction+GAP+softmax。
以下均以VGG16为例(其他模型原理一样),在做完卷积激活池化操作后,每张图像特征提取可得到7x7x512大小的特征图,为了在全连接层作分类,需要将提取的特征图(三维)转化成一维的特征。这两种类型分类模型的唯一差异就在将三维特征转化成一维特征的方式,
1、特征提取(feature extraction)+flatten+softmax
这种方式使用keras的flatten层将三维特征直接从左向右排列成一维向量(类似于np.flatten())。例如将7x7x512的特征图flatten后拉成了25088长的一维向量。然后接全连接层(FCN,可看到多层感知机分类器),最后经过softmax层输出每类的概率。
2、 feature extraction+GAP+softmax
Global average pooling(GAP)是论文Network In Network[1]提出的,主要为了解决全连接层参数过多,不易训练且容易过拟合等问题,例如VGG16网络模型的参数90%左右的参数是全连接层的参数。用GAP替换flatten操作将三维特征转化成一维向量。GAP层实现非常简单,就是一个与特征图大小相同的平均池化层。例如7x7x512的特征图,使用7x7大小的平均池化层,池化后可得到512长的一维向量。这里可以对比一下这两种方式的参数量,假设全连接层节点个数1024个,flatten的参数量是25088x1024。而GAP的参数量是512x1024。这个差异还是很大的。
思考一个问题,经过GAP层后7x7x512的参数只有512个,会不会参数个数太少而导致分类效果不好,效果肯定很好这是不可能的,毕竟gap也不是万金油。只能说对大多数分类任务来说不会因为做了gap让特征变少而让模型性能下降。因为GAP层是一个非线性操作层,这512个特征相当于是从7x7x512经过非线性变化选择出来的强特征,这里注意是选择(特征组合,7x7x512个特征都有参与)而并非是直接从7x7x512拿出512个特征。(个人理解)
二、CAM和Grad-cam
CAM和Grad-cam都是求如下图右侧的结果。可视化模型根据图像的那个区域判别该图是猫。可以帮组我们理解模型学习的特征。
而求改图只需要得到heatmap(上图左侧),将heatmap叠加到原图即可。代码如下。cam和Grad-cam也就是要求heatmap。
import cv2
superimposed_img = cv2.addWeighted(img,0.6,heatmap,0.4,0)
CAM和Grad-cam求heatmap的具体流程如下:
1)求图像经过特征提取后最后一次卷积后得到的特征图(也就是VGG16 conv5_3的特征图(7x7x512))
2)512张feature map在全连接层分类的权重肯定不同,利用反向传播求出每张特征图的权重。注意cam和Grad-cam的不同就在于求每张特征图权重的方式。其他流程都一样
3)用每张特征图乘以权重得到带权重的特征图(7x7x512),在第三维求均值得到7x7的map(np.mean(axis=-1)),relu激活,归一化处理(避免有些值不在0-255范围内)。
该步最重要的是relu激活(relu只保留大于0的值),relu后只保留该类别有用的特征。正数认为是该类别有用的特征,负数是其他类别的特征(或无用特征)。如下图,假设某类别最后加权后为0.8965,类别值越大则是该类别的概率就越高,那么属于该类别的特征既为wx值大于0的特征。小于0的特征可能是其他类的特征。通俗理解,假如图像中出现一个猫头,那么该特征在猫类别中为正特征,在狗类别中为负特征,要增加猫的置信度,降低狗的置信度。
假如不加relu激活的话,heatmap代表着多类别的特征,论文中是这样概述的:如果没有relu,定位图谱显示的不仅仅是某一类的特征。而是所有类别的特征。
4) 将处理后的heatmap放缩到图像尺寸大小,便于与图像加权
从流程中可看到,cam和Grad-cam不同之处在于求每个特征图的权重的方式,这也是这两个方法的亮点之处,(其他部分,如求特征图我们在可视化卷积特征也常做,CAM和Grad-cam只是给这些特征加上权重,突出显示我们关注的重点),下面分别介绍cam和Grad-cam求特征图权重的方式,并给出各自的实现的keras版本源码。
1、CAM
CAM方法要求模型必须使用GAP层。 因为GAP层使模型具有更强的可解释性,GAP后每张特征图对应一个节点(下图中红色的节点),CAM的思想是把该节点的权重w作为该特征图的权重。具体做法是选择softmax层值最大的节点反向传播,求GAP层的梯度作为(7x7x512)特征图的权重。
CAM实现源码,
# -*- coding=utf-8 -*-
"""
Created on 2019-6-19 21:39:53
@author: fangsh.suk
"""
import keras
import cv2
import numpy as np
import keras.backend as K
from keras.applications.resnet50 import preprocess_input
from keras.preprocessing.image import load_img,img_to_array
K.set_learning_phase(1) #set learning phase
weight_file_dir = '/data/sfang/logo_classify/keras_model/checkpoint/best_0617.hdf5'
img_path = '/data/sfang/logo_classify/keras_model/error_analyez/0614/0/09219.png'
model = keras.models.load_model(weight_file_dir)
image = load_img(img_path,target_size=(224,224))
x = img_to_array(image)
x = np.expand_dims(x,axis=0)
x = preprocess_input(x)
pred = model.predict(x)
class_idx = np.argmax(pred[0])
class_output = model.output[:,class_idx]
last_conv_layer = model.get_layer("block5_conv3")
gap_weights = model.get_layer("global_average_pooling2d_1")
grads = K.gradients(class_output,gap_weights.output)[0]
iterate = K.function([model.input],[grads,last_conv_layer.output[0]])
pooled_grads_value, conv_layer_output_value = iterate([x])
pooled_grads_value = np.squeeze(pooled_grads_value,axis=0)
for i in range(512):
conv_layer_output_value[:,:,i] *= pooled_grads_value[i]
heatmap = np.mean(conv_layer_output_value, axis=-1)
heatmap = np.maximum(heatmap,0)#relu激活。
heatmap /= np.max(heatmap)
#
img = cv2.imread(img_path)
img = cv2.resize(img,dsize=(224,224),interpolation=cv2.INTER_NEAREST)
# img = img_to_array(image)
heatmap = cv2.resize(heatmap,(img.shape[1],img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap,cv2.COLORMAP_JET)
superimposed_img = cv2.addWeighted(img,0.6,heatmap,0.4,0)
cv2.imshow('Grad-cam',superimposed_img)
cv2.waitKey(0)
2、Grad-cam
CAM要求模型必须使用GAP,也就是要想使用CAM必须要重构模型,重新训练;并且对于某些任务GAP并不适用。Grad-cam实现了不改变模型结构的求定位图谱的方法。具体流程还是上述的流程,只是巧妙的求出来每张特征图的权重。
Grad-cam的思想是选择softmax值最大的节点(对应置信度最高的类别)反向传播,对最后一层卷基层求梯度,每张特征图的梯度的均值作为该张特征图的权重。同时,论文中给出证明,当特征映射与输出有链接权重时,Grad-cam求得的权重与CAM的一样,这里不给出具体的公司推导,感兴趣的自行去看论文。
实现源码。与CAM仅仅在求特征图权重部分不一样。
需要根据自己情况修改的部分在注释中备注
# -*- coding=utf-8 -*-
"""
Created on 2019-6-19 21:39:53
@author: fangsh.suk
"""
import keras
import cv2
import numpy as np
import keras.backend as K
from keras.applications.resnet50 import preprocess_input
from keras.preprocessing.image import load_img,img_to_array
K.set_learning_phase(1) #set learning phase
#需根据自己情况修改1.训练好的模型路径和图像路径
weight_file_dir = '/data/sfang/logo_classify/keras_model/checkpoint/best_0617.hdf5'
img_path = '/data/sfang/logo_classify/keras_model/error_analyez/0614/0/09219.png'
model = keras.models.load_model(weight_file_dir)
image = load_img(img_path,target_size=(224,224))
x = img_to_array(image)
x = np.expand_dims(x,axis=0)
x = preprocess_input(x)
pred = model.predict(x)
class_idx = np.argmax(pred[0])
class_output = model.output[:,class_idx]
#需根据自己情况修改2. 把block5_conv3改成自己模型最后一层卷积层的名字
last_conv_layer = model.get_layer("block5_conv3")
grads = K.gradients(class_output,last_conv_layer.output)[0]
pooled_grads = K.mean(grads,axis=(0,1,2))
iterate = K.function([model.input],[pooled_grads,last_conv_layer.output[0]])
pooled_grads_value, conv_layer_output_value = iterate([x])
##需根据自己情况修改3. 512是我最后一层卷基层的通道数,根据自己情况修改
for i in range(512):
conv_layer_output_value[:,:,i] *= pooled_grads_value[i]
heatmap = np.mean(conv_layer_output_value, axis=-1)
heatmap = np.maximum(heatmap,0)
heatmap /= np.max(heatmap)
img = cv2.imread(img_path)
img = cv2.resize(img,dsize=(224,224),interpolation=cv2.INTER_NEAREST)
# img = img_to_array(image)
heatmap = cv2.resize(heatmap,(img.shape[1],img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap,cv2.COLORMAP_JET)
superimposed_img = cv2.addWeighted(img,0.6,heatmap,0.4,0)
cv2.imshow('Grad-cam',superimposed_img)
cv2.waitKey(0)