본문 바로가기

머신러닝 + 딥러닝

딥러닝 : 이미지를 위한 인공 신경망

728x90

전 시간에서 사용한 밀집층에는 뉴런마다 입력 개수만큼의 가중치가 있었다

모든 입력에 가중치를 곱하는 것이다

인공 신경망은 처음에 가중치 w1~w10과 절편 b를 랜덤하게 초기화한 다음 에포크를 반복하면서 경사 하강법 알고리즘을 사용하여 손실이 낮아지도록 최적의 가중치와 절편을 찾아가는 과정을 거치며 모델 훈련을 한다

 

합성곱(convolution)

- 수학에서는 하나의 함수와 또 다른 함수를 반전 이동한 값을 곱한 다음, 구간에 대해 적분하여 새로운 함수를 구하는 것

 

합성곱은 입력 데이터 전체에 가중치를 적용하는 것이 아니라 일부에 가중치를 곱한다

위 그림에서는 뉴런의 가중치가 3개씩 곱해지므로 총 8개의 출력이 만들어진다

 

합성곱 신경망(convolutional neural network, CNN)

: 행렬로 표현된 필터의 각 요소가 데이터 처리에 적합하도록 자동으로 학습되게 하는 것

 

합성곱 신경망에서는 뉴런을 필터(filter)커널(kernel)이라고 부른다

여기에서는 뉴런의 개수를 이야기할 때는 필터, 입력에 곱해지는 가중치를 의미할 때는 커널이라고 부른다

 

합성곱의 장점은 1차원이 아니라 2차원입력에도 적용할 수 있다는 것이다

필터(도장)의 커널 크기는 3x3이라고 가정하자

왼쪽 위 모서리부터 합성곱을 시작한다. 입력의 9개 원소와 커널의 9개 가중치를 곱한 후 절편을 더해서 한 개의 출력을 만든다

이런 식으로 필터(도장)를 이동시키면서 합성곱을 수행한다

위 그림에서 필터는 총 4번 이동할 수 있기 때문에 4개의 출력이 만들어진다

 

이때, 4개의 출력을 필터가 입력에 놓인 위치에 맞게 2차원으로 배치한다. 합성곱 계산을 통해 얻은 출력을 특성 맵(feature map)이라고 부른다

 

(2, 2) 크기의 특성맵을 쌓으면 3차원 배열이 된다. 즉, 3개의 필터를 사용했기 때문에 (2, 2, 3) 크기의 3차원 배열이 되는 것이다

 

합성곱층은 밀집층과 동일하게 단순히 입력과 가중치를 곱하는 것이지만 2차원 형태를 유지하는 점이 다르다.

 

 

케라스 합성곱 층

keras.layers.Conv2D(10, kernel_size=(3,3), activation='relu')

- 10은 필터의 개수(filters)

- kernel_size는 커널의 크기로 대부분 정방향이다

- activation은 활성화 함수, 여기에선 'relu'로, 렐루 함수를 지정했다. none이라고 쓰면 아무것도 적용되지 않는다

* 렐루 함수 : 입력이 양수일 경우 입력을 그대로 통과시키고 음수일 경우 0으로 만든다.

 

사용하는 방법은 밀집층(Dense)을 사용했던 자리에 Conv2D층을 넣으면 된다

 

합성곱 신경망은 1개 이상의 합성곱 층을 쓴 인공 신경망을 합성곱 신경망이라고 부른다

 

패딩(padding)

: 입력 배열의 주위를 가상의 원소로 채우는 것

이런 식으로 실제 입력 크기는 (4,4)이지만, (6,6)크기가 있다고 생각을 하고 (3,3)의 커널로 합성곱을 하는 것이다

그러면 총 16개의 픽셀이 만들어진다

 

세임 패딩(same padding) : 입력과 특성 맵의 크기를 동일하게 만들기 위해 입력 주위에 0으로 패딩하는 것

