Python 深度学习第二版(GPT 重译)(四)(3)https://developer.aliyun.com/article/1485279
结果图像张量是一个形状为(200,
200,
3)
的浮点数组,其值可能不是在[0,
255]
范围内的整数。因此,我们需要对这个张量进行后处理,将其转换为可显示的图像。我们使用以下简单的实用函数来实现。
列表 9.18 将张量转换为有效图像的实用函数
def deprocess_image(image): image -= image.mean() # ❶ image /= image.std() # ❶ image *= 64 # ❶ image += 128 # ❶ image = np.clip(image, 0, 255).astype("uint8") # ❶ image = image[25:-25, 25:-25, :] # ❷ return image
❶ 将图像值归一化到[0, 255]
范围内。
❷ 中心裁剪以避免边缘伪影。
让我们试试(见图 9.16):
>>> plt.axis("off") >>> plt.imshow(deprocess_image(generate_filter_pattern(filter_index=2)))
图 9.16 block3_sepconv1
层中第二通道响应最大的模式
看起来block3_sepconv1
层中的滤波器 0 对水平线模式有响应,有点类似水或毛皮。
现在来看有趣的部分:你可以开始可视化每一层中的每一个滤波器,甚至是模型中每一层中的每一个滤波器。
列表 9.19 生成层中所有滤波器响应模式的网格
all_images = [] # ❶ for filter_index in range(64): print(f"Processing filter {filter_index}") image = deprocess_image( generate_filter_pattern(filter_index) ) all_images.append(image) margin = 5 # ❷ n = 8 cropped_width = img_width - 25 * 2 cropped_height = img_height - 25 * 2 width = n * cropped_width + (n - 1) * margin height = n * cropped_height + (n - 1) * margin stitched_filters = np.zeros((width, height, 3)) for i in range(n): # ❸ for j in range(n): image = all_images[i * n + j] stitched_filters[ row_start = (cropped_width + margin) * i row_end = (cropped_width + margin) * i + cropped_width column_start = (cropped_height + margin) * j column_end = (cropped_height + margin) * j + cropped_height stitched_filters[ row_start: row_end, column_start: column_end, :] = image keras.utils.save_img( # ❹ f"filters_for_layer_{layer_name}.png", stitched_filters)
❶ 生成并保存层中前 64 个滤波器的可视化。
❷ 准备一个空白画布,供我们粘贴滤波器可视化。
❸ 用保存的滤波器填充图片。
❹ 将画布保存到磁盘。
这些滤波器可视化(见图 9.17)告诉你很多关于卷积神经网络层如何看待世界的信息:卷积神经网络中的每一层学习一组滤波器,以便它们的输入可以被表达为滤波器的组合。这类似于傅里叶变换将信号分解为一组余弦函数的方式。随着你在模型中深入,这些卷积神经网络滤波器组中的滤波器变得越来越复杂和精细:
- 模型中第一层的滤波器编码简单的方向边缘和颜色(或在某些情况下是彩色边缘)。
- 位于堆栈中稍微靠上的层,如
block4_sepconv1
,编码由边缘和颜色组合而成的简单纹理。 - 更高层的滤波器开始类似于自然图像中发现的纹理:羽毛、眼睛、叶子等。
图 9.17 层block2_sepconv1
、block4_sepconv1
和block8_sepconv1
的一些滤波器模式
9.4.3 可视化类激活热图
我们将介绍最后一种可视化技术——这对于理解哪些部分的图像导致卷积神经网络做出最终分类决策是有用的。这对于“调试”卷积神经网络的决策过程特别有帮助,尤其是在分类错误的情况下(一个称为模型可解释性的问题领域)。它还可以让你在图像中定位特定的对象。
这类技术的通用类别称为类激活映射(CAM)可视化,它包括在输入图像上生成类激活热图。类激活热图是与特定输出类相关联的一组分数的 2D 网格,针对任何输入图像中的每个位置计算,指示每个位置相对于考虑的类的重要性。例如,给定一个输入到狗与猫卷积神经网络中的图像,CAM 可视化将允许你为“猫”类生成一个热图,指示图像的不同部分有多像猫,还可以为“狗”类生成一个热图,指示图像的哪些部分更像狗。
我们将使用的具体实现是一篇名为“Grad-CAM: 基于梯度定位的深度网络的视觉解释”的文章中描述的实现。
Grad-CAM 包括获取给定输入图像的卷积层的输出特征图,并通过类别相对于通道的梯度对该特征图中的每个通道进行加权。直观地,理解这个技巧的一种方式是想象你正在通过“输入图像如何激活不同通道”的空间地图来“每个通道对于类别的重要性有多大”,从而产生一个“输入图像如何激活类别”的空间地图。
让我们使用预训练的 Xception 模型演示这种技术。
列表 9.20 加载带有预训练权重的 Xception 网络
model = keras.applications.xception.Xception(weights="imagenet") # ❶
❶ 请注意,我们在顶部包含了密集连接的分类器;在所有以前的情况下,我们都将其丢弃。
考虑图 9.18 中显示的两只非洲大象的图像,可能是母象和幼象,在热带草原上漫步。让我们将这幅图像转换为 Xception 模型可以读取的内容:该模型是在大小为 299×299 的图像上训练的,根据keras.applications.xception .preprocess_input
实用程序函数中打包的一些规则进行预处理。因此,我们需要加载图像,将其调整大小为 299×299,将其转换为 NumPy 的float32
张量,并应用这些预处理规则。
列表 9.21 为 Xception 预处理输入图像
img_path = keras.utils.get_file( fname="elephant.jpg", origin="https://img-datasets.s3.amazonaws.com/elephant.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) # ❹ array = keras.applications.xception.preprocess_input(array) # ❺ return array img_array = get_img_array(img_path, target_size=(299, 299))
❶ 下载图像并将其存储在本地路径 img_path 下。
❷ 返回一个大小为 299×299 的 Python Imaging Library(PIL)图像。
❸ 返回一个形状为(299,299,3)的 float32 NumPy 数组。
❹ 添加一个维度,将数组转换为大小为(1,299,299,3)的批处理。
❺ 预处理批处理(这样做是按通道进行颜色归一化)。
图 9.18 非洲大象的测试图片
您现在可以在图像上运行预训练网络,并将其预测向量解码回人类可读格式:
>>> preds = model.predict(img_array) >>> print(keras.applications.xception.decode_predictions(preds, top=3)[0]) [("n02504458", "African_elephant", 0.8699266), ("n01871265", "tusker", 0.076968715), ("n02504013", "Indian_elephant", 0.02353728)]
该图像的前三个预测类别如下:
- 非洲大象(概率为 87%)
- 雄象(概率为 7%)
- 印度大象(概率为 2%)
网络已将图像识别为包含非洲大象数量不确定的图像。预测向量中最大激活的条目对应于“非洲大象”类别,索引为 386:
>>> np.argmax(preds[0]) 386
为了可视化图像的哪些部分最像非洲大象,让我们设置 Grad-CAM 过程。
首先,我们创建一个模型,将输入图像映射到最后一个卷积层的激活。
列表 9.22 设置返回最后一个卷积输出的模型
last_conv_layer_name = "block14_sepconv2_act" classifier_layer_names = [ "avg_pool", "predictions", ] last_conv_layer = model.get_layer(last_conv_layer_name) last_conv_layer_model = keras.Model(model.inputs, last_conv_layer.output)
其次,我们创建一个模型,将最后一个卷积层的激活映射到最终的类别预测。
列表 9.23 重新应用最后一个卷积输出的分类器
classifier_input = keras.Input(shape=last_conv_layer.output.shape[1:]) x = classifier_input for layer_name in classifier_layer_names: x = model.get_layer(layer_name)(x) classifier_model = keras.Model(classifier_input, x)
然后,我们计算输入图像的顶部预测类别相对于最后一个卷积层的激活的梯度。
列表 9.24 检索顶部预测类别的梯度
import tensorflow as tf with tf.GradientTape() as tape: last_conv_layer_output = last_conv_layer_model(img_array) # ❶ tape.watch(last_conv_layer_output) # ❶ preds = classifier_model(last_conv_layer_output) # ❷ top_pred_index = tf.argmax(preds[0]) # ❷ top_class_channel = preds[:, top_pred_index] # ❷ grads = tape.gradient(top_class_channel, last_conv_layer_output) # ❸
❶ 计算最后一个卷积层的激活并让磁带观察它。
❷ 检索与顶部预测类别对应的激活通道。
❸ 这是顶部预测类别相对于最后一个卷积层的输出特征图的梯度。
现在我们对梯度张量应用池化和重要性加权,以获得我们的类别激活热图。
列表 9.25 梯度池化和通道重要性加权
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)).numpy() # ❶ last_conv_layer_output = last_conv_layer_output.numpy()[0] for i in range(pooled_grads.shape[-1]): # ❷ last_conv_layer_output[:, :, i] *= pooled_grads[i] # ❷ heatmap = np.mean(last_conv_layer_output, axis=-1) # ❸
❶ 这是一个向量,其中每个条目是给定通道的梯度的平均强度。它量化了每个通道相对于顶部预测类的重要性。
❷ 将最后一个卷积层的输出中的每个通道乘以“这个通道的重要性”。
❸ 结果特征图的通道均值是我们的类别激活热图。
为了可视化目的,我们还将将热图归一化到 0 和 1 之间。结果显示在图 9.19 中。
列表 9.26 热图后处理
heatmap = np.maximum(heatmap, 0) heatmap /= np.max(heatmap) plt.matshow(heatmap)
图 9.19 独立类别激活热图
最后,让我们生成一幅将原始图像叠加在我们刚刚获得的热图上的图像(见图 9.20)。
列表 9.27 将热图叠加在原始图片上
import matplotlib.cm as cm img = keras.utils.load_img(img_path) # ❶ img = keras.utils.img_to_array(img) # ❶ heatmap = np.uint8(255 * heatmap) # ❷ jet = cm.get_cmap("jet") # ❸ jet_colors = jet(np.arange(256))[:, :3] # ❸ jet_heatmap = jet_colors[heatmap] # ❸ jet_heatmap = keras.utils.array_to_img(jet_heatmap) # ❹ jet_heatmap = jet_heatmap.resize((img.shape[1], img.shape[0])) # ❹ jet_heatmap = keras.utils.img_to_array(jet_heatmap) # ❹ superimposed_img = jet_heatmap * 0.4 + img # ❺ superimposed_img = keras.utils.array_to_img(superimposed_img) # ❺ save_path = "elephant_cam.jpg" # ❻ superimposed_img.save(save_path) # ❻
❶ 加载原始图像。
❷ 将热图重新缩放到 0-255 的范围。
❸ 使用“jet”颜色图重新着色热图。
❹ 创建包含重新着色的热图的图像。
❺ 将热图和原始图像叠加,热图透明度为 40%。
❻ 保存叠加的图像。
图 9.20 测试图片上的非洲象类激活热图
这种可视化技术回答了两个重要问题:
- 网络为什么认为这幅图像包含非洲象?
- 非洲象在图片中的位置在哪里?
特别值得注意的是,小象的耳朵被强烈激活:这可能是网络区分非洲象和印度象的方式。
摘要
- 您可以使用深度学习执行三项基本的计算机视觉任务:图像分类、图像分割和目标检测。
- 遵循现代卷积神经网络架构的最佳实践将帮助您充分利用您的模型。其中一些最佳实践包括使用残差连接、批量归一化和深度可分离卷积。
- 卷积神经网络学习的表示易于检查——卷积神经网络与黑匣子相反!
- 您可以生成卷积神经网络学习的滤波器的可视化,以及类活动的热图。
¹ Kaiming He 等,“深度残差学习用于图像识别”,计算机视觉与模式识别会议(2015),arxiv.org/abs/1512.03385
。
² Sergey Ioffe 和 Christian Szegedy,“批量归一化:通过减少内部协变量转移加速深度网络训练”,第 32 届国际机器学习会议论文集(2015), arxiv.org/abs/1502.03167
。
³ François Chollet,“Xception:使用深度可分离卷积的深度学习”,计算机视觉与模式识别会议(2017),arxiv.org/abs/1610.02357
。
⁴ Liang-Chieh Chen 等,“具有空洞可分离卷积的编码器-解码器用于语义图像分割”,ECCV(2018),arxiv.org/abs/1802.02611
。
⁵ Ramprasaath R. Selvaraju 等,arXiv(2017),arxiv.org/abs/1610.02391
。