最近感覺類激活圖可視化是一件很有趣的事情。
CAM(傳送門:CAM實作的流程(pytorch))由于對網絡結構有定性要求,是以在可視化一些有多個全連接配接層的網絡時,表現不太友好,于是出現了Grad-CAM。
文章目錄
-
-
- 算法思路
- 實作過程
-
- 1.導入各種包
- 2.定義一些函數
- 3.運作函數
-
算法思路
引用的部落客 G5Lorenzo 一句話
Grad-CAM根據輸出向量,進行backward,求取特征圖的梯度,得到每個特征圖上每個像素點對應的梯度,也就是特征圖對應的梯度圖,然後再對每個梯度圖求平均,這個平均值就對應于每個特征圖的權重,然後再将權重與特征圖進行權重求和,最後經過relu激活函數就可以得到最終的類激活圖
實作過程
先準備圖檔、标簽以及模型
類别标簽下載下傳方法:
先安裝axel:
sudo apt-get install axel
執行下載下傳指令
axel -n 5 https://s3.amazonaws.com/outcome-blog/imagenet/labels.json
labels.json如果下不了就從網盤下:
連結:https://pan.baidu.com/s/1JAfwLtVEp1-ourEdd4VLhg
提取碼:1234
圖檔下載下傳:
axel -n 5 http://media.mlive.com/news_impact/photo/9933031-large.jpg
模型下載下傳:
senet1_1:
axel -n 5 https://download.pytorch.org/models/squeezenet1_1-f364aa15.pth
resnet18:
axel -n 5 https://download.pytorch.org/models/resnet18-5c106cde.pth
1.導入各種包
import cv2
import os
import numpy as np
import torch
import torchvision.transforms as transforms
from torchvision import models
import json
2.定義一些函數
圖檔預處理函數
# 圖檔預處理
def img_preprocess(img_in):
img = img_in.copy()
img = img[:, :, ::-1] # 1
img = np.ascontiguousarray(img) # 2
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize([0.4948052, 0.48568845, 0.44682974], [0.24580306, 0.24236229, 0.2603115])
])
img = transform(img)
img = img.unsqueeze(0) # 3
return img
1.之後讀取圖檔會使用opencv讀取,讀取的顔色通道為BGR,為了适應模型,需要将顔色通道轉回為RGB。
2.由于更改通道後,數組變為不連續,是以需要使用
np.ascontiguousarray
将img轉為連續數組,否則無法轉為tensor。
3.增加第一維的batch通道,使得圖檔能夠輸入網絡
定義擷取梯度和特征圖的函數
# 定義擷取梯度的函數
def backward_hook(module, grad_in, grad_out):
grad_block.append(grad_out[0].detach())
# 定義擷取特征圖的函數
def farward_hook(module, input, output):
fmap_block.append(output)
定義計算grad-cam并顯示的函數
# 計算grad-cam并可視化
def cam_show_img(img, feature_map, grads, out_dir):
H, W, _ = img.shape
cam = np.zeros(feature_map.shape[1:], dtype=np.float32) # 4
grads = grads.reshape([grads.shape[0],-1]) # 5
weights = np.mean(grads, axis=1) # 6
for i, w in enumerate(weights):
cam += w * feature_map[i, :, :] # 7
cam = np.maximum(cam, 0)
cam = cam / cam.max()
cam = cv2.resize(cam, (W, H))
heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)
cam_img = 0.3 * heatmap + 0.7 * img
path_cam_img = os.path.join(out_dir, "cam.jpg")
cv2.imwrite(path_cam_img, cam_img)
4.feature_map.shape[1:] 表示取第一次元及之後的其餘次元的尺寸,如 [512, 14, 14] --> (14, 14)
5-6.計算每個通道的權重均值
7.将梯度權重與特征圖相乘再累加
3.運作函數
if __name__ == '__main__':
path_img = './cam/bicycle.jpg'
json_path = './cam/labels.json'
output_dir = './cam'
with open(json_path, 'r') as load_f:
load_json = json.load(load_f)
classes = {int(key): value for (key, value)
in load_json.items()}
# 隻取标簽名
classes = list(classes.get(key) for key in range(1000))
# 存放梯度和特征圖
fmap_block = list()
grad_block = list()
# 圖檔讀取;網絡加載
img = cv2.imread(path_img, 1)
img_input = img_preprocess(img)
# 加載 squeezenet1_1 預訓練模型
net = models.squeezenet1_1(pretrained=False)
pthfile = './pretrained/squeezenet1_1-f364aa15.pth'
net.load_state_dict(torch.load(pthfile))
net.eval() # 8
print(net)
# 注冊hook
net.features[-1].expand3x3.register_forward_hook(farward_hook) # 9
net.features[-1].expand3x3.register_backward_hook(backward_hook)
# forward
output = net(img_input)
idx = np.argmax(output.cpu().data.numpy())
print("predict: {}".format(classes[idx]))
# backward
net.zero_grad()
class_loss = output[0,idx]
class_loss.backward()
# 生成cam
grads_val = grad_block[0].cpu().data.numpy().squeeze()
fmap = fmap_block[0].cpu().data.numpy().squeeze()
# 儲存cam圖檔
cam_show_img(img, fmap, grads_val, output_dir)
8.一定要加上net.eval(),不然深一點的網絡(如resnet)就會識别出錯,而且每次執行後的類激活圖都不一樣。
9.-1索引為features中最後一個卷積層,看列印的模型就知道了。
squeezenet1_1的Grad-Cam可視化效果
vgg16的Grad-Cam可視化效果
resnet50的Grad-Cam可視化效果
resnet101的Grad-Cam可視化效果
參考代碼連結:
https://github.com/TingsongYu/PyTorch_Tutorial/blob/master/Code/4_viewer/6_hook_for_grad_cam.py