본문 바로가기

Study/AI

[모두의 딥러닝] #4. MLP과 CNN로 MNIST 데이터 분류하기

반응형

본 포스트는 '모두의 딥러닝' 개정 2판을 바탕으로 공부한 것을 정리한 글입니다.

16장: 이미지 인식의 꽃, CNN

1) MLP로 MNIST 데이터 분류하기

MNIST 데이터셋은 미국 국립표준기술원(NIST)이 손글씨를 수집해 만든 데이터로 구성되어 있습니다. 7만 개의 글자 이미지가 각각 0~9의 라벨링을 붙여둔 데이터셋입니다. 

 

이번에는 이 데이터셋을 활용하여 이미지를 0~9로 분류하는 문제를 풀어보겠습니다.

 

# 1. 데이터 전처리

keras를 이용해 MNIST 데이터를 가져옵니다. keras.datasets에서 mnist를 import 하면 됩니다.

 

attribute와 class 데이터 분리하기

그리고 불러온 데이터를 X, Y로 분리해줍니다. 이미지를 X에, 0~9 라벨을 Y에 넣어주고 train, test를 분리합니다.

from keras.datasets import mnist
(X_train, Y_class_train), (X_test, Y_class_test) = mnist.load_data()

 

데이터를 다루기 위해서는 데이터가 어떻게 이루어져 있나 살펴보는 과정이 선행되어야 합니다.

keras의 mnist.load_data() 대해 살펴볼까요?

mnist.load_data() 확인하기

7만 개의 데이터 중 6만 개가 train에 사용되고 1만 개가 test에 사용됩니다.

반환 값은 2개의 튜플입니다. 각각 이미지 데이터 x와 정답 데이터 y에 대한 튜플입니다.

 

아래의 링크에서 확인할 수 있습니다.

https://keras.io/ko/datasets/#mnist

 

Datasets - Keras Documentation

데이터 셋 CIFAR10 소형 이미지 분류 50,000개의 32x32 컬러 학습 이미지, 10개 범주의 라벨, 10,000개의 테스트 이미지로 구성된 데이터셋. 사용법: from keras.datasets import cifar10 (x_train, y_train), (x_test, y_test)

keras.io

 

데이터 눈으로 확인하기

데이터를 눈으로 확인해봅시다. 우리가 불러온 데이터는 이미지이기 때문에 이를 눈으로 보기 위해서는 별도의 라이브러리가 필요합니다. matplotlib의 imshow() 함수를 이용하면 이미지를 출력할 수 있습니다.

import matplotlib.pyplot as plt
plt.imshow(X_train[0], cmap='Greys')
plt.show()

위와 같이 코드를 입력하면 데이터를 흑백으로 출력할 수 있습니다.

이런 이미지가 데이터로 들어있다는 것을 확인할 수 있습니다.

 

컴퓨터가 이미지를 인식/처리하는 법

그럼 과연 이 이미지를 컴퓨터가 어떻게 인식하는 걸까요?

 

위에서 mnist.load_data()에 대해 살펴볼 때 얼핏 봤듯이 이 이미지는 28(가로) × 28(세로)로 구성되어 총 784개의 픽셀로 이루어져 있습니다. 그리고 각 픽셀은 밝기에 따라 0~255의 값을 가집니다. 0에 가까울 수록 밝고 255에 가까울 수록 어둡습니다. 이렇게 0~255로 구성된 행렬로 하나의 이미지를 구성합니다.

 

코드로 확인해봅시다.

import sys

for x in X_train[0]:
  for i in x:
    sys.stdout.write('%d\t' %i)
  sys.stdout.write('\n')

이렇게 X_train[0] 데이터에 대한 코드를 작성하면 위에서 이미지로 출력한 데이터가 사실은 아래와 같은 행렬로 이루어져 있음을 확인할 수 있습니다. 큰 값을 따라서 선을 그려보면 5의 윤곽이 보입니다.

 

이런 식으로 이미지는 다시 숫자의 집합으로 바뀌어 사용됩니다. 때문에 이미지라고 하더라도 앞서 해보았던 머신러닝/딥러닝과 같은 문제라고 볼 수 있습니다.

 

즉, 28 × 28 = 784개의 속성(attribute)을 이용해 0~9까지 10개 클래스(class) 중 정답을 예측하는 문제라고 보면 됩니다.

 

2차원 배열을 1차원 배열로 바꾸기(reshape)

활성화 함수에 값을 넣기 위해서는 2차원 배열을 784의 길이를 가진 1차원 배열로 바꿔 주어야 합니다. reshape() 함수를 이용해 2차원 배열을 1차원 배열로 바꿀 수 있습니다.

 

reshape() 함수의 형식은 이렇습니다. 아래와 같이 코드를 작성해줍니다.

