CNN 시각화 : 피처맵 (2)

이전 글에서 얘기했던 것처럼
지금부터 설명하는 코드는 "케라스 창시자"를 토대로 했고
수업에 사용하기 위해서 필요하기는 하지만
정해진 수업 시간을 넘어가는 부분에 대해서는 과감하게 정리를 했다.
또한 나만의 스타일이 있어서
내식대로 변수 이름부터 상당한 부분을 수정했음을 밝힌다.
설명이 부족한 부분에 대해서는 "케라스 창시자"에 나오는 원본 코드를 참조하기 바란다.

이번 코드를 구동하기 위해서는
모델 파일과 시각화를 적용할 사진이 필요하다.
모델 파일은 "케라스 창시자"에 나오는 두 번째 모델을 학습한 파일이고
사진 파일은 도서에 첨부된 개와 고양이 파일 중에서 하나씩을 골랐다.
다운로드 받은 파일은 cats_and_dogs 폴더를 만들고 그 안에 복사하도록 한다.

개와 고양이 모델 파일 다운로드
개 사진 다운로드
고양이 사진 다운로드


그러면 순서대로 전체 코드를 살펴보도록 하자.


import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt


def show_image(img_path, target_size):
img = tf.keras.preprocessing.image.load_img(img_path, target_size=target_size)
img_tensor = tf.keras.preprocessing.image.img_to_array(img)

# print(img_tensor[0, 0]) # [31. 2. 6.]
# print(np.min(img_tensor), np.max(img_tensor)) # 0.0 255.0
# print(img_tensor.dtype, img_tensor.shape) # float32 (150, 150, 3)

# 스케일링하지 않으면 이미지 출력 안됨
img_tensor /= 255

plt.imshow(img_tensor)
plt.show()


cat_path = 'cats_and_dogs/cat.1574.jpg'
dog_path = 'cats_and_dogs/dog.1525.jpg'

show_image(cat_path, target_size=(150, 150))
show_image(dog_path, target_size=(150, 150))

특정 이미지를 화면에 표시하기 위해 만들었고
실제로 사용하지는 않는다.
전달한 이미지가 어떤 건지 확인하는 용도로 구현했다.
가장 중요한 부분은 스케일링.
딥러닝은 작은 숫자에 강하다고 해야 할까?
숫자가 클 경우 수렴하는데 오래 걸리기 때문에 스케일링이 필요하다.
원본은 0~255 사이의 RGB 정수 값을 갖기 때문에
이를 0~1 사이의 실수로 변환하는 것이 좋다.



나름 귀여운 사진을 골랐다.
피처맵을 출력할 때는 x축과 y축에 들어가는 눈금은 표시하지 않는다.


# 4개의 예제 중에서 두 번째 예제 사용
model_path = 'cats_and_dogs_2/cats_and_dogs_2.h5'
model = tf.keras.models.load_model(model_path)
model.summary()

먼저 모델을 로드해서 네트웍 구조를 보자.
여러 개의 모델 중에서 굳이 성능이 좋지 않은 두 번째 모델을 사용한 이유가 있다.


