原作者是在GPU平台上进行运行的: Featurize
对Pytorch预训练ImageNet图像分类模型,和自己训练得到的水果图像分类模型,通过各种CAM类激活热力图方法,进行可解释分析和显著性分析。
使用torch-cam工具包、pytorch-grad-cam工具包,在单张图像、视频文件、摄像头实时画面上绘制CAM热力图,观察神经网络预测指定类别 的 “脑回路” 和 “注意力” ,剖析深度学习黑箱子,知其然也知其所以然。
两种调用方式:
C1是对单张图像运行CAM算法
C2是对视频文件逐帧运行CAM算法
使用的文件夹路径为:
E:\Train_Custom_Dataset-main\图像分类\6-可解释性分析、显著性分析
中的
1.torch-cam工具包:CAM热力图
2.pytorch-grad-cam工具包:CAM热力图、Guided Grad-CAM热力图、DFF
A:安装配置环境
##安装配置torchcam代码库环境
!pip install numpy pandas matplotlib requests tqdm opencv-python pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
#下载安装Pytorch
!pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
#下载安装mmcv-full
!pip install mmcv-full -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10.0/index.html
#下载中文字体文件
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf
#下载ImageNet1000类别信息
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/meta_data/imagenet_class_index.csv
#创建目录
import os
#存放测试照片
os.mkdir('test_img')
#存放结果文件
os.mkdir('output')
#存放训练得到的模型权重
os.mkdir('checkpoint')
# 下载样例模型文件
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/checkpoints/fruit30_pytorch_20220814.pth -P checkpoint
# 下载 类别名称 和 ID索引号 的映射字典
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/fruit30/labels_to_idx.npy
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/fruit30/idx_to_labels.npy
# 下载测试图像文件 至 test_img 文件夹
# 边牧犬,来源:https://www.woopets.fr/assets/races/000/066/big-portrait/border-collie.jpg
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/test/border-collie.jpg -P test_img
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/test/cat_dog.jpg -P test_img
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/test/0818/room_video.mp4 -P test_img
# 草莓图像,来源:https://www.pexels.com/zh-cn/photo/4828489/
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/test/0818/test_草莓.jpg -P test_img
##安装torchcam
#删除原有的torch-cam目录(如有)
!rm -rf torch-cam
## 下载安装 torch-cam
!git clone https://github.com/frgfm/torch-cam.git
!pip install -e torch-cam/.
##重启kernel
#验证安装成功
import torchcam
#设置matplotlib中文字体
import matplotlib.pyplot as plt
%matplotlib inline
# # windows操作系统
# plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签
# plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
# Mac操作系统,参考 https://www.ngui.cc/51cto/show-727683.html
# 下载 simhei.ttf 字体文件
# !wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf
# Linux操作系统,例如 云GPU平台:https://featurize.cn/?s=d7ce99f842414bfcaea5662a97581bd1
# 如果报错 Unable to establish SSL connection.,重新运行本代码块即可
!wget https://zihao-openmmlab.obs.cn-east-3.myhuaweicloud.com/20220716-mmclassification/dataset/SimHei.ttf -O /environment/miniconda3/lib/python3.7/site-packages/matplotlib/mpl-data/fonts/ttf/SimHei.ttf --no-check-certificate
!rm -rf /home/featurize/.cache/matplotlib
#正式开始设置
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
matplotlib.rc("font",family = 'SimHei') #中文字体
plt.rcParams['axes.unicode_minus']=False #用来显示符号
plt.plot([1,2,3],[100,500,300])
plt.title('matplotlib中文字体测试',fontsize=25)
plt.xlabel('X轴',fontsize=15)
plt.xlabel('Y轴',fontsize=15)
plt.show()
#若上图绘制失败,重启kernel后重新运行,设置matplotliv中文字体部分代替代码
这个最后的输出有点问题(如果不是使用子豪兄推荐的GPU云平台的话)
通过命令行方式使用torchcam算法库,对图像进行基于CAM的可解释性分析
#导入工具包
import os
import pandas as pd
from PIL import Image
#命令行基本用法
!python torch-cam/scripts/cam_example.py --help
#ImageNet预训练图像分类模型
#ImageNet1000类别名称与ID号
df = pd.read_csv('imagenet_class_index.csv')
#图中只有一个类别
#类别-边牧犬
!python torch-cam/scripts/cam_example.py \
--img test_img/border-collie.jpg \
--savefig output/B1_border_collie.jpg \
--arch resnet18 \
--class-idx 232 \
--rows 2
Image.open('output/B1_border_collie.jpg')
##图中有多个类别
# 类别-虎斑猫
!python torch-cam/scripts/cam_example.py \
--img test_img/cat_dog.jpg \
--savefig output/B2_cat_dog.jpg \
--arch resnet18 \
--class-idx 282 \
--rows 2
Image.open('output/B2_cat_dog.jpg')
# 类别-边牧犬
!python torch-cam/scripts/cam_example.py \
--img test_img/cat_dog.jpg \
--savefig output/B3_cat_dog.jpg \
--arch resnet18 \
--class-idx 232 \
--rows 2
Image.open('output/B3_cat_dog.jpg')
#导入工具包
import matplotlib.pyplot as plt
%matplotlib inline
from PIL import Image
import torch
#有GPU就用GPU,没有就使用CPU
device = torch.device('cuda:0' if torch.cuba.is_available() else 'cpu')
print('device', device)
#导入pillow中文字体
from PIL import ImageFont, ImageDraw
#导入中文字体,指定字体大小
font = ImageFont.truetype('SimHei.ttf', 50)
#导入ImageNet预训练模型
from torchvision.models import resnet18
model = resnet18(pretrained=True).eval().to(device)
#导入可解释分析方法
from torchcam.methods import SmoothGradCAMpp
cam_extractor = SmoothGradCAMpp(model)
#预处理
from torchvision import transforms
#测试集图像预处理-RCTN:缩放、裁剪、转Tensor、归一化
test_transform = trasforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
#运行图像分类预测
img_path = 'test_img/cat_dog.jpg'
img_pil = Image.open(img_path)
input_tensor = test_transform(img_pil).unsqueeze(0).to(device) # 预处理
input_tensor.shape
pred_logits = model(input_tensor)
pred_top1 = torch.topk(pred_logits, 1)
pred_id = pred_top1[1].detach().cpu().numpy().squeeze().item()
#生成可解释性分析热力图
activation_map = cam_extractor(pred_id, pred_logits)
activation_map = avtivation_map[0][0].detach().cpu().numpy()
print(activation_map.shape)
print(activation_map)
#可视化
print(plt.imshow(activaiton_map))
print(plt.show())
from torchcam.utils import overlay_mask
result = overlay_mask(img_pil, Image.fromarray(activation_map), alpha=0.7)
print(result)
#整理代码:设置类别、中文类别显示
#载入ImageNet1000图像分类标签
#lmageNet 1000类别中文释义: https://github.com/ningbonb/imagenet_classes_chinese
import pandas as pd
df = pd.read_csv('imagenet_class_index.csv')
idx_to_labels = {}
idx_to_labels_cn = {}
for idx, row in df.iterrowa():
idx_to_labels[row['ID']] = row['class']
idx_to_labels_cn[row['ID']] = row['Chinese']
img_path = 'test_img/cat_dog.jpg'
# 可视化热力图的类别ID,如果为 None,则为置信度最高的预测类别ID
show_class_id = 231
# show_class_id = None
# 是否显示中文类别
Chinese = True
# Chinese = False
#前向预测
img_pil = Image.open(img_path)
input_tensor = test_transform(img_pil).unsqueeze(0).to(device) #预处理
pred_logits = model(input_tensor)
pred_top1 = torch.topk(pred_logits, 1)
pred_id = pre_top1[1].detach().cpu().numpy().squeeze().item()
#可视化热力图的类别ID,如果不确定,则为置信度最高的预测类别ID
if show_class_id:
show_id = show_class_id
else:
show_id = pred_id
show_class_id = pred_id
#生成可解释分析热力图
activation_map = cam_extractor(show_id, pred_logits)
activation_map = activation_map[0][0].detach().cpu().numpy()
result = overlay_mask(img_pil, Image.fromarray(activation_map), alpha=0.7)
#在图像上写字
draw = ImageDraw.Draw(result)
if Chinese:
# 在图像上写中文
text_pred = 'Pred Class: {}'.format(idx_to_labels_cn[pred_id])
text_show = 'Show Class: {}'.format(idx_to_labels_cn[show_class_id])
else:
# 在图像上写英文
text_pred = 'Pred Class: {}'.format(idx_to_labels[pred_id])
text_show = 'Show Class: {}'.format(idx_to_labels[show_class_id])
# 文字坐标,中文字符串,字体,rgba颜色
draw.text((50, 100), text_pred, font=font, fill=(255, 0, 0, 1))
draw.text((50, 200), text_show, font=font, fill=(255, 0, 0, 1))
print(result)
#通过Python API方式,使用torchcam算法库,对Pytorch预训练ImageNet-1000图像分类模型进行基于CAM的可解释性分析
#导入工具包
import os
import time
import shutil
import temfile
from tqdm import tqdm
import gc
import matplotlib.pyplot as plt
%matplotlib inline
import cv2
from PIL import Image
import mmcv
import torch
from torchcam.utils import overlay_mask
# 有 GPU 就用 GPU,没有就用 CPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('device', device)
#导入pillow中文字体
from PIL import ImageFont, ImageDraw
#导入中文字体,指定字体大小
font = ImageFont.truetype('SimHei.ttf', 50)
#导入ImageNet预训练模型
from torchvision.models import resnet18
model = resnet18(pretrained=True).eval().to(device)
#载入ImageNet 1000图像分类标签
#lmageNet 1000类别中文释义: https://github.com/ningbonb/imagenet_classes_chinese
import pandas as pd
df = pd.read_csv('imagenet_class_index.csv')
idx_to_labels = {}
idx_to_labels_cn = {}
for idx, row in df.iterrows():
idx_to_labels[row['ID']] = row['class']
idx_to_labels_cn[row['ID']] = row['Chinese']
#导入可解释性分析方法
from torchcam.methods import SmoothGradCAMpp
cam_extractor = SmoothGradCAMpp(model)
#预处理
from torchvision import transforms
# 测试集图像预处理-RCTN:缩放、裁剪、转 Tensor、归一化
test_transform = transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
#图像分类预测函数
def pred_single_frame(img, show_class_id=None, Chinese=True):
'''
输入摄像头画面bgr-array和用于绘制热力图的类别ID,输出写字的热力图PIL-Image
如果不指定类别ID,则为置信度最高的预测类别ID
'''
img_bgr = img
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR 转 RGB
img_pil = Image.fromarray(img_rgb) # array 转 pil
input_tensor = test_transform(img_pil).unsqueeze(0).to(device) # 预处理
pred_logits = model(input_tensor) # 执行前向预测,得到所有类别的 logit 预测分数
pred_top1 = torch.topk(pred_logits, 1)
pred_id = pred_top1[1].detach().cpu().numpy().squeeze().item()
# 可视化热力图的类别ID,如果为 None,则为置信度最高的预测类别ID
if show_class_id:
show_id = show_class_id
else:
show_id = pred_id
show_class_id = pred_id
# 生成可解释性分析热力图
activation_map = cam_extractor(show_id, pred_logits)
activation_map = activation_map[0][0].detach().cpu().numpy()
result = overlay_mask(img_pil, Image.fromarray(activation_map), alpha=0.7)
# 在图像上写字
draw = ImageDraw.Draw(result)
if Chinese:
# 在图像上写中文
text_pred = 'Pred Class: {}'.format(idx_to_labels_cn[pred_id])
text_show = 'Show Class: {}'.format(idx_to_labels_cn[show_class_id])
else:
# 在图像上写英文
text_pred = 'Pred Class: {}'.format(idx_to_labels[pred_id])
text_show = 'Show Class: {}'.format(idx_to_labels[show_class_id])
# 文字坐标,中文字符串,字体,rgba颜色
draw.text((50, 100), text_pred, font=font, fill=(255, 0, 0, 1))
draw.text((50, 200), text_show, font=font, fill=(255, 0, 0, 1))
return result
##视频预测
#输入输出视频路径
input_video = 'test_img/room_video.mp4'
#创建临时文件夹
# 创建临时文件夹,存放每帧结果
temp_out_dir = time.strftime('%Y%m%d%H%M%S')
os.mkdir(temp_out_dir)
print('创建文件夹 {} 用于存放每帧预测结果'.format(temp_out_dir))
#视频逐帧预测
# 读入待预测视频
imgs = mmcv.VideoReader(input_video)
prog_bar = mmcv.ProgressBar(len(imgs))
# 对视频逐帧处理
for frame_id, img in enumerate(imgs):
## 处理单帧画面
img = pred_single_frame(img, show_class_id=None)
# 将处理后的该帧画面图像文件,保存至 /tmp 目录下
img.save(f'{temp_out_dir}/{frame_id:06d}.jpg', "BMP")
prog_bar.update() # 更新进度条
# 把每一帧串成视频文件
mmcv.frames2video(temp_out_dir, 'output/output_pred.mp4', fps=imgs.fps, fourcc='mp4v')
shutil.rmtree(temp_out_dir) # 删除存放每帧画面的临时文件夹
print('删除临时文件夹', temp_out_dir)
#获取摄像头的一帧画面
# 导入opencv-python
import cv2
import time
# 获取摄像头,传入0表示获取系统默认摄像头
cap = cv2.VideoCapture(1)
# 打开cap
cap.open(0)
# 无限循环,直到break被触发
while cap.isOpened():
# 获取画面
success, frame = cap.read()
if not success:
print('Error')
break
## !!!处理帧函数
# frame = process_frame(frame)
frame = process_frame(frame) # 卫生纸
# 展示处理后的三通道图像
cv2.imshow('my_window',frame)
if cv2.waitKey(1) in [ord('q'),27]: # 按键盘上的q或esc退出(在英文输入法下)
break
# 关闭摄像头
cap.release()
# 关闭图像窗口
cv2.destroyAllWindows()
这个实时画面不知道为什么,我一直运行错误
部分代码与C2一样,就不重复展示了
#调用摄像头获取每帧(模板)
# 调用摄像头逐帧实时处理模板
# 不需修改任何代码,只需修改process_frame函数即可
# 同济子豪兄 2021-7-8
# 导入opencv-python
import cv2
import time
# 获取摄像头,传入0表示获取系统默认摄像头
cap = cv2.VideoCapture(1)
# 打开cap
cap.open(0)
# 无限循环,直到break被触发
while cap.isOpened():
# 获取画面
success, frame = cap.read()
if not success:
print('Error')
break
## !!!处理帧函数
# frame = process_frame(frame)
frame = process_frame(frame, show_class_id=999) # 卫生纸
# 展示处理后的三通道图像
cv2.imshow('my_window',frame)
if cv2.waitKey(1) in [ord('q'),27]: # 按键盘上的q或esc退出(在英文输入法下)
break
# 关闭摄像头
cap.release()
# 关闭图像窗口
cv2.destroyAllWindows()
D部分和C部分重合率很高,D部分着重于水果分类模型
#整理代码
img_path = 'test_img/test_fruits.jpg'
# 可视化热力图的类别,如果不指定,则为置信度最高的预测类别
show_class = '猕猴桃'
# 前向预测
img_pil = Image.open(img_path)
input_tensor = test_transform(img_pil).unsqueeze(0).to(device) # 预处理
pred_logits = model(input_tensor)
pred_id = torch.topk(pred_logits, 1)[1].detach().cpu().numpy().squeeze().item()
if show_class:
class_id = labels_to_idx[show_class]
show_id = class_id
else:
show_id = pred_id
# 获取热力图
activation_map = cam_extractor(show_id, pred_logits)
activation_map = activation_map[0][0].detach().cpu().numpy()
result = overlay_mask(img_pil, Image.fromarray(activation_map), alpha=0.4)
plt.imshow(result)
plt.axis('off')
plt.title('{}\nPred:{} Show:{}'.format(img_path, idx_to_labels[pred_id], show_class))
plt.show()
##视频预测
#输入输出视频路径
input_video = 'test_img/fruits_video.mp4'
# 创建临时文件夹,存放每帧结果
temp_out_dir = time.strftime('%Y%m%d%H%M%S')
os.mkdir(temp_out_dir)
print('创建文件夹 {} 用于存放每帧预测结果'.format(temp_out_dir))
##视频逐帧预测
# 读入待预测视频
imgs = mmcv.VideoReader(input_video)
prog_bar = mmcv.ProgressBar(len(imgs))
# 对视频逐帧处理
for frame_id, img in enumerate(imgs):
## 处理单帧画面
img = pred_single_frame(img, show_class_id=None)
# 将处理后的该帧画面图像文件,保存至 /tmp 目录下
img.save(f'{temp_out_dir}/{frame_id:06d}.jpg', "BMP")
prog_bar.update() # 更新进度条
# 把每一帧串成视频文件
mmcv.frames2video(temp_out_dir, 'output/output_pred.mp4', fps=imgs.fps, fourcc='mp4v')
shutil.rmtree(temp_out_dir) # 删除存放每帧画面的临时文件夹
print('删除临时文件夹', temp_out_dir)
前面使用torchcam工具包与pytorch Gradcam工具包对单张图像文件,视频文件,实时画面进行了基于CAM热力图的可解释性分析
Captum工具包是专门针对Pytorch的可解释性分析工具
可以对图像分类模型、自然语言处理、多模态任务做可解释性分析
这里主要讲解遮挡与梯度
这里的环境配置与CAM里的一样
遮挡用小滑块,滑动遮挡图像上的不同区域,观察哪些区域被遮挡后会显著影响模型的分类决策
更改滑块尺寸、滑动步长那个,对比效果
#导入工具包
import os
import json
import numpy as np
import pandas as pd
from PIL import Image
import torch
import torch.nn.functional as F
import torchvision
from torchvision import models
from torchvision import transforms
# from captum.attr import IntegratedGradients
# from captum.attr import GradientShap
from captum.attr import Occlusion
# from captum.attr import NoiseTunnel
from captum.attr import visualization as viz
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
%matplotlib inline
# 有 GPU 就用 GPU,没有就用 CPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('device', device)
#载入预训练ResNet模型
model = models.resnet18(pretrained=True)
model = model.eval().to(device)
#载入ImageNet1000图像分类标签
import pandas as pd
df = pd.read_csv('imagenet_class_index.csv')
idx_to_labels = {}
idx_to_labels_cn = {}
for idx, row in df.iterrows():
idx_to_labels[row['ID']] = row['class']
idx_to_labels_cn[row['ID']] = row['Chinese']
#图像预处理
from torchvision import transforms
# 缩放、裁剪、转 Tensor、归一化
transform_A = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor()
])
transform_B = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
#载入测试图像
img_path = 'test_img/swan-3299528_1280.jpg'
img_pil = Image.open(img_path)
print(img_pil)
##预处理
# 缩放、裁剪
rc_img = transform_A(img_pil)
# 调整数据维度
rc_img_norm = np.transpose(rc_img.squeeze().cpu().detach().numpy(), (1,2,0))
# 色彩归一化
input_tensor = transform_B(rc_img).unsqueeze(0).to(device)
##前向预测
pred_logits = model(input_tensor)
pred_softmax = F.softmax(pred_logits, dim=1) # 对 logit 分数做 softmax 运算
pred_conf, pred_id = torch.topk(pred_softmax, 1)
pred_conf = pred_conf.detach().cpu().numpy().squeeze().item()
pred_id = pred_id.detach().cpu().numpy().squeeze().item()
pred_label = idx_to_labels[pred_id]
print('预测类别的ID {} 名称 {} 置信度 {:.2f}'.format(pred_id, pred_label, pred_conf))
在输入图像上,用遮挡滑块,滑动遮挡不同区域,探索哪些区域被遮挡后会显著影响模型的分类决策。
提示:因为每次遮挡都需要分别单独预测,因此代码运行可能需要较长时间。
occlusion = Occlusion(model)
##中等遮挡滑块
# 获得输入图像每个像素的 occ 值
attributions_occ = occlusion.attribute(input_tensor,
strides = (3, 8, 8), # 遮挡滑动移动步长
target=pred_id, # 目标类别
sliding_window_shapes=(3, 15, 15), # 遮挡滑块尺寸
baselines=0) # 被遮挡滑块覆盖的像素值
# 转为 224 x 224 x 3的数据维度
attributions_occ_norm = np.transpose(attributions_occ.detach().cpu().squeeze().numpy(), (1,2,0))
viz.visualize_image_attr_multiple(attributions_occ_norm, # 224 224 3
rc_img_norm, # 224 224 3
["original_image", "heat_map"],
["all", "positive"],
show_colorbar=True,
outlier_perc=2)
print(plt.show())
# 更改遮挡滑块的尺寸
attributions_occ = occlusion.attribute(input_tensor,
strides = (3, 50, 50), # 遮挡滑动移动步长
target=pred_id, # 目标类别
sliding_window_shapes=(3, 60, 60), # 遮挡滑块尺寸
baselines=0)
# 转为 224 x 224 x 3的数据维度
attributions_occ_norm = np.transpose(attributions_occ.detach().cpu().squeeze().numpy(), (1,2,0))
viz.visualize_image_attr_multiple(attributions_occ_norm, # 224 224 3
rc_img_norm, # 224 224 3
["original_image", "heat_map"],
["all", "positive"],
show_colorbar=True,
outlier_perc=2)
print(plt.show())
##小遮挡滑块(运行时间较长,2分钟左右)
# 更改遮挡滑块的尺寸
attributions_occ = occlusion.attribute(input_tensor,
strides = (3, 2, 2), # 遮挡滑动移动步长
target=pred_id, # 目标类别
sliding_window_shapes=(3, 4, 4), # 遮挡滑块尺寸
baselines=0)
# 转为 224 x 224 x 3的数据维度
attributions_occ_norm = np.transpose(attributions_occ.detach().cpu().squeeze().numpy(), (1,2,0))
viz.visualize_image_attr_multiple(attributions_occ_norm, # 224 224 3
rc_img_norm, # 224 224 3
["original_image", "heat_map"],
["all", "positive"],
show_colorbar=True,
outlier_perc=2)
print(plt.show())
lntegrated Gradients 原理
输入图像像素由空白变为输入图像像素的过程中,模型预测为某一特定类别的概率相对于输入图像像素的梯度积分。
##lntegrated Gradients可解释性分析
# 初始化可解释性分析方法
integrated_gradients = IntegratedGradients(model)
##单张图像
# 获得输入图像每个像素的 IG 值
attributions_ig = integrated_gradients.attribute(input_tensor, target=pred_id, n_steps=200)
# 转为 224 x 224 x 3的数据维度
attributions_ig_norm = np.transpose(attributions_ig.detach().cpu().squeeze().numpy(), (1,2,0))
plt.imshow(attributions_ig_norm[:, :, 0] * 100)
# plt.imshow(attributions_ig_norm[:, :, 1] * 100)
# plt.imshow(attributions_ig_norm[:, :, 2] * 100)
print(plt.show())
# 设置配色方案
default_cmap = LinearSegmentedColormap.from_list('custom blue',
[(0, '#ffffff'),
(0.25, '#000000'),
(1, '#000000')], N=256)
# 可视化 IG 值
viz.visualize_image_attr(attributions_ig_norm, # 224,224,3
rc_img_norm, # 224,224,3
method='heat_map',
cmap=default_cmap,
show_colorbar=True,
sign='positive',
outlier_perc=1)
plt.show()
加入高斯噪声的多张图像,平滑输出
在输入图像中加入高斯噪声,构造nt_samples个噪声样本,分别计算IG值,再使用smoothgrad_sq(先平均再平方)平滑。
noise_tunnel = NoiseTunnel(integrated_gradients)
# 获得输入图像每个像素的 IG 值
attributions_ig_nt = noise_tunnel.attribute(input_tensor, nt_samples=12, nt_type='smoothgrad_sq', target=pred_id)
# 转为 224 x 224 x 3的数据维度
attributions_ig_nt_norm = np.transpose(attributions_ig_nt.squeeze().cpu().detach().numpy(), (1,2,0))
# 设置配色方案
default_cmap = LinearSegmentedColormap.from_list('custom blue',
[(0, '#ffffff'),
(0.25, '#000000'),
(1, '#000000')], N=256)
viz.visualize_image_attr_multiple(attributions_ig_nt_norm, # 224 224 3
rc_img_norm, # 224 224 3
["original_image", "heat_map"],
["all", "positive"],
cmap=default_cmap,
show_colorbar=True)
plt.show()
GradientShap是一种线性的模型可解释性分析方法,使用多张参考图像(在本例中为2张)解释模型预测结果。参考图像通过给定的baseline分布随机生成。计算每个像素分别采用原始输入图像像素值和baseline图像像素值的梯度期望。
gradient_shap = GradientShap(model)
# 设置 baseline distribution
rand_img_dist = torch.cat([input_tensor * 0, input_tensor * 1])
# 获得输入图像每个像素的 GradientShap 值
attributions_gs = gradient_shap.attribute(input_tensor,
n_samples=50,
stdevs=0.0001,
baselines=rand_img_dist,
target=pred_id)
# 转为 224 x 224 x 3的数据维度
attributions_gs_norm = np.transpose(attributions_gs.detach().cpu().squeeze().numpy(), (1,2,0))
# 设置配色方案
default_cmap = LinearSegmentedColormap.from_list('custom blue',
[(0, '#ffffff'),
(0.25, '#000000'),
(1, '#000000')], N=256)
viz.visualize_image_attr_multiple(attributions_gs_norm,
rc_img_norm,
["original_image", "heat_map"],
["all", "absolute_value"],
cmap=default_cmap,
show_colorbar=True)
plt.show()
根据实例分割标注图,分别除去图像中的不同语义分组区域,观察对模型预测结果的影响。
##载入图像文件和实例分割标注文件
img_path = 'test_img/2007_002953.jpg'
mask_path = 'test_img/2007_002953_mask.png'
img = Image.open(img_path)
print(img)
mask_img = Image.open(mask_path)
print(mask_img)
##预处理
from torchvision import transforms
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
input_tensor = transform(img).unsqueeze(0)
##模型预测
pred_logits = model(input_tensor)
pred_softmax = F.softmax(pred_logits, dim=1)
##解析图像分类预测结果
plt.figure(figsize=(8,4))
x = range(1000)
y = pred_softmax.cpu().detach().numpy()[0]
ax = plt.bar(x, y, alpha=0.5, width=0.3, color='yellow', edgecolor='red', lw=3)
# plt.ylim([0, 1.0]) # y轴取值范围
# plt.bar_label(ax, fmt='%.2f', fontsize=15) # 置信度数值
plt.xlabel('Class', fontsize=20)
plt.ylabel('Confidence', fontsize=20)
plt.tick_params(labelsize=16) # 坐标文字大小
plt.title(img_path, fontsize=25)
plt.show()
n = 10
top_n = torch.topk(pred_softmax, n)
pred_ids = top_n[1].cpu().detach().numpy().squeeze() # 解析出类别
confs = top_n[0].cpu().detach().numpy().squeeze() # 解析出置信度
for i in range(n):
class_name = idx_to_labels[pred_ids[i]] # 获取类别名称
confidence = confs[i] * 100 # 获取置信度
text = '{:<15} {:>.4f}'.format(class_name, confidence)
print(text)
##最高置信度预测结果
pred_conf, pred_id = torch.topk(pred_softmax, 1)
pred_conf = pred_conf.detach().cpu().numpy().squeeze().item()
pred_id = pred_id.detach().cpu().numpy().squeeze().item()
pred_label = idx_to_labels[pred_id]
print('最高置信度预测类别', pred_label)
提示:图像分类假设图中仅有一个物体,如果有多个类别的多个物体,会略微干扰预测结果。
feature group特征分组
在实例分割标注图中,每一个类别都被划为一—类feature group.
Feature Ablation 就是分析每个feature group存在(或者不存在)的影响。
# 将实例分割标注图像转为 count, channels, height, width 维度
feature_mask = np.array(mask_img.getdata()).reshape(1, 1, mask_img.size[1], mask_img.size[0])
# 将实例分割标注图转为从 1 开始的标注值(而不是 0-255 的256个标注值),便于后续处理。
feature_mask[feature_mask == 5] = 1 # bottle
feature_mask[feature_mask == 20] = 2 # tvmonitor
feature_mask[feature_mask == 255] = 3 # void
##Feature Ablation可解释性分析
ablator = FeatureAblation(model)
##最高置信度类别(wine bottle)
# 计算每个 feature group 对模型预测为 pred_id对应类别 概率的影响
attribution_map = ablator.attribute(input_tensor, target=pred_id, feature_mask=torch.tensor(feature_mask))
attribution_map = attribution_map.detach().cpu().numpy().squeeze()
attribution_map = np.transpose(attribution_map, (1,2,0))
viz.visualize_image_attr(attribution_map,
method="heat_map",
sign="all",
show_colorbar=True)
plt.show()
从图中可以看出,绿色最深的区域为酒瓶对应的区域,证明酒瓶区域对模型预测为wine_bottle的影响最大,如果抹掉该区域,会对模型预测为wine_bottle 的概率产生较大负面影响。
背景区域的影响较小。如果抹掉该区域,会对模型预测为wine_bottle的概率产生较小负面影响。
显示器区域为红色,如果抹掉该区域,会对模型预测为wine_bottle的概率产生正面积极影响。
##更换类别为tv_monitor (664)
attribution_map = ablator.attribute(input_tensor, target=664, feature_mask=torch.tensor(feature_mask))
attribution_map = attribution_map.detach().cpu().numpy().squeeze()
attribution_map = np.transpose(attribution_map, (1,2,0))
viz.visualize_image_attr(attribution_map,
method="heat_map",
sign="all",
show_colorbar=True)
plt.show()
从图中可以看出,绿色最深的区域为显示器对应的区域,证明显示器区域对模型预测为tv_monitor的影响最大,如果抹掉该区域,会对模型预测为tv_monitor的概率产生较大负面影响。
背景区域的影响较小。如果抹掉该区域,影响较小。
酒瓶区域和边缘区域为红色,如果抹掉该区域,会对模型预测为tv_monitor 的概率产生正面积极影响。
思考:边缘区域为什么是红色?
##Sanity check 抹掉酒瓶和边缘区域
import cv2
new_mask = np.array(feature_mask)
new_mask[feature_mask == 0] = 1 # wine_bottle
new_mask[feature_mask == 1] = 0 # 背景
new_mask[feature_mask == 2] = 1 # tv_monitor
new_mask[feature_mask == 3] = 0 # 边缘
new_mask = np.expand_dims(new_mask.squeeze(), axis=2).astype(np.uint8)
img_without_bottles = cv2.bitwise_and(np.array(img), np.array(img), mask=new_mask)
img_without_bottles = cv2.cvtColor(img_without_bottles, cv2.COLOR_BGR2RGB)
cv2.imwrite('img_without_bottles.jpg', img_without_bottles)
img = Image.open('img_without_bottles.jpg')
print(img)
##预处理、模型前向预测
input_tensor = transform(img).unsqueeze(0)
pred_logits = model(input_tensor)
pred_softmax = F.softmax(pred_logits, dim=1)
##解析模型预测结果
plt.figure(figsize=(8,4))
x = range(1000)
y = pred_softmax.cpu().detach().numpy()[0]
ax = plt.bar(x, y, alpha=0.5, width=0.3, color='yellow', edgecolor='red', lw=3)
# plt.ylim([0, 1.0]) # y轴取值范围
# plt.bar_label(ax, fmt='%.2f', fontsize=15) # 置信度数值
plt.xlabel('Class', fontsize=20)
plt.ylabel('Confidence', fontsize=20)
plt.tick_params(labelsize=16) # 坐标文字大小
plt.title(img_path, fontsize=25)
plt.show()
n = 10
top_n = torch.topk(pred_softmax, n)
pred_ids = top_n[1].cpu().detach().numpy().squeeze() # 解析出类别
confs = top_n[0].cpu().detach().numpy().squeeze() # 解析出置信度
for i in range(n):
class_name = idx_to_labels[pred_ids[i]] # 获取类别名称
confidence = confs[i] * 100 # 获取置信度
text = '{:<15} {:>.4f}'.format(class_name, confidence)
print(text)
总结:CAM在实际的操作上还是有一些苦难在的,比如环境配置与运行方面,需要有好的GPU,后面Camtum工具包莫名与之前的CAM论文里的一些部分相像,如遮挡与梯度,如果想更好地使用 Camtum工具包最好还是详细看文档,直接运用让人只能观其表像。