IT

R에서 딥러닝 API '케라스(Keras)'를 이용하여 신경망 모델 구현하기

유병혁 2017. 11. 3. 01:50

이번 글에서는 R에서 딥러닝 API '케라스(Keras)'가 어떻게 신경망 모델을 구현하는지 정리해 보도록 하겠습니다.

케라스의 쉽고 빠른 프로토타이핑을 알기 위한 목적인데요, 아래 글의 '엠니스트(MNIST)' 예제를 학습하였습니다.

R interface to Keras | https://keras.rstudio.com/index.html


참고로, R에서 케라스 설치 방법은 이전 글을 참조하시기 바랍니다:

R에서 딥러닝 API '케라스(Keras)' 설치하기 | http://blog.daum.net/geoscience/1161


엠니스트 데이터셋은 머신러닝 분야에서 훈련 및 시험 데이터로 폭넓게 이용되고 있습니다. 엠니스트는 Modified National

Institute of Standards and Technology의 약어로, 여기서 니스트는 미국 국립표준기술연구원(NIST)를 지칭하는 용어이기도 합니다.


데이터셋은 6만 개의 훈련 데이터와 1만 개의 실험 데이터로 구성되어 있습니다. 각각의 데이터는 28*28 화소 크기의 손글씨로 된 회색조

영상과 그에 상응하는 숫자 라벨(0부터 9까지)로 구성되어 있습니다. 예를 들면, 아래 영상 4개는 각각 5, 0, 4, 1이라는 라벨을 가지고 있습니다.
MNIST database | https://en.wikipedia.org/wiki/MNIST_database


케라스 R 패키지는 엠니스트 데이터셋을 접근할 수 있는 dataset_mnist() 함수가 제공됩니다.

library(keras)
mnist <- dataset_mnist()

아래와 같이 훈련 데이터와 시험 데이터를 각각의 객체로 정의합니다. 여기서 x 데이터는 3차원 배열

(images, width, height)로, y 데이터는 0부터 9까지 범위 값을 가지는 1차원 배열(벡터)로 구성되어 있습니다.

x_train <- mnist$train$x
y_train <- mnist$train$y
x_test <- mnist$test$x
y_test <- mnist$test$y

참고로, dim() 함수를 사용하여 객체들의 차원을 확인해 보면 아래와 같습니다.

> dim(x_train)
[1] 60000    28    28
> dim(y_train)
[1] 60000
> dim(x_test)
[1] 10000    28    28
> dim(y_test)
[1] 10000

x 데이터를 1차원 배열로 재변형합니다.

# reshape
x_train <- array_reshape(x_train, c(nrow(x_train), 784))
x_test <- array_reshape(x_test, c(nrow(x_test), 784))

이때 array_reshape() 함수를 사용하면 정해진 행과 열의 개수로 배열의 재변형을 처리할 수 있습니다.

nrow() 함수는 객체의 행 개수를 반환하므로 nrow(x_train)은 60000입니다. 아래는 array_reshape()의 예입니다.

> x <- 1:4 # 1차원 배열(벡터)
> x
[1] 1 2 3 4
> x <- array_reshape(x, c(2,2)) # 2차원 배열(행렬)
> x
     [,1] [,2]
[1,]    1    2
[2,]    3    4

재변형된 x_train과 x_test 객체의 0-255범위 화소값을 0부터 1사이 부동소수점 값으로 변경합니다.

# rescale
x_train <- x_train / 255
x_test <- x_test / 255

y 데이터의 각 분류(0부터 9까지 범위를 갖는 정수)가 편향되지 않게 신경망 학습되도록 '원-핫(one-hot) 인코딩'을 적용합니다.

원-핫 인코딩은 카테고리별 정수로 정의된 '클래스 벡터'를, 카테고리별 불린 컬럼으로 된 '바이너리 클래스 행렬'로 변환하는 방법입니다.


