안녕하세요 Pulluper입니다.
이번 포스팅은 Explainable AI (XAI) 관련이 있고, 그리고 WSOL(weakly supervised object localization)
의 대표적인 방법인 Grad-CAM에 대하여 알아보겠습니다.
논문은 다음과 같습니다.
https://arxiv.org/abs/1610.02391
이번 포스팅의 목표는 grad cam 의 이해와 구현입니다.
그리고 다음과 같은 map을 나오게 하는것입니다.
Grad-CAM에서 가장 중요한 수식은 다음과 같습니다.
Grad-CAM은 기본적으로 classificaiton에서는 특정 클래스의 softmax 이전의 값 $y^c$을 기준으로 합니다.
만약 내가 알고싶은 클래스가 강아지라면 강아지에 따른 특정 layer의 활성화 맵을 구할 수 있습니다.
$a_k^c = \frac{1}{Z} \sum_i \sum_j \frac{\partial y^c}{\partial A_{ij}^{k}}$ 는 backward result 이고
$A_k$ 의 값은 forward result 입니다.
즉, backward result 와 forward result 의 곱입니다.
코드는 다음과 같습니다. 다음 레포에서 도움을 받았습니다.
https://github.com/jacobgil/pytorch-grad-cam
vgg16의 30번째 layer에 관한 값을 input 이미지위에 그립니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
class GradCam(nn.Module):
def __init__(self, model, module, layer):
super().__init__()
self.model = model
self.module = module
self.layer = layer
self.register_hooks()
def register_hooks(self):
for modue_name, module in self.model._modules.items():
if modue_name == self.module:
for layer_name, module in module._modules.items():
if layer_name == self.layer:
module.register_forward_hook(self.forward_hook)
module.register_backward_hook(self.backward_hook)
def forward(self, input, target_index):
outs = self.model(input)
outs = outs.squeeze() # [1, num_classes] --> [num_classes]
# 가장 큰 값을 가지는 것을 target index 로 사용
if target_index is None:
target_index = outs.argmax()
outs[target_index].backward(retain_graph=True)
a_k = torch.mean(self.backward_result, dim=(1, 2), keepdim=True) # [512, 1, 1]
out = torch.sum(a_k * self.forward_result, dim=0).cpu() # [512, 7, 7] * [512, 1, 1]
out = torch.relu(out) / torch.max(out) # 음수를 없애고, 0 ~ 1 로 scaling # [7, 7]
out = F.upsample_bilinear(out.unsqueeze(0).unsqueeze(0), [224, 224]) # 4D로 바꿈
return out.cpu().detach().squeeze().numpy()
def forward_hook(self, _, input, output):
self.forward_result = torch.squeeze(output)
def backward_hook(self, _, grad_input, grad_output):
self.backward_result = torch.squeeze(grad_output[0])
if __name__ == '__main__':
def preprocess_image(img):
means = [0.485, 0.456, 0.406]
stds = [0.229, 0.224, 0.225]
preprocessed_img = img.copy()[:, :, ::-1]
for i in range(3):
preprocessed_img[:, :, i] = preprocessed_img[:, :, i] - means[i]
preprocessed_img[:, :, i] = preprocessed_img[:, :, i] / stds[i]
preprocessed_img = \
np.ascontiguousarray(np.transpose(preprocessed_img, (2, 0, 1)))
preprocessed_img = torch.from_numpy(preprocessed_img)
preprocessed_img.unsqueeze_(0)
input = preprocessed_img.requires_grad_(True)
return input
def show_cam_on_image(img, mask):
# mask = (np.max(mask) - np.min(mask)) / (mask - np.min(mask))
heatmap = cv2.applyColorMap(np.uint8(255 * mask), cv2.COLORMAP_JET)
heatmap = np.float32(heatmap) / 255
cam = heatmap + np.float32(img)
cam = cam / np.max(cam)
cv2.imshow("cam", np.uint8(255 * cam))
cv2.imshow("heatmap", np.uint8(heatmap * 255))
cv2.waitKey()
import os
import cv2
import glob
import numpy as np
from torchvision.models import vgg16
model = vgg16(pretrained=True)
print(model)
model.eval()
grad_cam = GradCam(model=model, module='features', layer='30')
root = './image'
img_list = os.listdir(root)
img_list = sorted(glob.glob(os.path.join(root, '*.jpg')))
for img_path in img_list:
img = cv2.imread(img_path, 1)
img = np.float32(cv2.resize(img, (224, 224))) / 255
input = preprocess_image(img)
mask = grad_cam(input, None)
show_cam_on_image(img, mask)
VGG16의 구조
GradCam(
(model): VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
)
Observation
- Grad-CAM의 활성화 위치가 image 에 가까울수록 (얕을수록) semantic한 정보가 없다.
- 특정 line이나 곡선에 반응하는 filter등의 모습을 갖는다.
- Grad-CAM의 활성화 layer위치가 softmax(loss)에 가까울수록 class 정보가 크다.
- 이때의 layer 는 보통 resolution 이 작다. (pooling, conv 등으로 feature map 이 작아지므로)
마지막 30 layer 에서는 score 가 가장 큰 index (강아지)의 모습에
잘 활성화가 되는것을 볼 수 있습니다.
다음 레포에서 다른 model에 대한 grad-cam을 볼 수 있습니다. :)
댓글