[DL] 혼공머닝 7편 합성곱(convolution) 신경망
카테고리: DL
태그: convolution, Deep Learning, 스트라이드, 커널, 패딩, 필터
💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.
08 이미지를 위한 인공 신경망
08-1 합성곱 신경망의 구성 요소
합성곱(convolution) 은 마치 입력 데이터에 도장을 찍어 유용한 특성만 드러나게 하는 것으로 비유할 수 있다. 가령 10개의 특성이 있다고 하자. 밀집층은 이 10개의 특성에 각각 10개의 가중치를 곱한다.
그러나 합성곱은 가중치가 10개가 아니라 3개로만 곱한다. 이 때 1~3번을 곱하고 그 다음은 2~4번, 3~5번의 순서로 총 8번의 계산을 수행한다.
밀집층의 뉴런은 특성 개수만큼 10개의 가중치로 1개의 출력을 만드는 반면, 합성곱 층의 필터(커널)는 3개의 가중치로 8개의 출력을 만든 셈이다. 이 때 합성곱으로 만들어진 출력을 특성맵(feature map)이라 한다.
왼쪽이 특성, 가운데가 가중치(커널), 오른쪽이 합성곱 계산을 통해 얻은 특성 맵
만약 커널 크기는 (3,3)인데 특성 맵은 원본처럼 (4,4)를 만들려면 어떻게 해야 할까? 마치 더 큰 입력을 합성곱하는 척을 해야 한다. 입력 배열 주위를 가상원 원소로 채우는 것을 패딩(padding) 이라고 한다. 이 때 0으로 채우는 패딩을 세임 패딩이라고 부른다.
패딩을 하는 이유는 정보가 고르게 전달되기 위함이다. 패딩 없이 합성곱을 한다면, 위의 예시에서 가장 모서리 부분들은 1번씩만 커널에 쓰이는 반면 중앙에 있는 값들은 중복해서 쓰인다. 적절한 패딩은 이미지 변두리에 있는 정보를 잃지 않도록 도와준다.
지금까지의 합성곱 연산은 커널을 움직일 때 한 칸씩 움직였다. 하지만 두 칸씩 이동하여 특성 맵의 크기를 더 작게 할 수도 있다. 이러한 방식을 스트라이드(stride)라고 한다.
from tensorflow import keras
keras.layers.Conv2D(10, kernel_size=(3,3), activation='relu', padding='same', strides=1)
#padding매개변수와 strdies매개변수를 주목하자.
풀링(pooling) 은 합성곱 층에서 만든 특성 맵의 가로세로 크기를 줄이는 역할을 수행한다. 그러나 특성 맵의 개수는 줄이지 않는다. 만약 특성 맵이 (2,2,3)크기라면 (1,1,3)으로 가로세로만 축소하고 개수는 그대로 둔다는 것이다. 이 때 가로세로를 줄일 때, 그 영역에서 가장 큰 값을 택하면 최대 풀링(max pooling) 이라 하고, 영역의 평균값을 택하면 평균 풀링(average pooling) 이라고 한다. 평균 풀링은 특성 맵에 있는 중요한 정보를 희석시킬 수도 있기 때문에 최대 풀링을 많이 사용한다.
위 예시는 최대 풀링을 사용했다.
08-1 핵심 키워드
- 합성곱(convolution) : 밀집층과 비슷하게 입력과 가중치를 곱하고 절편을 더하는 선형 계산.
- 필터: 합성곱 층에서 뉴런의 역할을 수행하는 부분. 필터의 가중치와 절편을 커널이라고 부른다.
- 특성 맵: 합성곱 층의 출력 배열.
- 패딩: 합성곱 층의 입력 주위에 추가한 0으로 채워진 픽셀
- 스트라이드: 합성곱 층에서 필터가 입력 위를 이동하는 크기
- 풀링: 가중치가 없고 특성 맵의 가로세로 크기를 줄이는 역할을 수행. 일반적으로 최대 풀링을 사용한다.
08-2 합성곱 신경망을 사용한 이미지 분류
이제 앞서 배운 합성곱을 바탕으로 패션MNIST 데이터를 분류해보자.
#데이터 준비
from tensorflow import keras
from sklearn.model_selection import train_test_split
(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()
train_scaled = train_input.reshape(-1,28,28,1)/255.0
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)
#합성곱 신경망 만들기
model = keras.Sequential()
#첫번째 합성곱층 Conv2D 추가. 활성화함수는 relu, 세임패딩 적용
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))
#풀링 층 추가. 최대풀링 MasPooling2D클래스 사용. (2,2)풀링 사용
model.add(keras.layers.MaxPooling2D(2))
#필터의 개수를 64개로 늘려 두번째 합성곱-풀링 층 생성
model.add(keras.layers.Conv2D(64, kernel_size=3, activation='relu', padding='same'))
model.add(keras.layers.MaxPooling2D(2))
model.add(keras.layers.Flatten()) #(7,7,64)크기의 특성맵을 일렬로 펼쳐주기
model.add(keras.layers.Dense(100, activation='relu')) #은닉층
model.add(keras.layers.Dropout(0.4)) #드롭아웃
model.add(keras.layers.Dense(10, activation='softmax')) #출력층
#model summary
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 28, 28, 32) 320
max_pooling2d (MaxPooling2D (None, 14, 14, 32) 0
)
conv2d_2 (Conv2D) (None, 14, 14, 64) 18496
max_pooling2d_1 (MaxPooling (None, 7, 7, 64) 0
2D)
flatten (Flatten) (None, 3136) 0
dense (Dense) (None, 100) 313700
dropout (Dropout) (None, 100) 0
dense_1 (Dense) (None, 10) 1010
=================================================================
Total params: 333,526
Trainable params: 333,526
Non-trainable params: 0
_________________________________________________________________
케라스는 층의 구성을 그림으로 표현해주는 plot_model() 함수를 제공한다.
keras.utils.plot_model(model, show_shapes=True)
이제 모델을 컴파일하고 훈련할 때이다. 과정은 7장 3절에서 사용한 방식과 흡사하다.
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-cnn-model.h5', save_best_only=True)
early_stopping_cb = keras.callbacks.EarlyStopping(patience=2, restore_best_weights=True)
history = model.fit(train_scaled, train_target, epochs=20, validation_data=(val_scaled, val_target), callbacks=[checkpoint_cb, early_stop
model.evaluate(val_scaled, val_target)
#결과값
375/375 [==============================] - 5s 13ms/step - loss: 0.2385 - accuracy: 0.9112
[0.23846960067749023, 0.9111666679382324]
#검증 세트에 대한 성능이 합성곱 이전보다 확연히 좋아졌다.
08-2 핵심 패키지와 함수
TensorFlow
Conv2D
: 합성곱 연산을 구현한 클래스- 첫번째 매개변수는 필터의 개수
kernel_size
매개변수로 필터의 커널 크기 지정strides
매개변수로 필터의 이동 간격 지저padding
매개변수로 입력의 패딩 타입 지정.
MaxPooling2D
: 최대 풀링 연산을 구현한 클래스- 첫번째 매개변수는 풀링의 크기
padding
매개변수로 입력의 패딩 타입 지정.strides
매개변수로 필터의 이동 간격 지정
plot_model()
은 케라스 모델 구조를 그림으로 나타내줌
08-3 합성곱 신경망의 시각화
2절에서 만든 모델이 어떤 가중치를 학습했는지 체크포인트 파일을 읽어 들여보자. 층의 가중치와 절편은 weights
속성에 저장되어 있다.
from tensorflow import keras
model = keras.models.load_model('best-cnn-model.h5')
conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape) #첫번째 원소가 가중치, 두번째 원소가 절편이다.
conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())
#결과값
-0.03212438 0.20858574
가중치의 평균값은 0에 가깝고 표준편차는 0.2 정도이다. 이 가중치가 어떤 분포를 가졌는지 히스토그램을 그려보자
import matplotlib.pyplot as plt
plt.hist(conv_weights.reshape(-1,1))
plt.xlabel('weight')
plt.ylabel('count')
plt.show()
가중치 시각화
이번에는 32개의 가중치를 그림으로 출력해보자. 앞서 만든 subplots()
함수를 사용해서 말이다.
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()
결과 그림을 보면 어떠한 패턴을 읽을 수 있다. 가장 첫번째 가중치를 보면 아랫부분 픽셀들의 가중치가 높다(밝을수록 가중치가 높음). 즉, 이 가중치는 아래에 놓인 부분을 만나면 크게 활성화될 것이다.
이번에는 훈련하지 않은 빈 합성곱 신경망을 만들어보고, 이 층의 가중치가 위의 가중치와 어떻게 다른지 그림으로 비교해보자.
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_model.layers[0]
no_training_weights = no_training_conv.weights[0].numpy()
print(no_training_weights.mean(), no_training_weights.std())
#결과값
-0.0031735036 0.08017581
평균은 아까처럼 0에 가깝지만, 표준편차는 아까보다 훨씬 작다. 히스토그램으로 표현하면 훨씬 고르게 분포하고 있음을 알 수 있다.
가중치들을 보면 전반적으로 밋밋하다는 것을 볼 수 있다. 즉, 합성곱신경망이 패션MNIST 데이터 분류 정확도를 높이는 데에 톡톡히 기여하고 있다는 점을 확인할 수 있다.
특성맵 시각화
이번에는 샘플을 conv_acti
모델에 주입하여 Conv2D 층이 만드는 특성맵으로 출력해보자.
conv_acti = keras.Model(model.input, model.layers[0].output)
#전처리
inputs=train_input[0:1].reshape(-1,28,28,1)/255.0
feature_maps = conv_acti.predict(inputs)
print(feature_maps.shape)
#특성 맵 그리기
fig, axs = plt.subplots(4,8,figsize=(15,8))
for i in range(4): #4행으로
for j in range(8):
axs[i,j].imshow(feature_maps[0,:,:,i*8+j])
axs[i,j].axis('off')
plt.show()
이 특성 맵은 32개의 필터로 인해 입력 이미지에서 강하게 활성화된 부분을 보여준다.
20번째 특성맵과 앞에서 본 20번째 가중치 그림을 비교해보자. 가중치는 전체적으로 어두운, 낮은 음수 값이다. 이 필터가 큰 양수와 곱해지면 더 큰 음수가 되고, 배경처럼 0에 가까운 값과 곱해지면 작은 음수가 될 것이다. 따라서 사진의 배경이 상대적으로 크게 활성화되는 것이다. 이를 특성맵에서도 확인할 수 있다.
08-3 핵심 키워드
- 가중치 시각화: 합성곱 층의 가중치를 이미지로 출력하는 것
- 특성맵 시각화: 합성곱 층의 활성화 출력을 이미지로 그리는 것
- 함수형 API: 케라스에서 신경망 모델을 만드는 방법 중 하나