..

Class Activation Map(Learning Deep Features for Discriminative Localization)

CAM

오늘은 Learning Deep Features for Discriminative Localization이라는 논문을 읽고, 정말 간단하게 리뷰해본 다음 이를 Pytorch를 통해 구현해보고자 합니다.

CNN을 해석하고자하는 시도는 계속해서 있어왔습니다. 하지만 (제가 아는 한) 대부분의 시도들은 각 filter들을 시각화한다거나, 어떤 filter에서 최대의 활성을 갖는 입력을 찾아내는 등의 filter 단위의 해석이었습니다. 이러한 해석이 나쁘다는 것은 아니지만, 이를 통해서 얻을 수 있는 것은 “얕은 곳에 있는 것은 edge를 찾아내고 깊어질수록 고차원적인 feature를 찾는다”정도에 불과합니다. CNN을 사용해 분류를 한다면, 우리가 결국 알고 싶은 것은 “도대체 얘가 데이터의 뭘 보고 이런 판단을 내렸는가” 아닐까요? Class Activation Map(이하 CAM)을 사용한다면 이를 어느정도 알아낼 수 있습니다.

Introduction

본격적으로 들어가기 전에, 우리가 흔히 생각하는 CNN의 기본적인 구성을 떠올려봅시다. 처음에는 Convoluton layer를 몇개 쌓고, 사이사이 Pooling을 섞어쓰겠죠. 그러다가 어느 정도 특징을 추출할 수 있을 정도로 깊어지면 Fully-Connected layer(FC)를 사용해서 최종적으로 어떤 class에 속하게 될지 확률을 뽑아내게 됩니다. 보통 Convolution을 사용하면 어느정도의 위치 정보를 활용한다고 하지만, 이를 FC에 넣기 위해서는 결국 일렬로 늘어뜨려야하고(flatten), 이러한 feature들이 FC를 거치게 되면서 우리가 이해하지 못하는 정보로 바뀌게 되겠죠. 하지만 이런 일반적인 구조 외의 다른 CNN 구조 또한 가능하고, 실제로 논문 또한 존재합니다.

Network in Network라는 논문을 보면(이전 포스트 참조), 마지막에 FC를 사용하는 대신 Global Average Pooling, GAP를 사용합니다. 정말 간단하게 설명해보자면, 마지막 Convolution Layer에서 우리가 분류해야되는 Class의 수만큼 채널을 갖게 합니다. 10개의 class를 분류해야하는 문제라 해봅시다. 마지막 Convoluton Layer를 거치고 난 다음 얻는 feature map의 채널 수가 10이 되도록하는 것이죠. 이렇게 얻은 10개의 channel에서 각 채널을 기준으로 평균(합)을 구합니다. 이 각각의 값들이 class에 대응하는 값들이 되는 것이고, 가장 큰 값을 가지는 부분으로 예측을 하게 됩니다. 논문에서는 GAP가 regularizer의 역할을 수행한다고 하지만, 우리는 조금 다른 부분에 주목해봅시다. 이런 방식으로 CNN을 구현하게 되면 마지막 FC를 사용하지 않게 되므로 일단 파라미터의 수를 조금 더 줄일 수 있게 됩니다. 또한 위치 정보를 그대로 사용한다고 해석할 수도 있겠죠. 이 구조를 그대로 끌고 나가 봅시다.

Class Activation Map

마지막 Convolution layer를 거치고 feature map을 얻었다고 해봅시다. 이렇게 얻은 feature map에서 GAP를 수행하고, 이렇게 얻은 값을 softmax에 넣어 확률값을 얻어냅니다. 이를 수식으로 조금 더 표현해보죠.

