• Home

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이 편한 이유 하나 때문이다. ㅋ

RNN을 이용한 한글 자동 띄어쓰기

필자가 한글자동띄어쓰기를 처음 R로 구현한 결과에 대한 링크를 참고하면 한글 자동띄어쓰기가 어떠한 방식으로 구현되는지 기초적인 부분을 알 수 있을 것이다. 개인적으로 한글 텍스트 분석에서 띄어쓰기는 생각보다 중요한 부분을 차지하고 있다고 생각한다. 이 부분이 잘 되지 않는다면 이후의 다양한 한글분석 퀄리티에 큰 영향을 끼질 수 있기 때문이다.

KoNLP 역시 한글 자동 띄어쓰기 모듈이 있기는하나, 매우 조악한 수준이며 이를 위해 RNN을 이용한 한글 띄어쓰기 모델링을 진행하고 있다. 지금부터 소개할 모듈은 RNN을 이용한 한글 자동띄어쓰기의 일부분을 소개할 것인데, 개인적으로 추가 연구한 부분의 코드는 연구가 진행됨에 따라 이곳에 공개할 예정이다(학습데이터와 코드를 함께 정리해 공개할 예정이다).

이 작업은 크게 두 부분으로 나뉘어져 있다.

  1. 음절단위 속성 벡터 추출
  2. bidirectional LSTM과 Linear CRF를 이용한 모형 학습

일단 필요한 패키지를 로딩한다. hashmap은 문자열에 대한 인덱스를 조회할때 사용되고, caret은 학습셋 셔플링시, 그리고 stringi,stringr의 경우 문자열 전처리에 쓰일 예정이다.

library(tensorflow)
library(hashmap)
library(wordVectors)
library(caret)
library(stringr)
library(stringi)
library(data.table)
library(reshape2)

#문자열 전체 빈도로 분포로 볼때 7회 미만은 빈도 1분위 미만이며, 이들에 대해서는 아래 문자로 대체한다. 
specl_char <- '⊙'

음절단위 속성 벡터 추출

코드가 다소 복잡한데, 사실 과정은 그리 복잡하지는 않다. 코퍼스에서 아래와 같은 문장을 문자열과 공백으로 쪼개는 과정을 수행하고 wordVectors패키지를 이용해 char vector형태의 속성 매트릭스를 만들면 된다. R에서도 Python과 같이 매우 간단한 명령어로 word2vector를 만들 수 있는 패키지가 있는 관계로 굳이 복잡한 코드를 구현하지 않아도 된다.

train_word2vec함수에서는 공백으로 구분되는 문자열을 하나의 학습셋으로 받아들이는 구조를 가지고 있기 때문에 아래와 같이 character레벨의 벡터를 생성하기 위해 공백으로 문장을 구분해 파일을 생성한 뒤 이를 기반으로 학습을 시키도록 한다.

아버지가 방을 나가셨다. -> 아 버 지 가 방 을 나 가 셨 다.

corpus <- readLines(bzfile("input.txt.bz2"))

corpus_cl <- stri_replace_all_regex(stri_trans_nfkc(corpus), pattern = '[:blank:]', replacement = '')

corpus_cl_split <- lapply(corpus_cl, function(x){
   paste(str_split(x, pattern = '')[[1]], collapse = ' ')
})

tbl_a <- table(unlist(corpus_cl_split))
summary(as.numeric(tbl_a))

#빈도 분포 확인 후 7회 이하 빈도는 다른 문자로 대체 
length(tbl_a[tbl_a < 7])
pat <- paste0('[', paste0(Filter(function(x) {nchar(x) == 1}, names(tbl_a[tbl_a < 7])),  collapse = ''), ']')


replaced_sent <- lapply(corpus_cl_split, function(x){str_replace(x, pattern = pat, replacement = specl_char)})


writeLines(unlist(replaced_sent), 'sejong_char_seq.txt')

w2v_model <- train_word2vec("sejong_char_seq.txt","sejong_char_seq.bin",
                       vectors=50,threads=8,window=10,iter=20,negative_samples=10, force=T)

