“你的神经网络是如何生成这个结果的?”这个问题也曾让许多数据科学家陷入了困境。其实,让我们去解释一个层数较少的简单神经网络工作原理并不难,但是当我们将计算机视觉项目中的神经网络层数增加到1000层时,它的可解释性就非常差了。
现实情况是,我们的用户或者终端需要可解释性——他们想知道我们的模型是如何得到最终的结果的。但是,深度神经网络的工作原理没有办法通过文字被清楚地描述出来,这个时候深度神经网络就被打上了“黑盒子”的标签,那我们该如何摆脱这个标签呢?
我们可以通过可视化来解决这个问题!当一个神经网络通过可视化的方式展示出来时,他的可解释性将会得到极大的提升,可视化可以将神经网络模型中处理数据的过程清晰展现。尤其当我们处理基于成千上万数据的卷积神经网络(CNN)时更是如此。
在本文中,我们将介绍多种用于可视化卷积神经网络的技术。 此外,我们还可以从这些可视化中加深观察信息,以调整我们的CNN模型。
内容表
1.我们为什么要使用可视化解码神经网络?
2.设置模型的体系结构
3.访问CNN的每个层
4.滤波器—可视化CNN的构成模块
5.最大化激活—可视化模型所期望的内容
6.遮挡贴图—可视化输入中的重要内容
7.显著性贴图—可视化输入特征的贡献
8.分类激活映射
9.可视化分层输出—可视化过程
我们为什么要使用可视化解码神经网络?
除可视化之外,有很多方法可以帮助我们去理解神经网络是如何工作的,那么这篇文章为什么要转向可视化这种非常规途径呢?
我们通过一个例子来回答这个问题。假设我们正在做一个对动物图片进行分类的项目,如分类雪豹与阿拉伯豹。从图片来看,我们可以使用图像背景来区分这两种动物。
两种动物的栖息地可以形成鲜明的对比。大多数雪豹图像的背景里都会有雪,而大多数阿拉伯豹图片里都会有一片茫茫沙漠。
问题来了——模型可以通过分类雪与沙漠的图像从而去分类雪豹与阿拉伯豹。那么,我们如何确保我们的模型正确地学习了这两种不同类型豹子的不同特征呢?可视化会给我们答案。
可视化有助于我们了解哪些特征正在指导模型对图像进行分类的决策。
将模型可视化有许多种方法,在本文中,我们将展示其中的一些。
设置模型的体系结构
实践是最好的学习方式之一。因此,我们立刻开始研究模型的代码。
在本文中,我们在ImageNet数据集上使用VGG16架构模型和预训练权重。首先我们先将模型程序导入并开始理解其架构。
我们将使用Keras中的'model.summary()'函数来可视化模型体系结构。在我们进入模型构建部分之前,这是非常重要的一步。我们需要确保输入和输出形状与我们的问题陈述相匹配,因此我们先可视化模型摘要。
importing required modules
from keras.applications import VGG16
loading the saved model
we are using the complete architecture thus include_top=True
model = VGG16(weights='imagenet',include_top=True)
show the summary of model
model.summary()
以下是上述代码生成的模型摘要:
我们有了模型的详细架构以及每层的可训练参数的数量。上面的输出可以多花一点时间去浏览,这样才能了解我们现有的数据情况。
我们仅训练模型层的一个子集(特征提取)是很重要的。 我们可以生成模型摘要,并确保不可训练参数的数量与我们不想训练的层匹配。
此外,我们可以使用可训练参数的总数来检查我们的GPU是否能够为训练模型分配足够的内存。 这对于我们大多数在个人电脑上工作的人来说,是一个比较熟悉的挑战!
访问CNN的每个层
既然我们知道如何获得模型的整体架构,让我们深入探索并尝试探索每个独立的层。
实际上,访问Keras模型的各个层并提取与每个层相关的参数非常容易。 这包括图层权重和其他信息,如滤波器的数量。
现在,我们将创建将图层名称映射到其相应特征和图层权重的字典:
creating a mapping of layer name ot layer details
we will create a dictionary layers_info which maps a layer name to its charcteristics
layers_info = {}
for i in model.layers:
layers_info[i.name] = i.get_config()
here the layer_weights dictionary will map every layer_name to its corresponding weights
layer_weights = {}
for i in model.layers:
layer_weights[i.name] = i.get_weights()
print(layers_info['block5_conv1'])
上面的代码给出了以下输出,它由block5_conv1层的不同参数组成:
{'name': 'block5_conv1',
'trainable': True,
'filters': 512,
'kernel_size': (3, 3),
'strides': (1, 1),
'padding': 'same',
'data_format': 'channels_last',
'dilation_rate': (1, 1),
'activation': 'relu',
'use_bias': True,
'kernel_initializer': {'class_name': 'VarianceScaling',
'config': {'scale': 1.0,
'mode': 'fan_avg',
'distribution': 'uniform',
'seed': None}},
'bias_initializer': {'class_name': 'Zeros', 'config': {}},
'kernel_regularizer': None,
'bias_regularizer': None,
'activity_regularizer': None,
'kernel_constraint': None,
'bias_constraint': None}
不知你有没有注意到图层'block5_conv1'的可训练参数是否为真? 这意味着我们可以通过进一步训练模型来更新图层权重。
滤波器—可视化CNN的构成模块
滤波器是任何卷积神经网络的基本构建模块。不同的滤波器从图像中提取不同类型的特征。下面的GIF图非常清楚地说明了这一点:
如图所示,每个卷积层都由多个滤波器组成。查看我们在上一节中生成的输出 - 'block5_conv1'层由512个滤波器组成。这是对应的,是吧?
让我们绘制每个VGG16块的第一个卷积层的第一个滤波器:
layers = model.layers
layer_ids = [1,4,7,11,15]
plot the filters
fig,ax = plt.subplots(nrows=1,ncols=5)
for i in range(5):
ax[i].imshow(layers[layer_ids[i]].get_weights()[0][:,:,:,0][:,:,0],cmap='gray')
ax[i].set_title('block'+str(i+1))
ax[i].set_xticks([])
ax[i].set_yticks([])
我们可以在上面的输出中看到不同层的滤波器。由于VGG16仅使用3×3滤波器,因此所有滤波器都具有相同的形状。
最大化激活—可视化模型所期望的内容
我们使用下面的图片来理解最大化激活的概念:
你认为对模型去识别大象来说哪些特征是重要的?下面是我想到的一些主要的特征:
- 象牙
- 象鼻
- 象耳
那就是我们在本能情况下如何识别大象的,对吧?现在,当我们看看当我们尝试优化任意分类大象图像的模型时,我们在过程中得到了什么。
我们知道CNN中的每个卷积层都在前一层的输出中寻找相似的模式。当输入由它正在寻找的模式组成时,卷积层的激活被最大化。
在最大化激活技术中,我们更新每层的输入,以便将最大化激活的损失降到最低。
我们是如何做到这一点的?我们通过计算相对于输入的激活损失函数的梯度,然后相应得更新输入:
下面是这个操作的代码:
importing the required modules
from vis.visualization import visualize_activation
from vis.utils import utils
from keras import activations
from keras import applications
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (18,6)
creating a VGG16 model using fully connected layers also because then we can
visualize the patterns for individual category
from keras.applications import VGG16
model = VGG16(weights='imagenet',include_top=True)
finding out the layer index using layer name
the find_layer_idx function accepts the model and name of layer as parameters and return the index of respective layer
layer_idx = utils.find_layer_idx(model,'predictions')
changing the activation of the layer to linear
model.layers[layer_idx].activation = activations.linear
applying modifications to the model
model = utils.apply_modifications(model)
Indian elephant
img3 = visualize_activation(model,layer_idx,filter_indices=385,max_iter=5000,verbose=True)
plt.imshow(img3)
我们的模型使用随机输入对印度象进行分类,生成了以下输出:
从上面的图像中,我们可以观察到该模型需要像牙齿,大眼睛和象牙这样的结构。现在,这些信息对于我们检查数据集的完整性非常重要。 因为印度象通常存在于长满树木或长草的栖息地中,所以模型可能会侧重于背景中的栖息地特征,而这是不对的。
可视化输入中的重要内容—Occlusion Maps(遮挡贴图)
激活最大化用于可视化在图像中的模型期望的输出。在另一方面,遮挡部分图,可以帮助我们找到对模型来说那个部分是重要的。
现在,为了了解遮挡图后的模型是如何工作的,我们想根据制造商对汽车进行分类的模型,如丰田,奥迪等:
你能知道是那家生产商制造了上面这辆车吗?很大程度上是不能的,因为放置公司标志的部分在图片里被遮挡了。我们是以分类为目的,所以被遮挡的这部分对我们来说很重要。
类似地,因为这样的遮挡图的出现,我们遮挡图片的某些部分,然后计算出它属于哪一类的概率。如果概率降低,则意味着图像的被遮挡部分对于该类是重要的。否则,这就是不重要的。
这里,我们根据图像每个部分的像素值做概率分配,然后将它标准化并生成热图:
import numpy as np
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Activation, Conv2D, MaxPooling2D
from keras.optimizers import Adam
from keras.callbacks import EarlyStopping, ModelCheckpoint
from keras.preprocessing.image import ImageDataGenerator
from keras.activations import relu
%matplotlib inline
import matplotlib.pyplot as plt
def iter_occlusion(image, size=8):
occlusion = np.full((size * 5, size * 5, 1), [0.5], np.float32)
occlusion_center = np.full((size, size, 1), [0.5], np.float32)
occlusion_padding = size * 2
print('padding...')
image_padded = np.pad(image, (
(occlusion_padding, occlusion_padding), (occlusion_padding, occlusion_padding), (0, 0) ), 'constant', constant_values = 0.0)
for y in range(occlusion_padding, image.shape[0] + occlusion_padding, size):
for x in range(occlusion_padding, image.shape[1] + occlusion_padding, size):
tmp = image_padded.copy()
tmp[y - occlusion_padding:y + occlusion_center.shape[0] + occlusion_padding, x - occlusion_padding:x + occlusion_center.shape[1] + occlusion_padding]
= occlusion
tmp[y:y + occlusion_center.shape[0], x:x + occlusion_center.shape[1]] = occlusion_center
yield x - occlusion_padding, y - occlusion_padding,
tmp[occlusion_padding:tmp.shape[0] - occlusion_padding, occlusion_padding:tmp.shape[1] - occlusion_padding]
上面的代码定义了一个函数iter_occlusion,它返回一个具有不同被遮挡部分的图像。
现在,让我们导入图像并绘制它:
from keras.preprocessing.image import load_img
load an image from file
image = load_img('car.jpeg', target_size=(224, 224))
plt.imshow(image)
plt.title('ORIGINAL IMAGE')
image
现在,我们将进行下面三个步骤:
- 预处理原图像
- 计算有不同遮挡部分的图片的概率
- 生成热图
from keras.preprocessing.image import img_to_array
from keras.applications.vgg16 import preprocess_input
convert the image pixels to a numpy array
image = img_to_array(image)
reshape data for the model
image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
prepare the image for the VGG model
image = preprocess_input(image)
predict the probability across all output classes
yhat = model.predict(image)
temp = image[0]
print(temp.shape)
heatmap = np.zeros((224,224))
correct_class = np.argmax(yhat)
for n,(x,y,image) in enumerate(iter_occlusion(temp,14)):
heatmap[x:x+14,y:y+14] = model.predict(image.reshape((1, image.shape[0], image.shape[1], image.shape[2])))[0][correct_class]
print(x,y,n,' - ',image.shape)
heatmap1 = heatmap/heatmap.max()
plt.imshow(heatmap)
image
这非常有意思。 我们现在将使用标准化的热图概率创建一个掩模并绘制它:
import http://skimage.io as io
creating mask from the standardised heatmap probabilities
mask = heatmap1 < 0.85
mask1 = mask *256
mask = mask.astype(int)
io.imshow(mask,cmap='gray')
最后,我们将遮挡码强加在输入图像上并绘制:
import cv2
read the image
image = cv2.imread('car.jpeg')
image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
resize image to appropriate dimensions
image = cv2.resize(image,(224,224))
mask = mask.astype('uint8')
apply the mask to the image
final = cv2.bitwise_and(image,image,mask = mask)
final = cv2.cvtColor(final,cv2.COLOR_BGR2RGB)
plot the final image
plt.imshow(final)
你能猜到为什么我们只看到图像的某些部分吗? 这其实是正确的 - 只有输入图像中对其输出类概率有重大贡献的那些部分才是可见的。 简而言之,这就是被遮挡图的全部内容。
可视化输入特征的贡献—显著性贴图
显著性图是另一种基于梯度的可视化技术。
显着图计算每个像素值对模型输出的影响。 这涉及计算输出相对于输入图像的每个像素的梯度。这告诉我们如何根据输入图像像素的微小变化输出类别变化。梯度的所有正值意味着像素值的微小变化将增加输出值:
这些梯度与图像形状相同(梯度是根据每个像素值计算的),为我们提供了直观的重点。让我们看看如何为任何图像生成显著性图。 首先,我们将使用以下代码段读取输入图像。
现在,我们将使用VGG16模型为图像生成显著性图:
Utility to search for layer index by name.
Alternatively we can specify this as -1 since it corresponds to the last layer.
layer_idx = utils.find_layer_idx(model, 'predictions')
Swap softmax with linear
model.layers[layer_idx].activation = activations.linear
model = utils.apply_modifications(model)
generating saliency map with unguided backprop
grads1 = visualize_saliency(model, layer_idx,filter_indices=None,seed_input=image)
plotting the unguided saliency map
plt.imshow(grads1,cmap='jet')
我们看到该模型更侧重于狗的面部部分。现在,让我们看看反向传播的结果:
generating saliency map with guided backprop
grads2 = visualize_saliency(model, layer_idx,filter_indices=None,seed_input=image,backprop_modifier='guided')
plotting the saliency map as heatmap
plt.imshow(grads2,cmap='jet')
引导反向传播将所有负梯度截断为0,这意味着仅更新对分类概率具有正影响的像素。
分类激活图(梯度加权)
分类激活图也是一种神经网络可视化技术,它基于根据激活图的梯度或它们对输出的贡献来权衡激活图这样的想法。
以下摘自Grad-CAM论文给出了该技术的要点:
梯度加权分类激活映射(Grad-CAM),使用任何目标概念的梯度(比如“狗”或甚至是标题的对数),流入最终的卷积层以生成粗略的定位图,突出显示重要区域中的重要区域。用于预测概念的图像。
从本质上来说,我们采用最后一层卷积层的特征映射,并使用相对于特征映射的输出的梯度对每个滤波器进行加权(乘)。 Grad-CAM涉及以下步骤:
1.获取最终卷积层的输出要素图。对于VGG16,此功能图的形状为14x14x512; 2.计算输出相对于要素图的梯度 3.将全局平均池化应用于梯度 4.将要素图与相应的池化梯度相乘 我们可以在下面看到输入图像及其对应的分类激活图:
现在,我们来给上面的图像生成分类激活图:
可视化过程—分层输出可视化
CNN的起始层通常寻找像边缘这样的低级特征。随着我们的深入,功能也会发生变化。
可视化模型的不同层的输出有助于我们看到在相应层突出显示图像的是哪些特征。此步骤对于针对我们的问题微调架构特别重要。为什么?因为我们可以看到哪些图层提供了哪种特征,然后决定我们要在模型中使用哪些图层。
例如,可视化图层输出可以帮助我们比较神经样式转移问题中不同层的性能。
让我们看看如何在VGG16模型的不同层获得输出:
importing required libraries and functions
from keras.models import Model
defining names of layers from which we will take the output
layer_names = ['block1_conv1','block2_conv1','block3_conv1','block4_conv2']
outputs = []
image = image.reshape((1, image.shape[0], image.shape[1], image.shape[2]))
extracting the output and appending to outputs
for layer_name in layer_names:
intermediate_layer_model = Model(inputs=model.input,outputs=model.get_layer(layer_name).output)
intermediate_output = intermediate_layer_model.predict(image)
outputs.append(intermediate_output)
plotting the outputs
fig,ax = plt.subplots(nrows=4,ncols=5,figsize=(20,20))
for i in range(4):
for z in range(5):
ax[i][z].imshow(outputs[i][0,:,:,z])
ax[i][z].set_title(layer_names[i])
ax[i][z].set_xticks([])
ax[i][z].set_yticks([])
plt.savefig('layerwise_output.jpg')
上图显示了VGG16的每一层从图像中提取的不同特征(模块5除外)。我们可以看到起始层对应于边缘等低级特征,而后面的层则看到汽车的车顶,排气等特征。
结语
可视化永远不会让我感到惊讶。有多种方法可以理解技术的工作原理,但可视化可以使它变得更加有趣。以下是您应该查看的几个资源:
- 神经网络中的特征提取过程是一个活跃的研究领域,并导致了Tensorspace和Activation Atlases等令人敬畏的工具的开发。
- TensorSpace也是一种支持多种模型格式的神经网络可视化工具。它允许您加载模型并以交互方式对其进行可视化。TensorSpace还有一个操作平台,可以使用多种架构进行可视化,您可以随意使用。