예를 들어, 국립공원에서 촬영된 트랩카메라 사진 x마다 1번 반달가슴곰, 2번 멧돼지, 3번 삵, 4번 고라니, 5번 다람쥐라고 라벨 y을 달았을 때:

신경망 훈련에서 머신이 '5번 다람쥐가 1번 반달가슴곰보다 크다'는 식으로 카테고리 순서를 학습하지 않도록 부호화하는 처리가 되겠습니다.


이를 위해 케라스 R 패키지에서는 to_categorical() 함수가 제공되고 있습니다.

y_train <- to_categorical(y_train, 10)
y_test <- to_categorical(y_test, 10)

[참고] What is one hot encoding and when is it used in data science?

https://www.quora.com/What-is-one-hot-encoding-and-when-is-it-used-in-data-science


원핫 인코딩이 적용된 y_train 데이터의 예시는 아래와 같습니다. 위 그림의 5, 0, 4, 1 라벨입니다.

> head(y_train, 4)
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    0    0    0    0    0    1    0    0    0     0
[2,]    1    0    0    0    0    0    0    0    0     0
[3,]    0    0    0    0    1    0    0    0    0     0
[4,]    0    1    0    0    0    0    0    0    0     0


자, 이제 데이터의 준비가 끝났으면 케라스에서 신경망 모델을 구현해 보도록 하겠습니다. 케라스는 계층을 조합하여

모델을 만들 수 있으며, 가장 간단한 유형의 모델은 ‘순차 모델(Sequential model)’로써 계층을 선형으로 나열하는 방식입니다.


신경망 모델은 그림으로 나타내면 아래와 같이 크게 입력층, 은닉층, 출력층으로 구분됩니다. 아래 예시에서

신경망은 모두 3층이지만, 가중치를 갖는 층은 2개이므로 2층 신경망이라고 정의합니다(문헌에 따라 차이가 있음).


[출처] 밑바닥부터 시작하는 딥러닝(Deep Learning from Scratch)

한국어 깃허브 저장소 | https://github.com/WegraLee/deep-learning-from-scratch
도서정보(한빛미디어) | http://www.hanbit.co.kr/store/books/look.php?p_code=B8475831198


위 신경망은 인접하는 계층의 모든 뉴런들이 서로 결합되어 있는데요, 이를 전결합(fully-connected)이라고 하며 이렇게 완전히 연결된

계층을 어파인(Affine), 또는 밀집(Dense) 계층이라고 합니다. 케라스에서는 layer_dense()를 통해 이러한 계층을 쉽게 설계할 수 있습니다.

model <- keras_model_sequential() 
model %>% 
  layer_dense(units = 256, activation = 'relu', input_shape = c(784)) %>% 
  layer_dense(units = 128, activation = 'relu') %>%
  layer_dense(units = 10, activation = 'softmax')

위의 5줄 코딩으로 케라스에서 순차모델을 객체로 정의한 후 3층 신경망을 구성하였습니다. 여기서 파이프(%>%) 기호는 층을 연결하는 연산자입니다.

현재 3층신경망은 입력층(0층)은 784개, 첫번째 은닉층(1층)은 256개, 두번째 은닉층(2층)은 128개, 출력층(3층)은 10개의 뉴런으로 구성되어 있습니다.


layer_dense() 함수에서 units은 뉴런의 개수, activation은 활성화 함수, input_shape은 입력층을 정의합니다.

이전 단계 계층의 가중치 합(가중 신호와 편향의 총합)은 활성화 함수를 거쳐 신호로 변환되어 다음 계층으로 전달됩니다.

현재 모델에서는 은닉층의 활성화 함수로 '렐루(LeRU)' 함수를 사용하고, 출력층에서는 분류에 유리한 '소프트맥스(Softmax)' 함수를 사용하였습니다.


summary() 함수를 이용하여 위 모델을 요약한 결과인데요, 여기서 매개변수는 가중치와

편향이므로 1층은 200,960개(785 * 256), 2층은 32,896개(257*128), 3층은 1,290개(129*10)입니다.

