卷积神经网络如何看待世界

注意:这篇文章最初写于 2016 年 1 月。现在已经过时了。请参阅此示例,了解如何可视化卷积网络过滤器以获取最新的替代方案,或查看我的书“使用 Python 进行深度学习(第 2 版)”的第 9 章。

使用 Keras 探索卷积网络过滤器

在这篇文章中,我们将看看深度卷积神经网络 (convnets) 真正学到了什么,以及它们如何理解我们输入的图像。我们将使用 Keras 可视化在 ImageNet 上训练的 VGG16 架构中不同层中最大化过滤器激活的输入。这篇文章中使用的所有代码都可以在 Github 上找到。

Some convnet filters

VGG16(也称为 OxfordNet)是一种以牛津大学视觉几何组命名的卷积神经网络架构,该架构由他们开发。它曾被用于赢得 2014 年 ILSVR(ImageNet)竞赛。尽管它在某种程度上已经被 Inception 和 ResNet 等更新的进展所超越,但它至今仍被认为是一个优秀的视觉模型。

首先,让我们从在 Keras 中定义 VGG16 模型开始

from keras import applications

# build the VGG16 network
model = applications.VGG16(include_top=False,
                           weights='imagenet')

# get the symbolic outputs of each "key" layer (we gave them unique names).
layer_dict = dict([(layer.name, layer) for layer in model.layers])

请注意,我们只到最后一个卷积层——我们不包括全连接层。原因是添加全连接层会强制您对模型使用固定的输入大小(224x224,原始 ImageNet 格式)。通过仅保留卷积模块,我们的模型可以适应任意输入大小。

该模型加载了一组在 ImageNet 上预先训练的权重。

现在让我们定义一个损失函数,它将寻求最大化特定层 (layer_name) 中特定过滤器 (filter_index) 的激活。我们通过 Keras 后端函数来做到这一点,它允许我们的代码在 TensorFlow 和 Theano 之上运行。

from keras import backend as K

layer_name = 'block5_conv3'
filter_index = 0  # can be any integer from 0 to 511, as there are 512 filters in that layer

# build a loss function that maximizes the activation
# of the nth filter of the layer considered
layer_output = layer_dict[layer_name].output
loss = K.mean(layer_output[:, :, :, filter_index])