w2v_model %>% closest_to('컴')

결국 위의 w2v_model(고유문자수 x 50)의 형태를 가지는 가중치 매트릭스를 가지게 된다. 물론 이 부분은 word2vector 알고리즘을 그대로 사용하기도 하지만 목적에 맞는 vector를 생성하는 네트워크를 직접 구축해 볼 수도 있을 것이다.

Korean Word Spacing with RNN

띄어쓰기는 대표적인 sequence-to-sequence 문제이며, “예제문장입니다.”이 입력되었을 때 “01000001”와 같은 띄어쓰기 시퀀스를 출력하는 문제로 정의할 수 있다. 여기서 0은 다음 문자를 붙여쓴다는 의미이고 1은 띄어쓴다는 의미이다.

여러 네트웍 구축 및 테스트해본 결과 필자는 Bidirectional LSTM-CRF Models 모형을 기반으로 학습을 시켰다. 학습 데이터는 세종 코퍼스에서 추출한 약 8만 문장을 활용하였다.

마지막 loss를 계산할때 개별 문자열의 softmax output정보만을 활용하기 보다는 CRF를 이용해서 이전 시퀀스의 결과에 따른 영향을 고려할 수 있도록 했는데, 이러한 이유는 띄어쓰기가 연속으로 일어날 확률은 실제 매우 적기 때문이고 LSTM만으로는 이러한 부분을 케어하지 못하기 때문이다. 이 부분은 기존의 띄어쓰기 알고리즘이 HMM이나 CRF모형을 많이 사용하는 이유와 매우 유사한데, 직접 구현할 필요 없이 TensorFlow에 포함된 함수를 기반으로 구현했다.

#analyze to extract word spacing info.
# makeCorpus("옷을 만드느라 늘")
#$status
# [1] 0 1 0 0 0 1 1
# 
# $char
# [1] "옷" "을" "만" "드" "느" "라" "늘"
# 
# $nchar
# [1] 7
makeCorpus <- function(str){
  strv <- strsplit(str,split="")[[1]]
  lenstrv <- length(strv)
  spacev <- vector(mode="numeric",length=lenstrv)
  charv  <- vector(mode="character",length=lenstrv)
  vidx <- 1
  for(i in 1:lenstrv){
    if(strv[i] == " ") {
      next
    }
    if(i + 1 <= lenstrv && strv[i + 1] == " "){
      spacev[vidx] <- 1
    }else{
      if(i == lenstrv){
        spacev[vidx] <- 1
      }else{
        spacev[vidx] <- 0
      }
    }
    charv[vidx] <- strv[i]
    vidx <- vidx + 1
  }
  charv_f <- Filter(function(x){x!=''},charv)
  status <- spacev[1:length(charv_f)]
  char <- charv_f
  nchar <-length(charv_f)
  return(list(status=status, char=char, nchar=nchar))
}

#read char2vector object 
m <- read.vectors("sejong_char_seq.bin")
#read corpus
sents <-readLines(bzfile('input.txt.bz2'),encoding='UTF-8')


#5어절씩 학습 문장을 만든다. 
sents_eojeol <- str_split(stri_trans_nfkc(sents), pattern = '[:blank:]')
wordchunk <- lapply(sents_eojeol, function(x){
  x <- Filter(function(y){y != ''}, x)
  v <- c()
  k <- 5
  if(length(x) < k) return(paste( c( x, ''),collapse = " "))
  
  for(i in 1:length(x)){
    if((i + k - 1) > length(x)) break
    v <- c(v, paste(c(x[i:(i + k - 1)], ''), collapse = " "))
  }
  return(v)
  })


wordchunk <- unlist(wordchunk)

space_coding  <- lapply(wordchunk, makeCorpus)

#extract each word chunk length
charsents <- lapply(space_coding, function(x){x$char})
uniq_chars <- unique(unlist(charsents))
max_seq_len <- max(unlist(lapply(space_coding, function(x){x$nchar})))

#make sentence coding 
seq_mat_x <- matrix(0, ncol=max_seq_len, nrow=length(wordchunk))