> summary(model)
__________________________________________________________________________________________________________
Layer (type)                                   Output Shape                               Param #         
==========================================================================================================
dense_1 (Dense)                                (None, 256)                                200960          
__________________________________________________________________________________________________________
dense_2 (Dense)                                (None, 128)                                32896           
__________________________________________________________________________________________________________
dense_3 (Dense)                                (None, 10)                                 1290            
==========================================================================================================
Total params: 235,146
Trainable params: 235,146
Non-trainable params: 0
__________________________________________________________________________________________________________

이제 적절한 손실함수, 최적화기, 측정기준과 함께 R언어로 작성한 모델을 기계어로 통역(컴파일, compile)합니다.

model %>% compile(
  loss = 'categorical_crossentropy',
  optimizer = optimizer_rmsprop(),
  metrics = c('accuracy')
)

손실(loss)함수는 현재 신경망 성능의 ‘나쁨’을 나타내는 지표입니다.  따라서 손실함수는 최소값일수록 좋습니다.

손실함수의 일종인 교차 엔트로피 오차(cross entropy error, CEE)의 산식은 -자연로그(신경망 출력)과 같습니다.

예컨데, 손글씨 정답에 대해 신경망 출력이 0.7이면 CEE는 0.36, 출력이 0.2이면 CEE는 1.61의 보다 높은 값을 가집니다.


최적화기(optimizer)는 손실함수의 값을 낮추기 위한 매개변수 최적값을 찾는 문제(최적화, optimization)를 수행합니다.

예제에서는 RMSProp optimizer를 사용하지만, 케라스에서는 다양한 최적화기(Adadelta optimizer, Adagrad optimizer,

Adamax optimizer, Adam optimizer, Nesterov Adam optimizer, Stochastic gradient descent)들을 제공하고 있습니다.


측정기준(metrics)은 훈련 및 시험하는 동안 모델 평가항목을 정의하며, 여기서는 정확도(accuracy)로 확인하겠습니다.


자, 이제 케라스 모델을 학습시켜 볼까요?! 모델 학습에는 fit() 함수가 사용됩니다.

history <- model %>% fit(
  x_train, y_train, 
  epochs = 10, batch_size = 128, 
  validation_split = 0.2
)

신경망 학습에서는 훈련 데이터의 일부를 무작위로 추출하여 최적화를 갱신합니다. 이것을 미니배치(mini-batch)라고 하는데요, 하나의 배치를

모두 소진했을 때 횟수를 1에폭(epoch) 단위로 정의합니다. 여기서는 128개 데이터를 미니배치로 추출해서 10회 학습하는 것으로 설정하였습니다.

신경망 학습을 위한 데이터셋은 크게 3가지 유형으로 구분됩니다:

  • 훈련 데이터(training data): 매개변수(가중치와 편향) 학습
  • 검증 데이터(validation data): 하이퍼파라미터 성능 평가
  • 시험 데이터(test data): 신경망의 범용 성능 평가

검증 데이터는 훈련 데이터의 일부를 사용하는데요, 위 코드에서 validation_split은 비율을 정의합니다.

이에 따라 데이터셋 60,000개 중 48,000개는 훈련 데이터로, 12,000개는 검증 데이터로 사용되었습니다.


아래는 1부터 10까지 에폭이 진행됨에 따라 변화하는 훈련 데이터와 검증 데이터의 손실값(loss), 정확도(acc)를 각각 보여줍니다. 

