CNN 시각화 : 피처맵 (3)

다 만들어진 예제를 정리해서 올리는 데만 3~4시간이 걸린다.
올리려다 보면
수업 시간에 말로 하던 부분을 다른 방식으로 표현하게 되고
그러면서 내용이 추가된다.

전체 코드를 사용하려면
이전 글의 코드와 현재 글의 코드를 순서대로 나열해야 한다.


def predict_and_get_outputs(model, img_path):
# (conv, pool) 레이어만 출력. flatten 레이어 이전까지.
layer_outputs = [layer.output for layer in model.layers[:8]]
layer_names = [layer.name for layer in model.layers[:8]]

print([str(output.shape) for output in layer_outputs])
# ['(?, 148, 148, 32)', '(?, 74, 74, 32)', '(?, 72, 72, 64)', '(?, 36, 36, 64)',
# '(?, 34, 34, 128)', '(?, 17, 17, 128)', '(?, 15, 15, 128)', '(?, 7, 7, 128)']

# 마지막 레이어에 해당하는 7번째 레이어의 출력만 전달해도 될 것 같지만, 7번째 결과만 나옴.
# train 연산에 loss가 포함되어 있지만, loss를 명시하지 않으면 결과를 얻지 못하는 것과 같다.
activation_model = tf.keras.models.Model(inputs=model.input, outputs=layer_outputs)

input_shape = (model.input.shape[1], model.input.shape[2]) # (150, 150)
img_tensor = load_image(img_path, target_size=input_shape)

for layer in layer_outputs:
print(layer.shape)
print('-' * 50)
# (?, 148, 148, 32)
# (?, 74, 74, 32)
# (?, 72, 72, 64)
# (?, 36, 36, 64)
# (?, 34, 34, 128)
# (?, 17, 17, 128)
# (?, 15, 15, 128)
# (?, 7, 7, 128)

# 앞에 나온 layer_outputs 변수를 덮어썼다.
# 하는 역할이 같고 미세한 부분이 다르지만 내용도 같기 때문에 다른 이름으로 만들 필요가 없다.
layer_outputs = activation_model.predict(img_tensor)
for layer in layer_outputs:
print(layer.shape)
# (1, 148, 148, 32)
# (1, 74, 74, 32)
# (1, 72, 72, 64)
# (1, 36, 36, 64)
# (1, 34, 34, 128)
# (1, 17, 17, 128)
# (1, 15, 15, 128)
# (1, 7, 7, 128)

return layer_outputs, layer_names


layer_outputs, layer_names = predict_and_get_outputs(model, cat_path)

모델과 사진 경로를 전달하면 해당 레이어의 출력 결과를 알려주는 함수를 구현했다.
이 코드에서 중요한 것은
원하는 출력 결과가 레이어 8개의 결과이기 때문에
8개의 연산이 들어있는 layer_outputs를 Model 객체를 생성할 때 전달하는 코드다.
연산을 구동하면 출력이 나오고
컨볼루션의 출력 결과를 피처맵이라고 부른다.


def show_activation_maps(layer, title, layer_index, n_cols=16):
# size는 높이와 너비, 모두를 가리킨다. 가로/세로 크기가 같다. (150, 150)
size, n_features = layer.shape[1], layer.shape[-1]
assert n_features % n_cols == 0 # 반드시 나누어 떨어져야 한다

n_rows = n_features // n_cols

# 각각의 이미지를 따로 출력하지 않고, 큰 이미지에 모아서 한번에 출력하기 위한 변수
big_image = np.zeros((n_rows*size, n_cols*size), dtype=np.float32)

# 행과 열의 정해진 위치에 출력하기 위한 2차원 반복문
for row in range(n_rows):
for col in range(n_cols):
# 특정 부분에 값들이 편중되어 있는 상태
# 피처에 대해 반복하기 때문에 channel 변수는 피처맵 1개를 가리킨다.
channel = layer[0, :, :, row * n_cols + col] # shape : (size, size)

# 특성이 잘 보이도록 0~255 사이로 재구성. 범위를 벗어나는 일부 값들은 클리핑.
# 대략적으로 정규 분포에 따르면 95% 정도의 데이터 포함
channel -= channel.mean()
channel /= channel.std()
channel *= 64
channel += 128
channel = np.clip(channel, 0, 255).astype('uint8')

# 큰 이미지에서 자신의 영역을 찾아 복사
big_image[row*size:(row+1)*size, col*size:(col+1)*size] = channel

# 그래프에서는 행열(n_rows, n_cols) 순서가 xy로 바뀐다. n_cols가 앞에 와야 한다.
plt.figure(figsize=(n_cols, n_rows))

plt.xticks(np.arange(n_cols) * size)
plt.yticks(np.arange(n_rows) * size)
plt.title('layer {} : {}'.format(layer_index, title))
plt.tight_layout()
plt.imshow(big_image) # cmap='gray'

한 줄에 16개의 피처를 출력한다.
모든 레이어의 필터 개수를 16의 배수로 설정했기 때문에
한 줄에 16개씩 출력하는 코드는 현재 모델에 대해서는 동작하지만
다른 모델에 대해서는 동작하지 않을 수 있다.

이 함수는 피처맵에 포함된 전체 피처를 모두 출력한다.
32개, 64개, 128개로 피처맵의 개수가 많아지는 것을 아래에서 볼 수 있다.


for i, (layer, name) in enumerate(zip(layer_outputs, layer_names)):
show_activation_maps(layer, name, i)

plt.show()

