Visualizing What a Convolutional Neural Network Learns
作者:禅与计算机程序设计艺术
1.简介
本文通过系统分析卷积神经网络(CNN)中的可视化技巧展开深入探讨,旨在探析其在图像识别领域的应用价值。作为深度学习的重要组成部分,卷积神经网络由一系列关键组件协同作用构成:首先,卷积层与池化层通过协同作用共同提取图像的空间特征;随后,全连接层随后负责将提取的空间特征映射至输出的类别或数值上;然而,为了深入理解卷积神经网络在图像识别中的功能机制及其调试训练后的模型,掌握相关可视化技巧具有重要意义
2.CNN简介
卷积神经网络(Convolutional Neural Networks, CNNs)是深度学习中的重要组成部分,在其中包含了多个卷积层。每个卷积层都配备若干过滤器(filter),用于提取图像中的特定模式特征。随后经过最大池化过程处理后将每个区域的局部响应降低到单个值,并丢弃非主要特征信息。随后将输出传递至几个全连接层进行进一步计算和处理,并最终输出分类结果或回归预测值。CNN名称来源于其独特的数据处理机制:通过卷积操作使得图像中不同位置的像素高度重合,在此过程中能够有效提取并捕捉到图像的整体特征信息。
3.可视化工具
通常情况下,可视化工具有助于我们清晰地观察到模型在学习过程中的行为特征。这些工具包括但不限于常见的分析模块。
-
权重可视化(Weight Visualization): 该方法也被称为"核可视化"或"核心视图"(Weight Visualization),它旨在考察模型在学习过程中如何通过调整权值来影响最终输出的结果。该可视化技术采用热力图的形式展示权值矩阵中各权值的大小及其应用情况之间的关系。通过这一方法可以直观地识别出哪些权值对模型输出起着关键作用而哪些则不重要。
-
激活函数可视化(Activation Function Visualization): 可视化技术也被广泛称为"特征图可视化"(Feature Visualization),主要用于分析模型如何将输入图像转化为输出结果。该方法通过展示神经网络各层对输入图像激活值的变化过程来生成特征图(Feature Map),从而能够清晰地观察到模型在不同层次的学习过程中所提取的信息变化情况。
-
梯度可视化(Gradient Visualization):也被称为“梯度可解释性映射”(Saliency Map),其主要作用是揭示模型在反向传播过程中对输入数据的关注程度。该方法通过计算输入图像对模型参数的梯度变化情况,进而帮助理解模型在学习过程中是否有效捕捉到了关键特征信息或遗漏了重要模式。
决策边界可视化(Decision Boundary Visualization):该技术旨在比较模型预测结果与实际标签以分析分类依据。该方法有助于评估模型预测结果与其对应的真实标签之间的差异程度。
4.实验
4.1 数据集准备
在本研究中,我们基于CIFAR-10数据集开展了一系列实验研究。该数据集是由Krizhevsky、LeCun以及Hadsell团队于2009年发布的重要计算机视觉基准库。该数据集包含了总共60,000张高分辨率彩色图像,并且每张图像的尺寸均为32x32x3。这些图像被划分为十个典型类别:飞机、汽车、鸟类、猫科动物(如猫)、鹿类动物(如鹿)、狗科动物(如狗)、青蛙科动物(如青蛙)、马类动物(如马)、船只(如船)和卡车(Truck)。为了深入评估模型的可视化特性,我们特意收集并整理了包含手写数字在内的丰富测试样例集合。
4.2 模型训练
首先,我们需要导入相应的库。
import tensorflow as tf
from tensorflow import keras
from matplotlib import pyplot as plt
import numpy as np
import cv2
import os
np.random.seed(7)
tf.random.set_seed(7)
代码解读
接着导入了CIFAR-10数据集。在本研究中,我们仅考虑CIFAR-10中的头两个类别:飞机和汽车。这些类别的样本总数则被设定为以批大小为128的小批量处理。
num_classes = 2 # 只用 CIFAR-10 中的前两个类别:飞机、汽车
(X_train, y_train), (X_test, y_test) = keras.datasets.cifar10.load_data()
class_names = ['airplane', 'car']
Y_train = keras.utils.to_categorical(y_train[:num_classes*128], num_classes=num_classes)
Y_test = keras.utils.to_categorical(y_test[:num_classes*128], num_classes=num_classes)
X_train = X_train[:num_classes*128]/255.0
X_test = X_test[:num_classes*128]/255.0
model = keras.models.Sequential([
keras.layers.Conv2D(filters=32, kernel_size=(3,3), activation='relu', input_shape=[32,32,3]),
keras.layers.MaxPooling2D(pool_size=(2,2)),
keras.layers.Conv2D(filters=64, kernel_size=(3,3), activation='relu'),
keras.layers.MaxPooling2D(pool_size=(2,2)),
keras.layers.Flatten(),
keras.layers.Dense(units=128, activation='relu'),
keras.layers.Dropout(rate=0.5),
keras.layers.Dense(units=num_classes, activation='softmax')
])
model.summary()
optimizer = keras.optimizers.Adam(lr=0.001)
loss = 'categorical_crossentropy'
metrics = ['accuracy']
model.compile(optimizer=optimizer, loss=loss, metrics=metrics)
history = model.fit(X_train, Y_train, epochs=20, validation_split=0.1, verbose=1, batch_size=128)
代码解读
模型结构如下所示: 训练结果如下所示:
train_acc val_acc loss accuracy
epoch 0 0.69 0.06 0.97
epoch 1 0.78 0.12 0.95
epoch 2 0.80 0.07 0.97
epoch 3 0.85 0.05 0.98
epoch 4 0.84 0.05 0.98
epoch 5 0.84 0.05 0.98
epoch 6 0.84 0.06 0.98
epoch 7 0.85 0.05 0.98
epoch 8 0.85 0.05 0.98
epoch 9 0.84 0.06 0.98
代码解读
4.3 可视化技巧示例
4.3.1 权重可视化
为了更好地观察第一层卷积核的结构特性及其权重分布情况,我们对其进行了可视化分析。
weight = model.layers[0].get_weights()[0]
plt.figure(figsize=(10,10))
for i in range(32):
for j in range(32):
ax = plt.subplot(32, 32, i * 32 + j + 1)
ax.imshow(weight[:, :, 0, i * 32 + j], cmap="coolwarm")
plt.axis('off')
代码解读
结果如右图所示:
能够观察到的是,在网络过滤器的设计过程中,默认情况下会优先考虑网络流量属性中的端到端包长度字段作为过滤依据,并将其作为判断条件的关键参数之一
4.3.2 激活函数可视化
我们也可以通过可视化某一层的激活函数来了解该层产生的特征图。
def get_activations(model, layer_idx, X_batch):
get_layer_output = keras.backend.function([model.layers[0].input], [model.layers[layer_idx].output])
activations = get_layer_output([X_batch])[0]
return activations
def display_activation(activations, col_size, row_size, act_index):
activation = activations[:, :, :, act_index]
activation_min = np.min(activation)
activation_max = np.max(activation)
activated_cells = activation > 0
cols = col_size
rows = row_size
fig, axes = plt.subplots(rows, cols, figsize=(cols * 2.5, rows * 1.5))
for row in range(rows):
for col in range(cols):
img = np.zeros((28, 28))
if activated_cells[row][col]:
img = X_test[activated_cells[row][col]]
axes[row][col].imshow(img, cmap='gray')
axes[row][col].axis('off')
plt.show()
# 用训练好的模型获取训练数据中的前1000个样本的激活函数输出
activations = get_activations(model, -1, X_test[:1000])
display_activation(activations, 10, 10, 0)
代码解读
如上图所示的结果表明,在一个前馈神经网络中各层次都对应着各自的激活函数输出。针对每个层次的激活函数输出我们通常会识别出每个单元中数值最大的那个,并将其与相应的输入图像建立联系。各个层次表现出独特的特性:有些侧重于特定方向特征的表现 others则倾向于呈现块状结构上的差异性。每种激活函数都能捕捉到图像的不同特性例如 sigmoid 函数更适合区分正负两种类别而 ReLU 函数则更适合于捕捉局部细节特征的信息差异性分布情况通过分析不同层次的不同表现我们可以深入理解模型在学习过程中可能出现的问题所在
4.3.3 梯度可视化
通过梯度可视化技术可以深入理解模型在训练阶段所学到的知识与未掌握的知识;同时也可以识别出模型未能有效学习的部分知识。利用梯度计算工具对模型参数进行求导运算,并展示各参数对应的梯度分布情况。
from tensorflow.keras.applications.resnet50 import ResNet50
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.resnet50 import preprocess_input, decode_predictions
# 获取ResNet50模型,并移除顶层分类器
model = ResNet50(include_top=False, pooling='avg')
# 从测试集中随机选择10张图片
img_paths = []
for file in sorted(os.listdir('./test')):
img_paths.append(file)
selected_imgs = random.sample(img_paths, 10)
imgs = []
labels = []
for path in selected_imgs:
img = image.load_img('./test/' + 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)[0]
label = decode_predictions(preds.reshape(-1, 1))[0][0][1]
labels.append(label)
imgs.append(cv2.imread('./test/' + path, cv2.IMREAD_COLOR))
def deprocess_image(x):
"""标准化输入图像"""
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1
x += 0.5
x = np.clip(x, 0, 1)
x *= 255
x = np.clip(x, 0, 255).astype('uint8')
return x
def show_gradients(gradient):
gradient = np.maximum(gradient, 0.)
gradient /= np.max(gradient)
img = gradient.transpose(1, 2, 0)
img = cv2.resize(img, dsize=(224, 224))
cv2.imshow('Gradients', deprocess_image(img[..., ::-1]))
cv2.waitKey(0)
def visualize_gradcam():
nb_imgs = len(imgs)
heatmap = np.zeros((nb_imgs,) + imgs[0].shape[:-1], dtype=np.float32)
# 计算每个样本的梯度
grads = []
outputs = []
with tf.GradientTape() as tape:
inputs = tf.constant(imgs) / 255.
for i in range(len(model.layers)):
print(i+1, end='\r')
tape.watch(inputs)
predictions = model(inputs)
top_pred_index = int(tf.argmax(predictions, axis=-1))
grad = tape.gradient(predictions[0][top_pred_index], inputs)
grads.append(grad)
outputs.append(predictions)
# 对每一个样本计算GradCAM
for i in range(nb_imgs):
print(f"Visualizing {i+1}/{nb_imgs}...", end="\r")
grad = grads[-1][i]
output = outputs[-1][i]
last_conv_layer = model.layers[-1]
weights = last_conv_layer.get_weights()[0]
pooled_grad = tf.reduce_mean(grad, axis=(0, 1, 2))
iterate = tf.keras.backend.function([model.layers[0].input], [pooled_grad, last_conv_layer.output[0]])
pooled_grad, conv_layer_output = iterate([np.array([imgs[i]], dtype=np.float32)])
for j in range(weights.shape[-1]):
heatmap[i][:,:,j] = np.maximum(heatmap[i][:,:,j], np.dot(weights[:,:,j], conv_layer_output[0,:,:,j]))
# 将heatmap标准化
max_heatmaps = np.max(heatmap, axis=(1,2))
heatmaps = heatmap / max_heatmaps[:,None,None]
for i in range(nb_imgs):
# 生成叠加的heatmap
overlayed_hm = cv2.addWeighted(imgs[i], 0.5, cv2.cvtColor(deprocess_image(heatmaps[i]), cv2.COLOR_RGB2BGR)*255, 0.5, gamma=0)
# 显示原始图像和热力图
f, axarr = plt.subplots(1, 2, figsize=(10,5))
axarr[0].imshow(imgs[i])
axarr[0].set_title(labels[i])
axarr[0].axis('off')
axarr[1].imshow(heatmaps[i])
axarr[1].axis('off')
plt.show()
cv2.imshow('', imgs[i])
cv2.setWindowTitle('', f'{i}_{labels[i]}')
cv2.moveWindow('', i*100, 0)
cv2.imshow('HeatMap', cv2.cvtColor(deprocess_image(heatmaps[i]*255), cv2.COLOR_RGB2BGR))
cv2.moveWindow('HeatMap', i*100+500, 0)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Grad-CAM可视化结果
visualize_gradcam()
代码解读