Train on 48000 samples, validate on 12000 samples
Epoch 1/10
48000/48000 [==============================] - 6s - loss: 0.2951 - acc: 0.9122 - val_loss: 0.1641 - val_acc: 0.9537
Epoch 2/10
48000/48000 [==============================] - 5s - loss: 0.1131 - acc: 0.9653 - val_loss: 0.1064 - val_acc: 0.9679
Epoch 3/10
48000/48000 [==============================] - 5s - loss: 0.0755 - acc: 0.9762 - val_loss: 0.0913 - val_acc: 0.9721
Epoch 4/10
48000/48000 [==============================] - 4s - loss: 0.0547 - acc: 0.9833 - val_loss: 0.0856 - val_acc: 0.9762
Epoch 5/10
48000/48000 [==============================] - 4s - loss: 0.0405 - acc: 0.9876 - val_loss: 0.0861 - val_acc: 0.9773
Epoch 6/10
48000/48000 [==============================] - 4s - loss: 0.0299 - acc: 0.9907 - val_loss: 0.0895 - val_acc: 0.9768
Epoch 7/10
48000/48000 [==============================] - 4s - loss: 0.0238 - acc: 0.9922 - val_loss: 0.0937 - val_acc: 0.9773
Epoch 8/10
48000/48000 [==============================] - 4s - loss: 0.0173 - acc: 0.9943 - val_loss: 0.0922 - val_acc: 0.9792
Epoch 9/10
48000/48000 [==============================] - 4s - loss: 0.0142 - acc: 0.9959 - val_loss: 0.1007 - val_acc: 0.9783
Epoch 10/10
48000/48000 [==============================] - 5s - loss: 0.0120 - acc: 0.9959 - val_loss: 0.1046 - val_acc: 0.9781


플롯으로 그려본 결과는 아래와 같습니다.

plot(history)


이제 학습 데이터를 통해 모델의 성능을 평가해볼까요?!

> model %>% evaluate(x_test, y_test)
 9568/10000 [===========================>..] - ETA: 0s$loss
[1] 0.1080414
$acc
[1] 0.9774

이상으로 케라스 기반의 신경망 구현 과정을 간단히 정리해 봤는데요, ‘오버피팅(overfitting)’에 관한 내용을 보완해 보겠습니다. 오버피팅은

신경망이 훈련 데이터에만 지나치게 최적화된 상태를 나타냅니다. 앞서 검증, 시험 데이터를 굳이 구분하는 이유도 시험데이터에 오버피팅을

억제하기 위해서인데요, 신경망이 훈련 데이터에만 지나치게 맞춰지면 그 외의 데이터에 제대로 대응하지 못해 ‘범용 성능’이 떨어지게 됩니다.


오버피팅 억제를 위한 ‘가중치 감소(weight decay)’ 기법은 손실함수에 가중치의 제곱(L2 법칙)을 더합니다.

이렇게 되면 큰 가중치에 대해서 그에 상응하는 큰 페널티를 부과할 수 있어 오버피팅에 대응할 수 있습니다.


하지만 신경망 모델이 복잡해지면 가중치 감소만으로는 억제가 어려워지는데요, 이때 사용하는 ‘드롭아웃(Dropout)'이라는 기법이 있습니다.

드롭아웃은 은닉층의 뉴런을 무작위로 삭제하고 학습하여 범용 성능을 높이는 방법인데요, 여기서는 1층 40%, 2층 30%를 적용해 보겠습니다.

> model <- keras_model_sequential() 
> model %>% 
+     layer_dense(units = 256, activation = 'relu', input_shape = c(784)) %>% 
+     layer_dropout(rate = 0.4) %>% 
+     layer_dense(units = 128, activation = 'relu') %>%
+     layer_dropout(rate = 0.3) %>%
+     layer_dense(units = 10, activation = 'softmax')
> summary(model)
__________________________________________________________________________________________________________________________
Layer (type)                                          Output Shape                                     Param #            
==========================================================================================================================
dense_1 (Dense)                                       (None, 256)                                      200960             
__________________________________________________________________________________________________________________________
dropout_1 (Dropout)                                   (None, 256)                                      0                  
__________________________________________________________________________________________________________________________
dense_2 (Dense)                                       (None, 128)                                      32896              
__________________________________________________________________________________________________________________________
dropout_2 (Dropout)                                   (None, 128)                                      0                  
__________________________________________________________________________________________________________________________
dense_3 (Dense)                                       (None, 10)                                       1290               
==========================================================================================================================
Total params: 235,146
Trainable params: 235,146
Non-trainable params: 0
__________________________________________________________________________________________________________________________

