R’s way for Deep Learning with Keras

Keras는 high level 딥러닝 API의 표준을 달리고 있는 딥러닝 프레임웍 중에 하나이다. TensorFlow를 기점으로 Theano, CNTK를 지원하고 있으며, 현재 MXNet까지 관련 인터페이스를 개발하고 있어 점점 딥러닝의 표준으로 자리잡고 있다.

필자는 Keras(or TensorFlow) + Python 기반으로 실무를 하고 있는데, 사실 딥러닝 프레임웍을 제외하고는 데이터를 다루는 모든면에서 R이 더 효과적이라고 생각하고 있는 사람중에 하나이고 많은 분들이 이 부분에 대해서 공감하고 있다. 일단 시각화도 그렇고 data.frame의 대응인 pandas의 거의 재앙에 가까운 API 구조에 질려본 사용자들은 이 부분에 대해서 공감할 것이다. 물론 pandas + numpy 조합으로 그나마 데이터 기반 코딩하는 재미를 살려볼 여지는 충분히 있어서 그나마 다행이라 생각하고 있다.
개인적으로 생각하는 데이터 분석에서의 Python의 강점은 풍부한 언어적인 특징 덕분에 코드의 성능향상 엔지니어링을 발휘할 수 있다는 것과 코드가 깔끔하게 정리된다는 장점 등 많은 장점이 존재한다고 생각한다.

일단 필자는 Python과 R 모두 사용하기로 했고 나름의 언어 사용의 재미 또한 느끼고 있다.

왜 필자가 Python과 R 이야기를 이 시점에서 하는냐면 약 1달전 R 기반의 Keras 패키지를 사용하고 상당히 감명을 받아서 이런 저런 테스트를 하다가 Keras github에 이러한 이슈를 남기고 사용하기를 중지했기 때문이다. 물론 시도를 해보긴 했지만 yield구문을 통한 python generator 구현은 R에서 거의 불가능하다고 생각했기 때문이다. 일단 이 기능은 메모리에 모두 올라가지 않는 데이터에 대한 학습 예측을 위한 기능을 구현할 수 있는 언어적인 기법이었는데, Python에서는 내부적으로 thread로 구현되어 있기 때문이었다. 해당 문법도 없고, 언어적으로 thread 기능을 지원하지 않는 관계로 R에서 불가능하다고 생각했다. 이 부분은 ggplot2 개발자인 헤들리 위컴도 동의한 부분이었다.

실제 fit_generator()는 실무를 한다면 반드시 활용해야 되는 중요한 학습 함수이다. 위에 이야기한 큰 데이터를 학습하기 위한 목적 이외에 데이터의 클래스 벨런스 및 셔플링 등등 학습에 반드시 필요한 작업들을 수행할 수 있는 유일한 부분이기 때문이었다.

일단 이러한 중요 문제 때문에 필자도 덮어두고 아예 Python으로 데이터 전처리 및 모델링을 하고 있었는데, 아래와 같은 commit이 올라와 확인해보니 관련 기능이 구현된 것을 확인하고 깜짝 놀라기까지 했다. 게다가 관련 문서까지 업데이트 된 것을 확인했다.
물론 이 기능은 py_iterator()를 구현하고 있는 reticulate패키지의 공이긴 하다.

일단 필자는 R에서 구현된 Keras가 얼마나 깔끔한 문법 구조와 더불어 사용자 편의성을 추구하고 있으며 강점인 시각화를 어떻게 활용하는지 간단한 예제를 통해 보여주고자 한다. 아래 예제는 이 코드를 기반으로 추가 코드를 작성했다는 걸 밝힌다.

library(keras)


batch_size <- 128
num_classes <- 10
epochs <- 10

# MNIST 데이터 가져오기 
mnist <- dataset_mnist()
x_train <- mnist$train$x
y_train <- mnist$train$y
x_test <- mnist$test$x
y_test <- mnist$test$y

x_train <- array(as.numeric(x_train), dim = c(dim(x_train)[1], 784))
x_test <- array(as.numeric(x_test), dim = c(dim(x_test)[1], 784))

x_train <- x_train / 255
x_test <- x_test / 255

cat(dim(x_train)[1], 'train samples\n')
## 60000 train samples
cat(dim(x_test)[1], 'test samples\n')
## 10000 test samples
# 더미변수로 변환 
y_train <- to_categorical(y_train, num_classes)
y_test <- to_categorical(y_test, num_classes)