reshape(총 샘플 수, 1차원 속성의 수)
X_train = X_train.reshape(X_train.shape[0], 784)

총 샘플의 수는 X_train.shape[0]이고 1차원 속성의 수는 28x28=784입니다.

 

데이터 정규화(normalization)

keras는 데이터 값이 모두 0~1 값을 가질 때 좋은 성능을 보인다고 합니다. 앞서 보았듯이 현재 데이터는 0~255 값을 갖고 있기 때문에 이 값들을 모두 0~1 사이의 값이 되도록 스케일링 해주어야 합니다. 이렇게 데이터의 범위가 클 때 분산의 정도를 바꾸는 것을 데이터 정규화(normalization)이라고 합니다.

 

직관적으로 알 수 있듯이 0~255 사이의 값을 갖게 하려면 각 데이터를 255로 나누어주면 됩니다. 일반적으로 이 과정은 scaler 함수를 사용하여 해결합니다만 이번에는 직접 수식을 입력하여 진행하겠습니다.

 

현재 데이터는 int type이므로 float형으로 바꾸어 준 후 255로 나누어줍니다. test 데이터도 동일한 과정으로 처리해줍니다.

X_train = X_train.astype('float64')
X_train = X_train / 255
X_test = X_test.reshape(X_test.shape[0], 784).astype('float64') / 255

 

원-핫 인코딩

그런데 문제가 있습니다. 우리의 class는 0~9의 값을 갖는 반면 딥러닝에 사용할 수 있는 값은 0 또는 1이라는 것입니다. 때문에 원-핫 인코딩을 통하여 0~9의 정수형 값을 갖는 데이터를 0과 1로만 이루어진 벡터로 바꾸어 주어야 합니다.

 

keras의 np_utils.to_categorical() 함수를 사용하면 class가 [5]라면 [0,0,0,0,0,1,0,0,0,0]으로 변환할 수 있습니다.

형식은 이렇습니다.

to_categorical(클래스, 클래스의 개수)
from keras.utils import np_utils

Y_train = np_utils.to_categorical(Y_class_train, 10)
Y_test = np_utils.to_categorical(Y_class_test, 10)

print(Y_train[0])

위 코드를 실행하고 Y_train[0]을 print하여 class를 확인하면 아래와 같이 원-핫 인코딩이 잘 된 것을 알 수 있습니다.

 

# 2. 딥러닝 기본 프레임 만들기

딥러닝을 실행하기 위한 프레임을 만들 차레입니다. 총 784개의 속성과 10개의 클래스를 가집니다. 아래와 같이 딥러닝 프레임을 만들 수 있습니다.

  • 입력값(input_dim): 784개
  • 은닉층: 512개
  • 출력값(class): 10개
  • 활성화 함수(activation)
    • 은닉층: relu
    • 출력층: softmax
  • 오차 함수: categorical_crossentropy
  • 최적화 함수: adam
  • 기타
    • ModelCheckpoint, EarlyStopping(10회) 적용
from keras.models import Sequential
from keras.layers import Dense
from keras.callbacks import ModelCheckpoint, EarlyStopping

model = Sequential()
model.add(Dense(512, input_dim=784, activation='relu')) # 은닉층. 입력값: 784개, 활성화 함수: relu
model.add(Dense(10, activation='softmax'))  # 출력층. class: 10개, 활성화 함수: softmax

model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

import os
from keras.callbacks import ModelCheckpoint, EarlyStopping

MODEL_DIR = './model/'
if not os.path.exists(MODEL_DIR):
  os.mkdir(MODEL_DIR)
        
modelpath = "./model/{epoch:02d}-{val_loss:.4f}.hdf5"

 

checkpointer 변수와 early_stopping_callback 변수에 각각의 함수를 저장합니다. 이 변수들은 뒤의 fit 메소드를 이용하여 콜백하여 사용할 예정입니다.

checkpointer = ModelCheckpoint(filepath=modelpath, monitor='val_loss', verbose=1, save_best_only=True)
early_stopping_callback = EarlyStopping(monitor='val_loss', patience=10)

ModelCheckpoint() 함수의 arguments는 이렇습니다. 

 

아래의 케라스 공식 문서 사이트에서 모델을 훈련할 때 사용하는 API들에 대해 더 자세하게 볼 수 있습니다.

https://keras.io/api/models/model_training_apis/

 

Keras documentation: Model training APIs

