가중치 시각화
- 합성곱 층의 가중치를 이미지로 출력하는 것
- 가중치가 시각적인 패턴을 학습하는지 여부를 확인할 수 있음
합성곱 층의 각 필터는 커널이라 부르는 가중치와 절편을 가지고 있습니다.
가중치는 입력 이미지의 2차원 영역에 적용되어 어떤 특징을 크게 두드러지게 표현합니다.
예를 들어 위 그림에서 가중치는 둥근 모서리가 있는 영역에서 크게 활성화되고 그렇지 않은 영역에서는 낮은 값을 만듭니다.
from tensorflow import keras
model = keras.models.load_model('best-cnn-model.keras')
이전 장에서 만든 모델이 어떤 가중치를 학습했는지 확인하기 위해 체크포인트 파일을 읽어들입니다.
model.layers
[<Conv2D name=conv2d, built=True>, <MaxPooling2D name=max_pooling2d, built=True>, <Conv2D name=conv2d_1, built=True>, <MaxPooling2D name=max_pooling2d_1, built=True>, <Flatten name=flatten, built=True>, <Dense name=dense, built=True>, <Dropout name=dropout, built=True>, <Dense name=dense_1, built=True>]
model.layers를 사용하면 케라스 모델에 추가한 층의 속성을 확인할 수 있습니다.
Conv2D - MaxPooling2D - Conv2D - MaxPooling2D - Flatten - Dense - Dropout - Dense
합성곱 모델의 층 구성은 위와 같습니다.
첫 번째 합성곱 층의 가중치를 확인해보겠습니다.
conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)
(3, 3, 1, 32) (32,)
층의 가중치와 절편은 weights 속성에 저장되어 있습니다. layers[0]에는 첫 번째 층의 가중치(3, 3, 1, 32)와 절편의 크기(32,)가 담겨있습니다.
conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())
-0.014383553 0.23351653
weights 속성은 다차원 배열인 Tensor 클래스의 객체인데요, 다루기 쉽게 numpy 배열로 변환합니다.
가중치의 평균은 0에 가깝고 표준편차는 약 0.27입니다.
가중치의 분포를 히스토그램으로 나타내보겠습니다.
import matplotlib.pyplot as plt
plt.hist(conv_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()
hist( )
- matplotlib에서 히스토그램을 그리는 함수
- 1차원 배열을 입력 받음
히스토그램을 그리기 위해 reshape 매서드를 사용하여 conv_weights 배열을 1차원 배열로 변환했습니다.
히스토 그램을 보면 0을 중심으로 종 모양의 분포를 띠고 있습니다.
이번에는 32개의 커널을 16개씩 두 줄에 출력해보겠습니다.
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()
imshow( )
- 배열에 있는 최댓값과 최솟값을 사용해 픽셀의 강도 표현
- vmin, vmax: matplotlib의 컬러맵으로 표현할 범위 지정
가중치의 값을 컬러맵으로 표현했을 때, 예를들어 첫 번째 줄의 맨 왼쪽 가중치는 오른쪽 3개의 픽셀값이 높다(밝을 수록 높음)는 것을 알 수 있습니다.
훈련 전과 훈련 후의 합성곱 신경망의 가중치가 어떻게 달라지는지 비교해보겠습니다.
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)))
Sequential 클래스로 모델을 만들고 Conv2D 층을 하나 추가합니다.
no_training_conv = no_training_model.layers[0]
print(no_training_conv.weights[0].shape)
(3, 3, 1, 32)
그 다음 첫 번째 층의 가중치를 no_training_conv 변수에 저장합니다.
이 가중치의 크기도 앞서 그래프로 출력한 가중치와 같습니다.
동일하게 (3, 3) 커널을 가진 필터를 32개 사용했기 때문입니다.
no_training_weights = no_training_conv.weights[0].numpy()
print(no_training_weights.mean(), no_training_weights.std())
가중치의 평균과 표준편차를 출력했습니다. 평균은 이전과 동일하게 0에 가깝지만 표준편차는 이전과 달리 매우 작습니다.
가중치 배열을 히스토그램으로 표현해보겠습니다.
plt.hist(no_training_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()
대부분의 가중치가 -0.15~-.15 사이에 있고 비교적 고르게 분포하고 있습니다. 그 이유는 텐서플로가 신경망의 가중치를 처음 초기화할 때 균등 분포에서 랜덤하게 값을 선택하기 때문입니다.
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()
가중치의 값을 imshow( ) 함수를 사용해서 그림으로 출력해보았습니다. 학습된 합성곱 신경망과 달리 가중치가 밋밋하게 초기화되어 있습니다. 따라서 합성곱 신경망이 MNIST 데이터셋 분류 정확도를 높이기 위해 유용한 패턴을 학습했다는 것을 알 수 있습니다.
합성곱 신경망의 학습을 시각화 하는 두 번째 방법은 출력된 특성 맵을 그리는 것입니다. 이를 통해 입력 이미지를 신경망 층이 어떻게 바라보는 지 알 수 있습니다. 이를 위해 함수형 API에 대해서 먼저 공부해보겠습니다.
함수형 API
- 케라스에서 신경망 모델을 만드는 방법 중 하나
- Model 클래스에 모델의 입력과 출력을 지정
- 입력: Input( ) 함수 사용하여 정의
- 출력: 마지막 층의 출력으로 정의
지금까지 신경망 모델을 만들 때 케라스의 Sequential 클래스를 사용했습니다. 딥러닝에서는 좀 더 복잡한 모델이 많은데요, 예를들어 입력이 2개이거나 출력이 2개인 경우에는 Sequential 클래스를 사용하기 어렵습니다. 이 때 함수형 API(functional API)를 사용합니다.
7장에서 만들었던 Dense 층 2개로 이루어진 완전 연결 신경망을 함수형 API로 구현하면 아래와 같습니다.
dense1 = keras.layers.Dense(100, activation='sigmoid')
dense2 = keras.layers.Dense(10, activation='softmax')
이 객체를 Sequential 클래스의 add( ) 매서드에 전달할 수 있지만 아래와 같이 함수처럼 호출할 수 있습니다.
hidden = dense1(inputs)
이처럼 케라스의 층은 객체를 함수처럼 호출했을 때 적절히 동작할 수 있도록 미리 준비해뒀기 때문에 함수형 API라고 부릅니다.
outputs = dense2(hiden)
이번에는 첫번째 층의 출력을 입력으로 받아서 두 번재 층을 호출합니다.
model = keras.Model(inputs, outputs)
그 다음 inputs와 outputs를 Model 클래스로 연결해줍니다.
입력에서 출력까지 층을 호출한 결과를 계속 이어주고 Model 클래스에 입력과 최종 출력을 지정합니다.
Sequential 클래스는 InputLayer 클래스를 자동으로 추가하고 호출해 주지만 Model 클래스는 수동으로 만들어서 호출해야 합니다. 즉, inputs는 InputLayer 클래스의 출력값이 되어야 합니다.
inputs = keras.Input(shape=(784, ))
케라스에서는 InputLayer 클래스 객체를 쉽게 다룰 수 있도록 Input( ) 함수를 제공합니다.
inputs 값까지 적용한 Model 클래스의 구조는 아래와 같습니다.
이전 장에서 정의한 model 객체의 층을 순서대로 나열하면 아래와 같습니다.
Conv2D 층이 출력한 특성 맵을 확인하기 위해서 model 객체의 입력과 Conv2D 출력을 이어서 새로운 모델을 만든 후 그 출력을 확인해보겠습니다.
Conv2D 객체의 output 속성에서 해당 층의 출력값을 얻을 수 있고, model.layers[0].output 처럼 참조할 수 있습니다.
print(model.inputs)
[<KerasTensor shape=(None, 28, 28, 1), dtype=float32, sparse=False, name=input_layer>]
케라스 모델은 input 속성으로 입력을 참조합니다.
이제 model.input과 model.layers[0].output을 연결하는 새로운 conv_acti 모델을 만들 수 있습니다.
conv_acti = keras.Model(model.input, model.layers[0].output)
model 객체의 predict( ) 매서드를 호출하면 최종 출력층의 확률을 반환하듯이 conv_acti의 predict( ) 매서드를 호출해서 Conv2D의 출력을 반환할 것입니다.
특성 맵 시각화
- 합성곱 층의 활성화 출력을 이미지로 그리는 것
- 가중치 시각화와 함께 비교하여 각 필터가 이미지의 어느 부분을 활성화 시키는 지 확인 가능
케라스로 MNIST 데이터셋을 읽은 후 훈련 세트의 첫 번 째 샘플을 확인해보겠습니다.
(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()
plt.imshow(train_input[0], cmap='gray_r')
plt.show()
앵클 부츠입니다. 이 샘플을 conv_acti 모델에 주입하여 Conv2D 층이 만드는 특성 맵을 출력해보겠습니다.
inputs = train_input[0:1].reshape(-1, 28, 28, 1)/255.0
feature_maps = conv_acti.predict(inputs)
predict( ) 매서드
- 입력의 첫번째 차원이 배치 차원이어야 함
- 슬라이싱 연산자를 사용해 샘플을 선택한 후 입력함
- (784, ) 크기를 (28, 28, 1) 크기로 변경하고 255로 나눔
print(feature_maps.shape)
(1, 28, 28, 32)
conv_acti.predict( ) 매서드가 출력한 feature_maps의 크기는 (1, 28, 28, 32)입니다. 첫 번째 차원은 배치 차원이며 샘플을 하나 입력했기 때문에 1 입니다.
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()
이 특성맵은 32개의 필터로 인해 입력 이미지에서 강하게 활성화된 부분을 보여줍니다.
앞서 32개 필터의 가중치를 출력한 그림과 비교해보겠습니다. 첫 번째 필터는 오른쪽에 있는 수직선을 감지합니다. 첫 번째 특성맵은 이 필터가 감지한 수직선이 활성화되어있는 것을 알 수 있습니다.
다음은 두 번째 합성곱 층이 만든 특성 맵을 확인해보겠습니다.
conv2_acti = keras.Model(model.inputs, model.layers[2].output)
model 객체의 입력과 두번째 합성곱 층인 model.layers[2]의 출력을 연결한 conv2_acti 모델을 만듭니다.
feature_maps = conv2_acti.predict(train_input[0:1].reshape(-1, 28, 28, 1)/255.0)
첫 번째 샘플을 conv2_acti 모델의 predict( ) 매서드에 전달합니다.
print(feature_maps.shape)
(1, 14, 14, 64)
첫 번째 풀링 층에서 가로 세로 크기가 절반으로 줄었고, 두 번째 합성곱 층의 필터 개수는 64개이므로 feature_maps의 크기는 배치 차원을 제외하면 (14, 14, 64) 입니다.
64개의 특성 맵을 8개씩 나누어 imshow( ) 함수로 그려보겠습니다.
fig, axs = plt.subplots(8, 8, figsize=(12,12))
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()
이 맵은 시각적으로 이해하기 어렵습니다.
두 번째 합성곱 층의 필터 크기는 (3, 3, 32) 입니다. 두 번째 합성곱 층의 첫 번째 필터가 앞서 출력한 32개의 특성 맵과 곱해져 두 번째 합성곱 층의 첫 번째 특성 맵이 됩니다. 따라서 이렇게 계산된 출력은 (14, 14, 32) 특성 맵에서 어떤 부위를 감지하는지 직관적으로 이해하기 어렵습니다.
합성곱 층이 깊어지면서 앞에서 감지한 시각 정보를 바탕으로 추상적인 정보를 학습한다는 점을 알 수 있습니다.
자료 출처: 한빛 미디어