共计 3683 个字符,预计需要花费 10 分钟才能阅读完成。
应用 grad-cam 对 ViT 的输入进行可视化
[TOC]
前言
Vision Transformer (ViT) 作为当初 CV 中的支流 backbone,它能够在图像分类工作上达到与卷积神经网络 (CNN) 相媲美甚至超过的性能。ViT 的核心思想是将输出图像划分为多个小块,而后将每个小块作为一个 token 输出到 Transformer 的编码器中,最终失去一个全局的类别 token 作为分类后果。
ViT 的劣势在于它能够更好地捕获图像中的长距离依赖关系,而不须要应用简单的卷积操作。然而,这也带来了一个挑战,那就是如何解释 ViT 的决策过程,以及它是如何关注图像中的不同区域的。为了解决这个问题,咱们能够应用一种叫做 grad-cam 的技术,它能够依据 ViT 的输入和梯度,生成一张热力求,显示 ViT 在做出分类时最关注的图像区域。
原理
grad-cam 对 ViT 的输入进行可视化的原理是利用 ViT 的最初一个注意力块的输入和梯度,计算出每个 token 对分类后果的贡献度,而后将这些贡献度映射回原始图像的空间地位,造成一张热力求。具体来说,grad-cam+ViT 的步骤如下:
- 给定一个输出图像和一个指标类别,将图像划分为 14×14 个小块,并将每个小块转换为一个 768 维的向量。在这些向量之前,还要加上一个非凡的类别 token,用于示意全局的分类信息。这样就失去了一个 197×768 的矩阵,作为 ViT 的输出。
- 将 ViT 的输出通过 Transformer 的编码器,失去一个 197×768 的输入矩阵。其中第一个向量就是类别 token,它蕴含了 ViT 对整个图像的了解。咱们将这个向量通过一个线性层和一个 softmax 层,失去最终的分类概率。
- 计算类别 token 对指标类别的梯度,即 $\frac{\partial y_c}{\partial A}$,其中 $y_c$ 是指标类别的概率,$A$ 是 ViT 的输入矩阵。这个梯度示意了每个 token 对分类后果的重要性。
- 对每个 token 的梯度求平均值,失去一个 197 维的向量 $w$,其中 $w_i = \frac{1}{Z}\sum_k \frac{\partial y_c}{\partial A_{ik}}$,$Z$ 是梯度的维度,即 768。这个向量 $w$ 能够看作是每个 token 的权重。
- 将 ViT 的输入矩阵和权重向量相乘,失去一个 197 维的向量 $s$,其中 $s_i = \sum_k w_k A_{ik}$。这个向量 $s$ 能够看作是每个 token 对分类后果的贡献度。
- 将贡献度向量 $s$ 除去第一个元素(类别 token),并重塑为一个 14×14 的矩阵 $M$,其中 $M_{ij} = s_{(i-1) \times 14 + j + 1}$。这个矩阵 $M$ 能够看作是每个小块对分类后果的贡献度。
- 将贡献度矩阵 $M$ 进行归一化和上采样,失去一个与原始图像大小雷同的矩阵 $H$,其中 $H_{ij} = \frac{M_{ij} – \min(M)}{\max(M) – \min(M)}$。这个矩阵 $H$ 就是咱们要求的热力求,它显示了 ViT 在做出分类时最关注的图像区域。
- 将热力求 $H$ 和原始图像进行叠加,失去一张可视化的图像,能够直观地看到 ViT 的注意力散布。
应用代码
import argparse
import cv2
import numpy as np
import torch
from pytorch_grad_cam import GradCAM, \
ScoreCAM, \
GradCAMPlusPlus, \
AblationCAM, \
XGradCAM, \
EigenCAM, \
EigenGradCAM, \
LayerCAM, \
FullGrad
from pytorch_grad_cam import GuidedBackpropReLUModel
from pytorch_grad_cam.utils.image import show_cam_on_image, \
preprocess_image
from pytorch_grad_cam.ablation_layer import AblationLayerVit
# 加载预训练的 ViT 模型
model = torch.hub.load('facebookresearch/deit:main',
'deit_tiny_patch16_224', pretrained=True)
model.eval()
# 判断是否应用 GPU 减速
use_cuda = torch.cuda.is_available()
if use_cuda:
model = model.cuda()
接下来,咱们须要定义一个函数来将 ViT 的输入层从三维张量转换为二维张量,以便 grad-cam 可能解决:
def reshape_transform(tensor, height=14, width=14):
# 去掉类别标记
result = tensor[:, 1:, :].reshape(tensor.size(0),
height, width, tensor.size(2))
# 将通道维度放到第一个地位
result = result.transpose(2, 3).transpose(1, 2)
return result
而后,咱们须要抉择一个指标层来计算 grad-cam。因为 ViT 的最初一层只有类别标记对预测类别有影响,所以咱们不能抉择最初一层。咱们能够抉择倒数第二层中的任意一个 Transformer 编码器作为指标层。在这里,咱们抉择第 11 层作为示例:
# 创立 GradCAM 对象
cam = GradCAM(model=model,
target_layer=model.blocks[5],
use_cuda=use_cuda,
reshape_transform=reshape_transform)
接下来,咱们须要筹备一张输出图像,并将其转换为适宜 ViT 的格局:
# 读取输出图像
image_path = "cat.jpg"
rgb_img = cv2.imread(image_path, 1)[:, :, ::-1]
rgb_img = cv2.resize(rgb_img, (224, 224))
# 预处理图像
input_tensor = preprocess_image(rgb_img,
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
# 将图像转换为批量模式
input_tensor = input_tensor.unsqueeze(0)
if use_cuda:
input_tensor = input_tensor.cuda()
最初,咱们能够调用 cam
对象的 forward
办法,传入输出张量和预测类别(如果不指定,则默认为最高概率的类别),失去 grad-cam 的输入:
# 计算 grad-cam
target_category = None # 能够指定一个类别,或者应用 None 示意最高概率的类别
grayscale_cam = cam(input_tensor=input_tensor,
target_category=target_category)
# 将 grad-cam 的输入叠加到原始图像上
visualization = show_cam_on_image(rgb_img, grayscale_cam)
# 保留可视化后果
cv2.imwrite('cam.jpg', visualization)
这样,咱们就实现了应用 grad-cam 对 ViT 的输入进行可视化的过程。咱们能够看到,ViT 次要关注了图像中的猫的头部和身材区域,这与咱们的直觉相符。通过应用 grad-cam,咱们能够更好地了解 ViT 的工作原理,以及它对不同图像区域的重要性。
Pytorch-grad-cam 库的更多办法
除了经典的 grad-cam,库里目前反对的办法还有:
Method | What it does |
---|---|
GradCAM | 应用均匀梯度对 2D 激活进行加权 |
GradCAM++ | 相似 GradCAM,但应用了二阶梯度 |
XGradCAM | 相似 GradCAM,但通过归一化的激活对梯度进行了加权 |
EigenCAM | 应用 2D 激活的第一主成分(无奈辨别类别,但成果仿佛不错) |
EigenGradCAM | 相似 EigenCAM,但反对类别辨别,应用了激活 * 梯度的第一主成分,看起来和 GradCAM 差不多,然而更洁净 |
LayerCAM | 应用正梯度对激活进行空间加权,对于浅层有更好的成果 |
这里给出 MMpretrain 提供的比照示例:
在 MMpretrain 中应用
如果你刚好在用 MMpretrain,那么有着不便的脚本文件来帮忙你更加不便的进行下面的工作,具体可见:类别激活图(CAM)可视化 — MMPretrain 1.0.0rc7 文档
示例
这里也放一些我本人试过的例子:
总结
通过应用 grad-cam,咱们能够更好地了解 ViT 的工作原理,以及它是如何从图像中提取有用的特色的。grad-cam 也能够用于其余基于 Transformer 的模型,例如 DeiT、Swin Transformer 等,只须要依据不同的模型构造和输入,调整相应的计算步骤即可。
本文参加了 SegmentFault 思否写作挑战赛,欢送正在浏览的你也退出。