#make hash map to extract row index of m
chmap <- hashmap(rownames(m), 0:(nrow(m)-1))

for(i in 1:length(wordchunk)){
  sent <- space_coding[[i]]$char
  for(j in 1:length(sent)){
    idx <- chmap[[sent[j]]]
    if(is.na(idx)) idx <- chmap[[specl_char]]
    seq_mat_x[i,j] <- idx
  }
}


seq_mat_y <- matrix(0, ncol=max_seq_len, nrow=length(wordchunk))
loss_mask <-matrix(0, ncol=max_seq_len, nrow=length(wordchunk))

for(i in 1:length(wordchunk)){
  sent <- space_coding[[i]]$status
  for(j in 1:length(sent)){
    seq_mat_y[i,j] <- sent[j] 
    loss_mask[i,j] <- 1
  }
}


len_list <-  unlist(lapply(space_coding, function(x){x$nchar}))
sent_chars <- lapply(space_coding, function(x){x$char})
library(R6)

WordSpacing <- R6Class("WordSpacing",
    public = list(
      char_dic_size=NULL, 
      n_neurons=NULL, 
      num_classes=NULL, 
      batch_size=NULL,
      max_sequence_length=NULL, 
      word_spacing_graph=NULL,
      config_proto=NULL, 
      mem_fraction=NULL, 
      x=NULL, y=NULL, 
      sent_len=NULL, loss=NULL, prediction=NULL, optimizer=NULL,
      init=NULL, saver=NULL, global_step=NULL, num_out_classes=NULL,weight_mask=NULL,
      c2v=NULL, embeddings=NULL, seg_loss=NULL, accuracy=NULL,chmap=NULL, 
      transition_params=NULL, logit=NULL,
      
      initialize=function(char_dic_size, n_neurons, num_classes, num_out_classes, max_sequence_length,
                           c2v, mem_fraction=0.999,global_step = 1L){
        self$char_dic_size <- as.integer(char_dic_size)
        self$n_neurons <- as.integer(n_neurons)
        self$num_classes <- as.integer(num_classes)
        self$num_out_classes <- as.integer(num_out_classes)
        self$max_sequence_length <- as.integer(max_sequence_length)
        self$global_step <- as.integer(global_step)
        self$c2v <- c2v
        #self$is_training <- FALSE
        self$chmap <- hashmap(rownames(self$c2v), 0:(nrow(self$c2v)-1))
        
        
        
        gpu_options <- tf$GPUOptions(per_process_gpu_memory_fraction=mem_fraction)
        self$config_proto <- tf$ConfigProto(allow_soft_placement=T,log_device_placement=F, gpu_options=gpu_options)
        
        self$word_spacing_graph <- tf$Graph()
        
        with(self$word_spacing_graph$as_default(), {
          with(tf$name_scope("kor_word_spacing"),{
            with(tf$device("/gpu:0"), {
              
              #(batch  x max_sequence_length)
              self$x <- tf$placeholder(tf$int32, list(NULL, self$max_sequence_length), name='x') 
              # WordVectors로 학습된 char vector (char_dic_size x 100)
              self$embeddings <- tf$Variable(self$c2v, dtype=tf$float32, trainable=FALSE, 
                                             name = 'embeddings')
              # (batch x max_sequence_length)
              self$y <- tf$placeholder(tf$int32, list(NULL, self$max_sequence_length), name='y')  
              # (batch)
              self$sent_len <- tf$placeholder(tf$int32, list(NULL), name='sent_len') 
              #Loss 계산을 위한 masking 생성
              #문장 길이가 서로 다르기 때문임...
              self$weight_mask <- tf$sequence_mask(self$sent_len)
              self$batch_size <-  tf$placeholder(tf$int32, shape = list(), name='batch_size')

            })              
            with(tf$device("/gpu:1"), {

              with(tf$name_scope('rnn_cell'),{
                x_emb <- tf$nn$embedding_lookup(self$embeddings, self$x)
                
                cell <- tf$contrib$rnn$LSTMCell(num_units=self$n_neurons,use_peepholes=T)
                
                outputs_states <- tf$nn$bidirectional_dynamic_rnn(cell, cell, 
                                     x_emb, sequence_length=self$sent_len,dtype=tf$float32)
                
              }) 
              output_fw_output_bw <- outputs_states[[1]]
              #( (max_sequence_length + batch)  x n_neurons * 2)
              outputs <- tf$concat(list(output_fw_output_bw[[1]], output_fw_output_bw[[2]]), axis = -1L)
              
              with(tf$name_scope('fc1'),{
                
                x_fc <- tf$reshape(outputs, list(-1L, self$n_neurons * 2L))
                fc_w <- tf$get_variable("fc_w", list(self$n_neurons * 2L,
                                                     self$num_out_classes),
                                        initializer=tf$contrib$layers$xavier_initializer())
                fc_b <- tf$get_variable("fc_b", list(self$num_out_classes),
                                        initializer=tf$zeros_initializer())
                fc1 <-  tf$matmul(x_fc, fc_w) + fc_b
              })
              
              #l2_losses <- tf$reduce_sum(tf$abs(fc_w)) 
  
              # reshape out for sequence_loss
              self$logit <- tf$reshape(fc1, list(-1L, self$max_sequence_length,
                                              self$num_out_classes))
              
              
              log_likelihood_transition_params <- tf$contrib$crf$crf_log_likelihood(
                  self$logit, self$y, self$sent_len)
              self$loss <- tf$reduce_mean(-log_likelihood_transition_params[[1]])
              self$transition_params <- log_likelihood_transition_params[[2]]
              

              self$optimizer <- tf$train$AdamOptimizer(learning_rate=0.001)$minimize(self$loss, name='optimizer')
                
              # Define a saver op
              self$init <- tf$global_variables_initializer()
              self$saver <- tf$train$Saver(max_to_keep=0L, name='saver')
            })
          })
          
        })
      }, 
      decoding = function(logit, transition_params, nchar){
        
        tags <- tf$contrib$crf$viterbi_decode(
                        logit[1:nchar,], transition_params)
        return(matrix(unlist(tags[[1]]), nrow=1))
      },
      train = function(seq_mat_x, seq_mat_y, sent_len_x, batch_n, epoch=10L, retrain_from=0){
        tr_idx <- 1:(0.95 * nrow(seq_mat_x))
        
        seq_mat_x_train <- seq_mat_x[tr_idx, ]
        seq_mat_x_test <- seq_mat_x[-tr_idx, ]
        
        seq_mat_y_train <- seq_mat_y[tr_idx, ]
        seq_mat_y_test <- seq_mat_y[-tr_idx, ]
        len_list_train <- sent_len_x[tr_idx]
        len_list_test <- sent_len_x[-tr_idx]

        loss_v <- c()
        loss_vt <- c()
        #self$is_training <- TRUE
        

        x <- self$x
        embeddings <- self$embeddings
        y <- self$y
        sent_len <- self$sent_len
        batch_size <- self$batch_size
        
        with(tf$Session(config=self$config_proto, graph=self$word_spacing_graph) %as% sess, {
          if(retrain_from > 0){
            self$saver$restore(sess, sprintf("model/model_%d.chkp-%d",retrain_from, self$global_step))
            st_epoch <- retrain_from + 1
          }else{
            sess$run(self$init)
            st_epoch <- 1
          }
          for(i in st_epoch:epoch){
            #shufle 
            rnd_idx <- sample(1:nrow(seq_mat_x_train), nrow(seq_mat_x_train))
            
            seq_mat_x_ <- seq_mat_x_train[rnd_idx,]
            seq_mat_y_ <- seq_mat_y_train[rnd_idx,]
            sent_len_x_ <- len_list_train[rnd_idx]
            
            j <- 0
            for(k in seq(1, nrow(seq_mat_x_train), batch_n)){
              if( k + batch_n - 1 > nrow(seq_mat_x_train)){
                bat_size <- nrow(seq_mat_x_train)  + 1 - k
              }else{
                bat_size <- batch_n
              }
              self$c2v
              l <- sess$run(list(self$loss, self$optimizer), feed_dict=
                                dict(
                                    x=matrix(seq_mat_x_[k:(k + bat_size - 1),], byrow=T, nrow=bat_size),
                                    y= matrix(seq_mat_y_[k:(k + bat_size - 1),], byrow=T, nrow=bat_size), 
                                    sent_len= sent_len_x_[k:(k + bat_size - 1)],
                                    batch_size=as.integer(bat_size)
                                ))
              j <- j + 1
              if(j %% 300 == 0){
                #self$is_training <- F
                print(sprintf("%d:%d train loss : %f, seg loss : .., accuracy : ..", i, j, l[[1]]))
                loss_v <- c(loss_v, l[[1]])
                test_sent <- "아버지가방에들어가셨다."
                coding_mat <- self$sent_to_code(test_sent)
                logits_transition_params <- sess$run(list(self$logit,self$transition_params),
                                   feed_dict=dict(
                                                  x=coding_mat[[1]],
                                                  sent_len=list(coding_mat[[2]]),
                                                  batch_size=c(1L)))
                result <- self$decoding(logits_transition_params[[1]][1,,], 
                                        logits_transition_params[[2]], nchar(test_sent))
                
                print(self$code_to_sent(test_sent, result, nchar(test_sent)))
                loss_eval <- sess$run(list(self$loss), feed_dict=
                                dict(
                                     x=seq_mat_x_test,
                                     y= seq_mat_y_test, 
                                     sent_len= len_list_test,
                                     batch_size=as.integer(dim(seq_mat_x_test)[1])
                                ))
                print(sprintf("%d:%d test loss : %f, accuracy : ...", i, j, loss_eval[[1]]))
                loss_vt <- c(loss_vt, loss_eval[[1]])
                
              }
            }
           
            save_path <- self$saver$save(sess=sess, save_path = sprintf("model/model_%d.chkp", i),
                                    global_step =self$global_step)
           
            print(sprintf("Model saved in file: %s",  save_path))
          }
          
        })
        return(list(loss_v, loss_vt))
      },
      predict = function(test_sent, best_epoc, glob_step=1L){
        x <- self$x
        embeddings <- self$embeddings
        sent_len <- self$sent_len
        batch_size <- self$batch_size
        coding_mat <- self$sent_to_code(test_sent)
        with(tf$Session(config=self$config_proto, graph=self$word_spacing_graph) %as% sess, {
          self$saver$restore(sess, sprintf("model/model_%d.chkp-%d",best_epoc, glob_step))
          #self$is_training <- F
          logits_transition_params <- sess$run(list(self$logit,self$transition_params),feed_dict=dict(
                                                            x=coding_mat[[1]],
                                                            sent_len=list(coding_mat[[2]]), 
                                                            batch_size=c(1L)))
        })
        #print(logits_transition_params[[2]])
        result <- self$decoding(logits_transition_params[[1]][1,,], 
                                        logits_transition_params[[2]], nchar(test_sent))
        
        return(self$code_to_sent(test_sent, result, nchar(test_sent)))
      },
      code_to_sent=function(input_sent, coding_mat, coding_len){
        char_sent <- str_split(input_sent, '')[[1]]
        ch <- c()
        for(i in 1:coding_len){
          if(coding_mat[1,i] == 1){
            ch <- c(ch, char_sent[i] ,' ')
          }else{
            ch <- c(ch, char_sent[i])
          }
        }
        return(paste0(ch, collapse = ''))
      },
      sent_to_code=function(sentence){
        seq_mat_test <- matrix(0, ncol=max_seq_len, nrow=1)
  
        sent_t <- str_split(sentence, pattern = '')[[1]]
        for(j in 1:length(sent_t)){
          idx <- self$chmap[[sent_t[j]]]
          if(is.na(idx)) idx <- self$chmap[[specl_char]]
          seq_mat_test[1,j] <- idx
        }
        return(list(seq_mat_x=seq_mat_test, nchar=nchar(sentence)))
      }
))

