20. 합성곱 신경망의 시각화

  • 이번 시간에는 합성곱 층이 이미지에서 어떤 것을 학습했는지 알아보기 위해 합성곱 층의 가중치와 특성 맵을 그림으로 시각화 해보도록 한다.
  • 지금까지는 케라스의 Sequential 클래스만 사용했다. 하지만 케라스는 좀 더 복잡한 모델을 만들 수 있는 함수형 API를 제공한다. 이번 시간에서 함수형 API가 무엇인지 살펴보고 합성곱 층의 특성 맵을 시각화하는데 사용해 보도록 한다.

1. 가중치 시각화

  • 합성곱 층은 여러 개의 필터를 사용해 이미지에서 특징을 학습한다. 각 필터는 커널이라 부르는 가중치와 절편을 가지고 있다. 일반적으로 절편은 시각적으로 의미가 있지는 않다.
  • 지난 시간에 만들 모델의 가중치를 들여다 보도록 하자.
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
model = keras.models.load_model('best-cnn-model.h5')
  • 케라스 모델에 층은 layers 속성에 저장되어 있다. 이 속성은 파이썬 리스트 이다.
model.layers
[<keras.layers.convolutional.Conv2D at 0x17d23f0d0>,
 <keras.layers.pooling.MaxPooling2D at 0x17d23f7c0>,
 <keras.layers.convolutional.Conv2D at 0x17d2b89a0>,
 <keras.layers.pooling.MaxPooling2D at 0x17d2e96a0>,
 <keras.layers.core.Flatten at 0x17d2b8d90>,
 <keras.layers.core.Dense at 0x17d2f2970>,
 <keras.layers.core.Dropout at 0x17d2fd250>,
 <keras.layers.core.Dense at 0x17d2f2af0>]
  • model.layers 리스트에 이전 시간에 추가했던 Conv2D, MaxPooling2D 층이 번갈아 2번 연속 등장한다. 그 다음 Flatten 층과 Dense 층, Droupout 층이 차례대로 등장한다. 마지막에 Dense 출력층이 놓여있다.
  • 첫 번쨰 합성곱 층의 가중치를 조사해보자. 층의 가중치와 절편은 층의 weights 속성에 저장되어있다. weight도 파이썬 리스트이다. layers 속성의 첫 번쨰 원소를 선택해 weight의 첫 번째 원소(가중치)와 두 번쨰 원소(절편)의 크기를 출력해 보죠.
conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)
(3, 3, 1, 32) (32,)
  • weights 속성은 텐서플로의 다차원 배열인 Tensor 클래스의 객체이다. 이것을 다루기 쉽게 numpy() 메서드를 사용해 넘파이 배열로 변환하자. 그 다음 가중치 배열의 평균과 표준편차를 넘파이 mean()메서드와 std()메서드로 계산해보자.
conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())
-0.022901189 0.23767807
  • 가중치의 평균값은 0에 가깝고 표준편차는 0.23정도 된다. 이 가중치가 어떤 분포를 가졌는지 직관적으로 이해하기 쉽도록 히스토그램을 그려보자.
