포스트

CAM 히트맵이 엉뚱한 곳을 보는 이유: GAP 구조와 구현 체크리스트

CAM은 마지막 합성곱 특징 맵을 분류층 가중치로 합쳐, 특정 클래스를 고르는 데 기여한 위치를 보여주는 방법이다. 단, 아무 CNN에나 바로 붙일 수 있는 것은 아니다. 마지막 합성곱 뒤에 global average pooling(GAP)과 선형 분류층이 이어지는 구조여야 식이 그대로 성립한다.

CAM 히트맵은 언제 믿을 수 있을까?

히트맵이 선명하다는 사실만으로 설명이 옳다고 볼 수 없다. Target class, feature map channel, 분류층 weight index와 원본 이미지 좌표가 모두 맞아야 하며, 결과는 정답, 오답, 변형 입력에서 반복해 비교해야 한다.

figure1

분류 모델이 위치 정보를 잃는 지점

합성곱 특징 맵에는 “어디에서 어떤 패턴이 강하게 반응했는가”라는 공간 정보가 남아 있다. 그러나 이를 펼쳐 fully connected layer에 넣으면 위치와 채널의 관계가 뒤섞인다. CAM은 fully connected 구조 대신 각 채널을 GAP로 하나의 값으로 줄이고, 그 값을 선형 분류층에 전달한다.

figure2

bounding box나 segmentation mask 없이 이미지 수준의 클래스 라벨만으로 위치를 짐작할 수 있다는 점에서 weakly supervised localization에 활용할 수 있다. 다만 “고양이 사진”이라는 라벨만 학습한 모델이 고양이 전체를 찾는다고 보장되지는 않는다. 분류에 가장 유리한 귀나 얼굴만 강조할 수도 있다.

CAM 식을 한 줄씩 읽기

마지막 합성곱의 \(k\)번째 특징 맵을 \(f_k(x,y)\)라고 하자. GAP 결과는 공간 전체의 평균(논문 표기에서는 합으로 전개)이다.

gap

클래스 \(c\)의 분류 점수는 채널별 GAP 값과 분류 가중치 \(w_k^c\)의 합이다.

\[S_c = \sum_k w_k^c \sum_{x,y} f_k(x,y)\]

합의 순서를 바꾸면 위치 \((x,y)\)마다 클래스에 기여하는 값을 만들 수 있다.

\[M_c(x,y)=\sum_k w_k^c f_k(x,y)\]

이 \(M_c\)가 class activation map이다. 입력 영상 크기로 확대해 원본 위에 겹치면 모델이 해당 클래스를 판단할 때 강하게 사용한 영역을 볼 수 있다.

figure3

여기서 꼭 구분할 것이 있다. \(w_k^c\)는 “채널 \(k\)가 클래스 \(c\)에 기여하는 정도”이고, \(f_k(x,y)\)는 그 채널이 위치별로 반응한 정도다. 둘 중 하나만으로는 클래스별 공간 지도를 만들 수 없다.

구현할 때 확인할 세 가지

아래 코드는 원문의 STL10, ResNet18 실험에서 CAM 계산 부분만 추린 핵심 조각이다. ResNet18, 학습 가중치 경로, 마지막 합성곱 모듈 이름, NumPy와 이미지 전처리는 자신의 모델에 맞게 준비해야 하므로 단독 실행 예제가 아니다.

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

def hook_feature(module, inputs, output):
    feature_blobs.append(output.detach().cpu().numpy())

model._modules.get('FINAL_CONV_NAME').register_forward_hook(hook_feature)

# GAP 다음 선형 분류층의 [class, channel] 가중치라고 가정한다.
params = list(model.parameters())
class_weights = np.squeeze(params[-2].detach().cpu().numpy())

output = model(tensor_img)
pred = F.softmax(output, dim=1).squeeze().argmax().item()
final_conv = feature_blobs[0][0]

cam = np.zeros(final_conv.shape[1:3], dtype=np.float32)
for channel, weight in enumerate(class_weights[pred]):
    cam += weight * final_conv[channel]

cam -= cam.min()
cam /= cam.max()

확인 순서는 다음과 같다.

  1. hook이 정말 마지막 합성곱 출력을 가져오는지 shape을 출력한다.
  2. 분류 가중치의 채널 수와 특징 맵 채널 수가 같은지 본다.
  3. 전처리 크기와 정규화가 학습 때와 같은지 확인한다.

히트맵을 원본에 겹치는 핵심은 크기 조정과 색상 합성이다.

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

test

히트맵을 설명으로 과신하지 않는 법

CAM은 모델을 디버깅하는 좋은 출발점이지만 원인 증명은 아니다. 밝은 영역은 선택한 클래스 점수에 사용된 위치를 나타낼 뿐, 물체의 정확한 경계나 사람의 시선과 일치한다는 보장이 없다. 낮은 해상도의 마지막 특징 맵을 확대하므로 경계가 거칠 수 있고, 배경의 지름길을 사용한 모델이라면 배경이 밝게 나온다.