네트워크 아키텍처는 아래 도식과 같다.

bi-LSTM-CRF

bi-LSTM-CRF

bi-LSTM을 통해서 문장의 context를 학습하고 Fully Connected를 통해 나온 logit값과 state transition 정보를 가지고 log likelihood를 최대화 하는 과정으로 학습이 진행된다. 코드에서 다소 생소한 부분은 실제 최적 시퀀스를 구하는 viterbi 함수(viterbi_decode)가 TensorFlow Tensor를 입력으로 받지 않는다는 것이다. 다소 복잡도가 있는 알고리즘이기 때문에 accuracy와 같은 지표를 모델링이 진행되면서 대량의 셋으로 확인하기는 다소 무리가 있을 것으로 예상된다.

train and Evaluation


#각 셀별 뉴론의 개수는 5개, 음절속성벡터의 수는 50개로한다. 
wsp <- WordSpacing$new(char_dic_size=dim(m)[1], n_neurons=5L, num_out_classes=2L, 
                       num_classes=50L, 
                       max_sequence_length=max_seq_len, c2v=m, global_step = 1L)


tr_loss <- wsp$train(seq_mat_x, seq_mat_y, len_list, batch_n=100L, epoch = 1)
## [1] "1:300 train loss : 1.390187, seg loss : .., accuracy : .."
## [1] "아버지가방에들어가셨 다 . "
## [1] "1:300 test loss : 9.816381, accuracy : ..."
## [1] "1:600 train loss : 0.931453, seg loss : .., accuracy : .."
## [1] "아버지가방에들어가 셨 다 . "
## [1] "1:600 test loss : 8.425335, accuracy : ..."
## [1] "1:900 train loss : 0.982449, seg loss : .., accuracy : .."
## [1] "아버지가방에 들어가 셨 다 . "
## [1] "1:900 test loss : 7.537846, accuracy : ..."
## [1] "1:1200 train loss : 1.071610, seg loss : .., accuracy : .."
## [1] "아버지가방에 들어가 셨 다. "
## [1] "1:1200 test loss : 7.055751, accuracy : ..."
## [1] "1:1500 train loss : 0.964378, seg loss : .., accuracy : .."
## [1] "아버지가방에 들어가셨다. "
## [1] "1:1500 test loss : 6.723727, accuracy : ..."
## [1] "1:1800 train loss : 0.865009, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:1800 test loss : 6.533759, accuracy : ..."
## [1] "1:2100 train loss : 0.937080, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:2100 test loss : 6.376132, accuracy : ..."
## [1] "1:2400 train loss : 0.782063, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:2400 test loss : 6.290747, accuracy : ..."
## [1] "1:2700 train loss : 0.958704, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:2700 test loss : 6.205781, accuracy : ..."
## [1] "1:3000 train loss : 0.918696, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:3000 test loss : 6.123788, accuracy : ..."
## [1] "1:3300 train loss : 0.769656, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:3300 test loss : 6.074191, accuracy : ..."
## [1] "1:3600 train loss : 0.966061, seg loss : .., accuracy : .."
## [1] "아버지가방에 들어가셨다. "
## [1] "1:3600 test loss : 6.045763, accuracy : ..."
## [1] "1:3900 train loss : 0.957004, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:3900 test loss : 5.997510, accuracy : ..."
## [1] "1:4200 train loss : 0.879998, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:4200 test loss : 5.992090, accuracy : ..."
## [1] "1:4500 train loss : 0.816798, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:4500 test loss : 5.972754, accuracy : ..."
## [1] "1:4800 train loss : 0.872573, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:4800 test loss : 5.969009, accuracy : ..."
## [1] "1:5100 train loss : 0.977625, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:5100 test loss : 5.955671, accuracy : ..."
## [1] "1:5400 train loss : 0.999991, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가 셨다. "
## [1] "1:5400 test loss : 5.947856, accuracy : ..."
## [1] "1:5700 train loss : 1.029374, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가셨다. "
## [1] "1:5700 test loss : 5.944112, accuracy : ..."
## [1] "1:6000 train loss : 0.970027, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가 셨다. "
## [1] "1:6000 test loss : 5.926543, accuracy : ..."
## [1] "1:6300 train loss : 0.764954, seg loss : .., accuracy : .."
## [1] "아버지가 방에 들어가 셨다. "
## [1] "1:6300 test loss : 5.959241, accuracy : ..."
## [1] "Model saved in file: model/model_1.chkp-1"
tr_te_loss <- data.table(train_loss= tr_loss[[1]], test_loss=tr_loss[[2]])