plt.hist(conv_weights.reshape(-1,1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

output_12_0

  • 히스토그램을 그리기 위해서는 1차원 배열로 바꿔주는 과정을 해야한다. 이를위해 넘파이 reshape 메서드로 conv_weights 배열을 1개의 열이 있는 배열로 변환했다.
  • 이번에는 32개의 커널을 16개씩 두 줄에 출력해 보자. 이전장에서 사용했던 맷플롯립의 subplots()함수를 사용해 32개의 그래프 영역을 만들고 순서대로 커널을 출력한다.
fig, axs = plt.subplots(2, 16, figsize=(15,2))
for i in range(2):
    for j in range(16):
        axs[i, j].imshow(conv_weights[:,:,0,i*16 + j], vmin=-0.5, vmax=0.5)
        axs[i, j].axis('off')
plt.show()

output_14_0

  • conv_weights에 32개의 가중치를 저장했다. 이 배열의 마지막 차원을 순화하면서 0부터 i*16 + j번째 까지의 가중치 값을 차례대로 출력한다. 여기에서 i는 행 인덱스를 말하고 j는 열인덱스로 각각 0~1, 0~15까지의 범위를 가진다. 따라서 conv_weights[:,:,:0,0]에서 conv_weights[:,:,:0.31]까지 출력한다.
  • imshow() 함수는 배열에 있는 최댓값과 최솟값을 사용해 픽셀의 강도를 표현한다. 즉 0.1이나 0.4나 어떤 값이든지 그 배열의 최댓값이면 가장 밝은 노란 색으로 그린다. 만약 두 배열을 imshow() 함수로 비교하려면 이런 동작은 바람직하지 않다. 어떤 절댓값으로 기준을 정해서 픽셀의 강도를 나타내야 비교하기 좋기 때문이다. 이를 위해 위 코드에서는 vmin과 vmax로 맷플롯립의 컬러맵으로 표현할 범위를 정했다.
  • 이번에는 존재하지 않는 빈 합성곱 신경망을 만들어보도록 하자.
no_training_model = keras.Sequential()
no_training_model.add(keras.layers.Conv2D(32, kernel_size=3, activation=\
                                         'relu', padding='same', input_shape=(28, 28, 1)))
  • 그다음 이 모델의 첫 번째 층의 가중치를 no_training_conv 변수에 저장한다.
no_training_conv = no_training_model.layers[0]
print(no_training_conv.weights[0].shape)
(3, 3, 1, 32)
  • 이 가중치의 크기도 앞서 그래프로 출력한 가중치와 같다. 동일하게 (3, 3)커널을 가진 필터를 32개 사용했기 때문이다. 이 가중치의 평균과 표준편차를 확인해 보자. 이전처럼 먼저 넘파이 배열로 만든 다음 mean(), std() 메서드를 호출한다.
no_training_weights = no_training_conv.weights[0].numpy()
print(no_training_weights.mean(), no_training_weights.std())
-0.0031840743 0.08262698
  • 평균은 이전과 동일하게 0에 가깝지만 표준편차는 이전과 달리 매우 작다. 이 가중치 배열을 히스토그램으로 표현해 보도록 하자.
plt.hist(no_training_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

output_22_0

  • 해당 그래프는 이전 그래프와 달리 대부분 가중치가 골고루 분포되어있다. 이렇게 보이는 이유는 텐서플로가 신경망의 가중치를 처음 초기화할 때 균등 분포에서 랜덤하게 값을 선택하기 때문이다.
  • 이 가중치 값을 맷플롯립의 imshow() 함수를 사용해 이전처럼 그림으로 출력해 보자. 학습된 가중치와 비교하기 위해 동일하게 vmin과 vmax를 설정하도록 한다.
fig, axs = plt.subplots(2, 16, figsize=(15,2))
for i in range(2):
    for j in range(16):
        axs[i, j].imshow(no_training_weights[:,:,0,i*16 + j], vmin=-0.5, vmax=0.5)
        axs[i, j].axis('off')
plt.show()

output_24_0

  • 히스토그램에서 볼 수 있었듯이 전체적으로 가중치가 밋밋하게 초기화되었다. 합성곱 신경망이 패션 MNIST 데이터셋의 분류 정확도를 높이기 위해 유용한 패턴을 학습했다는 사실을 알 수 있다.
  • 합성곱 신경망 학습을 시각화하는 두 번째 방법은 합성곱 층에서 출력된 특성 맵을 그려보는 것이다. 이를 통해 입력 이미지를 신경망 층이 어떻게 바라보는지 살펴볼 수 있다. 합성곱 층의 출력을 만들기 전에 케라스 함수형API에 대해 잠시 알아보도록 하자.

2. 함수형 API

  • 지금까지는 신경망 모델을 만들 때 케라스 Sequential 클래스를 사용했다. 이 클래스는 층을 차례대로 쌓은 모델을 만든다. 딥러닝에는 좀 더 복잡한 모델이 많이 존재한다. 입력이 2개이고 출력이 2개일 경우 같을 떄는 Sequential 클래스를 사용하기 어렵다. 이럴 때 함수형 API를 사용한다.
  • 함수형 API는 케라스의 Model 클래스를 사용하여 모델을 만든다. 입력에서 출력까지 층을 호출한 결과를 게속 이어주고 Model 클래스에 입력과 최종 출력을 지정한다. Sequential 클래스는 InputLayer 클래스를 자동으로 추가하고 호출해 주지만 Model 클래스에서는 우리가 수동으로 만들어서 호출해야 한다.
  • 케라스 InputLayer 클래스 객체를 쉽게 다룰 수 있도록 Input() 함수를 별로도 제공한다. 입력의 크기를 지정하는 shape 매개변수와 함께 이 함수를 호출하면 InputLayer 클래스 객체를 만들어 출력을 반환해 준다.
  • 그렇다면 왜 특성 맵 시각화에 함수형 API가 필요한가?
    • 특성 맵을 그리는데 필요한 것은 Conv2D의 출력물이다. model 객체의 입력과 Conv2D의 출력을 알 수 있다면 이 둘을 연결해 새로운 모델을 얻을 수도 있기 때문이다.
    • model.layers[0].output에서 Conv2D 첫번째 층의 출력을 얻을 수 있다.
    • 케라스 모델은 input속성으로 입력을 참조할 수 있다. model.input으로 이 모델의 입력을 얻을 수 있다.
    • 이 두가지를 이용해 새로운 conv_acti 모델을 만들 수 있다.
conv_acti = keras.Model(model.input, model.layers[0].output)
  • model 객체의 predict() 메서드를 호출하면 최종 출력층의 확률을 반환한다.
(train_input, train_target), (test_input, test_target) =\
    keras.datasets.fashion_mnist.load_data()
plt.imshow(train_input[0], cmap='gray_r')
plt.show()

output_30_0

  • 앵클부츠이다. 이 샘플을 conv_acti 모델에 주입해 Conv2D 층이 만드는 특성 맵을 출력해 보자. predict() 메서드는 항상 입력의 첫 번째 차원이 배치 차원일 것으로 기대한다. 따라서 이를 위해 슬라이싱 연산자를 이용해 첫 번쨰 샘플을 선택한다. 그 다음 (784,)크기를 (28, 28, 1)크기로 변경하고 255로 나눈다.
inputs = train_input[0:1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv_acti.predict(inputs)
2021-10-12 16:35:22.735802: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)
print(feature_maps.shape)
(1, 28, 28, 32)
  • 세임 패딩과 32개의 필터를 사용한 합성곱 층의 출력이므로 (28, 28 ,32)이다. 첫 번째 차원은 배치차원이다. 샘플이 1개이기 때문에 1로 표시된다.
  • 맷플롯립의 imshow를 이용해 특성 맵을 그려보자. 총 32개의 특성 맵이 있으므로 4개의 행으로 나누어 그려보자.
fig, axs = plt.subplots(4, 8, figsize=(15,8))
for i in range(4):
    for j in range(8):
        axs[i,j].imshow(feature_maps[0,:,:,i*8+j])
        axs[i,j].axis('off')
plt.show()

output_35_0

  • 이 특성맵은 32개의 필터로 인해 입력 이미지에서 강하게 활성화된 부분을 보여준다.
  • 다른 맵들도 이와같이 확인이 가능하다. 두 번째 합성곱 층이 만든 특성 맵도 확인해보자.
conv2_acti = keras.Model(model.input, model.layers[2].output)
inputs = train_input[0:1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv2_acti.predict(inputs)
  • 첫 번째 풀링 층에서 가로세로 크기가 절반으로 줄었고 두 번째 합성곱 층이 필터 개수는 64개 이므로 feature_maps의 크기는 배치차원을 제외하면 (14, 14, 64)일 것이다.
print(feature_maps.shape)
(1, 14, 14, 64)
fig, axs = plt.subplots(8, 8, figsize=(15,8))
for i in range(8):
    for j in range(8):
        axs[i,j].imshow(feature_maps[0,:,:,i*8+j])
        axs[i,j].axis('off')
plt.show()


output_41_0

  • 이 특성 맵은 시각적으로 이해하기가 어렵다.
  • 두 번째 합성곱 층의 필터 크기는 (3, 3, 32)이다. 두 번째 합성곱 층의 첫 번째 필터가 앞서 출력한 32개의 특성 맵과 곱해져 두 번째 합성곱 층의 첫 번쨰 특성 맵이 됩니다. 다음의 그림처럼 이렇게 계산된 출력은 (14, 14, 32)특성 맵에서 어떤 부위를 감지하는지 직관적으로 이해하기 어렵다. 이런 현상은 합성곱 층이 많이 쌓이면 쌓일수록 심해진다.
  • 합성곱 신경망의 일부분에 있는 합성곱 층은 이미지의 시각적인 정보를 감지하고 뒤쪽에 있는 합성곱 층은 앞쪽에서 감지한 시각적인 정보를 바탕으로 추상적인 정보를 학습한다. 합성곱 신경망이 패션 MNIST 이미지를 인식하여 10개의 클래스를 찾아낼 수 있는 이유가 여기에 있다.

3. 마치며

  • 이번 시간에는 특성 맵 시각화에 대해 배워보았다. 가중치 시각화와, 함수형API를 이용한 각 층의 특성 맵 시각화를 배워보면서 세부적인 층의 작동 원리에 대해 생각해보고 알 수 있는 시간이였다.

Categories:

Updated:

Leave a comment