前言
Grad-CAM论文传送门:arxiv.org/abs/1610.02...
Grad-CAM,全名为Gradient-weighted Class Activation Mapping,中文名叫梯度加权类激活映射。 是一种用于理解卷积神经网络决策的可视化技术,简单来说,它可以帮助我们"看到"神经网络在做出决策时,到底关注了图像的哪些部分,也就是卷积层的注意力部分呗
此项技术可以应用在图像分类,图像转文字,视觉问题回答等任何特定任务的网络
Grad-CAM无疑是一把解锁神经网络黑箱的钥匙。通过它,可以更直观地理解模型的内部工作机制,从而优化模型、提高性能(这句是水文章)
Grad-CAM 可以用来解释深度网络中任何卷积层,但我们只专注于解释最后一层卷积层
python
import cv2
import numpy as np
import tensorflow as tf
from tensorflow import keras
from matplotlib import pyplot as plt
加载数据集
数据集来自Kaggle - Fruits Dataset (Images),该图像数据集展示了9种流行的水果,包括苹果、香蕉、樱桃、奇库(chickoo)、葡萄、奇异果、芒果、橙子和草莓。每种水果有40个图像,并具有不同的尺寸
在数据预处理中,将图像尺寸调整为(224, 224)
,且进行归一化处理
python
path = '/kaggle/input/fruits-dataset-images/images'
batch_size = 64
target_size = (224, 224)
# 定义数据增强
data_generator = keras.preprocessing.image.ImageDataGenerator(
rescale=1.0 / 255,
validation_split=0.2,
)
# 读取、预处理练集数据
train_set = data_generator.flow_from_directory(
directory=path,
target_size=target_size,
batch_size=batch_size,
class_mode='categorical',
color_mode='rgb',
shuffle=True,
subset='training'
)
# 读取、预处理验证集数据
valid_set = data_generator.flow_from_directory(
directory=path,
target_size=target_size,
batch_size=batch_size,
class_mode='categorical',
color_mode='rgb',
shuffle=True,
subset='validation'
)
构建模型
采用以下方式,以提高模型学习能力与速度
- 使用了卷积层堆叠
- 使用了L2权重正则化
- 在激活函数上,使用
ELU
代替ReLU
最后一层卷积层命名为conv
,便于后期提取该层的输出。虽然在论文中指出relu
的Grad-CAM效果更好,但奈不住elu
的训练更快啊
python
model = keras.Sequential([
keras.layers.Conv2D(64, (5, 5), activation='elu'),
keras.layers.Conv2D(64, (1, 1), activation='elu'),
keras.layers.MaxPool2D(pool_size=(3, 3), strides=2),
keras.layers.Conv2D(128, (3, 3), activation='elu'),
keras.layers.Conv2D(128, (1, 1), activation='elu'),
keras.layers.Conv2D(128, (1, 1), activation='elu'),
keras.layers.MaxPool2D(pool_size=(3, 3), strides=2),
keras.layers.Conv2D(256, (3, 3), activation='elu'),
keras.layers.Conv2D(256, (1, 1), activation='elu'),
keras.layers.Conv2D(256, (1, 1), activation='elu'),
keras.layers.MaxPool2D(pool_size=(3, 3), strides=2),
keras.layers.Conv2D(512, (3, 3), activation='elu'),
keras.layers.Conv2D(512, (1, 1), activation='elu', name='conv'),
keras.layers.MaxPool2D(pool_size=(3, 3), strides=1),
keras.layers.GlobalAveragePooling2D(),
keras.layers.Dropout(rate=0.5),
keras.layers.Dense(512, activation='elu'),
keras.layers.Dropout(rate=0.5),
keras.layers.Dense(9, activation='softmax'),
])
model.build(input_shape=(None, 224, 224, 3))
编译模型
学习率使用了指数衰减(ExponentialDecay)方式,使模型在训练后期"学"得更加稳定
python
initial_learning_rate = 0.001
lr_schedule = keras.optimizers.schedules.ExponentialDecay(initial_learning_rate,
decay_steps=30,
decay_rate=0.76,
staircase=True)
optimizer = keras.optimizers.Adam(learning_rate=lr_schedule)
loss = keras.losses.CategoricalCrossentropy()
model.compile(optimizer=optimizer,
loss=loss,
metrics=['accuracy'])
模型 Run!
Model 启动!!!
python
model.fit(train_set, epochs=40, validation_data=valid_set)
40个epochs的训练结果如下图所示。虽然val_accuracy
才80%,垃的一批,但没有出现最头疼的过拟合
Grad-CAM
上面水了好多好多,至此,终于到了真真正正的正文了
python
# 获取图片、标签
images, labels = train_set.next()
# 随机选取 10 张图片
index = np.random.choice(images.shape[0], 10)
images = images[index]
# 提取最后一层卷积层
conv_layer = model.get_layer('conv')
# 获取卷积层与输出层的输出,便于后期的输出对卷积层求导
gard_model = keras.Model(inputs=model.inputs, outputs=[conv_layer.output, model.output])
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> α k c = 1 Z ∑ i ∑ j ⏞ g l o b a l a v e r a g e p o o l i n g ∂ y c ∂ A i j k ⏟ g r a d i e n t s v i a b a c k p r o p \alpha_{k}^{c} = \overbrace{\frac{1}{Z} \sum_{i} \sum_{j}}^{\scriptsize global\ average\ pooling} \underbrace{\frac{\partial y^{c}}{\partial A_{ij}^{k}}}_{\scriptsize gradients\ via\ backprop} </math>αkc=Z1i∑j∑ global average poolinggradients via backprop ∂Aijk∂yc
输出层输出为 <math xmlns="http://www.w3.org/1998/Math/MathML"> y c y^{c} </math>yc,卷积层输出为 <math xmlns="http://www.w3.org/1998/Math/MathML"> A i j k A_{ij}^{k} </math>Aijk。先计算模型输出对卷积层的导数,再对导数进行全局平均池化处理,最终获得神经元重要性权重 <math xmlns="http://www.w3.org/1998/Math/MathML"> α k c \alpha_{k}^{c} </math>αkc
python
with tf.GradientTape() as tape:
conv_output, pred = gard_model(images)
# 获取输出层的最大值
pred = tf.reduce_max(pred, axis=-1)
# 输出层对卷积层求导
grad = tape.gradient(pred, conv_output)
# 对卷积层梯度求全局平均值,作为每个维度的权重
weights = keras.layers.GlobalAvgPool2D(keepdims=True)(grad)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L G r a d − C A M c = R e L U ( ∑ k α k c A k ) ⏟ l i n e a r c o m b i n a t i o n L_{Grad-CAM}^{c}=\underbrace{ReLU \left( \sum_{k}\alpha_{k}^{c} A^{k} \right)}_{linear\ combination} </math>LGrad−CAMc=linear combination ReLU(k∑αkcAk)
前向传播神经元重要性权重与卷积层输出的加权组合,并使用ReLU去除负梯度,如果没有ReLU,注意力在定位上可能会效果欠佳
python
# 去除负梯度
L = tf.nn.relu(weights * conv_output)
# 对512个维度求平均值
heatmaps = np.mean(L, axis=-1)
# 图像归一化
heatmaps = heatmaps / heatmaps.max()
显示注意力热力图
python
# float32 转 uint8 格式,便于后期图片合并
images = np.uint8(255 * images)
heatmaps = np.uint8(255 * heatmaps)
heatmaps = 255 - heatmaps
fig, axes = plt.subplots(5, 6, dpi=166)
num = 0
for j in range(5):
for i in [0, 3]:
img = images[num]
heatmap = heatmaps[num]
axes[j][0 + i].imshow(img)
axes[j][0 + i].axis('off')
# 修改热力图尺寸
heatmap = cv2.resize(heatmap, (224, 224))
# 灰度图渲染成热力图
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
axes[j][1 + i].imshow(heatmap)
axes[j][1 + i].axis('off')
# 原图与热力图合并
img_add_heatmap = cv2.addWeighted(img, 0.5, heatmap, 0.5, 0)
axes[j][2 + i].imshow(img_add_heatmap)
axes[j][2 + i].axis('off')
num += 1
plt.subplots_adjust(wspace=0.01, hspace=0.01)
plt.show()
效果如下所示,在对于只有单个类别物体的图像中,Grad-CAM可以有不错的效果,但对于有一个图像中存在多个相同类别的物体嘛,就一言难尽咯......