Grad-CAM:CNN看到了啥

前言

Grad-CAM论文传送门:http://arxiv.org/abs/1610.02391

Grad-CAM,全名为Gradient-weighted Class Activation Mapping,中文名叫梯度加权类激活映射。 是一种用于理解卷积神经网络决策的可视化技术,简单来说,它可以帮助我们“看到”神经网络在做出决策时,到底关注了图像的哪些部分,也就是卷积层的注意力部分呗

此项技术可以应用在图像分类,图像转文字,视觉问题回答等任何特定任务的网络

Grad-CAM无疑是一把解锁神经网络黑箱的钥匙。通过它,可以更直观地理解模型的内部工作机制,从而优化模型、提高性能(这句是水文章)

Grad-CAM 可以用来解释深度网络中任何卷积层,但我们只专注于解释最后一层卷积层

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),且进行归一化处理

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的训练更快啊

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)方式,使模型在训练后期“学”得更加稳定

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 启动!!!

model.fit(train_set, epochs=40, validation_data=valid_set)

40个epochs的训练结果如下图所示。虽然val_accuracy才80%,垃的一批,但没有出现最头疼的过拟合

2.png

Grad-CAM

上面水了好多好多,至此,终于到了真真正正的正文了

# 获取图片、标签
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])

α 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} αkc=Z1ij global average poolinggradients via backprop Aijkyc
输出层输出为 y c y^{c} yc,卷积层输出为 A i j k A_{ij}^{k} Aijk。先计算模型输出对卷积层的导数,再对导数进行全局平均池化处理,最终获得神经元重要性权重 α k c \alpha_{k}^{c} αkc

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)

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} LGradCAMc=linear combination ReLU(kαkcAk)
前向传播神经元重要性权重与卷积层输出的加权组合,并使用ReLU去除负梯度,如果没有ReLU,注意力在定位上可能会效果欠佳

# 去除负梯度
L = tf.nn.relu(weights * conv_output)
# 对512个维度求平均值
heatmaps = np.mean(L, axis=-1)
# 图像归一化
heatmaps = heatmaps / heatmaps.max()

显示注意力热力图

# 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可以有不错的效果,但对于有一个图像中存在多个相同类别的物体嘛,就一言难尽咯……

1.png
download.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

余将董道而不豫兮

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

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

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

打赏作者

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

抵扣说明:

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

余额充值