对于深度学习这种端到端模型来说,如何说明和理解其中的训练过程是大多数研究者关注热点之一,这个问题对于那种高风险行业显得尤为重视,比如医疗、军事等。在深度学习中,这个问题被称作“黑匣子(Black Box)”。如果不能解释模型的工作过程,我们怎么能够就轻易相信模型的输出结果呢?
以深度学习模型检测癌症肿瘤为例,该模型告诉你它能够检测出癌症的准确率高达99%,但它并没有告诉你它是如何工作并给出判断结果的。那么该模型是在核磁共振扫描片子中发现了重要线索吗?或者仅仅是将扫描结果上的污点错误地认为是肿瘤呢?模型的输出结果关系到病人的生死问题及治疗方案,医生是不能承担起这种错误的。
在本文中,将探讨如何可视化卷积神经网络(CNN),该网络在计算机视觉中使用最为广泛。首先了解CNN模型可视化的重要性,其次介绍可视化的几种方法,同时以一个用例帮助读者更好地理解模型可视化这一概念。
1.卷积神经网络模型可视化的重要性
正如上文中介绍的癌症肿瘤诊断案例所看到的,研究人员需要对所设计模型的工作原理及其功能掌握清楚,这点至关重要。一般而言,一名深度学习研究者应该记住以下几点:
1. 理解模型是如何工作的
2. 调整模型的参数
3. 找出模型失败的原因
4. 向消费者/终端用户或业务主管解释模型做出的决定
现在让我们看一个例子,可视化一个神经网络模型有助于理解其工作原理和提升模型性能。
曾几何时,美国陆军希望使用神经网络自动检测伪装的敌方坦克。研究人员使用50张迷彩坦克照片及50张树林照片来训练一个神经网络。使用有监督学习方法来训练模型,当研究人员训练好网络的参数后,网络模型能够对训练集做出正确的判断——50张迷彩坦克全都输出“Yes”,50张树林照片全都输出“No”。但是这并不能保证模型对于新的样本也能正确分类。聪明的是,研究人员最初拍摄了200张照片,其中包含了100张迷彩坦克照片、100张树木照片。从中分别选取50张照片合计100张照片作为训练集,剩余的100张照片作为测试集。结果发现,模型对测试集也能正确分类。因此,研究人员觉得模型没有问题了,就将最终成果交付给军方。原以为军方会很满意这份研究成果,结果军方做出的反馈是他们进行测试后发现效果并不好。
研究人员感觉此事有点蹊跷,为什么之前测试时百分百准确,而军方测试的时候又掉链子了呢?最后终于发现,原来是研究者的数据集出现了问题,采集迷彩坦克的时候是阴天,而采集树林的时候是晴天,神经网络最终学会的是区分晴天和阴天,而不是区分迷彩坦克和树林。这真是令人哭笑不得啊,那造成这个问题的主要原因还是没有弄清楚模型的具体的工作原理及其功能。
2.可视化CNN模型的方法
根据其内部的工作原理,大体上可以将CNN可视化方法分为以下三类:
1.初步方法:一种显示训练模型整体结构的简单方法
2.基于激活的方法:对单个或一组神经元的激活状态进行破译以了解其工作过程
3.基于梯度的方法:在训练过程中操作前向传播和后向传播形成的梯度
下面将具体介绍以上三种方法,所举例子是使用Keras深度学习库实现,另外本文使用的数据集是由“识别数字”竞赛提供。因此,读者想复现文中案例时,请确保安装好Kears以及执行了这些步骤。
1初步方法
1.1 绘制模型结构图
研究者能做的最简单的事情就是绘制出模型结构图,此外还可以标注神经网络中每层的形状及参数。在keras中,可以使用如下命令完成模型结构图的绘制:
- model.summary()
- _________________________________________________________________
- Layer (type) Output Shape Param #
- =================================================================
- conv2d_1 (Conv2D) (None, 26, 26, 32) 320
- _________________________________________________________________
- conv2d_2 (Conv2D) (None, 24, 24, 64) 18496
- _________________________________________________________________
- max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64) 0
- _________________________________________________________________
- dropout_1 (Dropout) (None, 12, 12, 64) 0
- _________________________________________________________________
- flatten_1 (Flatten) (None, 9216) 0
- _________________________________________________________________
- dense_1 (Dense) (None, 128) 1179776
- _________________________________________________________________
- dropout_2 (Dropout) (None, 128) 0
- _________________________________________________________________
- preds (Dense) (None, 10) 1290
- =================================================================
- Total params: 1,199,882
- Trainable params: 1,199,882
- Non-trainable params: 0
还可以用一个更富有创造力和表现力的方式呈现模型结构框图,可以使用keras.utils.vis_utils函数完成模型体系结构图的绘制。
1.2 可视化滤波器
另一种方法是绘制训练模型的过滤器,这样就可以了解这些过滤器的表现形式。例如,第一层的第一个过滤器看起来像:
- top_layer = model.layers[0]
- plt.imshow(top_layer.get_weights()[0][:, :, :, 0].squeeze(), cmap='gray')
一般来说,神经网络的底层主要是作为边缘检测器,当层数变深时,过滤器能够捕捉更加抽象的概念,比如人脸等。
2.激活方法
2.1 最大化激活
为了理解神经网络的工作过程,可以在输入图像上应用过滤器,然后绘制其卷积后的输出,这使得我们能够理解一个过滤器其特定的激活模式是什么。比如,下图是一个人脸过滤器,当输入图像是人脸图像时候,它就会被激活。
- from vis.visualization import visualize_activation
- from vis.utils import utils
- from keras import activations
- from matplotlib import pyplot as plt
- %matplotlib inline
- plt.rcParams['figure.figsize'] = (18, 6)
- # 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, 'preds')
- # Swap softmax with linear
- model.layers[layer_idx].activation = activations.linear
- model = utils.apply_modifications(model)
- # This is the output node we want to maximize.filter_idx = 0
- img = visualize_activation(model, layer_idx, filter_indices=filter_idx)
- plt.imshow(img[..., 0])
同理,可以将这个想法应用于所有的类别,并检查它们的模式会是什么样子。
- for output_idx in np.arange(10):
- # Lets turn off verbose output this time to avoid clutter and just see the output.
- img = visualize_activation(model, layer_idx, filter_indices=output_idx, input_range=(0., 1.))
- plt.figure()
- plt.title('Networks perception of {}'.format(output_idx))
- plt.imshow(img[..., 0])
2.2 图像遮挡
在图像分类问题中,可能会遇到目标物体被遮挡,有时候只有物体的一小部分可见的情况。基于图像遮挡的方法是通过一个灰色正方形系统地输入图像的不同部分并监视分类器的输出。这些例子清楚地表明模型在场景中定位对象时,若对象被遮挡,其分类正确的概率显著降低。
为了理解这一概念,可以从数据集中随机抽取图像,并尝试绘制该图的热图(heatmap)。这使得我们直观地了解图像的哪些部分对于该模型而言的重要性,以便对实际类别进行明确的区分。
- def iter_occlusion(image, size=8):
- # taken from https://www.kaggle.com/blargl/simple-occlusion-and-saliency-maps
- 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]i = 23 # for exampledata = val_x[i]correct_class = np.argmax(val_y[i])
- # input tensor for model.predictinp = data.reshape(1, 28, 28, 1)# image data for matplotlib's imshowimg = data.reshape(28, 28)
- # occlusionimg_size = img.shape[0]
- occlusion_size = 4print('occluding...')heatmap = np.zeros((img_size, img_size), np.float32)class_pixels = np.zeros((img_size, img_size), np.int16)
- from collections import defaultdict
- counters = defaultdict(int)for n, (x, y, img_float) in enumerate(iter_occlusion(data, size=occlusion_size)):
- X = img_float.reshape(1, 28, 28, 1)
- out = model.predict(X)
- #print('#{}: {} @ {} (correct class: {})'.format(n, np.argmax(out), np.amax(out), out[0][correct_class]))
- #print('x {} - {} | y {} - {}'.format(x, x + occlusion_size, y, y + occlusion_size))
- heatmap[y:y + occlusion_size, x:x + occlusion_size] = out[0][correct_class]
- class_pixels[y:y + occlusion_size, x:x + occlusion_size] = np.argmax(out)
- counters[np.argmax(out)] += 1
3. 基于梯度的方法
3.1 显著图
正如之前的坦克案例中看到的那样,怎么才能知道模型侧重于哪部分的预测呢?为此,可以使用显著图解决这个问题。显著图首先在这篇文章中被介绍。
使用显著图的概念相当直接——计算输出类别相对于输入图像的梯度。这应该告诉我们输出类别值对于输入图像像素中的微小变化是怎样变化的。梯度中的所有正值告诉我们,像素的一个小变化会增加输出值。因此,将这些梯度可视化可以提供一些直观的信息,这种方法突出了对输出贡献最大的显著图像区域。
- class_idx = 0indices = np.where(val_y[:, class_idx] == 1.)[0]
- # pick some random input from here.idx = indices[0]
- # Lets sanity check the picked image.from matplotlib import pyplot as plt%matplotlib inline
- plt.rcParams['figure.figsize'] = (18, 6)plt.imshow(val_x[idx][..., 0])
- from vis.visualization import visualize_saliency
- from vis.utils import utilsfrom keras import activations# 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, 'preds')
- # Swap softmax with linearmodel.layers[layer_idx].activation = activations.linear
- model = utils.apply_modifications(model)grads = visualize_saliency(model, layer_idx, filter_indices=class_idx, seed_input=val_x[idx])
- # Plot with 'jet' colormap to visualize as a heatmap.plt.imshow(grads, cmap='jet')
- # This corresponds to the Dense linear layer.for class_idx in np.arange(10):
- indices = np.where(val_y[:, class_idx] == 1.)[0]
- idx = indices[0]
- f, ax = plt.subplots(1, 4)
- ax[0].imshow(val_x[idx][..., 0])
- for i, modifier in enumerate([None, 'guided', 'relu']):
- grads = visualize_saliency(model, layer_idx, filter_indices=class_idx,
- seed_input=val_x[idx], backprop_modifier=modifier)
- if modifier is None:
- modifier = 'vanilla'
- ax[i+1].set_title(modifier)
- ax[i+1].imshow(grads, cmap='jet')
3.2 基于梯度的类别激活映射
类别激活映射(CAM)或grad-CAM是另外一种可视化模型的方法,这种方法使用的不是梯度的输出值,而是使用倒数第二个卷积层的输出,这样做是为了利用存储在倒数第二层的空间信息。
- from vis.visualization import visualize_cam
- # This corresponds to the Dense linear layer.for class_idx in np.arange(10):
- indices = np.where(val_y[:, class_idx] == 1.)[0]
- idx = indices[0]f, ax = plt.subplots(1, 4)
- ax[0].imshow(val_x[idx][..., 0])
- for i, modifier in enumerate([None, 'guided', 'relu']):
- grads = visualize_cam(model, layer_idx, filter_indices=class_idx,
- seed_input=val_x[idx], backprop_modifier=modifier)
- if modifier is None:
- modifier = 'vanilla'
- ax[i+1].set_title(modifier)
- ax[i+1].imshow(grads, cmap='jet')
总结
本文简单说明了CNN模型可视化的重要性,以及介绍了一些可视化CNN网络模型的方法,希望对读者有所帮助,使其能够在后续深度学习应用中构建更好的模型。