深度学习

十四、计算机视觉中的神经网络:可视化卷积网络所学到的东西

2018-12-25  本文已影响128人  抄书侠

文章代码来源:《deep learning on keras》,非常好的一本书,大家如果英语好,推荐直接阅读该书,如果时间不够,可以看看此系列文章,文章为我自己翻译的内容加上自己的一些思考,水平有限,多有不足,请多指正,翻译版权所有,若有转载,请先联系本人。
个人方向为数值计算,日后会向深度学习和计算问题的融合方面靠近,若有相近专业人士,欢迎联系。


系列文章:
一、搭建属于你的第一个神经网络
二、训练完的网络去哪里找
三、【keras实战】波士顿房价预测
四、keras的function API
五、keras callbacks使用
六、机器学习基础Ⅰ:机器学习的四个标签
七、机器学习基础Ⅱ:评估机器学习模型
八、机器学习基础Ⅲ:数据预处理、特征工程和特征学习
九、机器学习基础Ⅳ:过拟合和欠拟合
十、机器学习基础Ⅴ:机器学习的一般流程十一、计算机视觉中的深度学习:卷积神经网络介绍
十二、计算机视觉中的深度学习:从零开始训练卷积网络
十三、计算机视觉中的深度学习:使用预训练网络
十四、计算机视觉中的神经网络:可视化卷积网络所学到的东西


常常说深度学习模型是“黑盒”,学习表示很难提取表示成人类可以读懂的形式。尽管这对于特定类型的深度学习模型是对的,但那时对于卷积网络一定是错的。我们从卷积网络学到的表示都是高度可视化的,很大一部分是因为它们表示成了可视化的概念。自从2013以来,各种各样的技术都被提出用来可视化和揭示这些表示。我们不会调查所有的,但我们会涵盖三种最容易接触最有用的:

在第一种模式——激活值可视化——我们将会使用我们从零训练的小的卷积网络(cat vs. dog)分类问题。在接下来的两种方法,我们将会使用VGG16模型。

可视化中间的激活值

可视化中间的激活值包含展示通过不同卷积和池化层以后的输出,给一个特定的输入(这层输出叫做“激活值”,激活函数的输出值)这给了一个视角来看一个输入是如何分解到不同的网络学到的滤波器的。这些我们想要可视化的特征有三个维度:长、高、深度。每一个通道编码了相互独立的特征,所以合适的可视化这些特征的方法是通过单独画出每一个通道里面的内容,作为二维图像,让我们开始加载之前训练过的模型吧:

>>> from keras.models import load_model
>>> model = load_model('cats_and_dogs_small_2.h5')
>>> model.summary() # As a reminder.
________________________________________________________________
Layer (type) Output Shape Param #
================================================================
conv2d_5 (Conv2D) (None, 148, 148, 32) 896
________________________________________________________________
maxpooling2d_5 (MaxPooling2D) (None, 74, 74, 32) 0
________________________________________________________________
conv2d_6 (Conv2D) (None, 72, 72, 64) 18496
________________________________________________________________
maxpooling2d_6 (MaxPooling2D) (None, 36, 36, 64) 0
________________________________________________________________
conv2d_7 (Conv2D) (None, 34, 34, 128) 73856
________________________________________________________________
maxpooling2d_7 (MaxPooling2D) (None, 17, 17, 128) 0
________________________________________________________________
conv2d_8 (Conv2D) (None, 15, 15, 128) 147584
________________________________________________________________
maxpooling2d_8 (MaxPooling2D) (None, 7, 7, 128) 0
________________________________________________________________
flatten_2 (Flatten) (None, 6272) 0
________________________________________________________________
dropout_1 (Dropout) (None, 6272) 0
________________________________________________________________
dense_3 (Dense) (None, 512) 3211776
________________________________________________________________
dense_4 (Dense) (None, 1) 513
================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0

接下来我们要在网络上用的猫的图像是在网络上没有训练过的:
先预处理图像:

