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

 

 

 

rOpenSci Text Workshop 참석차 런던에…

rOpenSci Text Workshop에 참석하기 위해 가족과 영국에 왔다.

 

 

이 워크샵은 올 연초에 초대를 받아 Europian Rearch Conceil의 펀딩으로 숙박과 항공료를 제공받아 오게 되었다. 무엇보다 변방의 언어인 한글에 대해서 관심을 가지고 초청해준 관계자 분들에게 감사의 마음을 전하고 싶다.

이 워크샵의 목적은 R을 기반으로 하는 텍스트 분석 패키지들의 개발자들이 모여 서로의 경험을 공유하고 추후 협력의 방향을 잡는것이다.  얼굴을 보고 서로의 스페셜리티를 확인하고 앞으로 잘 해보자하고 결의하는 그러한 자리이다. 물론 그 자리에서 여러가지 테스트와 더불어 코드 작성도 할 것으로 보인다.

이 모임에서 CJK(Chinese, Japanese, Korean) 논의그룹에 참여하고 있고, 주로 지금까지 하던대로 R에서의 한글 텍스트 분석 이슈에 대해서 이야기하고 이들에 대한 해결방안 계획까지 이야기할 것이다.

이번 기회로 다른 패키지 개발자들과 협력포인트를 만들어 좀더 생산적인 패키지 개발과 한글 지원이 가능해졌으면 하는 소망이 있다.