포스트

Grad-CAM은 CAM과 무엇이 다른가: GAP 없이 CNN 판단 영역 찾기

Grad-CAM은 선택한 클래스 점수를 마지막 합성곱 특징 맵으로 미분해, 모델 구조를 바꾸지 않고 판단 영역을 그리는 방법이다. GAP와 선형 분류층이 꼭 필요한 CAM과 달리 gradient를 가중치로 사용하므로 더 다양한 CNN 구조와 출력에 적용할 수 있다.

다만 target class와 target layer를 정확히 고르고 forward feature와 backward gradient가 같은 실행에서 나온 것인지 확인해야 한다. 히트맵 하나는 결론이 아니라 입력, class, layer를 바꿔 볼 다음 실험의 출발점이다.

figure1

CAM으로는 왜 부족했나

CAM은 마지막 특징 맵 \(A^k\)와 클래스 \(c\)의 분류 가중치 \(w_k^c\)를 결합한다.

\[L_{CAM}^c = \sum_k w_k^c A^k\]

이 식을 쓰려면 합성곱 특징을 GAP로 줄인 뒤 선형 분류층에 연결해야 한다. 이미 학습한 모델에 fully connected layer가 있거나, softmax 분류가 아닌 image captioning, visual question answering 같은 출력이라면 구조를 바꾸지 않고 같은 가중치를 얻기 어렵다.

Grad-CAM은 분류층 가중치 대신 현재 입력과 목표 출력에서 실제로 흘러온 gradient를 사용한다. 따라서 “이 이미지에서 이 클래스 점수에 어떤 채널이 영향을 줬는가”를 직접 계산한다.

gradient가 히트맵이 되는 과정

마지막 합성곱의 \(k\)번째 특징 맵을 \(A^k\), softmax 이전의 목표 클래스 점수를 \(y^c\)라고 하자. 먼저 점수를 각 위치의 activation으로 미분하고 공간 평균을 낸다.

\[\alpha_k^c = \frac{1}{Z}\sum_i\sum_j\frac{\partial y^c}{\partial A_{ij}^k}\]

\(\alpha_k^c\)는 클래스 \(c\)에 대해 채널 \(k\)가 얼마나 중요한지를 나타내는 입력별 가중치다. 이 값을 특징 맵에 곱해 더한 뒤 ReLU를 적용한다.

\[L_{Grad-CAM}^c = ReLU\left(\sum_k \alpha_k^c A^k\right)\]

figure4

ReLU는 선택한 클래스 점수에 양의 영향을 준 위치에 초점을 맞춘다. 결과는 마지막 특징 맵 해상도의 거친 지도이며, 입력 크기로 확대해 원본 위에 겹쳐 본다.

CAM 구조에서는 \(y^c\)를 GAP 특징으로 미분하면 기존 분류 가중치와 비례하는 값이 나온다. 그래서 Grad-CAM은 CAM을 포함하는 일반화로 이해할 수 있다. 차이는 단순히 “더 최신”이라는 데 있지 않고, 고정된 분류 가중치 대신 입력별 gradient를 쓴다는 데 있다.

Guided Grad-CAM은 무엇을 더하나

Grad-CAM은 클래스를 구분하는 영역은 보여주지만 공간 해상도가 낮아 세부 경계를 표현하기 어렵다. Guided Backpropagation은 역전파할 때 양의 gradient를 남겨 입력 픽셀 수준의 세밀한 패턴을 만든다.

Guided Grad-CAM은 두 결과를 결합한다.

  • Grad-CAM: 어떤 영역이 목표 클래스와 관련됐는지 보여준다.
  • Guided Backpropagation: 그 영역 안의 세밀한 픽셀 패턴을 보여준다.

다만 세밀해 보이는 결과가 곧 더 정확한 설명이라는 뜻은 아니다. 목적은 보기 좋은 그림이 아니라, 모델이 예상치 못한 배경이나 지름길을 사용하는지 점검하는 것이다.

PyTorch 구현에서 자주 틀리는 지점

아래는 원문의 STL10, ResNet18 실험에서 계산 흐름만 정리한 핵심 조각이다. ResNet18, 모델 가중치, 입력 전처리, FINAL_CONV_NAME은 별도로 준비해야 하므로 그대로 실행되는 완성 프로그램은 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gradients = []

def save_gradient(grad):
    gradients.append(grad)

feature = tensor_img
for name, module in model.named_children():
    if name == 'Linear':
        feature = feature.view(feature.size(0), -1)
    feature = module(feature)
    if name == 'FINAL_CONV_NAME':
        feature.register_hook(save_gradient)
        final_conv = feature

target = feature.argmax(dim=1).item()
one_hot = torch.zeros_like(feature)
one_hot[0, target] = 1

model.zero_grad()
feature.backward(gradient=one_hot, retain_graph=True)

weights = gradients[0].mean(dim=(2, 3), keepdim=True)
mask = F.relu((weights * final_conv).sum(dim=1)).squeeze(0)
mask = mask.detach().cpu().numpy()
mask = (mask - mask.min()) / (mask.max() - mask.min())

확인할 것은 네 가지다.

  1. 목표값은 softmax 확률보다 softmax 이전 점수인지 확인한다.
  2. hook을 건 층이 정말 원하는 합성곱 층인지 shape으로 검증한다.
  3. 이전 실행의 gradient가 섞이지 않도록 매번 초기화한다.
  4. 직접 클래스를 비교할 때는 예측 클래스가 아니라 비교할 클래스 인덱스를 명시한다.

