内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
查看完整代码,请看: https://github.com/ubwshook/MachineLearning
神经网络的模型是“黑盒”,也就是说模型很难用人类可以理解的方式提取和呈现。其实对于卷积神经网络来说,它非常适合可视化,因为他们是视觉概念的表示。人们开发了多种技术对齐进行可视化解释。最容易理解的方法如下:
- 可视化卷积神经网络的中间输出(中间激活):有助于理解卷积神经网络连续的层如何对输入进行变换,也有助于初步了解卷积神经网络每个过滤器的含义
- 可视化卷积神经网络的过滤器:有助于精确理解卷积神经网络中每个过滤器容易接受的视觉模式或视觉概念。
- 可视化图像中类激活的热力图: 有助于理解图像的哪个部分被识别为属于某个类别,从而可以定位图像中的物体。
一、可视化中间激活
我们使用之前保存的解决猫狗分类问题时保存的模型(github相关代码位置)。
可视化中间激活,是对给定输入展示网络中各个卷积层和池化层的输出特征图。这让我们可以看到输入如何被分解为网络学到的不同过滤器。使用model.summary()可以观察模型结构:
>>>model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 7, 7, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 6272) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 6272) 0
_________________________________________________________________
dense_1 (Dense) (None, 512) 3211776
_________________________________________________________________
dense_2 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________
我们选择一张图片作为输入图片,我选择的是罗纳威犬的图片,非常像GTA5里面小富的那条狗,哈哈!
罗纳威犬
1.加载图片
下面这段代码,从指定路径加载图片,将图片转化为张量,并且增加维度,使其成为一个(1, 150, 150, 3)的4D张量,这样才能符合模型的输入。
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) # 增加维度使其成为一个4维张量,符合模型的输入,第一维是图片的索引
img_tensor /= 255. # 输入的图片都要进行归一化处理
plt.imshow(img_tensor[0])
plt.show()
2.使用预测模式运行模型并画出激活的图像
我们实例化一个模型,它的拥有多个输出,每层的激活对应一个输出。我们获取其中第1层激活的第4个通道将其可视化:
activations = activation_model.predict(img_tensor) # 将图片张量送到模型中进行进行激活
first_layer_activation = activations[0]
# 将第4个通道可视化
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')
plt.show()
激活特征图
可以看出这个通道应该是检测边缘。我们继续将8个层的激活图像都展示出来, 将所有通道激活的图像拼接起来:
layer_names = []
for layer in model.layers[:8]:
layer_names.append(layer.name)
images_per_row = 16
for layer_name, layer_activation in zip(layer_names, activations):
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1]
n_cols = n_features // images_per_row
display_grid = np.zeros((size * n_cols, images_per_row * size))
for col in range(n_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.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')
plt.show()
下面的四张图是四个卷积层的图像,我们可以看出:
第1层激活图像 第2层激活图像 第3层激活图像 第4层激活图像
- 第一层是各种边缘探测器,这个时候激活几乎保留的原始图像的所有信息。
- 随着层数的加深,激活变得越来越抽象,并且越来越难以理解,他们开始表示更高层次的概念,比如耳朵、眼镜的形状等
- 随着层数的增加,激活的稀疏度也在增加,后面的层里出现了空白,也就是说输入图像找不到这种特征。
神经网络学到的表示的一个重要普遍特征:随着层数的加深,层所提取的特征变得越来越抽象。更高的激活层包含关于特定输入的信息越来越少,关于目标的信息越来越多。深度神经网络可以有效的作为信息蒸馏管道。
二、可视化神经网络的过滤器
如果想观察神经网络的过滤器,可以通过在输入空间中进行梯度上升来实现:从空白输入图像开始,将梯度下降应用于卷积神经网络输入图像的值,其目的是让某个过滤器响应最大化。得到的输入图像是选定过滤器具有最大化的图像。
我们需要构建一个损失函数,其目的是让某个卷积层的摸个过滤器的值最大化。然后,我们将要使用随机梯度下降来调节输入图像的值,以便让这个激活值最大。我们使用的模型是VGG16模型。
构造梯度迭代函数,进行40轮的迭代,获取具有最大响应的特征图。
def generate_pattern(model, layer_name, filter_index, size=150):
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index]) # 第n个过滤器的输出
grads = K.gradients(loss, model.input)[0] # 获取损失相对于输入的梯度
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
iterate = K.function([model.input], [loss, grads]) # 指定输入输出构造一个迭代函数
input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
step = 1.
# 进行40轮的梯度下降,构造出过滤器最大响应的图
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step
img = input_img_data[0]
return deprocess_image(img)
其中deprocess_image的作用是将张量转化为有效图像:
def deprocess_image(x):
# 对张量做标准化,使其均值为0,标准差为0.1
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1
# 将x裁剪到[0,1]区间
x += 0.5
x = np.clip(x, 0, 1)
# 将x转换为RGB数组
x *= 255
x = np.clip(x, 0, 255).astype('uint8')
return x
单个滤波器的响应
接下来我们展示所有过滤器的的响应模式,使用8 * 8的网格图来展示:
def visualizing_filter(model, layer_name):
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(model, layer_name, i + (j * 8), size=size)
horizontal_start = i * size + i * margin
horizontal_end = horizontal_start + size
vertical_start = j * size + j * margin
vertical_end = vertical_start + size
results[horizontal_start: horizontal_end, vertical_start: vertical_end, :] = filter_img
plt.figure(figsize=(20, 20))
plt.imshow(results/255.)
plt.show()
第1个卷积层过滤器模式
第4个卷积层过滤器模式
可以看出最初的过滤器对应简单的边缘和颜色,而更高层的开始类似于自然中一些纹理,类似羽毛树叶等。
三、可视化类激活的heatmaps
通过类激活热力图(CAM, class activation map), 我们可以了解到图像的哪一部分让神经网络做出最终的分类决策。特别是在分类出现错误的时候,这种方法有助于定位图像中的特定目标。
对输入图像的每个位置进行计算,表示每个位置对该类别的重要成图。这个方法很简单:给定一张输入图像,对于一个卷积层的输出特征图,用类别相对于通道梯度对这个特征图中的每个通道进行加权。
还是使用罗纳威犬的图片,将其输入到VGG16模型中,获取预测的结果:
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('Predicted:', decode_predictions(preds, top=3)[0])
max_index = np.argmax(preds[0])
预测的结果是:
Predicted: [('n02106550', 'Rottweiler', 0.9289004), ('n02107142', 'Doberman', 0.021247152), ('n02101006', 'Gordon_setter', 0.014357827)]
预测的类别:
- 罗纳威犬 92.8%
- 杜宾 2.1%
- 哥顿赛特犬 1.4%
预测向量中被最大激活的是“罗纳威犬”类别,索引是max_index=234
接下来我们使用Grad-CAM进行处理:
predict_output = model.output[:, max_index]
last_conv_layer = model.get_layer('block5_conv3') # 选用最后一个卷积层
# 类别相对于block5_conv3输出特征的梯度
grads = K.gradients(predict_output, last_conv_layer.output)[0]
# 其中每个元素是特定特征通道的梯度平均大小
pooled_grads = K.mean(grads, axis=(0, 1, 2))
# 创建迭代器
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])
pooled_grads_value, conv_layer_output_value = 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)
plt.show()
使用openCV对原图和热力图进行叠加并保存
# 使用openCV将热力图和原图进行叠加
img = cv2.imread(img_path)
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
superimposed_img = heatmap * 0.4 + img
cv2.imwrite('Rottweiler_heatmap.jpg', superimposed_img)
罗纳威犬的热力图像
可以看出狗的头部附近是判别它是罗纳威的主要特点。
网友评论