Model training APIs compile method Model.compile( optimizer="rmsprop", loss=None, metrics=None, loss_weights=None, weighted_metrics=None, run_eagerly=None, steps_per_execution=None, **kwargs ) Configures the model for training. Example model.compile(optimi

keras.io

 

# 3. 모델 학습시키기

그리고 fit 메소드를 이용해 샘플 200개를 모두 30번 반복하여 실행하게끔 코드를 작성합니다. 

  • epochs: 30
  • batch_size: 200
epoch: 전체 데이터셋에 대해 한 번의 학습 과정을 거치는 횟수
iteration: 1 epoch를 나누어 실행하는 횟수
batch_size: 1 iteration(1 batch)마다 학습하는 데이터의 양(size)

즉 이 모델은 6만 개의 데이터가 들어있는 데이터셋을 총 30번 돌면서 학습합니다.

다만 한 번에 6만 개 데이터를 학습하는 것은 힘들기 때문에, 6만 개의 데이터를 1 iteration 당 200개의 데이터로 쪼개서 학습을 합니다. 그래서 1 epoch 당 300 iteration의 학습을 거칩니다.

 

위에서 설정한 변수들을 fit() 함수의 callbacks argument로 넣어줍니다.

history = model.fit(X_train, Y_train, validation_data=(X_test, Y_test), epochs=30, batch_size=200, verbose=0, callbacks=[early_stopping_callback, checkpointer])

 

이 코드를 실행하면 아래와 같은 결과를 얻습니다.

30 epoch에 도달하지 못했지만 earlystopping을 10번 patience로 설정하였기에 10번 이상 성능이 개선되지 않자 조기 종료된 것을 확인할 수 있습니다.

 

ModelCheckpoint() 함수를 사용하였기 때문에 지정한 filepath에 최고의 성능을 가진 모델이 저장되었습니다.

그리고 테스트셋으로 최종 모델의 성과를 측정하면 아래와 같습니다.

print("\n Test Accuracy: %.4f" %(model.evaluate(X_test, Y_test)[1]))

최종 모델의 성능은 정확도 97.7%를 보이네요.

 

# 4. 실행 결과 그래프로 확인하기

학습셋의 오차와 검증셋의 오차를 그래프로 나타내서 과적합이 일어났는지 확인해봅시다.

import matplotlib.pyplot as plt
import numpy as np

# 검증셋의 오차
y_vloss = history.history['val_loss']

# 학습셋의 오차
y_loss = history.history['loss']

# 그래프로 표현
x_len = np.arange(len(y_loss))
plt.plot(x_len, y_vloss, marker='.', c="red", label='Testset_loss')
plt.plot(x_len, y_loss, marker='.', c="blue", label='Trainset_loss')

# 그래프에 그리드 넣고 레이블 표시
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

실행 결과 아래와 같은 그래프를 얻을 수 있습니다.

학습셋에 대한 오차와 함께 테스트셋의 오차도 줄어드는 것으로 보아 과적합이 일어나기 전에 적절히 학습을 끝낸 것 같습니다.

 

작업한 딥러닝 프레임은 하나의 은닉층을 가진 단순한 모델이라고 볼 수 있습니다. 도식화 하면 아래와 같습니다.

 

2) 컨볼루션 신경망(CNN)

# 1. 컨볼루션 신경망(CNN)이란?

컨볼루션 신경망(CNN)은 입력된 이미지에서 특징을 추출하기 위해 커널(슬라이딩 윈도)을 도입하는 기법입니다. 

 

예를 들어서 이해해봅시다.

입력된 이미지가 다음과 같은 값을 가지고 있다고 해봅시다.

그리고 2×2 크기의 커널을 준비합니다.

각 칸에 들어있는 것은 가중치입니다. 샘플 가중치를 ×1, ×0으로 설정했습니다.

커널을 맨 왼쪽, 맨 위칸부터 적용합니다.

그리고 연산의 결과를 합하여 새로운 값을 추출합니다.

위의 경우는 (1×1)+(0×0)+(0×0)+(1×1)=2라는 새로운 값을 얻을 수 있습니다.

 

그리고 커널을 오른쪽으로 한 칸씩 옮겨 모든 칸에 적용하여 각 칸 마다 새로운 값을 얻어냅니다.

출처: 모두의 딥러닝

 

그렇게 모든 칸에 대해 커널을 적용해 얻은 결과는 아래와 같습니다.

이런 과정을 거쳐 새롭게 얻은 층을 컨볼루션(합성곱)이라고 합니다. 컨볼루션을 통해 입력 데이터의 특징을 더욱 정교하게 추출할 수 있습니다. 

 

따라서 위의 MNIST 데이터셋에 컨볼루션 층을 적용하면 인식률을 높일 수 있을 것입니다.

 

# 2. keras에서 컨볼루션 신경망 사용해보기

keras에서 Conv2D() 함수를 이용하면 컨볼루션 층을 추가할 수 있습니다.

 

아래와 같이 작성하여 두 개의 컨볼루션 층을 추가합니다.

model.add(Conv2D(32, kernel_size(3,3), input_shape=(28, 28, 1), activation='relu'))
model.add(Conv2D(64,(3,3), activation='relu'))