마지막으로 heatmap을 입력 크기로 확대해 합성한다.

1
2
3
4
5
6
7
resized = cv2.resize(mask, (128, 128))
heatmap = cv2.applyColorMap(
    np.uint8(255 * resized), cv2.COLORMAP_JET
)
image = cv2.resize(cv2.imread(img_path), (128, 128))
overlay = heatmap * 0.3 + image * 0.5
cv2.imwrite('./grad_cam.jpg', overlay)

gradtest

한 장의 히트맵만으로 모델을 신뢰하지 말자. 정답과 오답 클래스, 여러 이미지, 조금 변형한 입력을 나란히 비교해야 반복되는 패턴이 보인다. Grad-CAM은 “왜 그런 결정을 내렸는지”를 완전히 증명하는 도구가 아니라, 어디를 의심하고 다음 실험을 할지 알려주는 진단 도구다.

PyTorch 구현을 작은 단계로 확인하는 법

먼저 model을 평가 모드로 두고 학습 때와 같은 전처리를 적용한다. 입력 batch shape와 target class index를 출력하고, 선택한 convolution layer의 forward output shape를 저장한다. 공간 차원이 없는 layer를 고르면 heatmap을 만들 수 없다.

Forward hook과 backward hook이 같은 layer의 feature와 gradient를 가져오는지 확인한다. 이전 실행의 값이 남지 않도록 gradient를 초기화하고 target class score 하나를 선택해 backward한다. Batch 안의 어느 샘플을 설명하는지도 명시해야 다른 이미지의 gradient를 섞지 않는다.

Gradient를 공간 방향으로 평균해 channel weight를 만들고, feature map에 가중 합산한다. ReLU 적용 전후와 normalization에서 0으로만 된 지도를 어떻게 처리하는지 확인한다. 최대값이 0인데 나누면 숫자가 깨질 수 있으므로 빈 heatmap도 의미 있는 실패로 남긴다.

원본 overlay에서는 모델 입력의 crop, resize를 되돌린다. 단순 확대만 하면 원본과 좌표가 어긋날 수 있다. 원본, 전처리 입력, raw heatmap, overlay를 같은 파일 묶음으로 저장하면 시각화 오류를 찾기 쉽다.

해석을 과신하지 않기 위한 비교 실험

예측 class뿐 아니라 정답 class와 관련 없는 class의 지도를 만든다. 모든 class가 같은 영역을 밝힌다면 class별 gradient가 제대로 바뀌는지, 모델이 공통 배경에 의존하는지 살펴본다. Score가 매우 낮은 class 지도도 같은 의미로 해석하지 않는다.

같은 이미지를 조금 crop하거나 밝기를 바꿔 prediction과 지도를 함께 비교한다. Class가 유지되는데 지도만 크게 흔들리면 단일 그림을 안정적인 설명으로 제시하기 어렵다. 여러 sample에서 반복되는 패턴을 봐야 한다.

Layer를 바꾸면 해상도와 의미 수준이 달라진다. 앞 layer의 선명한 가장자리가 더 정확한 설명이라는 뜻은 아니고, 마지막 layer의 거친 지도가 쓸모없다는 뜻도 아니다. 질문이 texture인지 object 위치인지에 따라 비교 목적을 정한다.

Target class를 명시하지 않으면 구현이 현재 최고 확률 클래스의 gradient를 사용해, 사용자가 설명하려던 정답 클래스와 다른 히트맵을 만들 수 있습니다. 정답과 예측이 다른 표본에서는 두 클래스의 Grad-CAM을 나란히 계산하면 모델이 무엇을 놓쳤는지 더 잘 보입니다. Backward 전에 남은 gradient를 정리하고, hook이 매 호출마다 중복 등록되지 않는지도 확인해야 반복 실행 결과를 신뢰할 수 있습니다.

Layer 선택은 분해능과 의미 수준의 교환입니다. 앞쪽 층은 위치가 세밀하지만 클래스 의미가 약하고, 뒤쪽 층은 의미가 강하지만 공간 크기가 작습니다. 마지막 convolution 하나만 정답으로 고정하지 말고 인접한 층 몇 곳에서 결과가 일관되는지, 입력을 조금 바꾸었을 때 강조 영역이 갑자기 뒤집히지 않는지 비교해야 합니다.

함께 읽으면 이해가 이어지는 글

자주 묻는 질문

Grad-CAM은 왜 CAM처럼 GAP 구조가 필요하지 않나요?

고정된 선형 분류층 weight 대신 선택한 class 점수를 feature map으로 미분한 gradient를 channel 가중치로 사용하기 때문입니다.

Grad-CAM은 어느 layer에서 구해야 하나요?

보통 공간 정보가 남은 마지막 convolution layer를 출발점으로 봅니다. 더 앞 layer는 세밀하지만 의미가 다를 수 있으므로 목적에 따라 layer별 결과를 비교해야 합니다.

Grad-CAM이 밝힌 영역은 정확한 segmentation mask인가요?

아닙니다. Class 점수에 민감한 대략적 공간 영역을 보여 주는 진단 지도이며, 경계가 정확한 mask나 인과적 설명으로 사용하면 안 됩니다.

THE END / OPSOAI

여기까지 읽었습니다

핵심 장면을 한 번 더 떠올려 보세요. 이해가 남았다면 이 책은 제 역할을 다했습니다.

다른 책 고르기
표지 1

키와 좌우 스와이프를 지원합니다. 읽던 페이지는 이 기기에 저장됩니다.

CONTENTS

이 책의 목차

    8개 장 14 분읽는 시간