tr_te_loss[,idx:=1:nrow(tr_te_loss)]

ggplot(melt(tr_te_loss, id.vars = 'idx'), aes(idx, value)) + 
  geom_line(aes(colour=variable)) + xlab('every 300 batch') + ylab('loss')

1에폭만 실행한 loss 추이를 보여주고 있는데, 좀더 학습을 오래 하면 최적화된 성능을 보여줄 수 있을거라 생각한다.

wsp$predict("크리스마스는친구와함께!",best_epoc = 1)
## [1] "크리스마스는 친구와 함께 !"
wsp$predict("대표직을사퇴한그는새로운사업을시작했다.",best_epoc = 1)
## [1] "대표직을 사퇴한 그는 새로 운사업을 시작했다. "
wsp$predict("일정한조건에따르면-자유롭게-이것을재배포할수가있습니다.",best_epoc = 1)
## [1] "일정한 조건에 따르면 -자유롭게 -이것을 재배포할 수가있습니다. "

 

후기

약 1주일 동안 틈틈히 이런 저런 실험을 하면서 많은 부분을 찾아보고 공부하고 했었던거 같다. 아주 재미 있었고 만족스런 성능이 도출되면 KoNLP에도 탑재를 할 수 있을거로 예상된다. 다만 아쉬운 부분은 양질의 학습셋 확보가 어렵다는 것인데, 이 부분도 조만간 주변의 도움을 통해 가능할 수 있을 것이라 생각된다. HMM으로 하는 띄어쓰기와 RNN으로 하는 띄어쓰기가 서로 아이디어를 공유하고 있다는 것을 이번 기회에 알았으며, 모델링 페러다임이 바뀌었다는 점이 서로 다르게 보이게 하는 요인이 되었다는 것을 인지한게 가장 큰 성과였다. 따라서 성급한 마음을 이 개인 연구를 통해 가라앉힐 수 있게 되는 계기가 되었다.