#기존의 model.add 를 Pipe 연산자로 구성해 더 깔끔해졌다.
#이런게 바로 R's way...  
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)
## 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
## ___________________________________________________________________________
## 
## 
model %>% compile(
  loss = 'categorical_crossentropy',
  optimizer = optimizer_rmsprop(),
  metrics = c('accuracy')
)

history <- model %>% fit(
  x_train, y_train,
  batch_size = batch_size,
  epochs = epochs,
  verbose = 1,
  callbacks = callback_tensorboard(log_dir = "logs/run_b"),
  validation_split = 0.2
)

Train on 48000 samples, validate on 12000 samples
Epoch 1/10
48000/48000 [==============================] - 0s - loss: 0.5470 - acc: 0.8288 - val_loss: 0.1896 - val_acc: 0.9437
Epoch 2/10
48000/48000 [==============================] - 0s - loss: 0.2630 - acc: 0.9246 - val_loss: 0.1368 - val_acc: 0.9606
Epoch 3/10
48000/48000 [==============================] - 0s - loss: 0.2080 - acc: 0.9407 - val_loss: 0.1271 - val_acc: 0.9635
Epoch 4/10
48000/48000 [==============================] - 0s - loss: 0.1773 - acc: 0.9486 - val_loss: 0.1188 - val_acc: 0.9679
Epoch 5/10
48000/48000 [==============================] - 0s - loss: 0.1660 - acc: 0.9532 - val_loss: 0.1115 - val_acc: 0.9716
Epoch 6/10
48000/48000 [==============================] - 0s - loss: 0.1506 - acc: 0.9578 - val_loss: 0.1065 - val_acc: 0.9732
Epoch 7/10
48000/48000 [==============================] - 0s - loss: 0.1458 - acc: 0.9595 - val_loss: 0.1058 - val_acc: 0.9733
Epoch 8/10
48000/48000 [==============================] - 0s - loss: 0.1415 - acc: 0.9628 - val_loss: 0.1090 - val_acc: 0.9735
Epoch 9/10
48000/48000 [==============================] - 0s - loss: 0.1333 - acc: 0.9644 - val_loss: 0.1114 - val_acc: 0.9735
Epoch 10/10
#아래 텐서보드 명령어로 R shell에서 바로 web browser를 열어 텐서보드를 띄울 수 있다.  
#tensorboard()

#아래 명령어로 매트릭에 대한 에폭별 성능을 바로 시각화 한다. 
plot(history)

plot of chunk unnamed-chunk-1

아래는 간단히 fit_generator()를 사용하는 예시를 보여준다.

#아래 코드는 학습셋 레코드를 300여개의 배치 그룹으로 리셈플링 기법으로 구성한 예시이다. 
seq.gen <- function(x_train, y_train, v=0){
  value <- v
  idx <- sample(1:300, dim(x_train)[1], replace = T)
  function() {
    value <<- value + 1
    list(x_train[which(idx == (value %% 300 + 1)), ], y_train[which(idx == (value %% 300 + 1)), ])
  }
}


history2 <- model %>% fit_generator(generator=seq.gen(x_train, y_train, 0), steps_per_epoch=300, epochs = 5)
Epoch 1/5
300/300 [==============================] - 2s - loss: 0.1333 - acc: 0.9658 
Epoch 2/5
300/300 [==============================] - 2s - loss: 0.1207 - acc: 0.9679 
Epoch 3/5
300/300 [==============================] - 2s - loss: 0.1169 - acc: 0.9697 
Epoch 4/5
300/300 [==============================] - 2s - loss: 0.1114 - acc: 0.9707

사실 위 generator는 Python의 그것과는 다른 존재이다. 왜냐면 Python의 generator를 흉내만 낸 실상 iterator이기 때문이다. 이 때문에 헤들리 위컴 등 개발자들이 R용 yield문 등 generator를 구현하는 걸 고민하고 있다. 그렇다고 R의 그것이 성능이 떨어지는 건 절대 아닌데, 왜냐면 Python의 스레드도 GIL(global interpreter lock) 때문에 반쪽짜리 쓰레드여서 거의 하나의 CPU를 사용하는 것과 유사한 효과를 발휘하기 때문이다. ㅎㅎ

여튼 업무에는 Python 기반, 개인적인 연구엔 R기반의 Keras를 사용할 예정이다. 무엇보다 데이터 핸들링/시각화 하기가 R이 편한 이유 하나 때문이다. ㅋ

CC BY-NC 4.0 R’s way for Deep Learning with Keras by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.