Python 深度学习第二版(GPT 重译)(四)(3)

简介: Python 深度学习第二版(GPT 重译)(四)

Python 深度学习第二版(GPT 重译)(四)(2)https://developer.aliyun.com/article/1485278

9.4 解释卷积神经网络学习的内容

在构建计算机视觉应用程序时的一个基本问题是可解释性:当您只能看到一辆卡车时,为什么您的分类器认为特定图像包含一个冰箱?这在深度学习用于补充人类专业知识的用例中尤为重要,比如在医学成像用例中。我们将通过让您熟悉一系列不同的技术来结束本章,以便可视化卷积神经网络学习的内容并理解它们所做的决定。

人们常说深度学习模型是“黑匣子”:它们学习的表示很难以提取并以人类可读的形式呈现。尽管对于某些类型的深度学习模型来说这在一定程度上是正确的,但对于卷积神经网络来说绝对不是真的。卷积神经网络学习的表示非常适合可视化,这在很大程度上是因为它们是视觉概念的表示。自 2013 年以来,已经开发出了各种技术来可视化和解释这些表示。我们不会对它们进行全面调查,但我们将介绍其中三种最易于访问和有用的方法:

  • 可视化中间卷积网络输出(中间激活) — 有助于理解连续的卷积网络层如何转换其输入,并初步了解单个卷积滤波器的含义
  • 可视化卷积神经网络滤波器 — 有助于准确理解卷积神经网络中每个滤波器对哪种视觉模式或概念具有接受性
  • 可视化图像中类激活的热图 — 有助于理解图像的哪些部分被识别为属于给定类别,从而使您能够在图像中定位对象

对于第一种方法 — 激活可视化 — 我们将使用我们在第 8.2 节中从头开始在狗与猫分类问题上训练的小型卷积网络。对于接下来的两种方法,我们将使用一个预训练的 Xception 模型。

9.4.1 可视化中间激活

可视化中间激活包括显示模型中各种卷积和池化层返回的值,给定某个输入(层的输出通常称为激活,激活函数的输出)。这可以让我们看到输入是如何被网络学习的不同滤波器分解的。我们想要可视化具有三个维度的特征图:宽度、高度和深度(通道)。每个通道编码相对独立的特征,因此正确的可视化这些特征图的方式是独立绘制每个通道的内容作为 2D 图像。让我们从加载你在第 8.2 节保存的模型开始:

>>> from tensorflow import keras
>>> model = keras.models.load_model(
 "convnet_from_scratch_with_augmentation.keras")
>>> model.summary()
Model: "model_1" 
_________________________________________________________________
Layer (type)                 Output Shape              Param # 
=================================================================
input_2 (InputLayer)         [(None, 180, 180, 3)]     0 
_________________________________________________________________
sequential (Sequential)      (None, 180, 180, 3)       0 
_________________________________________________________________ 
rescaling_1 (Rescaling)      (None, 180, 180, 3)       0 
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 178, 178, 32)      896 
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 89, 89, 32)        0 
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 87, 87, 64)        18496 
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 43, 43, 64)        0 
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 41, 41, 128)       73856 
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 20, 20, 128)       0 
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 18, 18, 256)       295168 
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 9, 9, 256)         0 
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 7, 7, 256)         590080 
_________________________________________________________________
flatten_1 (Flatten)          (None, 12544)             0 
_________________________________________________________________
dropout (Dropout)            (None, 12544)             0 
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 12545 
=================================================================
Total params: 991,041 
Trainable params: 991,041 
Non-trainable params: 0 
_________________________________________________________________

接下来,我们将获得一张输入图像——一张猫的图片,不是网络训练过的图片的一部分。

列表 9.6 对单个图像进行预处理

from tensorflow import keras 
import numpy as np
img_path = keras.utils.get_file(                            # ❶
    fname="cat.jpg",                                        # ❶
    origin="https://img-datasets.s3.amazonaws.com/cat.jpg") # ❶
def get_img_array(img_path, target_size):
    img = keras.utils.load_img(                             # ❷
        img_path, target_size=target_size)                  # ❷
    array = keras.utils.img_to_array(img)                   # ❸
    array = np.expand_dims(array, axis=0)                   # ❹
    return array
img_tensor = get_img_array(img_path, target_size=(180, 180))

❶ 下载一个测试图片。

❷ 打开图像文件并调整大小。

❸ 将图像转换为形状为(180, 180, 3)的 float32 NumPy 数组。

❹ 添加一个维度,将数组转换为“批量”中的单个样本。现在其形状为(1, 180, 180, 3)。

让我们展示这张图片(见图 9.12)。

列表 9.7 显示测试图片

