cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)

这篇博客主要是想和大家分享一下我学习卷积神经网络可视化之后的总结和心得。学习完卷积神经网络的大致流程之后,会感觉到它和其他深度学习网络一样,像个“黑盒子”。我们只知道它有几层,每层有多少个通道,是如何连接的。但是我们不知道他们是怎么处理接收的数据。想进一步了解,却又不想接触底层的算法?其实“眼见为实”可以让我们对卷积神经网络有更充分的认识。话不多说,本文将通过三个不同的视角来让卷积神经网络”可视化“!
(1)可视化卷积神经网络的中间输出(中间激活):这里的可视化中间输出是可以让我们看清每一层的每一个通道输出的图片都是什么样的
(2)可视化卷积神经网络的过滤器:通过可视化过滤器,我们可以了解是什么样的视觉模式和视觉概念最容易让过滤器接受。
(3)可视化图像中类激活的热力图:通过可视化热力图,我们可以了解卷积神经网络在识别分类图像的依据是什么(图像的哪个部分或者特征),从而定位到图像中的物体(位置信息)。
对于第一种可视化方法,我们采用一个小的卷积神经网络进行分析(只有简单的几层,方便理解)。后面两种我们采用经典的卷积神经网络模型VGG16进行分析。
(1)可视化卷积神经网络的中间输出(中间激活) 【cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)】对于给定的输入数据,我们将展示各个卷积层和池化层的输出特征图(激活函数的输出)。
首先让我们先来看看此次用到的小型模型的结构吧。(如果想实践又懒得敲代码,我将这个简单的模型代码放在文章的最下面啦)
cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

有了模型,我们需要一个”实验品“。就选只小猫吧。我们通过代码将其可视化看看(如下图)

#显示测试图像 from keras.preprocessing import image img_path = '/home/hjl/Desktop/D/kaggle_original_data/dogs-vs-cats/train/cat.365.jpg' img = image.load_img(img_path,target_size=(150,150)) img_tensor = image.img_to_array(img) img_tensor = np.expand_dims(img_tensor,axis=0) img_tensor /= 255. plt.imshow(img_tensor[0]) plt.show()

cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

步骤一:将模型实例化,提取前8层的特征图输出。并将第九通道(随便选的,好看一点哈哈哈)可视化(如下图)
from keras import models layer_output = [layer.output for layer in model.layers[:8]] #提取前八层输出 activation_model = models.Model(inputs=model.input,outputs=layer_output) activations = activation_model.predict(img_tensor) # 返回8个层的输出组成的Nummpy数组所组层的列表 plt.matshow(activations[0][0,:,:,9],cmap='viridis')

cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

展示一个这个通道似乎是对猫(有三只猫)的轮廓进行检测(轮廓检测器)。
步骤二:接下来我们将3层的通道(只取前64个,超过了就不展示了)都显示出来看看。经过处理,效果将变得十分直观。
layers_name = [layer.name for layer in model.layers[:8]] # 前八层的名字 images_per_row = 16 #平铺在一张大图像的列数 for layer_name, layer_activation in zip(layers_name,activations): n_feature = layer_activation.shape[-1] size = layer_activation.shape[1] # 一张图像的长/宽 cols = n_feature // images_per_row display_grid = np.zeros((size*cols,size*images_per_row)) #平铺在一张大图像的行数 for col in range(cols): for row in range(images_per_row): channel_image = layer_activation[0,:,:,col*images_per_row+row] #接下来几行是对图像美化,方便观看 channel_image -= channel_image.mean() channel_image /= channel_image.std() channel_image *= 64 channel_image += 128 channel_image = np.clip(channel_image,0,255).astype('uint8') display_grid[col * size:(col + 1) * size, row * size:(row + 1) * size] = channel_image scale = 1./ size plt.figure(figsize=(scale * display_grid.shape[1],scale * display_grid.shape[0])) plt.grid(False) plt.imshow(display_grid,aspect='auto',cmap='viridis')

cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

直到最后一层(第八层),是这样的。cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

我们可以观察到一些现象从而应正我们之前所学的结论。
(1)第一层,大多是检测轮廓。这一层包含了原始图像中的所有信息。随着层数增高,我们越来越看不懂(变得抽象了)。比如最后一层的其中一张可能是一张识别“猫耳朵”的数据。层数越深,关于原始图像的信息就越少,关于分类的信息就更多。
(2)层数越到后面,图像中的”空位“就越多。这里我们称为“稀疏度”越来越高。也就是说输入图像找不到关于这些过滤器所编码的模式了。
阶段总结: 随着层数加深,提取的特征也越来越抽象。层数越高,包含输入信息越来越少,目标信息越来越多。这种感觉就像是在过滤,过滤对目标检测没有用的信息。而卷积神经网络就像是信息蒸馏管道。这和我们人类的视觉对物体的反映其实很像,我们第一眼一般是记住物体的轮廓,而不是细节。