# compute the gradient of the input picture wrt this loss
grads = K.gradients(loss, input_img)[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([input_img], [loss, grads])

一切都非常简单。这里唯一的技巧是规范化输入图像像素的梯度,这避免了非常小和非常大的梯度,并确保了平滑的梯度上升过程。

现在我们可以使用我们定义的 Keras 函数,根据我们的过滤器激活损失,在输入空间中进行梯度上升

import numpy as np

# we start from a gray image with some noise
input_img_data = np.random.random((1, 3, img_width, img_height)) * 20 + 128.
# run gradient ascent for 20 steps
for i in range(20):
    loss_value, grads_value = iterate([input_img_data])
    input_img_data += grads_value * step

此操作在使用 TensorFlow 的 CPU 上需要几秒钟。

然后我们可以提取并显示生成的输入

from scipy.misc import imsave

# util function to convert a tensor into a valid image
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 = x.transpose((1, 2, 0))
    x = np.clip(x, 0, 255).astype('uint8')
    return x

img = input_img_data[0]
img = deprocess_image(img)
imsave('%s_filter_%d.png' % (layer_name, filter_index), img)

结果

conv5_1_filter_0

可视化所有过滤器!

现在是有趣的部分。我们可以使用相同的代码来系统地显示哪种输入(它们不是唯一的)最大化每一层中的每个过滤器,从而让我们清楚地了解卷积网络对其视觉空间的模块化-层次分解。

第一层基本上只编码方向和颜色。然后,这些方向和颜色过滤器被组合成基本的网格和斑点纹理。这些纹理逐渐组合成越来越复杂的图案。

您可以将每一层中的过滤器视为一组基向量,通常是过完备的,可用于以紧凑的方式对该层的输入进行编码。当过滤器开始合并来自越来越大的空间范围的信息时,它们会变得更加复杂。

vgg16_filters_overview

一个非凡的观察结果:许多过滤器是相同的,但旋转了某个非随机因子(通常为 90 度)。这意味着我们有可能通过找到一种使卷积过滤器旋转不变的方法,将卷积网络中使用的过滤器数量压缩很大一部分。我可以看到几种可以实现这一点的方法——这是一个有趣的研究方向。

令人震惊的是,即使对于相对高级的过滤器(例如 block4_conv1 中的过滤器),旋转观察结果也成立。

在最高层(block5_conv2block5_conv3)中,我们开始识别出与网络训练分类的对象(如羽毛、眼睛等)中发现的纹理相似的纹理。

卷积网络梦

另一个有趣的事情是将这些过滤器应用于照片(而不是应用于嘈杂的全灰度输入)。这就是去年谷歌推广的“深度梦境”的原理。通过选择特定的过滤器组合而不是单个过滤器,您可以获得相当漂亮的结果。如果您对此感兴趣,您还可以查看 Keras 中的深度梦境示例,以及介绍该技术的 Google 博文

Deep filter dreams

查找最大化特定类的输入

现在换个说法——如果您将全连接层包含在网络的末尾,并尝试最大化网络特定输出的激活,会怎么样?您会得到一张看起来像该输出编码的类的图像吗?我们来试试吧。

我们可以只定义这个非常简单的损失函数

model = applications.VGG16(include_top=True,
                           weights='imagenet')
loss = K.mean(model.output[:, output_index])

让我们将其应用于 output_index = 65(这是 ImageNet 中的海蛇类)。我们很快达到了 0.999 的损失,这意味着卷积网络有 99.9% 的置信度认为生成的输入是一条海蛇。让我们来看看生成的输入。

VGG16 sea snake

哦,好吧。看起来不像海蛇。这肯定是个意外,所以让我们用 output_index = 18(喜鹊类)再试一次。

VGG16 magpie

好吧,就这样。所以我们的卷积网络对喜鹊的概念看起来一点也不像喜鹊——充其量,唯一的相似之处是在局部纹理的水平上(羽毛,也许还有一两个喙)。这是否意味着卷积网络是不好的工具?当然不是,它们很好地服务于它们的用途。这意味着我们应该克制我们自然而然地将它们拟人化并相信它们“理解”例如狗的概念或喜鹊的外观的倾向,仅仅因为它们能够以高精度对这些物体进行分类。他们没有,至少在任何对我们人类有意义的程度上都没有。

那么他们真正“理解”的是什么呢?两件事:首先,他们将视觉输入空间的分解理解为卷积过滤器的分层模块化网络;其次,他们理解某些过滤器组合与一组任意标签之间的概率映射。自然,这在任何人类意义上都不符合“看见”的条件,而且从科学的角度来看,这当然也不意味着我们在这一点上以某种方式解决了计算机视觉问题。不要相信炒作;我们只是站在一个非常高的梯子的第一步。

有人说,卷积网络学习到的视觉空间的分层模块化分解类似于人类视觉皮层所做的事情。这可能是真的,也可能不是真的,但没有强有力的证据支持这一点。当然,人们会期望视觉皮层学习到类似的东西,因为它构成了我们视觉世界的“自然”分解(就像傅立叶分解是周期性音频信号的“自然”分解一样)。但过滤器和层次结构的确切性质,以及它们学习的过程,很可能与我们弱小的卷积网络几乎没有共同之处。视觉皮层一开始就不是卷积的,虽然它在结构上是分层的,但这些层本身被组织成皮层柱,其确切用途尚不清楚——这是我们的人工网络中没有的特征(尽管 Geoff Hinton 正在研究它)。此外,视觉感知比静态图片的分类要复杂得多——人类的感知从根本上说是顺序的和主动的,而不是静态的和被动的,并且与运动控制(例如眼跳)紧密相连。

下次当你听到一些风险投资家或大牌 CEO 出现在新闻中,警告你警惕我们最近在深度学习方面取得的进展所带来的生存威胁时,请想一想这一点。今天,我们拥有比以往任何时候都更好的工具来绘制复杂的信息空间,这很棒,但归根结底,它们是工具,而不是生物,而且它们所做的任何事情都不能合理地被视为“思考”。在岩石上画一张笑脸并不能让它“快乐”,即使你的灵长类动物新皮层这样告诉你。

也就是说,可视化卷积网络的学习内容非常有趣——谁会想到,在一个足够大的数据集上进行简单梯度下降和合理的损失函数就足以学习到这个美丽的、分层模块化的模式网络,从而设法解释了一个令人惊讶的良好的复杂视觉空间。深度学习在任何真正意义上可能都不是智能,但它仍然比几年前任何人预期的效果都要好得多。现在,要是我们知道为什么就好了……;-)

@fchollet,2016 年 1 月