import matplotlib.pyplot as plt
plt.axis("off")
plt.imshow(img_tensor[0].astype("uint8"))
plt.show()

图 9.12 测试猫图片

为了提取我们想要查看的特征图,我们将创建一个接受图像批量作为输入的 Keras 模型,并输出所有卷积和池化层的激活。

列表 9.8 实例化一个返回层激活的模型

from tensorflow.keras import layers
layer_outputs = []
layer_names = [] 
for layer in model.layers:                                                # ❶
    if isinstance(layer, (layers.Conv2D, layers.MaxPooling2D)):           # ❶
        layer_outputs.append(layer.output)                                # ❶
        layer_names.append(layer.name)                                    # ❷
activation_model = keras.Model(inputs=model.input, outputs=layer_outputs) # ❸

❶ 提取所有 Conv2D 和 MaxPooling2D 层的输出,并将它们放入列表中。

❷ 保存层的名称以备后用。

❸ 创建一个模型,给定模型输入,将返回这些输出。

当输入一张图像时,这个模型会返回原始模型中层的激活值,作为一个列表。这是你在本书中第一次实际遇到多输出模型,因为你在第七章学习过它们;到目前为止,你看到的模型都只有一个输入和一个输出。这个模型有一个输入和九个输出:每个层激活一个输出。

列表 9.9 使用模型计算层激活

activations = activation_model.predict(img_tensor)     # ❶

❶ 返回一个包含九个 NumPy 数组的列表:每个数组代表一层的激活。

例如,这是原始模型第一卷积层对猫图像输入的激活:

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

这是一个具有 32 个通道的 178×178 特征图。让我们尝试绘制原始模型第一层激活的第五个通道(见图 9.13)。

列表 9.10 可视化第五个通道

import matplotlib.pyplot as plt
plt.matshow(first_layer_activation[0, :, :, 5], cmap="viridis")

图 9.13 测试猫图片上第一层激活的第五个通道

这个通道似乎编码了一个对角边缘检测器,但请注意,你自己的通道可能会有所不同,因为卷积层学习的特定滤波器并不是确定性的。

现在,让我们绘制网络中所有激活的完整可视化(见图 9.14)。我们将提取并绘制每个层激活中的每个通道,并将结果堆叠在一个大网格中,通道并排堆叠。

列表 9.11 可视化每个中间激活的每个通道

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 + 1) * n_cols - 1,                      # ❸
                             images_per_row * (size + 1) - 1))             # ❸
    for col in range(n_cols):
        for row in range(images_per_row):
            channel_index = col * images_per_row + row
            channel_image = layer_activation[0, :, :, channel_index].copy()# ❹
            if channel_image.sum() != 0:                                   # ❺
                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 + 1): (col + 1) * size + col,                  # ❻
                row * (size + 1) : (row + 1) * size + row] = 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.axis("off")                                                        # ❼
    plt.imshow(display_grid, aspect="auto", cmap="viridis")                # ❼

❶ 迭代激活(和相应层的名称)。

❷ 层激活的形状为(1, size, size, n_features)。

❸ 准备一个空网格,用于显示该激活中的所有通道。

❹ 这是一个单通道(或特征)。

❺ 将通道值归一化到[0, 255]范围内。所有零通道保持为零。

❻ 将通道矩阵放入我们准备好的空网格中。

❼ 显示该层的网格。

图 9.14 测试猫图片上每个层激活的每个通道

这里有几点需要注意:

  • 第一层充当各种边缘检测器的集合。在这个阶段,激活保留了初始图片中几乎所有的信息。
  • 随着深入,激活变得越来越抽象,越来越难以直观解释。它们开始编码更高级别的概念,如“猫耳朵”和“猫眼”。更深层次的表现包含的关于图像视觉内容的信息越来越少,包含的与图像类别相关的信息越来越多。
  • 激活的稀疏性随着层的深度增加而增加:在第一层中,几乎所有滤波器都被输入图像激活,但在后续层中,越来越多的滤波器为空白。这意味着滤波器编码的模式在输入图像中找不到。

我们刚刚证明了深度神经网络学习的表示的一个重要普遍特征:随着层深度的增加,由层提取的特征变得越来越抽象。更高层的激活包含的关于特定输入的信息越来越少,包含的关于目标的信息越来越多(在本例中,图像的类别:猫或狗)。深度神经网络有效地充当信息蒸馏管道,原始数据输入(在本例中是 RGB 图片),并被反复转换,以便过滤掉不相关的信息(例如,图像的具体视觉外观),并放大和精炼有用的信息(例如,图像的类别)。

