卷积神经网络的可视化——热力图Grad CAM

前言

在大部分人看来,卷积神经网络是一种黑盒技术,通过理论推导,以及梯度传播,去不断逼近局部最优解。这也导致人们对于神经网络研究进展的缓慢,因为这种黑盒模型无法给出研究人员进行改进的思路。所幸的是,近几年来的论文,也从常规的神经网络结构转向神经网络可视化,目的是让我们能看见卷积神经网络“学习”到了什么,神经网络是怎么判别物体的类别

今天就要为大家介绍一种卷积神经网络可视化的技巧,它发表于一篇论文,名叫Grad CAM,通过热力图来对神经网络进行可视化。

简介

热力图通常是用来对类别进行划分的图像,它有点像红外成像图,温度高的地方就很红,温度低的部分就呈现蓝色。同理,我们使用热力图可以以权重的形式来展现,神经网络对图片的哪一部分激活值最大。比如输入到猫狗分类,如果卷积神经网络判别是猫,那它的热力图普遍分散在猫身上,而至于它是根据猫的哪一部分来判别,就利用了热力图的原理

原理

我们卷积神经网络进行分类,最后一层通常是softmax层,其最大值对应的就是分类类别

我们从这个最大概率分类类别的节点出发,进行反向传播,对最后一层卷积层求得梯度,然后对每一张特征图求出均值,最后我们取出 最后一层卷积层的激活值,与前面我们对梯度特征图的均值进行相乘,这个过程可以理解为,每个通道的重要程度与我们卷积激活值进行相乘,就相当于是一个加权操作。最后根据这个乘积值生成一个热力图,与原图进行叠加。

代码实现

这里笔者参考的是deep learning with python。环境为最新的keras框架
模型是自己简单训练出来的一个vgg16模型,只不过将对应的卷积层改为depthwise Separable卷积,全连接层也调小了很多,最后做的是一个多分类(五个类别)

先看一下我自己实现的vgg16代码

from keras import layers
from keras import models
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator

base_dir = './train_data/'

train_data_gen = ImageDataGenerator(rescale=1./255,
                                    rotation_range=30,
                                    horizontal_flip=True)
train_generator = train_data_gen.flow_from_directory(
    base_dir,
    target_size=(224, 224),
    batch_size=20,
    class_mode='categorical'
)