마지막 Conv layer를 거쳐서 얻은 feature map이 있다고 해봅시다. $k$번째 채널의 값들 중 $(x, y)$에 위치한 값을 $f_k(x,y)$라고 표헌해보죠. 그렇다면 GAP를 거치고 난 다음 얻을 수 있는 값, $F^k$는 $\sum_{x, y}f_k(x,y)$가 됩니다. class $c$에 대해서 softmax에 입력으로 주어지는 값, $S_c$는 $\sum_k w_{k}^{c}F_k$가 되구요. 여기서 $w_{k}^{c}$가 $k$번째 채널과 class $c$에 대응하는 값이라 한다면, $w_{k}^{c}$는 본질적으로 class $c$에서 $F_k$의 중요성을 나타낸다고 할 수 있습니다. 즉 $w_{k}^{c}$가 클수록 $c$에서 $F_k$가 미치는 영향은 커지게 되는 것이죠. 이때 $S_c = \sum_k w_k^c F_k$를 조금 바꿔봅시다. $F_k = \sum_{x, y}f_k(x,y)$니까

\[\begin{matrix} S_c &=& \sum_k w_k^c F_k \\ &=& \sum_k w_c^k \sum_{x, y}f_k(x,y) \\ &=& \sum_{x, y} \sum_k w_k^c f_k(x,y) \end{matrix}\]

가 됩니다. $M_c$를 $c$에 대한 CAM이라고 하고, $M_c(x, y) = \sum_k w_k^c f_k(x, y)$라고 정의한다면, 결국 $S_c = \sum_{x, y} M_c(x, y)$라는 것을 얻어낼 수 있습니다. 즉 $M_c(x, y)$는 $(x, y)$에 위치한 값이 $c$라는 class로 분류되는데 미치는 중요도를 나타내게 되는 것이죠. 길고 복잡한 수식으로 설명했지만 사진 한장이면 더 쉽게 이해할 수 있습니다.

CAM

사진을 보고 다시 설명하자면, GAP을 취하기 전에 위치한 feature map들이 $f_k$가 됩니다. 이를 채널 단위로 합해주면 $F_k$가 되는 것이죠(각 원 하나). 이를 softmax에 집어넣기 위해 FC를 하나 추가해주고, 이 때의 weight들을 $w^c_k$라고 생각하시면 됩니다.

결국 어떤 class $c$로 분류될 확률을 구할 때 곱해지는 각각의 weight들을 feature map에 곱해준다음 이들을 합치면 무엇을 보고 이 feature map을 $c$라고 분류했는지 알 수 있게 되는 것입니다.

Implementation

Pytorch로 이를 구현했습니다. 코드는 https://github.com/KangBK0120/CAM 여기를 참고하세요. 가장 먼저 모델을 정의하고, 이를 학습시킨 다음, test set에서 무작위로 하나를 뽑아 이를 분류하도록 합니다. 이렇게 하면 가장 높은 확률을 지니는 class를 알아낼 수 있을 것이고, 이에 대응하는 weight를 각각의 feature map에 곱해준 다음 heat map을 그렸습니다. CAM을 그리는 코드만 조금 가져와봤습니다.

params = list(net.parameters())
# get only weight from last layer(linear)
weight_softmax = np.squeeze(params[-2].cpu().data.numpy())

def returnCAM(feature_conv, weight_softmax, class_idx):
	size_upsample = (128, 128)
	bz, nc, h, w = feature_conv.shape
	output_cam = []
	for idx in class_idx:
		cam = weight_softmax[class_idx].dot(feature_conv.reshape( (nc, h*w)))
		cam = cam.reshape(h, w)
		cam = cam - np.min(cam)
		cam_img = cam/np.max(cam)
		cam_img = np.uint8(255 * cam_img)
		output_cam.append(cv2.resize(cam_img, size_upsample))
	return output_cam

우선 가장 마지막에 위치한, Softmax layer의 가중치를 가져옵니다. 그 다음에는, GAP를 거치기 직전 feature map을 가져와 이를 가중치와 곱해주면 됩니다. 이러한 코드를 실행하게 되면 아래와 같은 결과를 얻을 수 있습니다.

test1 cam1

test2 cam2

test3 cam3

Reference

https://github.com/metalbubble/CAM CAM을 그리는 코드는 이곳에 있는 pytorch 코드를 참고했습니다.