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