본문 바로가기

머신러닝 + 딥러닝

[혼공딥] CHAPTER 08-3 합성곱 신경망의 시각화

728x90

합성곱 층이 이미지에서 어떤 것을 학습했는지 알아보기 위해 합성곱 층의 가중치와 특성 맵을 그림으로 시각화해보자

 

합성곱 층은 여러 개의 필터를 사용해 이미지에서 특징을 학습한다.

각 필터는 커널이라 부르는 가중치와 절편을 가지고 있는데, 일반적으로 절편은 시각적으로 의미가 있지 않다

가중치는 이미지의 2차원 영역에 적용되어 어떤 특징을 크게 두드러지게 표현하는 역할을 한다

 

2절에서 만든 모델이 어떤 가중치를 학습했는지 확인하기 위해 체크포인트 파일을 읽어들이자

from tensorflow import keras
model = keras.models.load_model('best-cnn-model.h5')

 

model.layers

케라스 모델에 추가한 층은 layers 속성에 저장되어 있다

[<keras.layers.convolutional.Conv2D at 0x7efe0b9c5dd0>,
 <keras.layers.pooling.MaxPooling2D at 0x7efe0d28c290>,
 <keras.layers.convolutional.Conv2D at 0x7efe12820750>,
 <keras.layers.pooling.MaxPooling2D at 0x7efe0d1d8390>,
 <keras.layers.core.flatten.Flatten at 0x7efe0b9c7850>,
 <keras.layers.core.dense.Dense at 0x7efe0b9d51d0>,
 <keras.layers.core.dropout.Dropout at 0x7efe0b9de950>,
 <keras.layers.core.dense.Dense at 0x7efe0d206390>]

Conv2D, MaxPooling2D 층이 2번씩 등장하고 Flatten층, Dense층, Dropout층이 차례로 등장하고 마지막에 Dense 출력층이 나온다

 

첫번째 합성곱 층의 가중치를 조사해보자

층의 가중치와 절편은 층의 weights 속성에 저장되어 있다.

conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)

첫 번째 원소는 가중치이고 두번째 원소는 절편이다

(3, 3, 1, 32) (32,)

이 합성곱 층에 전달되는 입력의 깊이가 1이어서 실제 커널 크기는 (3, 3, 1, 32)이다.

필터마다 1개의 절편이 있으므로 (32, ) 크기가 된다

 

conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())

weights 속성을 numpy() 메서드를 사용해 넘파이 배열로 변환한다

그 다음 가중치 배열의 평균과 표준편차를 mean()과 std() 메서드로 계산한다

-0.024093967 0.27398178

가중치의 평균값은 0에 가깝고 표준편차는 약 0.27이다 

 

import matplotlib.pyplot as plot
plt.hist(conv_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

hist() 함수에는 히스토그램을 그리기 위해 1차원 배열로 전달해야한다. 넘파이 reshape 메서드로 conv_weights 배열을 1개의 열이 있는 배열로 변환했다

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()

conv_weights에는 32개의 가중치가 저장되어 있다

이 배열의 마지막 차원을 순회하면서 0부터 i*16+j번째까지의 가중치 값을 차례대로 출력한다

여기에서 i는 행 인덱스이고, j는 열 인덱스로 각각 0~1, 0~15까지의 범위를 가진다

-> conv_weights[:, :, :0, 0]에서 conv_weights[:, :, :0, 31]까지 출력한다

밝은 부분의 값이 높음을 확인할 수 있는데, 맨 왼쪽 첫번째 줄의 가중치는 오른쪽에 놓인 직선을 만나면 크게 활성화될 것이다.

 

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)))

Sequential 클래스로 모델을 만들고 Conv2D층을 하나 추가한다

 

no_training_conv = no_training_model.layers[0]
print(no_training_conv.weights[0].shape)

이 모델의 첫번째 층(Conv2D층)의 가중치를 no_training_conv변수에 저장한다

(3, 3, 1, 32)

동일하게 (3, 3) 커널을 가진 필터를 32개 사용해서 앞서 출력한 가중치와 같다

 

no_training_weights = no_training_conv.weights[0].numpy()
print(no_training_weights.mean(), no_training_weights.std())

이 가중치의 평균과 표준편차를 확인해보면

-0.005247662 0.08134078

평균은 0에 가깝고 표준편차는 이전(0.27)보다 훨씬 작아졌음을 볼 수 있다

 