총 네 개의 인자를 사용했습니다.

  • 첫 번째 인자: 몇 개의 커널을 적용할 것인지. 32개의 커널을 적용해 서로 다른 32개의 컨볼루션을 얻습니다.
  • kernel_size: 커널의 크기.
  • input_shape: 맨 처음 층에는 입력되는 값을 알려줘야 합니다. input_shape(행, 열, 색 또는 흑) 형식입니다.(색: 3, 흑: 1)
  • activation: 활성화 함수를 정합니다.

더 많은 인자는 다음과 같습니다.

https://www.tensorflow.org/api_docs/python/tf/keras/layers/Conv2D

 

tf.keras.layers.Conv2D  |  TensorFlow Core v2.8.0

2D convolution layer (e.g. spatial convolution over images).

www.tensorflow.org

컨볼루션을 추가한 도식은 다음과 같습니다.

 

3) 맥스 풀링

# 1. 맥스 풀링(max pooling)이란?

애초에 데이터가 매우 컸다면 컨볼루션을 해도 결과가 여전히 크고 복잡할 수 있습니다. 이런 경우에는 컨볼루션의 결과를 다시 축소해주어야 하는데, 이를 풀링(pooling) 혹은 서브 샘플링(sub sampling)이라고 합니다.

 

풀링에도 여러가지 방법이 있습니다.

맥스 풀링(max pooling)은 정해진 구역 안에서 최댓값을 뽑아내는 것이고 평균 풀링(average pooling)은 평균값을 뽑아내는 것입니다. 평균 풀링보다는 맥스 풀링이 보편적으로 사용됩니다.

 

맥스 풀링은 다음과 같이 구획을 한 후에, 구역 내에서 가장 큰 값을 추출하여 새로운 값을 얻어냅니다.

 

# 2. keras에서 맥스 풀링 사용해보기

다음과 같은 코드를 작성하여 맥스 풀링 층을 추가할 수 있습니다.

from keras.layers import MaxPooling2D

model.add(MaxPooling2D(pool_size=2))

pool_size는 풀링 창의 크기로, 1/pool_size 만큼 전체 크기가 줄어듭니다. 따라서 pool_size가 2면 전체 크기가 절반으로 줄어들게 됩니다.

 

4) 드롭아웃, 플래튼

2번의 컨볼루션, 맥스 풀링까지 진행하다보니 노드와 층이 많아지게 되었습니다. 그러나 노드나 층이 지나치게 많으면 과적합이 발생하거나 학습이 제대로 진행되지 않을 수 있습니다. 그래서 이를 방지하기 위한 방법 중 하나로 드롭아웃(drop out)이 사용됩니다.

 

# 1. 드롭아웃(drop out)

드롭아웃은 은닉층에 있는 노드 중 몇 개를 임의로 선택하여 학습에 배제시키는 것입니다. 

그림과 같이 랜덤하게 선택한 노드의 연결을 끊어서 과적합을 방지해줍니다.

 

keras에서는 dropout() 함수를 이용하여 드롭아웃을 적용할 수 있습니다.

from keras.layers import Dropout

model.add(Dropout(0.25))

이렇게 코드를 작성하면 25%의 노드를 학습에서 배제시킬 수 있습니다.

 

# 2. 플래튼(Flatten)

여기까지 Dense() 함수를 통해 기본 층을 만들고 convolution, max pooling, dropout을 적용한 층을 만들어주었습니다. 그리고 이제는 Dense() 층에 다른 층들을 연결해주어야 합니다.

 

여기서 주의해야 할 점이 Dense 층은 이미지 데이터를 일차원 배열로 바꾸어 준 상태이지만 convolution 층과 maxpooling 층은 아직 이미지를 이차원 배열로 다루고 있다는 것입니다.

 

따라서 Flatten() 함수를 통해 2차원 배열을 1차원 배열로 바꾸어주는 과정이 필요합니다.

from keras.layers import Flatten
model.addd(Flatten())

이렇게 코드를 추가해줍니다.

 

5) 종합하여 CNN 모델 실행하기

지금까지 수정한 모델을 하나의 코드로 정리해보겠습니다.

이런 식으로 설계된 코드를 돌린 결과는 이렇습니다. 

 

 

확실히 은닉층과 노드의 수가 많아져서 학습에 오랜 시간이 소요되는 것을 알 수 있습니다. CPU를 사용하면 1 epoch를 실행하는데 2분 정도가 소요됩니다. 때문에 GPU로 런타임 유형을 변경하여 실행해주어야 합니다.

GPU를 사용하여 총 3분이 걸려 학습을 완료했습니다. 학습 결과를 보니 정확도가 99.28%로 향상되었습니다.

그리고 그래프를 통해 과적합 또한 일어나지 않은 것을 알 수 있습니다.

 

 

반응형