- 입력과 특성 맵의 크기를 동일하게 만드는 경우가 많다

 

밸리드 패딩(valid padding) : 패딩 없이 순수한 입력 배열에서만 합성곱을 하여 특성 맵을 만드는 경우

- 특성 맵의 크기가 줄어든다

 

keras.layers.Conv2D(10, kernel_size=(3,3), activation='relu', padding='same')

마지막에 padding 매개변수로 세임패딩(same)인지 밸리드패딩(valid)인지 지정할 수 있다

 

풀링(pooling) : 합성곱 층에서 만든 특성 맵의 가로세로 크기를 줄이는 역할을 수행

- 특성맵의 개수는 줄이지 않는다

- (2,2,3)크기의 특성 맵에 풀링을 적용하면 개수는 유지하고 너비와 높이만 줄어들어 (1,1,3) 크기의 특성 맵이 된다

 

풀링은 가중치가 없이 최댓값이나 평균값을 계산하는 역할을 수행한다.

파라미터를 줄이기 때문에 과대적합을 방지하고, 계산이 줄어들어 하드웨어 리소스를 절약하고 속도가 증가한다

 

  • 최대 풀링

가장 큰 값을 고르기 때문에 처음 (2,2)영역에서 9를 고른다. 이런 식으로 최댓값만 골라 (2,2) 크기의 출력을 만든다.

- 풀링의 크기가 (2,2)이면 가로세로 두 칸씩 이동한다

 

풀링은 가중치가 없고 풀링 크기가 같고 패딩도 없다

keras.layers.MaxPooling2D(2, strides=2, padding='valid')

- 첫번째 매개변수로 풀링의 크기 지정

- strides : 도장을 찍는 횟수 즉, 한 칸씩 도장을 이동시킬지, 두 칸씩 이동시킬지 결정. 기본값이 자동으로 풀링의 크기가 되어 따로 지정할 필요X

- padding : 기본값은 'valid'로 패딩을 하지 않는다

 

 

  • 평균 풀링

- 대상 영역의 평균을 계산, 해당 구역 내에서 데이터를 모두 더한 다음 나누어 평균이 출력된다

- 특성 맵에 있는 중요한 정보를 평균해서 희석시킬 수 있기 때문에 많이 사용하지 않는다

 

keras.layers.AveragePooling2D(pool_size=(2, 2), strides=None, padding='valid')

- pool_size : 튜플, (가로, 세로)의 기준이 되는 요소

- strides : None이면 기본값이 풀링의 크기

- padding : 'valid'이면 패딩이 없음을 의미, 'same'이면 출력이 입력과 동일한 높이/너비를 갖도록 입력의 왼쪽/오른쪽이나 위/아래로 균등하게 패딩된다.

 

 

  1. 합성곱의 필터는 3개, 각각 (3,3) 크기 가중치를 가지고 있고 필터마다 절편이 하나씩 있다.
  2. 만들어지는 특성 맵의 크기가 (4,4)이다. 3개의 필터가 하나씩 합성곱의 출력을 만들고 이들이 합쳐져서 (4,4,3) 크기의 특성 맵이 만들어진다. 활성화 함수로 주로 렐루 함수를 사용
  3. 풀링층은 합성곱 층에서 만든 특성 맵의 가로세로 크기를 줄인다. 보통 (2,2) 풀링을 사용하고, (4,4,3)에서 (2,2,3)으로 특성 맵 개수는 유지된 채로 줄여졌다.
  4. 밀집층에서 3차원 배열을 1차원으로 펼쳐 전달해야한다. 2x2x3으로 12개의 원소를 가진 1차원 배열이고 출력층의 입력이 된다
  5. 출력층에는 3개의 뉴런을 두어 소프트맥스 활성화함수를 거쳐서 최종 예측 확률로 만들 수 있다

 

컬러 이미지를 사용한 합성곱

컬러 이미지는 RGB 채널로 구성되어 있어 3차원 배열로 표시한다

