在大部分人看来,卷积神经网络是一种黑盒技术,通过理论推导,以及梯度传播,去不断逼近局部最优解。这也导致人们对于神经网络研究进展的缓慢,因为这种黑盒模型无法给出研究人员进行改进的思路。所幸的是,近几年来的论文,也从常规的神经网络结构转向神经网络可视化,目的是让我们能看见卷积神经网络“学习”到了什么,神经网络是怎么判别物体的类别
今天就要为大家介绍一种卷积神经网络可视化的技巧,它发表于一篇论文,名叫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')
下面我来解释各个函数的意思
这个函数很简单,读取模型h5文件
这个函数调用了keras的图像包
先对进来的图像进行缩放
转换成数组形式
而通常张量里面还有一个batch_size维数
所以我这里调用了numpy的expand_dims函数来给图像数组增加一个维度,注意这里要设置axis=0,因为keras图像数据格式第一维就是batch_size维
keras另外还自带了一个preprocess函数,对图像进行一个标准化操作
这里就是我们的重点了
preds先利用模型predict函数对图片进行预测,获取他的输出张量
然后我们利用argmax找出张量中,输出值最大的index索引
这里就是找模型预测可能性最大的那个索引
然后output就根据索引,取得对应的张量
last_layer这里我们调用了get_layer函数,传入你要的对应层的层名,这里我们通常要的是最后一层卷积层的结果,所以理所当然在后续调用里面,传入的是最后一层卷积层的名字
然后我们调用keras的后端来求梯度
因为last_layer 是一个层,我们需要调用其output属性才能获得它输出的张量,然后我们根据这个张量对输出output求得梯度
pooled_gradient这里是在0, 1, 2维度上求一个平均,即最后返回的是一个大小为通道数的梯度张量(因为是对整张特征图进行池化)
iterate这里也是调用keras的backend后端,对一个函数进行实例化,即执行我们前面定义的操作
最后我们获得的是梯度,也就是对应的权重
我们利用一个for循环进行加权
这个函数就是绘制热力图
首先对热力图矩阵做一些预处理,除去负值,进行归一化
然后缩放到原图的大小
然后用opencv自带的applycolormap函数对其做一个色图的转换
最后我们乘以0.4,这是一个热力图强度因子,你调的越大,它叠加的就更多
然后与原图进行累加,保存到你指定的路径
经过热力图叠加之后
这个特征很抽象,它似乎是根据这朵花的花瓣底下以及根茎来进行判别
我认为这个热力图的可视化还是很不错的,能比较清晰的展现出来神经网络对某一区域的响应程度
但目前我发现的一个bug是
当你训练的模型准确度非常高,接近过拟合的情况
你softmax的结果就是一堆0和一个1
这时候再调用后端函数进行求梯度会失败
具体原因目前还不明确