드롭아웃을 추가한 상태에서 훈련 및 검증 데이터의 에폭별 손실값, 정확도입니다.

Train on 48000 samples, validate on 12000 samples
Epoch 1/10
48000/48000 [==============================] - 5s - loss: 0.4289 - acc: 0.8692 - val_loss: 0.1609 - val_acc: 0.9539
Epoch 2/10
48000/48000 [==============================] - 3s - loss: 0.2013 - acc: 0.9395 - val_loss: 0.1249 - val_acc: 0.9622
Epoch 3/10
48000/48000 [==============================] - 3s - loss: 0.1571 - acc: 0.9536 - val_loss: 0.1019 - val_acc: 0.9695
Epoch 4/10
48000/48000 [==============================] - 3s - loss: 0.1307 - acc: 0.9615 - val_loss: 0.1019 - val_acc: 0.9718
Epoch 5/10
48000/48000 [==============================] - 3s - loss: 0.1131 - acc: 0.9664 - val_loss: 0.0958 - val_acc: 0.9731
Epoch 6/10
48000/48000 [==============================] - 4s - loss: 0.1052 - acc: 0.9699 - val_loss: 0.0898 - val_acc: 0.9763
Epoch 7/10
48000/48000 [==============================] - 3s - loss: 0.0956 - acc: 0.9729 - val_loss: 0.0952 - val_acc: 0.9756
Epoch 8/10
48000/48000 [==============================] - 3s - loss: 0.0938 - acc: 0.9730 - val_loss: 0.0902 - val_acc: 0.9755
Epoch 9/10
48000/48000 [==============================] - 3s - loss: 0.0843 - acc: 0.9746 - val_loss: 0.0953 - val_acc: 0.9771
Epoch 10/10
48000/48000 [==============================] - 3s - loss: 0.0830 - acc: 0.9755 - val_loss: 0.0912 - val_acc: 0.9787>


플롯으로 그려본 결과입니다.


드롭아웃을 적용한 신경망에 시험 데이터를 적용했을 때 정확도가 더 높아진 것을 알 수 있습니다. 

> model %>% evaluate(x_test, y_test)
 9600/10000 [===========================>..] - ETA: 0s$loss
[1] 0.08275385
$acc
[1] 0.97

자세히 보시면 훈련 데이터보다 검증 데이터의 정확도가 더 높은데요, 데이터가 적어서 에폭을 30으로 늘리고 다시 측정해 봤습니다(정확도 0.98).