너비와 높이, 깊이 차원이 있다

- 커널 배열의 깊이는 항상 입력의 깊이와 같다. 입력의 깊이가 3이면 도장의 깊이가 3이다

- (4,4,3) 크기의 입력이 있고 마지막 3이 깊이 차원이다. 필터의 커널 크기가 (3,3,3)이 된다

- 연산되는 부피는 (3x3x3)이 되고 가중치도 (3x3x3)이 곱해지고 절편 b가 추가된다

- 입력이나 필터의 차원이 몇 개인지 상관없이 항상 출력은 하나의 값이다.

 

흑백 이미지일 경우 깊이 차원이 1인 3차원 배열로 변환한다.

 


이제 합성곱 신경망을 사용해 이미지를 분류해보자

 

1. 패션 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 #48000개, 너비28, 높이28, 채널차원 1
	#train_scaled = train_input.reshape(-1, 28, 28, 1)
    	#train_scaled = train_scaled / 255.0
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)

위에서 흑백인 경우 깊이 차원이 1인 3차원 배열로 변환한다고 했다.

패션 MNIST 데이터셋은 흑백이므로 (28,28,1)로 3차원 배열로 변경한다

원래는 (28,28)로 2차원 배열이었음!

 

2. 합성곱 신경망 만들기

① 첫 번째 합성곱 층 만들기

model = keras.Sequential() #케라스에서 신경망 모델을 만드는 클래스
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))

 

- Conv2D로 첫 번째 합성곱 층을 추가한다

 

- 이 합성곱 층은 32개의 필터를 사용하고 커널 크기가 (3, 3)이다 -> 채널 차원을 따로 지정하지 않는다, 입력 데이터와 동일한 크기의 커널 채널 차원이 구성된다

- 활성화 함수로 렐루 함수를 사용하고, 패딩은 세임패딩을 사용한다.

* 렐루 함수 : 입력이 양수일 경우 입력을 그대로 통과시키고 음수일 경우 0으로 만든다.

* 세임 패딩 : 입력과 특성 맵의 크기를 동일하게 만들기 위해 입력 주위에 0으로 패딩하는 것

 

완전 신경망에서처럼 케라스 신경망 모델의 첫 번째 층에서 입력의 차원을 지정해주어야 한다

그래서 input_shape=(28, 28, 1)로 파이썬 튜플로 만들어서 지정해준다

 

필터 하나의 결과가 차곡차곡 쌓여서 32개가 쌓인다. same padding을 사용했기 때문에 너비와 높이가 28*28로 유지되고, depth 혹은 채널 차원은 필터의 개수와 동일하게 32개가 채워진다

 

② 첫 번째 풀링 층 추가

model.add(keras.layers.MaxPooling2D(2))

MaxPooling2D를 보아 최대 풀링을 사용하였음을 알 수 있다

 

너비와 높이가 2인 풀링을 사용해 28*28 크기가 14*14로 줄어들었음을 알 수 있다

풀링은 각 채널별로 따로따로 적용이 되어 depth 차원은 동일하게 유지된다

 

 

③ 두 번째 합성곱 층 추가

model.add(keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same')) #(14,14,64)
model.add(keras.layers.MaxPooling2D(2)) #(7,7,64)

합성곱 층에서 필터의 개수를 64로 늘린 것만 다르고 나머지는 모두 같다

 

풀링 층에서 입력의 가로, 세로 크기를 절반으로 줄인다. 64개의 필터를 사용했으므로 최종적으로 만들어지는 특성 맵의 크기는 (7, 7, 64)가 될 것이다.

 

④ flatten층, dense 은닉층, dense 출력층

model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(100, activation='relu'))
model.add(keras.layers.Dropout(0.4))
model.add(keras.layers.Dense(10, activation='softmax'))

3차원 특성 맵을 일렬로 펼친다 -> 10개의 뉴런을 가진 (밀집) 출력층에서 확률을 계산하기 때문이다