_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_68 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_68 (MaxPooling (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_69 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_69 (MaxPooling (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_70 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_70 (MaxPooling (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_71 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_71 (MaxPooling (None, 7, 7, 128) 0
_________________________________________________________________
flatten_21 (Flatten) (None, 6272) 0
_________________________________________________________________
dense_46 (Dense) (None, 512) 3211776
_________________________________________________________________
dropout_23 (Dropout) (None, 512) 0
_________________________________________________________________
dense_47 (Dense) (None, 1) 513
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________

뒤쪽 예제들은 VGG16 모델을 재사용하는 모델이라서
레이어의 개수가 너무 많아서 보여줘야 하는 것들도 많기 때문에 적절하지 않다.

여기서 보려고 하는 것은
필터 슬라이딩의 결과물인 피처맵이고
풀링을 포함해도 8개밖에 없어서 비교가 쉽다.
레이어 후반부에 해당하는 FC 레이어의 가중치는
피처를 추출하는 것이 아니라 분류를 목적으로 하기 때문에 출력하지 않는다.

궁금한 분들은 당연히 출력해서 결과를 확인하는 것도 좋을 것이다.
그러나 문제가 있다.
컨볼루션과 풀링에서는 정확하게 이미지 단위로 데이터가 존재하는 것에 반해
FC(dense) 레이어에서는 1차원이고
이걸 몇 개의 이미지로 분할할 것인지부터 크기를 어떻게 해야할지 막막하다.
다시 말해 인접한 데이터를 이미지처럼 상하좌우로 붙여서 출력하기 어렵다.
어떡해야 하는지는 나도 안해봐서 모른다.

어쨌거나 출력 결과에서 얘기하려고 하는 것은
컨볼루션과 풀링에 해당하는 8개 레이어의 피처맵만 출력한다는 것.


def load_image(img_path, target_size):
img = tf.keras.preprocessing.image.load_img(img_path, target_size=target_size)
img_tensor = tf.keras.preprocessing.image.img_to_array(img)

# 배치 사이즈 추가 + 스케일링 결과 반환
return img_tensor[np.newaxis] / 255 # (1, 150, 150, 3)


# 첫 번째 등장하는 컨볼루션 레이어의 모든 피처맵(32개) 출력
def show_first_feature_map(loaded_model, img_path):
first_output = loaded_model.layers[0].output
print(first_output.shape, first_output.dtype) # (?, 148, 148, 32) <dtype: 'float32'>

# 1개의 출력을 갖는 새로운 모델 생성
model = tf.keras.models.Model(inputs=loaded_model.input, outputs=first_output)

# 입력으로부터 높이와 너비를 사용해서 target_size에 해당하는 튜플 생성
target_size = (loaded_model.input.shape[1], loaded_model.input.shape[2])
img_tensor = load_image(img_path, target_size)

print(loaded_model.input.shape) # (?, 150, 150, 3)
print(img_tensor.shape) # (1, 150, 150, 3)

first_activation = model.predict(img_tensor)

# 컨볼루션 레이어에서 필터 크기(3), 스트라이드(1), 패딩(valid)을 사용했기 때문에
# 150에서 148로 크기가 일부 줄었음을 알 수 있다. 필터 개수는 32.
print(first_activation.shape) # (1, 148, 148, 32)
print(first_activation[0, 0, 0]) # [0.00675746 0. 0.02397328 0.03818807 0. ...]

# 19번째 활성 맵 출력. 기본 cmap은 viridis. gray는 흑백 컬러맵.
# [0, :, :, feature_index]
# 0은 첫 번째 데이터(원본 이미지)의 피처맵을 가리킨다. 사진은 1장만 사용했기 때문에 0만 가능
# 가운데 콜론(:)은 높이와 너비를 가리키는 차원의 모든 데이터
# feature_index는 보고 싶은 피처맵이 있는 채널을 가리킨다.
# 32개의 필터를 사용했다면 0부터 31까지의 피처맵이 존재한다.
plt.figure(figsize=(16, 8))
for i in range(first_activation.shape[-1]):
plt.subplot(4, 8, i + 1)

# 눈금 제거. fignum은 같은 피켜에 연속 출력
plt.axis('off')
plt.matshow(first_activation[0, :, :, i], cmap='gray', fignum=0)
plt.tight_layout()
plt.show()

개와 고양이 모델로부터 첫 번째 컨볼루션 레이어의 피처맵만 출력해 보자.
이전 글에서처럼 전체 레이어를 비교하는 것도 중요하지만
현재 피처에서 각각의 피처들이 어떤 역할을 했는지 비교하는 것도 중요하다.


아래의 한 줄짜리 코드를 통해 앞에서 정의한 함수가 어떻게 동작했는지 결과를 보도록 하자.

show_first_feature_map(model, cat_path)



show_first_feature_map(model, dog_path)


첫 번째 레이어의 피처맵만 출력해도
꽤 다양한 결과가 만들어졌다는 것을 알 수 있다.
또한 지금은 비슷해 보여도
두 번째 레이어로 넘어가면서 현저하게 달라지는 피처맵이 발생할 것이다.
이렇게 여러 개의 컨볼루션을 수행하게 되면
다양한 필터를 만나게 되고 결과적으로 하는 역할이 다른 피처맵이 만들어지게 된다.

'케라스' 카테고리의 다른 글

CNN 시각화 : 피처맵 (3)  (0) 2019.07.15
CNN 시각화 : 피처맵 (1)  (0) 2019.07.15