img_path = '/Users/fchollet/Downloads/cats_and_dogs_small/test/cats/cat.1700.jpg'
# We preprocess the image into a 4D tensor
from keras.preprocessing import image
import numpy as np
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)
# Remember that the model was trained on inputs
# that were preprocessed in the following way:
img_tensor /= 255.
# Its shape is (1, 150, 150, 3)
print(img_tensor.shape)

展示我们的图像:

import matplotlib.pyplot as plt
plt.imshow(img_tensor[0])
Our test cat picture

为了提取我们想要看到的特征,我们将会建立一个keras模型以图像批作为输入,输出所有卷积和池化层的激活值。我们将会使用 Keras class模型来做到这一点。一个模型的实例用了两个参数:一个输入张量,一个输出张量。结果的类别是一个keras模型,就和你熟知的sequential模型类似的,将特定输入映射到特定输出。让这二者有区别的是我们现在要用的模型可以有多个输出,不像sequential。想要了解更多有关Model class的信息,可以看书的第七章第一部分。

from keras import models
# Extracts the outputs of the top 8 layers:
layer_outputs = [layer.output for layer in model.layers[:8]]
# Creates a model that will return these outputs, given the model input:
activation_model = models.Model(inputs=model.input, outputs=layer_outputs)

当喂进去输入图像时,模型返回原始模型的层的激活值。这是你第一次在本书遇到多输出的模型:直到现在,你所看到的模型都是一个输入一个输出。一般的情况,一个模型能有任意多输入和输出。这里的有一个输入,五个输出,每一层输出一个层激活值。

# This will return a list of 5 Numpy arrays:
# one array per layer activation
activations = activation_model.predict(img_tensor)

举个例子,这就是我们猫图像输入卷积层第一层的激活值:

>>> first_layer_activation = activations[0]
>>> print(first_layer_activation.shape)
(1, 148, 148, 32)

这是一个148\times 148的特征,有着32个通道。让我们看一下第四个通道:

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 4], cmap='viridis')
4th channel of the activation of the first layer on our test cat picture

这个通道看起来编码了对角边缘探测器。让我们试一下第七个通道——但注意你自己的通道或许是多样的,因为卷积层选到的滤波器是不确定的。

plt.matshow(first_layer_activation[0, :, :, 7], cmap='viridis')
7th of the activation of the first layer on our test cat picture

这看起来就像是“亮绿点”探测器,对于编码猫眼很有用。在这一点上,让我们画出整个网络的可视化激活值。我们将会提取和画出五个激活图中的每一个通道,然后我们将会把这些结果堆在一个大的图像张量中,通道挨着堆。

# These are the names of the layers, so can have them as part of our plot
layer_names = []
for layer in model.layers[:8]:
 layer_names.append(layer.name)
images_per_row = 16
# Now let's display our feature maps
for layer_name, layer_activation in zip(layer_names, activations):
 # This is the number of features in the feature map
 n_features = layer_activation.shape[-1]
 # The feature map has shape (1, size, size, n_features)
 size = layer_activation.shape[1]
 # We will tile the activation channels in this matrix
 n_cols = n_features // images_per_row
 display_grid = np.zeros((size * n_cols, images_per_row * size))
# We'll tile each filter into this big horizontal grid
 for col in range(n_cols):
 for row in range(images_per_row):
 channel_image = layer_activation[0,
 :, :,
 col * images_per_row + row]
 # Post-process the feature to make it visually palatable
 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
 # Display the grid
 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')
Every channel of every layer activation on our test cat picture

有些需要注意的:

我们刚刚已经论证了一个非常重要普遍的深度神经网络学习表示的特点:层提取到的特征随着层数增加而变得更抽象。层的激活值会携带越来越少的有关特定输入的信息,随着层的增加,会携带越来越多有关目标的信息(在我们的例子中,图像类别是猫、狗)。一个深度神经网络有效的工作就像一个信息蒸馏管道,输入数据向量,在我们的例子中是输入了RGB图,就会反复转换使得无关信息被滤出去(例如图像特定的视觉外观)有用的信息就会被放大和提炼(例如图像类别)。
人类和动物感知世界的方式与之类似:在观察了一个场景几秒以后,一个人类能够记住物体的抽象但无法记住物体具体的表现。实际上,如果让你现在从记忆中画一个单车,你很可能都无法画出细节,尽管大致正确,虽然你一生中见过上千辆单车。现在马上试一下:效果属实。你的大脑学习了输入图像的完全抽象,把它转化成更高级的视觉内容,完全过滤掉不相关的视觉细节,让记住我们周围的东西的实际的样子非常的难。


Left: attempts to draw a bicycle from memory. Right: what a schematic bicycle should look like.

可视化卷积网络滤波器

另一个简单的事情是监视滤波器从卷积网络里学到的东西,并把它用可视化的方式展现出来。这能通过输入空间里的gradient ascent做到:使用gradient descent来评估输入图像的卷积网络以最大化特定滤波器的反馈,从一个空白输入图像开始。最终输入图像的结果是对于选择的滤波器具有最大响应的。
这个过程很简单:我们将会建立一个损失函数来最大化在给定卷积层中滤波器的值,我们会使用随机梯度下降来调节输入图像的值从而最大化激活值。举个例子,这里给出了VGG16网络中的"block3_conv1"的滤波器0的激活值损失。

from keras.applications import VGG16
from keras import backend as K
model = VGG16(weights='imagenet',
 include_top=False)
layer_name = 'block3_conv1'
filter_index = 0
layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

为了应用梯度下降法,我们将会需要遵循模型输入的损失下降。为了做到这一点,我们需要用gradients函数来打包keras的backend模型:

# The call to `gradients` returns a list of tensors (of size 1 in this case)
# hence we only keep the first element -- which is a tensor.
grads = K.gradients(loss, model.input)[0]

一个不明显的把戏来使用梯度下降处理光滑是标准化梯度张量,通过除以它的L2模(张量中的平方和均值的平方根)这保证输入图像更新总是在一个相同的范围。

# We add 1e-5 before dividing so as to avoid accidentally dividing by 0.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

现在我们需要一个方法来在给定输入图像时,计算损失张量的值以及梯度张量。我们能够定义一个keras 的backend函数来做到:iterate是一个函数,拿进去一个数组张量返回两个数组张量:损失值和梯度值。

iterate = K.function([model.input], [loss, grads])
# Let's test it:
import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])

此时我们就可以使用Python里面的loop来做随机梯度下降了。

# We start from a gray image with some noise
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128.
# Run gradient ascent for 40 steps
step = 1. # this is the magnitude of each gradient update
for i in range(40):
 # Compute the loss value and gradient value
 loss_value, grads_value = iterate([input_img_data])
 # Here we adjust the input image in the direction that maximizes the loss
 input_img_data += grads_value * step

图像张量的结果将会是浮点张量的形状(1,150,150,3)值在[0.255]之间。因此我们需要发布张量的流程来将其转化为可视化图像。我们通过以下实用函数来做到:

def deprocess_image(x):
 # normalize tensor: center on 0., ensure std is 0.1
 x -= x.mean()
 x /= (x.std() + 1e-5)
 x *= 0.1
 # clip to [0, 1]
 x += 0.5
 x = np.clip(x, 0, 1)
 # convert to RGB array
 x *= 255
 x = np.clip(x, 0, 255).astype('uint8')
 return x

现在我们有了所有的不见,让我们把它们放在一起,放进Python函数就像放进层的名字和滤波器指标,会返回一个有效图像张量,其代表着最大化特定滤波器的激活值。