3136개의 입력 뉴런이 된다

* flatten()층은 곱하고 더하는 연산이 없고, 가중치도 없고, 입력 배열을 1차원으로 펼치는 역할

 

특성 맵을 일렬로 펼쳐서 바로 출력층에 전달하지 않고 중간에 밀집 은닉층을 둔다

첫 번째 dense층에서 활성화 함수 relu를 가지는 뉴런 100개를 두었다.

3136개의 입력이 100개의 뉴런에 완전 연결이 된다

이 100개의 뉴런을 10개의 뉴런을 가지는 최종 출력층에 두었고, softmax 함수를 사용해 10개의 확률을 얻는다

 

두 dense층 사이에 과대적합을 막기 위해 dropout을 사용하였다. 40% 정도의 뉴런을 훈련시에 끄도록 지정했다.

검증 세트의 점수가 훈련 세트에 비해 좋지 않은 과대적합을 막기 위해 사용

 

⑤ 케라스 모델 구조 출력

model.summary()

 

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 28, 28, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 14, 14, 32)       0         
 )                                                               
                                                                 
 conv2d_1 (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
_________________________________________________________________

첫번째 합성곱 층(conv2d)를 통과하면 특성 맵의 깊이는 32가 된다. 두번째 합성곱 층(conv2d_1)에서는 특성 맵의 크기가 64로 늘어난다

 

특성 맵의 가로세로 크기가 첫 번째 풀링 층(max_pooling2d)에서 절반인 14로 줄어들고, 두번째 풀링층(max_pooling2d_1)에서 절반인 7로 줄어든다

최종 특성 맵의 크기는 (7, 7, 64)이다

 

모델 파라미터의 개수는, 첫번째 합성곱 층(conv2d)은 32개의 필터를 가지고 있고 크기가 (3,3)이며 깊이는 1이다.

3 x 3 x 1 x 32 + 32 = 320개의 파라미터

 

두번째 합성곱 층은 64개의 필터를 가지고 있고, 크기 (3, 3), 깊이 32이다.

3 x 3 x 32 x 64 + 64 = 18496개의 파라미터

합성곱 층은 적은 개수의 파라미터로도 효과적으로 이미지의 특징을 잘 잡아낼 수 있다.

Flatten 클래스에서 (7, 7, 64) 크기의 특성 맵을 1차원 배열로 펼치면 (3136, ) 크기의 배열이 된다. 이를 100개의 뉴런과 연결해야해서 은닉층의 모델 파라미터 개수는 3136 * 100 + 100 = 313700개이다. 

완전연결층은 과대적합 되기가 쉽다

 

출력층의 모델 파라미터 개수는 100 * 10 + 10 = 1010개

 

keras.utils.plot_model(model, show_shapes=True, to_file='cnn-architecture.png', dpi=300)

plot_model() 함수의 show_shapes 매개변수를 True로 설정하면 입력과 출력의 크기를 표시해준다.

to_file 매개변수에 파일 이름을 지정하면 출력한 이미지를 파일로 저장한다.

dpi 매개변수로 해상도를 지정할 수 있다.

 


모델 컴파일, 훈련

model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics='accuracy')
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-cnn-model.h5')
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_stopping_cb])

fit 메서드를 이용해 모델을 훈련시켰다

epochs를 20으로 지정해 20번 동안 훈련을 반복하도록 했다