(2)可视化卷积神经网络的过滤器 从(1)中我们已经了解到每一层,每一个管道的输出是什么样子的了。那么(2)中可视化过滤器是什么意思呢?我们知道通道输出的模样,但是我们不知道他们是如何判断的,是什么样的视觉模式让通道响应最剧烈?所以,可视化过滤器,就是可视化它的“最大响应图像”。我们将使用VGG16模型,这里如果想要实践可能会遇到VGG16导入的一些报错,可以参考我的这篇博客Keras导入VGG16模型(notop与top版本都有)时遇到文件缺失的报错(内附下载链接)_新手村霸的博客-CSDN博客
我们将通过梯度下降法作用于卷积神经网络输入的图像值,让过滤器的响应最大化。如果想要了解梯度下降可以参考我的这篇博客(我为自己代言哈哈哈)梯度下降法原理解析(大白话+公式推理)_新手村霸的博客-CSDN博客
步骤一:构建损失函数,计算梯度,通过随机梯度下降让损失最大化(没错就是最大化,这样才表明过滤器响应最大),得到可以生成过滤器可视化的函数。
#导入模型 model = VGG16(weights='imagenet',include_top='False')# 这是一个将张量转换为有效图像的实用函数 def deprocess_image(x): x -= x.mean() x /= (x.std() + 1e-5) x *= 0.1 x += 0.5 x = np.clip(x,0,1) x *= 255 x = np.clip(x,0,255).astype('uint8') return x# 生成过滤器可视化函数 def generate_pattern(layer_name, filter_index, size=64): layer_output = model.get_layer(layer_name).output loss = K.mean(layer_output[:,:,:,filter_index]) #构建损失函数 grads = K.gradients(loss,model.input)[0] #图像梯度 grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5) #对图像梯度进行标准化,1e-5是防止除数为0 interate = K.function([model.input],[loss,grads]) #创建函数 input_img_data = https://www.it610.com/article/np.random.random((1,size,size,3)) * 20 + 128. #这里建立输入带有噪声的图像 step = 1. #布长为1 for i in range(40): # 进行40次迭代(梯度上升) loss_value, gards_value = interate([input_img_data]) input_img_data += gards_value * stepimg = input_img_data[0] return deprocess_image(img)

我们可以尝试看一下‘block3_conv1’的第0个通道的最大响应模式图
plt.imshow(generate_pattern('block3_conv1',0))

cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片


看起来像是波尔卡点图案,别着急。
步骤二:我们将剩下的过滤器的最大响应图也展示出来看看。
layer_name = 'block1_conv1' size = 64 margin = 5 results = np.zeros((8 * size + 7 * margin, 8 * size + 7 * margin,3)) # 空图像,用于保存结果 #这里我们将所有图片放在一个网格里,方便观察 for i in range(8): #遍历网格的行 for j in range(8): #遍历网格的列 filter_img = generate_pattern(layer_name, i + (j * 8), size=size) #将结果放到results的第(i,j)个方块中 horizental_start = i * size + i * margin horizental_end = horizental_start + size vertical_start = j * size + j * margin vertical_end = size + vertical_start results[horizental_start:horizental_end,vertical_start:vertical_end,:] = filter_img results /= 255.plt.figure(figsize=(20,20)) plt.imshow(results)



cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

(左上图1)第一层(block1_conv1)的过滤器对应的响应图,这对应简单的方向边缘和颜色。
block2_conv1可能对应的就是边缘组合而成的简单纹理,更高层(block5_conv1,左上图2)可能就对应于‘羽毛’,‘眼睛’之类的。这些图像就是能让过滤器响应最大化的图像啦。

(3)可视化类激活的热力图 最后一个可视化部分就最直接啦。它可以让我们知道图像的哪一部分让卷积神经网络做出了最终的决策(就决定是你啦)。这还有助于对卷积神经网络的决策过程进行尝试,特别是出现了分类错误的情况下。它还可以对图像中的目标进行定位。
这种技术叫类激活图(CAM)可视化。举个栗子,在猫狗分类中,CAM可视化可以生成类别为“猫”的热力图,表示图像的各个部分与猫的相似程度。当然狗也可以哈哈哈
CAM大白话理解就是:用“每个通道对类别的重要程度”对“输入图像对不同通道的激活强度”的空间图进行加权,从而得到了“输入图像对类别的激活强度”。(多读几遍简单理解一下哈)
步骤一:为VGG16模型预处理一张图像(我选了一张非洲大象的图片)
cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

from keras.applications.vgg16 import VGG16,preprocess_input,decode_predictions from keras.preprocessing import image import numpy as npimg_path = '/home/hjl/Desktop/elephant.jpg' model = VGG16(weights='/home/hjl/.keras/datasets/vgg16_weights_tf_dim_ordering_tf_kernels.h5')img = image.load_img(img_path,target_size=(224,224)) x = image.img_to_array(img) x = np.expand_dims(x,axis=0) x = preprocess_input(x)

步骤二:输入数据,并且预测前三个类别,并找到概率最高的识别结果对应的元素编号
preds = model.predict(x) print(decode_predictions(preds,top=3)[0]) #元素编号 print(np.argmax(preds[0])) # 结果如下 [('n02504458', 'African_elephant', 0.7770794), ('n01871265', 'tusker', 0.21156743), ('n02504013', 'Indian_elephant', 0.010981791)] #元素编号 386

步骤三:使用CAM算法生成热力图
from keras import backend as K african_elephant_output = model.output[:,386] last_conv_layer = model.get_layer('block5_conv3')grads = K.gradients(african_elephant_output,last_conv_layer.output)[0] # 非洲象类别对于block5_conv3输出特征图的梯度 pooled_grads = K.mean(grads,axis=(0,1,2))# 形状为(512,)的向量,每个元素是特定特征图通道的梯度平均大小 iterate = K.function([model.input],[pooled_grads,last_conv_layer.output[0]]) pooled_grads_value, conv_layer_output_value = https://www.it610.com/article/iterate([x]) 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) plt.matshow(heatmap)

cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片

步骤四:将热力图与原图合并
cnn|“看得见的”卷积神经网络(图文并茂+代码解读)(卷积神经网络可视化)
文章图片


它似乎对大象的耳朵的激活强度比较大,这就是它区别与其他类似类别(比如其他地方的大象)的关键目标。

不知道有多少人看完了呢。希望卷积神经网络的可视化能加深你对卷积神经网络的认知。感谢阅读哈。

    推荐阅读