R기반으로 TensorFlow 코드를 구현하는 방식은 이제 어느정도 익숙해진거 같다. 얼마전까지만 해도 feed_dict 인자에 값을 주는 방식의 미묘한 문제 때문에 고생을 좀 하긴 했지만, 이제는 어느정도 생각대로 코드를 작성할 수 있게 되었다.  사실 데이터 핸들링 코드 빼놓고는 Python이랑 99% 같으나, R을 사용하는 이유는 데이터 핸들링이 어떠한 도구보다 간단하고 명료하기 때문이다.

R을 사용하면서 클래스를 거의 사용할 일이 없었는데 TensorFlow를 사용하면서 많이 사용하게 되었다. 경험상 TensorFlow 코드를 네임스페이스를 사용하지 않는 방식으로 코드를 구성하다보면 Tensor 객체가 꼬이는 문제가 발생해 잘못된 모델링 결과나 오류가 날 수 있어 반드시 사용해야 된다(이 부분은 Python도 마찬가지로 알고 있다). 그리고 가장 적합한 클래싱 방식은 R6라는 것도 이번 코드를 구현하면서 정리할 수 있었다.

 

rOpenSci Text Workshop 참석 후기

 

숙소에서 도보로 17분 거리에 위치한 LSE

 

 

LSE의 룸번호가 LG로 시작하는건….. 무엇때문인지는 모르겠다.

 