Epoch 1/20
1500/1500 [==============================] - 81s 53ms/step - loss: 0.5205 - accuracy: 0.8137 - val_loss: 0.3309 - val_accuracy: 0.8759
Epoch 2/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.3429 - accuracy: 0.8775 - val_loss: 0.2825 - val_accuracy: 0.8950
Epoch 3/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.2914 - accuracy: 0.8950 - val_loss: 0.2576 - val_accuracy: 0.9039
Epoch 4/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.2608 - accuracy: 0.9046 - val_loss: 0.2407 - val_accuracy: 0.9088
Epoch 5/20
1500/1500 [==============================] - 71s 48ms/step - loss: 0.2389 - accuracy: 0.9135 - val_loss: 0.2416 - val_accuracy: 0.9120
Epoch 6/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.2180 - accuracy: 0.9208 - val_loss: 0.2366 - val_accuracy: 0.9152
Epoch 7/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.2007 - accuracy: 0.9249 - val_loss: 0.2286 - val_accuracy: 0.9142
Epoch 8/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.1865 - accuracy: 0.9315 - val_loss: 0.2287 - val_accuracy: 0.9170
Epoch 9/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.1725 - accuracy: 0.9346 - val_loss: 0.2177 - val_accuracy: 0.9236
Epoch 10/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.1613 - accuracy: 0.9400 - val_loss: 0.2283 - val_accuracy: 0.9210
Epoch 11/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.1527 - accuracy: 0.9429 - val_loss: 0.2261 - val_accuracy: 0.9217

훈련 세트의 정확도가 좋아진 것을 확인할 수 있다.

 

import matplotlib.pyplot as plt
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

손실 그래프를 그려본다

대략 9번째 에포크가 최적이라고 생각할 수 있다

 

model.evaluate(val_scaled, val_target)

모델의 성능을 평가해보면, fit() 메서드의 출력 중 9번째 에포크의 출력과 동일하다

375/375 [==============================] - 5s 14ms/step - loss: 0.2177 - accuracy: 0.9236
[0.21768520772457123, 0.9235833287239075]

 

plt.imshow(val_scaled[10].reshape(28, 28), cmap='gray_r')
plt.show()

흑백 이미지에 깊이 차원은 없어서 (28, 28, 1) 크기를 (28, 28)로 바꾸어 출력해야한다.

 

preds = model.predict(val_scaled[10:11])
print(preds)

출력을 확인해보면 4번째 값이 거의 1에 가깝고 나머지는 예측값이 보다 낮음을 볼 수 있다.

[[1.4831944e-03 1.5091890e-08 2.0763587e-06 9.9661571e-01 3.6763682e-05
  1.5661797e-09 1.8609024e-03 5.0563796e-11 1.3901989e-06 2.8029434e-10]]

 

 

값을 보면 4번째 값이 9.96으로 가장 높다는 것을 알 수 있다. 즉 패션 MNIST의 10가지 데이터 중 4번째 데이터에 가장 가깝다는 뜻이다.

plt.bar(range(1, 11), preds[0])
plt.xlabel('class')
plt.ylabel('prob.')
plt.show()

막대그래프를 그려본다

그래프로 보아도 4번째 값이 가장 높다

 

classes = ['티셔츠', '바지', '스웨터', '드레스', '코트', '샌달', '셔츠', '스니커즈', '가방', '앵클부츠']

import numpy as np
print(classes[np.argmax(preds)])

4번째 클래스가 실제로 무엇인지 알기위해 MNIST 데이터셋의 레이블을 리스트로 저장해서 출력해본다.

preds 배열에서 가장 큰 인덱스를 찾아 classes 리스트의 인덱스로 사용한다.

드레스

실제로 잘 예측했음을 확인할 수 있다

 

테스트 세트로 합성곱 신경망의 일반화 성능을 측정해본다

test_scaled = test_input.reshape(-1, 28, 28, 1) / 255.0

픽셀값의 범위를 0~1 사이로 바꾸고 이미지 크기를 (28, 28)에서 (28, 28, 1)로 바꾼다

 

model.evaluate(test_scaled, test_target)

evaluate() 메서드로 테스트 세트에 대한 성능을 측정하면

313/313 [==============================] - 5s 14ms/step - loss: 0.2465 - accuracy: 0.9152
[0.24654965102672577, 0.9151999950408936]

테스트세트의 점수는 검증세트 점수보다 조금 더 작음을 볼 수 있다.

실전에 투입하면 약 91%의 성능을 기대할 수 있다.

 

728x90