출력 결과를 보면
레이어를 거쳐 갈수록, 정확하게는 풀링 레이어를 거칠 때마다
피처맵의 크기가 절반으로 줄어드는 것을 확인할 수 있다.
크기를 확인하기 위해 이번에는 일부러 눈금을 지우지 않고 두었다.

또한 이전 예제에서는 gray 컬러맵을 사용했는데
이번에는 viridis 기본 컬러맵을 사용했다.
흑백이 좋을 때도 있고 제한된 색상으로 표시하는 것이 좋을 때도 있다.

출력 결과에서 공백으로 표시되는 것은
해당 필터가 현재 이미지에 반응하지 않았다는 것을 의미한다.
결과물이 많아 고양이 사진만 사용했는데
개 사진을 사용하면 공백이었던 필터에 결과가 나타날 수도 있다.
또한 공백으로 가기 직전의 거의 공백처럼 보이는 피처맵도 있다.
뜬금없이 공백이 표시되는 것은 아니다.


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

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

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

CNN 시각화 : 피처맵 (1)

가끔 수업을 들으러 오는 수강생들로부터
파이쿵 블로그에 대한 얘기를 듣는다.
아직 내공이 깊지 않아
기본적인 것들에 대해서만 게시하고 있을 뿐인데
자주 방문했고 도움이 되었다고 한다.

과거에 수업했던 자료들을 모두 올려놓긴 했지만
최근 자료들은 괜히 부담스러워서 수업에서만 사용하고 있었는데
수강생들의 얘기를 듣다 보니
대단하지 않아도 많은 분들에게 보여드려야 하겠다는 압박을 은연 중에 받았다.

"케라스 창시자"라는 도서에 딥러닝 관련 좋은 예제들이 많다.
최근에 나온 책이라 모두 잘 동작하고
박해선님께서 번역해서 번역도 깔끔하지만
도서 하단의 역자 주가 아주 매력적인 책이다.
최근의 수업에서는 이 책에 나오는 내용 일부를 수정해서 진행한다.
수업을 진행하면서
이 책에서 발췌했다고 하고
괜히 무안해서 내 식대로 많이(?) 수정해서 사용한다고 얘기한다.



앞에 있는 사진이 딥러닝 기초를 수업할 때 가장 중요하게 생각하는 사진이다.
이 사진 한 장이면 딥러닝이 무엇인지 알 수 있다고 수업을 한다.

사진을 보면 레이어가 뒤쪽으로 넘어가면서
모양이 선명하지 않고 뭉개지는 것을 볼 수 있다.
이게 사람이 사물을 기억하는 방식이다.
앞에서는 선명하게 기억하려고 노력하겠지만
결국에는 사물에 대한 흐릿하게 기억할 수밖에 없다는 것을 보여준다.

멀리서 엄마가 걸어오고 있다고 판단했지만
가까이 와서 보니 엄마가 아니었다.
이게 CNN이다.
레이어 후반에 있는 흐릿한 이미지가 될 수 있는 사진은 매우 많다.
선명한 사진을 압축하는 과정에서 동일한 이미지처럼 보여지는 것이다.

CNN 아키텍처에서는 일반적으로 풀링(pooling)을 거치게 되면
가로와 세로 크기가 절반으로 줄어든다.
여기서 절반으로 줄어든다는 것은 필터 슬라이딩의 결과물인 피처 맵(feature map, activation map)이다.
따라서 앞의 사진에서 주의해야 할 점은 
레이어 뒤쪽으로 갈수록 크기가 줄어들고 있다는 점이다.
다만 보기 좋게 출력하기 위해 모두 같은 크기로 했지만
실제로는 풀링을 거칠수록 줄어들기 때문에 픽셀이 거칠게 표시되는 것이다.
대신 채널(channel, 두께)의 깊이가 깊어진다.
다만 이 사진에서는 특정 피처맵 하나만 출력해야 하기 때문에 채널을 표시할 수는 없다.

참.. 앞의 사진이 피처맵을 보여준다는 것은 알고 있겠지?
설마..

CNN 아키텍처에서 이미지로 표현할 수 있는 데이터에 어떤 것이 있을까?
가중치로 표현되는 필터와 필터의 결과물인 피처맵밖에 없다.
피처맵은 컨볼루션과 풀링 레이어의 결과물이고
첫 번째 입력인 원본 사진과 달리
RGB 컴포넌트로 구성된 것이 아니라서 색상을 입힐 수가 없다.
16개의 필터를 사용했다면 채널 16개인 피처맵이 만들어지고
그중에서 1개의 피처맵만 표시하게 된다.

사진 또는 이미지라고 불리는 것은
픽셀이라는 형태로 표현되는 데이터이긴 하지만
인접한 데이터(픽셀)는 연관성이 깊을 수밖에 없고
이것을 상관관계가 강하다고 한다.
내 친구가 검정이라면 나 또한 검정일 확률이 높다는 뜻이다.
피처맵이 이미지는 아니지만
이미지로 표시할 수 있는 데이터의 범위로 스케일링한다면
각각의 픽셀(데이터)이 갖는 상관관계를 시각적으로 볼 수 있게 된다.

실제로 표시를 해보면
앞의 사진에서 보는 것처럼
완전 엉뚱한 형태의 이미지가 나타나지 않는다는 것을 알 수 있다.
원본 사진을 한 번 필터링했다고 해서
자동차가 알 수 없는 무언가로 변하지는 않는다.

간단하게 코드만 올리려고 했는데
강사의 직업병으로 인해 설명이 길어졌다.
코드는 다음 글에서 따로 게시한다.

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

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