[可解释学习]Tasko5:【代码实战】CAM、Captum

一、图像分类可解释性分析实战-CAM热力图系列算法

原作者是在GPU平台上进行运行的: Featurize

对Pytorch预训练ImageNet图像分类模型,和自己训练得到的水果图像分类模型,通过各种CAM类激活热力图方法,进行可解释分析和显著性分析。

使用torch-cam工具包、pytorch-grad-cam工具包,在单张图像、视频文件、摄像头实时画面上绘制CAM热力图,观察神经网络预测指定类别 的 “脑回路” 和 “注意力” ,剖析深度学习黑箱子,知其然也知其所以然。

两种调用方式:

  1. 命令行调用
  2. Python API调用

C1是对单张图像运行CAM算法

C2是对视频文件逐帧运行CAM算法

使用的文件夹路径为:

E:\Train_Custom_Dataset-main\图像分类\6-可解释性分析、显著性分析

中的

1.torch-cam工具包:CAM热力图

2.pytorch-grad-cam工具包:CAM热力图、Guided Grad-CAM热力图、DFF

1.torch-cam工具包:CAM热力图

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云平台的话)

B-torchcam命令行

通过命令行方式使用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')

 [可解释学习]Tasko5:【代码实战】CAM、Captum_第1张图片

##图中有多个类别
# 类别-虎斑猫
!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')

 [可解释学习]Tasko5:【代码实战】CAM、Captum_第2张图片

# 类别-边牧犬
!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')

[可解释学习]Tasko5:【代码实战】CAM、Captum_第3张图片

C1-Pytorch预训练ImageNet图像分类-单张图像.

#导入工具包
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())

[可解释学习]Tasko5:【代码实战】CAM、Captum_第4张图片

from torchcam.utils import overlay_mask
result = overlay_mask(img_pil, Image.fromarray(activation_map), alpha=0.7)
print(result)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第5张图片 

#整理代码:设置类别、中文类别显示
#载入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)

 [可解释学习]Tasko5:【代码实战】CAM、Captum_第6张图片

C2-Pytorch预训练lmageNet图像分类-视频文件.

#通过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)

C3-Pytorch预训练lmageNet图像分类-摄像头实时画面

#获取摄像头的一帧画面
# 导入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部分着重于水果分类模型

D1-自己训练的水果分类模型-单张图像

#整理代码
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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第7张图片 

 D2-自己训练的水果分类模型-视频文件.

##视频预测
#输入输出视频路径
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)

二、Captum工具包

前面使用torchcam工具包与pytorch Gradcam工具包对单张图像文件,视频文件,实时画面进行了基于CAM热力图的可解释性分析

Captum工具包是专门针对Pytorch的可解释性分析工具

可以对图像分类模型、自然语言处理、多模态任务做可解释性分析

这里主要讲解遮挡与梯度

这里的环境配置与CAM里的一样

B1-遮挡可解释性分析-ImageNet图像分类

 遮挡用小滑块,滑动遮挡图像上的不同区域,观察哪些区域被遮挡后会显著影响模型的分类决策

 更改滑块尺寸、滑动步长那个,对比效果

#导入工具包
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)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第8张图片

##预处理
# 缩放、裁剪
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())

 [可解释学习]Tasko5:【代码实战】CAM、Captum_第9张图片

# 更改遮挡滑块的尺寸
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())

[可解释学习]Tasko5:【代码实战】CAM、Captum_第10张图片

##小遮挡滑块(运行时间较长,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())

[可解释学习]Tasko5:【代码实战】CAM、Captum_第11张图片

lntegrated Gradients可解释性分析

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())

[可解释学习]Tasko5:【代码实战】CAM、Captum_第12张图片

# 设置配色方案
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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第13张图片

加入高斯噪声的多张图像,平滑输出
在输入图像中加入高斯噪声,构造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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第14张图片

GradientShap可解释性分析

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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第15张图片

E-Feature Ablation特征消融可解释性分析

根据实例分割标注图,分别除去图像中的不同语义分组区域,观察对模型预测结果的影响。

##载入图像文件和实例分割标注文件
img_path = 'test_img/2007_002953.jpg'
mask_path = 'test_img/2007_002953_mask.png'

img = Image.open(img_path)
print(img)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第16张图片

mask_img = Image.open(mask_path)
print(mask_img)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第17张图片

##预处理
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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第18张图片

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)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第19张图片

##最高置信度预测结果
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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第20张图片

从图中可以看出,绿色最深的区域为酒瓶对应的区域,证明酒瓶区域对模型预测为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()

[可解释学习]Tasko5:【代码实战】CAM、Captum_第21张图片

从图中可以看出,绿色最深的区域为显示器对应的区域,证明显示器区域对模型预测为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)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第22张图片

##预处理、模型前向预测
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()

 [可解释学习]Tasko5:【代码实战】CAM、Captum_第23张图片

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)

[可解释学习]Tasko5:【代码实战】CAM、Captum_第24张图片

总结:CAM在实际的操作上还是有一些苦难在的,比如环境配置与运行方面,需要有好的GPU,后面Camtum工具包莫名与之前的CAM论文里的一些部分相像,如遮挡与梯度,如果想更好地使用 Camtum工具包最好还是详细看文档,直接运用让人只能观其表像。

你可能感兴趣的:(学习)