plt.hist(no_training_weights.reshape(-1, 1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()

가중치가 -0.15~0.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()

전체적으로 가중치가 밋밋하게 초기화되었음을 볼 수 있다. 합성곱 신경망이 패션 MNIST 데이터셋의 분류 정확도를 높이기 위해 유용한 패턴을 학습했다는 것을 알 수 있다.

 


함수형 API

 

딥러닝에서 입력이 2개이거나 출력이 2개인 경우가 있을수도 있는데 이럴 때는 Sequential 클래스를 사용하기 어렵다. 그래서 대신 함수형 API(functional API)를 사용한다

 

dense1 = keras.layers.Dense(100, activation='sigmoid')
dense2 = keras.layers.Dense(10, activation='softmax')

함수형 API는 케라스의 Model 클래스를 사용하여 모델을 만든다.

2개의 Dense 층 객체를 만든다

 

inputs = keras.Input(shape=(784,))

2장에서 plot_model() 함수로 모델의 층을 도식화 했을 때 InputLayer 클래스가 맨 처음 나왔는데, Sequential 클래스는 InputLayer 클래스를 자동으로 추가하고 호출해주지만, Model 클래스는 수동으로 만들어 호출해야한다

즉, inputs가 InputLayer 클래스의 출력값이 되어야한다

 

hidden = dense1(inputs)

이 객체를 Sequential 클래스 객체의 add() 메서드에 전달할 수도 있지만 위의 함수처럼 호출할 수도 있다

입력값 inputs를 Dense층에 통과시킨 후 출력값 hidden을 만들어준다

 

outputs = dense2(hidden)

첫번째 층의 출력을 입력으로 사용해 두번째 층을 호출한다

 

model = keras.Model(inputs, outputs)

그리고 inputs와 outputs를 Model 클래스로 연결하면 된다

 

마치 체인처럼 입력에서 출력까지 연결하고 마지막에 Model 클래스에 입력과 출력을 지정하여 모델을 만든다. 이렇게 모델을 만들게 되면 중간에 다양한 형태로 층을 연결할 수 있다

 

특성맵 시각화를 만드는데 함수형 API가 필요한 이유는 무엇일까

 

우리가 필요한 것은 첫 번재 Conv2D의 출력인데, model 객체의 입력과 Conv2D의 출력을 알 수 있다면 이 둘을 연결하여 새로운 모델을 얻을 수 있기 때문이다.

 

model 객체의 predict 매서드를 호출하면 입력부터 마지막 층까지 모든 계산을 수행한 후 최종 출력을 반환한다. 첫번째 Conv2D 층이 출력한 특성 맵이 필요한데 이는 output 속성에서 얻을 수 있다. model.layers[0].output처럼 참조할 수 있다.

 

print(model.input)

model 객체의 입력은 model.input으로 참조할 수 있다.

KerasTensor(type_spec=TensorSpec(shape=(None, 28, 28, 1), dtype=tf.float32, name='conv2d_input'), name='conv2d_input', description="created by layer 'conv2d_input'")

 

이제 model.input과 model.layers[0].output을 연결하는 새로운 conv_acti 모델을 만들 수 있다

conv_acti = keras.Model(model.input, model.layers[0].output)

 


케라스로 패션 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 층이 만든는 특성 맵을 출력해보자. predict() 메서드는 항상 입력의 첫 번째 차원이 배치 차원일 것으로 기대한다. 하나의 샘플을 전달하더라도 꼭 첫번째 차원을 유지해야한다.

 

슬라이싱 연산자를 사용해 첫 번째 샘플을 선택한다

inputs = train_input[0:1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv_acti.predict(inputs)

(784, ) 크기를 (28, 28, 1) 크기로 변경하고 255로 나눈다

 

print(feature_maps.shape)

conv_acti.predict() 메서드가 출력한 feature_maps의 크기를 확인해보면

(1, 28, 28, 32)

세임 패딩과 32개의 필터를 사용한 합성곱 층의 출력이므로 (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개의 필터로 인해 입력 이미지에서 강하게 활성화된 부분을 보여준다

 

- 첫번째 필터는 오른쪽에 있는 수직선을 감지한다. 첫번째 특성 맵은 이 필터가 감지한 수직선이 강하게 활성화되었다.

- 맨 아래 줄 세번째 특성 맵은 전체적으로 밝은색이어서 전면이 모두 칠해진 영역을 감지한다

- 세번째 필터는 전체적으로 낮은 음수값이다. 이 필터와 큰 양수가 곱해지면 더 큰 음수가 되고, 배경처럼 0에 가까운 값과 곱해지면 작은 음수가 될 것이다. 부츠의 배경이 상대적으로 크게 활성화될 수 있다

 

conv2_acti = keras.Model(model.input, model.layers[2].output)

두번째 합성곱 층이 만든 특성맵도 같은 방식으로 확인할 수 있다.

model 객체의 입력과 두번째 합성곱 층인 model.layers[2]의 출력을 연결한 conv2_acti 모델을 만든다

 

그리고 첫번째 샘플을 conv2_acti 모델의 predict() 메서드에 전달한다.

inputs = train_input[0:1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv2_acti.predict(inputs)

 

 

print(feature_maps.shape)

첫번째 풀링 층에서 가로세로 크기가 절반으로 줄었고, 두번째 합성곱 층의 필터 개수는 64개이므로

(1, 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) 특성맵에서 어떤 부위를 감지하는지 직관적으로 이해하기 어렵다

 

이런 현상은 합성곱 층을 많이 쌓을수록 심해진다. 합성곱 신경망의 앞부분에 있는 합성곱 층은 이미지의 시각적인 정보를 감지하고 뒤쪽에 있는 합성곱 층은 앞쪽에서 감지한 시각적인 정보를 바탕으로 추상적인 정보를 학습한다고 볼 수 있다!

 

가중치 시각화는 합성곱 층의 가중치를 이미지로 출력하는 것으로, 가중치가 시각적인 패턴을 학습하는지 알아볼 수 있다

특성 맵 시각화는 합성곱 층의 활서오하 출력을 이미지로 그리는 것을 말한다. 각 필터가 이미지의 어느 부분을 활성화시키는지 확인할 수 있다.

728x90