Train on 48000 samples, validate on 12000 samples
Epoch 1/30
48000/48000 [==============================] - 6s - loss: 0.4274 - acc: 0.8712 - val_loss: 0.1587 - val_acc: 0.9525
Epoch 2/30
48000/48000 [==============================] - 4s - loss: 0.1972 - acc: 0.9413 - val_loss: 0.1218 - val_acc: 0.9653
Epoch 3/30
48000/48000 [==============================] - 4s - loss: 0.1533 - acc: 0.9556 - val_loss: 0.1053 - val_acc: 0.9684
Epoch 4/30
48000/48000 [==============================] - 4s - loss: 0.1286 - acc: 0.9620 - val_loss: 0.0955 - val_acc: 0.9725
Epoch 5/30
48000/48000 [==============================] - 4s - loss: 0.1156 - acc: 0.9660 - val_loss: 0.0895 - val_acc: 0.9745
Epoch 6/30
48000/48000 [==============================] - 4s - loss: 0.0987 - acc: 0.9711 - val_loss: 0.0934 - val_acc: 0.9749
Epoch 7/30
48000/48000 [==============================] - 4s - loss: 0.0938 - acc: 0.9726 - val_loss: 0.0891 - val_acc: 0.9764
Epoch 8/30
48000/48000 [==============================] - 4s - loss: 0.0892 - acc: 0.9744 - val_loss: 0.0904 - val_acc: 0.9762
Epoch 9/30
48000/48000 [==============================] - 3s - loss: 0.0833 - acc: 0.9756 - val_loss: 0.0912 - val_acc: 0.9763
Epoch 10/30
48000/48000 [==============================] - 3s - loss: 0.0793 - acc: 0.9770 - val_loss: 0.0914 - val_acc: 0.9782
Epoch 11/30
48000/48000 [==============================] - 4s - loss: 0.0758 - acc: 0.9776 - val_loss: 0.0963 - val_acc: 0.9774
Epoch 12/30
48000/48000 [==============================] - 4s - loss: 0.0752 - acc: 0.9792 - val_loss: 0.0908 - val_acc: 0.9790
Epoch 13/30
48000/48000 [==============================] - 4s - loss: 0.0715 - acc: 0.9789 - val_loss: 0.0934 - val_acc: 0.9784
Epoch 14/30
48000/48000 [==============================] - 4s - loss: 0.0694 - acc: 0.9807 - val_loss: 0.0912 - val_acc: 0.9793
Epoch 15/30
48000/48000 [==============================] - 4s - loss: 0.0627 - acc: 0.9820 - val_loss: 0.0948 - val_acc: 0.9780
Epoch 16/30
48000/48000 [==============================] - 4s - loss: 0.0656 - acc: 0.9816 - val_loss: 0.0964 - val_acc: 0.9783
Epoch 17/30
48000/48000 [==============================] - 4s - loss: 0.0622 - acc: 0.9819 - val_loss: 0.0946 - val_acc: 0.9783
Epoch 18/30
48000/48000 [==============================] - 4s - loss: 0.0614 - acc: 0.9830 - val_loss: 0.1006 - val_acc: 0.9784
Epoch 19/30
48000/48000 [==============================] - 4s - loss: 0.0601 - acc: 0.9826 - val_loss: 0.1005 - val_acc: 0.9793
Epoch 20/30
48000/48000 [==============================] - 4s - loss: 0.0571 - acc: 0.9839 - val_loss: 0.1039 - val_acc: 0.9782
Epoch 21/30
48000/48000 [==============================] - 4s - loss: 0.0585 - acc: 0.9840 - val_loss: 0.1036 - val_acc: 0.9787
Epoch 22/30
48000/48000 [==============================] - 4s - loss: 0.0551 - acc: 0.9844 - val_loss: 0.1050 - val_acc: 0.9786
Epoch 23/30
48000/48000 [==============================] - 4s - loss: 0.0562 - acc: 0.9846 - val_loss: 0.1031 - val_acc: 0.9792
Epoch 24/30
48000/48000 [==============================] - 4s - loss: 0.0567 - acc: 0.9849 - val_loss: 0.1001 - val_acc: 0.9803
Epoch 25/30
48000/48000 [==============================] - 4s - loss: 0.0538 - acc: 0.9851 - val_loss: 0.1066 - val_acc: 0.9781
Epoch 26/30
48000/48000 [==============================] - 4s - loss: 0.0549 - acc: 0.9856 - val_loss: 0.1049 - val_acc: 0.9794
Epoch 27/30
48000/48000 [==============================] - 4s - loss: 0.0534 - acc: 0.9857 - val_loss: 0.1068 - val_acc: 0.9793
Epoch 28/30
48000/48000 [==============================] - 4s - loss: 0.0547 - acc: 0.9858 - val_loss: 0.1078 - val_acc: 0.9792
Epoch 29/30
48000/48000 [==============================] - 4s - loss: 0.0510 - acc: 0.9862 - val_loss: 0.1046 - val_acc: 0.9808
Epoch 30/30
48000/48000 [==============================] - 4s - loss: 0.0522 - acc: 0.9862 - val_loss: 0.1066 - val_acc: 0.9805> 
> plot(history)
> 
> model %>% evaluate(x_test, y_test)
 9632/10000 [===========================>..] - ETA: 0s$loss
[1] 0.1123058
$acc
[1] 0.98