전 세계에서 24명이 모여서 R에서 텍스트 분석을 어떻게 하면 효과적으로 수행할지 많이 논의가 이어졌고 대략적인 내용은 이곳에서 해당 워크샵을 주최한 교수가 소개하고 있다. 이런 저런 이야기가 오갔고, 내가 주로 설명한 내용은 한글 오타 교정에 대한 hunspell 패키지 개발자가 물어보는 질문에 답하고 설명하는 것으로 첫날을 시작해 둘째날은 한글 색인어 추출에 대한 기능지원을 stringi에 적용 가능성을 타진하는 것을 계기로 이 기능이 ICU에 들어가면 바로 지원해주겠다는 stringi개발자의 제안을 듣고 이런 이슈까지 만들어 팔로업을 하고 있다.  KoNLP의 한글 데이터들을 적절히 사용해 일본어 ICU 토크나이저를 기반으로 간단하게 만들면 어떨까 하는 생각만을 공유하고 워크샵 참석을 마치게 되었다. 그곳 개발자들은 ICU에 한글 토크나이저가 없다는 사실에 매우 놀라는 눈치였는데, 그도 그럴것이 태국문자에 대한 처리 모듈이 있는 상황에 한글 모듈이 단순히 공백을 기준으로 자르는 기능만 있는것은 다소 어이없다는 눈치였다. 물론 단순히 공백을 기준으로 잘라서는 제대로된 분석이 안된다는 부연설명이 필요했지만 말이다.