这类似于人类和动物感知世界的方式:观察一个场景几秒钟后,人类可以记住其中存在的抽象对象(自行车,树),但无法记住这些对象的具体外观。事实上,如果你试图凭记忆画一辆普通的自行车,很可能你无法得到一个近似正确的结果,即使你一生中见过成千上万辆自行车(例如,参见图 9.15)。现在就试试吧:这种效应绝对是真实的。你的大脑已经学会完全抽象化其视觉输入——将其转化为高级视觉概念,同时过滤掉不相关的视觉细节——这使得记住你周围事物的外观变得极其困难。

图 9.15 左:试图凭记忆画一辆自行车。右:原理图自行车的样子。

9.4.2 可视化卷积滤波器

检查卷积网络学习的滤波器的另一种简单方法是显示每个滤波器应该响应的视觉模式。这可以通过输入空间中的梯度上升来实现:将梯度下降应用于卷积网络的输入图像的值,以最大化特定滤波器的响应,从一个空白输入图像开始。生成的输入图像将是所选滤波器最大响应的图像。

让我们尝试使用在 ImageNet 上预训练的 Xception 模型的滤波器。这个过程很简单:我们将构建一个损失函数,最大化给定卷积层中给定滤波器的值,然后我们将使用随机梯度下降来调整输入图像的值,以最大化这个激活值。这将是我们利用GradientTape对象进行低级梯度下降循环的第二个示例(第一个示例在第二章中)。

首先,让我们实例化加载了在 ImageNet 数据集上预训练权重的 Xception 模型。

列表 9.12 实例化 Xception 卷积基础

model = keras.applications.xception.Xception(
    weights="imagenet",
    include_top=False)      # ❶

❶ 分类层对于这个用例是无关紧要的,所以我们不包括模型的顶层。

我们对模型的卷积层感兴趣——Conv2DSeparableConv2D层。我们需要知道它们的名称,以便检索它们的输出。让我们按深度顺序打印它们的名称。

列表 9.13 打印 Xception 中所有卷积层的名称

for layer in model.layers:
    if isinstance(layer, (keras.layers.Conv2D, keras.layers.SeparableConv2D)):
        print(layer.name)

你会注意到这里的SeparableConv2D层都被命名为类似block6_sepconv1block7_sepconv2等。Xception 被结构化为包含几个卷积层的块。

现在,让我们创建一个第二个模型,返回特定层的输出——一个特征提取器模型。因为我们的模型是一个功能 API 模型,它是可检查的:我们可以查询其一个层的 output 并在新模型中重用它。无需复制整个 Xception 代码。

第 9.14 节 创建特征提取器模型

layer_name = "block3_sepconv1"                                            # ❶
layer = model.get_layer(name=layer_name)                                  # ❷
feature_extractor = keras.Model(inputs=model.input, outputs=layer.output) # ❸

❶ 您可以将其替换为 Xception 卷积基中的任何层的名称。

❷ 这是我们感兴趣的层对象。

❸ 我们使用 model.input 和 layer.output 来创建一个模型,给定一个输入图像,返回我们目标层的输出。

要使用这个模型,只需在一些输入数据上调用它(请注意,Xception 需要通过 keras.applications.xception.preprocess_input 函数对输入进行预处理)。

第 9.15 节 使用特征提取器

activation = feature_extractor(
    keras.applications.xception.preprocess_input(img_tensor)
)

现在,让我们使用我们的特征提取器模型定义一个函数,该函数返回一个标量值,量化给定输入图像在给定层中“激活”给定滤波器的程度。这是我们在梯度上升过程中将最大化的“损失函数”:

import tensorflow as tf
def compute_loss(image, filter_index):                            # ❶
    activation = feature_extractor(image)
    filter_activation = activation[:, 2:-2, 2:-2, filter_index]   # ❷
    return tf.reduce_mean(filter_activation)                      # ❸

❶ 损失函数接受一个图像张量和我们正在考虑的滤波器的索引(一个整数)。

❷ 请注意,我们通过仅涉及损失中的非边界像素来避免边界伪影;我们丢弃激活边缘两侧的前两个像素。

❸ 返回滤波器激活值的平均值。

model.predict(x)model(x) 的区别

在上一章中,我们使用 predict(x) 进行特征提取。在这里,我们使用 model(x)。这是为什么?

y = model.predict(x)y = model(x)(其中 x 是输入数据的数组)都表示“在 x 上运行模型并检索输出 y”。然而它们并不完全相同。

predict() 在批处理中循环数据(实际上,您可以通过 predict(x, batch_size=64) 指定批处理大小),并提取输出的 NumPy 值。它在原理上等同于这样:

def predict(x):
    y_batches = []
    for x_batch in get_batches(x):
        y_batch = model(x).numpy()
        y_batches.append(y_batch)
    return np.concatenate(y_batches)