model = models.Sequential()
model.add(layers.SeparableConv2D(64, 3, padding='SAME', activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.SeparableConv2D(128, 3, padding='SAME', activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.BatchNormalization())
model.add(layers.SeparableConv2D(256, 3, padding='SAME', activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.BatchNormalization())
model.add(layers.SeparableConv2D(512, 3, padding='SAME', activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.SeparableConv2D(512, 3, padding='SAME', activation='relu'))
model.add(layers.MaxPooling2D(pool_size=(2, 2)))
model.add(layers.SeparableConv2D(512, 3, padding='SAME', activation='relu'))
model.add(layers.BatchNormalization())
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(5, activation='softmax'))

model.build(input_shape=(None, 224, 224, 3))
model.summary()

model.compile(loss='mean_squared_error',
              optimizer=optimizers.Adam(),
              metrics=['acc'])

history = model.fit_generator(
    train_generator,
    steps_per_epoch=20,
    epochs=15
)
model.save('five_flowers_categorical_vgg16.h5')

我这里通过keras的图像生成器加载目录下的训练图片
然后指定目标图像的大小,为224 224

最后将模型保存

现在我们正式开始进行热力图的实现
首先我把源代码都贴上来,这是我自己封装的一个代码

from keras import backend as K
from keras.models import load_model
from keras.preprocessing import image
from keras.utils import plot_model
from keras import Model
import numpy as np
import matplotlib.pyplot as plt
from keras.applications.vgg16 import preprocess_input
import cv2


def load_model_h5(model_file):
    """
    载入原始keras模型文件
    :param model_file: 模型文件,h5类型
    :return: 模型
    """
    return load_model(model_file)

def load_img_preprocess(img_path, target_size):
    """
    加载图片并进行预处理
    :param img_path: 图片文件名
           target_size: 要加载图片的缩放大小
                        这是一个tuple元组类型
    :return: 预处理过的图像文件
    """
    img = image.load_img(img_path, target_size=target_size)
    img = image.img_to_array(img) # 转换成数组形式
    img = np.expand_dims(img, axis=0) # 为图片增加一维batchsize,直接设置为1

    img = preprocess_input(img) # 对图像进行标准化

    return img

def gradient_compute(model, layername, img):
    """
    计算模型最后输出与你的layer的梯度
    并将每个特征图的梯度进行平均
    再将其与卷积层输出相乘
    :param model: 模型
    :param layername: 你想可视化热力的层名
    :param img: 预处理后的图像
    :return:
    卷积层与平均梯度相乘的输出值
    """
    preds = model.predict(img)
    idx = np.argmax(preds[0]) # 返回预测图片最大可能性的index索引

    output = model.output[:, idx] # 获取到我们对应索引的输出张量
    last_layer = model.get_layer(layername)

    grads = K.gradients(output, last_layer.output)[0]

    pooled_grads = K.mean(grads, axis=(0, 1, 2)) # 对每张梯度特征图进行平均,
                                                 # 返回的是一个大小是通道维数的张量
    iterate = K.function([model.input], [pooled_grads, last_layer.output[0]])

    pooled_grads_value, conv_layer_output_value = iterate([img])

    for i in range(pooled_grads.shape[0]):
        conv_layer_output_value[:, :, i] *= pooled_grads_value[i]

    return conv_layer_output_value

def plot_heatmap(conv_layer_output_value, img_in_path, img_out_path):
    """
    绘制热力图
    :param conv_layer_output_value: 卷积层输出值
    :param img_in_path: 输入图像的路径
    :param img_out_path: 输出热力图的路径
    :return:
    """
    heatmap = np.mean(conv_layer_output_value, axis=-1)
    heatmap = np.maximum(heatmap, 0)
    heatmap /= np.max(heatmap)

    img = cv2.imread(img_in_path)
    heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
    heatmap = np.uint8(255 * heatmap)

    heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
    superimopsed_img = heatmap * 0.4 + img

    cv2.imwrite(img_out_path, superimopsed_img)

img_path = r'./train_data/daisy/4534460263_8e9611db3c_n.jpg'
model_path = r'./five_flowers_categorical_vgg16.h5'
layername = r'separable_conv2d_6'

img = load_img_preprocess(img_path, (224, 224))
model = load_model_h5(model_path)

conv_value = gradient_compute(model, layername, img)
plot_heatmap(conv_value, img_path, './packagetest.jpg')

下面我来解释各个函数的意思

load_model_h5

这个函数很简单,读取模型h5文件

load_img_preprocess

这个函数调用了keras的图像包
先对进来的图像进行缩放
转换成数组形式
而通常张量里面还有一个batch_size维数

所以我这里调用了numpy的expand_dims函数来给图像数组增加一个维度,注意这里要设置axis=0,因为keras图像数据格式第一维就是batch_size维

keras另外还自带了一个preprocess函数,对图像进行一个标准化操作

gradient_compute

这里就是我们的重点了

preds先利用模型predict函数对图片进行预测,获取他的输出张量
然后我们利用argmax找出张量中,输出值最大的index索引

这里就是找模型预测可能性最大的那个索引

然后output就根据索引,取得对应的张量

last_layer这里我们调用了get_layer函数,传入你要的对应层的层名,这里我们通常要的是最后一层卷积层的结果,所以理所当然在后续调用里面,传入的是最后一层卷积层的名字

然后我们调用keras的后端来求梯度
卷积神经网络的可视化——热力图Grad CAM_第1张图片
因为last_layer 是一个层,我们需要调用其output属性才能获得它输出的张量,然后我们根据这个张量对输出output求得梯度

pooled_gradient这里是在0, 1, 2维度上求一个平均,即最后返回的是一个大小为通道数的梯度张量(因为是对整张特征图进行池化)

iterate这里也是调用keras的backend后端,对一个函数进行实例化,即执行我们前面定义的操作

最后我们获得的是梯度,也就是对应的权重
我们利用一个for循环进行加权

plot_heatmap

这个函数就是绘制热力图

首先对热力图矩阵做一些预处理,除去负值,进行归一化

然后缩放到原图的大小
然后用opencv自带的applycolormap函数对其做一个色图的转换

最后我们乘以0.4,这是一个热力图强度因子,你调的越大,它叠加的就更多
然后与原图进行累加,保存到你指定的路径

最终结果展示

卷积神经网络的可视化——热力图Grad CAM_第2张图片
这是我原始输入的图像

经过热力图叠加之后
卷积神经网络的可视化——热力图Grad CAM_第3张图片
这个特征很抽象,它似乎是根据这朵花的花瓣底下以及根茎来进行判别

总结

我认为这个热力图的可视化还是很不错的,能比较清晰的展现出来神经网络对某一区域的响应程度

但目前我发现的一个bug是
当你训练的模型准确度非常高,接近过拟合的情况
你softmax的结果就是一堆0和一个1

这时候再调用后端函数进行求梯度会失败
具体原因目前还不明确

你可能感兴趣的:(Keras,机器学习)