이 작업은 생각만큼 단순하지 않다. 왜냐면 ICU를 개선하게 되면 이를 사용하는 전 세계의 수많은 텍스트 처리 관련 라이브러리에 영향을 주게 되기 때문이다. 그만큼 신중하게 시간을 투자해서 개발해야 되는 부분이라는 것이며, 개인적으로 그런 시간이 날지는 확신이 서지 않는다. 공공 과제로 띄울 수 있다면 공익적인 측면에서 큰 기여를 할 수 있을 수 있겠다는 생각 정도만 했고, 물론 내년에 초대받을지는 확실하지 않지만 그때 자랑스러운 성과로도 이야기 할 수 있을 거라는 생각도 든다.

개인적으론 kerasR 개발자를 만나본게 좋았는데, 최근 딥러닝에 관심을 가지고 사용하고 하면서 R에서 keras를 잘 사용할 수 있는 패키지가 있었으면 좋겠다 생각하던 시점에 이를 개발해서 공개한 친구가 워크샵에 참석했기 때문이다. 이 친구는 리치몬드 대학에서 딥러닝 강의를 하는 교수인데, 학생들이 좀더 쉽게 딥러닝을 사용하게 할 수 있게 하려고 kerasR을 만들었다는 이야기를 듣고 상당히 감명을 받았다. kerasR의 몇몇 버그를 알려줬는데, 이미 github에서는 다 고쳐놓았다는 말을 듣고 역시나 하는 생각을 했다.(왼쪽 사진은 그와 함께 찍은 사진…)

 

 

이 이외에 상당히 많은 부분 느끼고 감동받고 배우고 했지만 이곳에 모든것을 열거하기는 어려울것 같다. 다만 대부분이 학교에 몸담고 있는 교수나 박사과정 학생이고, 또한 이들의 배경이 컴퓨터 사이언스인 경우는 30%도 되지 않는 다는 사실에 매우 놀랐다. 그러니까 사회과학 쪽 연구를 하는 분들이고 이들의 프로그래밍 능력과 지식이 상당했다는 것이다. 정말 세상은 넓고 배울 사람들은 정말 많은거 같다.

 

이틀간의 워크샵을 마치고 가족과 함께 아주 맛있는 저녁 만찬을 먹고 다음날 아침 서울행 비행기에 올랐다. 이번 워크샵을 통해 한글의 전산처리 이슈에 대해서 많은 개발자들과 논의할 수 있었다는 것에 큰 보람과 할일에 대한 부담을 느끼고 돌아왔다. 다만 내가 혼자만의 시간을 통해 기여를 얼마나 할 수 있을지는 미지수지만 말이다.

 

 

이번 영국 가족 여행중 만족스러웠던 장소중에 하나인 bourton on the water