这意味着 predict() 调用可以扩展到非常大的数组。与此同时,model(x) 在内存中进行,不会扩展。另一方面,predict() 不可微分:如果在 GradientTape 范围内调用它,则无法检索其梯度。

当您需要检索模型调用的梯度时,应该使用 model(x),如果只需要输出值,则应该使用 predict()。换句话说,除非您正在编写低级梯度下降循环(就像我们现在所做的那样),否则始终使用 predict()

让我们设置梯度上升步骤函数,使用 GradientTape。请注意,我们将使用 @tf.function 装饰器来加快速度。

为了帮助梯度下降过程顺利进行的一个不明显的技巧是通过将梯度张量除以其 L2 范数(张量中值的平方的平均值的平方根)来对梯度张量进行归一化。这确保了对输入图像的更新的幅度始终在相同范围内。

第 9.16 节 通过随机梯度上升最大化损失

@tf.function 
def gradient_ascent_step(image, filter_index, learning_rate):
    with tf.GradientTape() as tape:
        tape.watch(image)                             # ❶
        loss = compute_loss(image, filter_index)      # ❷
    grads = tape.gradient(loss, image)                # ❸
    grads = tf.math.l2_normalize(grads)               # ❹
    image += learning_rate * grads                    # ❺
    return image                                      # ❻

❶ 明确监视图像张量,因为它不是 TensorFlow 变量(只有变量在梯度磁带中会自动被监视)。

❷ 计算损失标量,指示当前图像激活滤波器的程度。

❸ 计算损失相对于图像的梯度。

❹ 应用“梯度归一化技巧”。

❺ 将图像稍微移动到更强烈激活目标滤波器的方向。

❻ 返回更新后的图像,以便我们可以在循环中运行步骤函数。

现在我们有了所有的部分。让我们将它们组合成一个 Python 函数,该函数接受一个层名称和一个滤波器索引作为输入,并返回表示最大化指定滤波器激活的模式的张量。

第 9.17 节 生成滤波器可视化的函数

img_width = 200 
img_height = 200 
def generate_filter_pattern(filter_index):
    iterations = 30                                                       # ❶
    learning_rate = 10.                                                   # ❷
    image = tf.random.uniform(
        minval=0.4,
        maxval=0.6,
        shape=(1, img_width, img_height, 3))                              # ❸
    for i in range(iterations):                                           # ❹
        image = gradient_ascent_step(image, filter_index, learning_rate)  # ❹
    return image[0].numpy()

❶ 应用的梯度上升步骤数

❷ 单步幅的振幅

❸ 使用随机值初始化图像张量(Xception 模型期望输入值在 [0, 1] 范围内,因此这里选择以 0.5 为中心的范围)。

❹ 反复更新图像张量的值,以最大化我们的损失函数。

Python 深度学习第二版(GPT 重译)(四)(4)https://developer.aliyun.com/article/1485280

相关文章
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(二)(1)
Python 深度学习第二版(GPT 重译)(二)
72 1
|
12天前
|
机器学习/深度学习 算法框架/工具 计算机视觉
Python 深度学习第二版(GPT 重译)(三)(4)
Python 深度学习第二版(GPT 重译)(三)
21 5
|
机器学习/深度学习 监控 算法框架/工具
Python 深度学习第二版(GPT 重译)(三)(2)
Python 深度学习第二版(GPT 重译)(三)
35 1
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(二)(3)
Python 深度学习第二版(GPT 重译)(二)
40 1
|
机器学习/深度学习 存储 计算机视觉
Python 深度学习第二版(GPT 重译)(四)(1)
Python 深度学习第二版(GPT 重译)(四)
24 3
|
机器学习/深度学习 搜索推荐 TensorFlow
Python 深度学习第二版(GPT 重译)(二)(2)
Python 深度学习第二版(GPT 重译)(二)
80 1
|
机器学习/深度学习 数据可视化 定位技术
Python 深度学习第二版(GPT 重译)(四)(4)
Python 深度学习第二版(GPT 重译)(四)
17 3
|
机器学习/深度学习 测试技术 API
Python 深度学习第二版(GPT 重译)(三)(1)
Python 深度学习第二版(GPT 重译)(三)
36 4
Python 深度学习第二版(GPT 重译)(三)(1)
|
机器学习/深度学习 搜索推荐 TensorFlow
Python 深度学习第二版(GPT 重译)(二)(4)
Python 深度学习第二版(GPT 重译)(二)
47 1
Python 深度学习第二版(GPT 重译)(二)(4)
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习第二版(GPT 重译)(四)(2)
Python 深度学习第二版(GPT 重译)(四)
27 2