def generate_pattern(layer_name, filter_index, size=150):
 # Build a loss function that maximizes the activation
 # of the nth filter of the layer considered.
 layer_output = model.get_layer(layer_name).output
 loss = K.mean(layer_output[:, :, :, filter_index])
 # Compute the gradient of the input picture wrt this loss
 grads = K.gradients(loss, model.input)[0]
 # Normalization trick: we normalize the gradient
 grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
 # This function returns the loss and grads given the input picture
 iterate = K.function([model.input], [loss, grads])
 # We start from a gray image with some noise
 input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.
 # Run gradient ascent for 40 steps
 step = 1.
 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)

可视化block3_conv1的滤波器0:

>>> plt.imshow(generate_pattern('block3_conv1', 0))
Pattern that the 0th channel in layer block3_conv1 maximally responds to
看起来就像是波尔卡圆点图。
现在是比较有趣的部分:我们能从每一层的单独滤波器开始可视化。简单来说,我们将只看到每一层的前64个滤波器,将会只看到第一层的每个卷积块(block1_conv1,block2_conv1,block3_conv1,block4_conv1,block5_conv1)我们将输出排列在 Filter patterns for layer block1_conv1
Filter patterns for layer block2_conv1
Filter patterns for layer block3_conv1
Filter patterns for layer block4_conv1

这些滤波器可视化告诉我们卷积网络层是如何看这个世界的:每一层都简单的学习一类滤波器,使得他们的输入能够被表示成滤波器的组合。这个和傅里叶分解信号成一个余弦函数库很类似。这些卷积网络滤波器库中的滤波器当我们的层升高时变得更加复杂和精致:

分类激活的热力图可视化

我们还将介绍一些可视化技术,对于理解给定图像的哪一部分会导致卷积网络作出最终的分类决定。这对于调试卷积网络决定的错误非常管用,特别是在分类错误的情况下。这也将允许你找到图像中的特定物体。
这种一般的分类方法叫做“分类激活图”CAM可视化,是通过在输入图像上画分类激活值的热力图得到的。一个分类激活值的热力图是一个二维的和特定输出类别相关的分数,计算每一个输入图像的位置,指示着每一个位置对于分类结果的重要程度。例如,给一个图像进我们的"cat vs. dog"卷积网络,分类激活图允许我们生成一幅关于猫的热力图,指示猫样子的图在不同的地方是什么样子,类似的可以画出狗的。
我们用的具体的实现过程在Grad-CAM中详细描述。其实很简单:衡量每一个特征对于分类的权重,最后画出来。
我们将使用预训练过的VGG16网络来讨论这个技术。

from keras.applications.vgg16 import VGG16
# Note that we are including the densely-connected classifier on top;
# all previous times, we were discarding it.
model = VGG16(weights='imagenet')

让我们考虑下面这幅有着两只非洲象的图片,或许是一个母亲和它的幼崽漫步在大草原:

Our test picture of African elephants
让我们将图像转化成VGG16模型能读得懂的数据:这个模型在大小为 African elephant class activation heatmap over our test picture

最后,我们使用OpenCV来生成一幅原图和热力图的叠加:

import cv2
# We use cv2 to load the original image
img = cv2.imread(img_path)
# We resize the heatmap to have the same size as the original image
heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
# We convert the heatmap to RGB
heatmap = np.uint8(255 * heatmap)
# We apply the heatmap to the original image
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
# 0.4 here is a heatmap intensity factor
superimposed_img = heatmap * 0.4 + img
# Save the image to disk
cv2.imwrite('/Users/fchollet/Downloads/elephant_cam.jpg', superimposed_img)
Superimposing the class activation heatmap with the original picture

这项技术回答了两个重要的问题:

特别的,我们可以看到非洲象宝宝的耳朵被强烈激活:这或许是网络认为的非洲象和印第安象的区别。

总结:计算机视觉中的深度学习

这里有一些你需要从本章打包带走的东西:

此外,你应当选择一些实用的技巧:

上一篇下一篇

猜你喜欢

热点阅读