따라서 한 장의 보기 좋은 결과보다 다음 비교가 더 유용하다.

  • 정답 클래스와 오답 클래스의 CAM을 나란히 본다.
  • 같은 클래스의 여러 이미지에서 반복되는 배경이 있는지 확인한다.
  • 원본을 조금 바꿨을 때 예측과 CAM이 함께 안정적으로 변하는지 본다.

모델 구조에 GAP가 없거나 다른 작업에 같은 방식으로 적용해야 한다면, 고정된 분류 가중치 대신 gradient를 이용하는 Grad-CAM 같은 방법을 검토해야 한다.

구현 결과가 맞는지 단계별로 검증하는 법

먼저 마지막 convolution layer의 출력 shape를 확인한다. Batch, channel, height, width 순서가 코드와 맞는지 보고 GAP를 직접 계산했을 때 분류층 입력 크기와 같은지 확인한다. Channel 수와 class weight의 길이가 다르면 heatmap을 올바르게 합칠 수 없다.

한 이미지의 target class를 고정하고 해당 class weight vector를 가져온다. 각 feature map에 대응 weight를 곱해 합산한 결과의 최소, 최대값과 공간 크기를 기록한다. 다른 class를 선택했을 때 weight가 실제로 바뀌는지도 확인해야 같은 heatmap을 재사용하는 index 오류를 찾을 수 있다.

작은 feature map을 원본 크기로 확대할 때는 원본의 resize, crop 과정을 되돌려야 한다. 단순히 가로, 세로만 맞추면 학습 입력에서 잘려 나간 영역과 overlay 좌표가 어긋날 수 있다. 원본, 모델 입력, heatmap overlay를 함께 저장한다.

CAM을 데이터 진단에 쓰는 구체적인 비교

정답 class와 모델이 예측한 class의 CAM을 나란히 둔다. 틀린 예측에서 두 지도 모두 같은 배경을 밝히면 분류기가 class보다 촬영 조건에 의존했을 가능성을 조사할 근거가 된다. 하지만 지도 하나만으로 그 원인을 확정하지 않고 배경을 가리거나 바꾼 입력으로 prediction 변화를 본다.

같은 class에서 배경, 각도, 크기가 다른 샘플을 고른다. 객체의 공통 부분이 반복해서 강조되는지, 특정 폴더의 테두리나 워터마크만 나타나는지 확인한다. Training과 test에서 반복 패턴이 다르면 높은 validation 수치가 split 누수에 의존했는지도 살펴야 한다.

입력의 작은 변화에도 CAM이 크게 튄다면 안정성을 의심한다. 밝기나 crop을 조금 바꾸고 class score와 heatmap을 함께 비교한다. Prediction은 같은데 지도가 완전히 달라지는 경우, 하나의 설명 그림을 대표 결과로 쓰지 않는다.

히트맵 해상도는 선택한 convolution feature map보다 세밀해질 수 없습니다. 마지막 feature map이 작다면 원본 크기로 보간한 색 영역은 부드러워 보여도 정확한 경계가 새로 복원된 것이 아닙니다. 작은 결함이나 세포 경계를 찾아야 하는 작업에서 CAM을 segmentation 결과처럼 임계값 처리하면 위치 오차를 숨길 수 있습니다. 이때는 분류 설명과 픽셀 단위 위치 추정이라는 목적을 분리해야 합니다.

여러 이미지를 비교할 때 색 범위도 고정해야 합니다. 각 히트맵을 따로 최댓값으로 정규화하면 매우 약한 반응도 항상 붉게 보이므로 샘플 간 강도를 비교할 수 없습니다. 원본 prediction score, target class, 사용한 layer와 정규화 방식을 함께 기록하고, 같은 이미지에서 target class를 바꾸었을 때 강조 영역이 달라지는지도 확인합니다.

마지막으로 가림 실험을 붙일 수 있습니다. CAM이 강하게 표시한 영역과 같은 크기의 임의 영역을 각각 가리고 예측이 어떻게 변하는지 비교하면 적어도 표시와 출력 변화의 연관성을 점검할 수 있습니다. 하지만 가림 자체가 학습 분포에 없던 인공 패턴을 만들 수 있으므로, 점수 하락만으로 원인임을 확정하지 말고 여러 크기와 채움 방식에서 일관되는지 봐야 합니다.

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

자주 묻는 질문

CAM은 어떤 CNN에도 그대로 적용할 수 있나요?

아닙니다. 기본 CAM 식은 마지막 convolution feature 뒤에 global average pooling과 선형 분류층이 직접 이어지는 구조를 전제로 합니다.

CAM에서 밝은 영역은 모델 판단의 원인인가요?

그렇게 단정할 수 없습니다. 해당 class 점수에 기여한 공간 패턴을 보여 주는 진단 자료이며, 인과적 설명이나 정확한 object mask로 해석하면 안 됩니다.

CAM이 배경만 강조하면 어떻게 확인해야 하나요?

같은 class의 여러 이미지와 다른 class, 배경을 바꾼 입력을 비교합니다. 반복되는 촬영 흔적이나 색 배경이 예측과 함께 움직이면 데이터 지름길을 의심할 수 있습니다.

THE END / OPSOAI

여기까지 읽었습니다

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

다른 책 고르기
표지 1

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

CONTENTS

이 책의 목차

    9개 장 15 분읽는 시간