모델링 그리고 Boosting

응용 예측 모델링과 통계 학습 모델링

위 두 단어에 대해서 차이를 안다는건 두 모델링의 목적이 다르다는 것을 안다는 것을 의미하며, 그러한 관점으로 목적에 맞게 두 도구를 사용할 수 있다는 것을 의미할 것이다.

최근 간만에 Machine Learning Modeling을 하면서 다시금 모델의 이상적인 모습과 그 유용성에 대해서 고민을 많이 했다. 일단 우리가 모델을 만든다는 건 아래와 같은 목적이 많은 부분을 차지한다.

  1. 모델링은 우리가 특정 시스템이 어떻게 동작할 것이라는 것에 대한 이해의 과정
  2. 데이터의 요약 방식 자체가 모델링
  3. 예측값과 실측값의 비교를 통한 시스템 이해
  4. 의사 결정을 위한 예측

대부분 4번에 집중을 하고, 이는 곧 우리가 우리 자신의 환경이나 자신을 이해하는 용도(1,2,3)보다는 한정적인 용도이며, 특히나 자동화에 많은 부분이 포커싱 되고 있다.

특히 우리 사회에 대한 이해를 하기 위한 학문인 사회학, 교육학, 철학, 경제, 경영 등과 같은 학문 영역은 1,2,3번 을 위해서 모델링을 많이 하는 편이며, 컴퓨터 공학과 같은 자동화나 인공지능에 집중된 학문 영역은 4번에 포커싱을 하는 경향을 보인다. 따라서 특정인이 어느 학문 출신이라는 것을 아는것만으로도 모델링에 대해서 어떠한 관점을 가지고 있는지 잘 알 수 있다.

성능상에만 목적을 두고 4번에 집중하고자 마음만 먹으면, 예측 성능을 높이는 수많은 기계학습 도구를 사용할 수 있는 여지가 생기나, 블랙박스 모델일 가능성이 많아져 모형 해석은 매우 어렵게 된다. 모형해석이 중요한 시점은 바로 모형 이면의 동작방식이 정확하게 어떻게 돌아가는지 알고 싶을때 중요하게 되는데, 수많은 데이터가 교호작용을 일으켜 모형이 만들어지고, 그러한 작은 모형이 수백개 수천개가 모여 weighted voting을 통해 의사결정을 이룬다 하면 과연 특정 예측이 어떻게 되는지 한마디로 설명이 어렵게 된다. 이 부분은 왜 알아야 되냐고 반문할 수 있으나, 문제는 그렇게 간단하지 않다. 예를 들어 이통사 해지를 할 고객을 아주 정확하게 예측했다 하자면, 과연 이 고객을 잡아두기 위해서 비용대비 가장 효과적인 마케팅 요소를 활용해 잡아두고 싶어할 것이다. 바로 이런 “왜?” 라는 질문을 던지기 시작하면 블랙박스 모형에서는 답변을 하기가 매우 어려워진다. 물론 이런 경우를 위해 예측을 위한 모형 별도… 설명을 위한 모형을 별도 만들기도 하지만, 정확한 설명은 사실상 매우 어렵다.

필자의 경우 Machine Learning 옹호론자로 이쪽에 발을 들여 놓았다가, 2년 전 쯤부터 최근까지 Linear Model이나 Bayesian과 같은 설명 가능한 통계 모형에 심취해 왔었고, 최근 다시금 Machine Learning기반 방법론을 활용해야 되는 프로젝트를 수행하면서 다시금 두 성격이 다른 모델링 작업에 대해서 고민하는 시간을 가지게 되었다. 개인적으로 Deep Learning 방법론은 인공지능 혹은 자동화 관점에서 문제를 접근할 때 이외에는 활용폭이 적다는 생각을 가지고 있어서 실무적으로 활용할 기회가 별로 없는 관계로 일단 논외로 두고, 실무적으로 가장 많은 활용을 하고 있는 모형인 Bagging과 Boosting방법론을 간단하게 설명하고자 한다. 필자가 참고로한 논문은 In The boosting: A new idea of building models이다.

Bagging은 학습셋에서 랜덤하게 서브셋을 추출해(boostrap resampling) 이를 기반으로 모형을 만드는데 이런 과정을 N번만큼 반복해 복수개의 모형을 만들어 이들의 voting으로 예측을 하는 모형을 의미한다. 대표적으로 최근 묻지마 모델로 불리는 randomForest가 이 계열인데, SVM과 더불어 기본 모형으로 가장 좋은 분류 성능을 보인다고 알려져 있기도 하다. 모형의 에러중에서 학습셋의 변동으로 생기는 모형의 에러를 모형의 variance 에러 라고 하는데 이를 줄여주는데 bagging이 효과적인 방법이라고 알려져 있다. 따라서 분류 클래스의 밸런스가 맞지 않거나 하는 문제일때 매우 안정적인 분류 결과를 보여주는 장점을 가지고 있다. 한마디로 막 돌려도 기본 이상을 하는 모델링 방식이라는 것이다.

Bagging이 variance를 줄이는 효과를 가지고 있는 반면 Boosting의 경우 모형 자체가 가지는 가정과 실제 데이터와의 모순으로 생기는 bias까지도 줄이는 효과를 보여준다. 참고로 bias와 varince의 차이를 알고 싶다면 필자의 포스팅(http://freesearch.pe.kr/archives/1412)을 참고하길 바란다. varince를 줄이는 방식은 bagging과 유사하게 다수의 모형의 voting(실제는 weighted된)을 통해서 줄이지만 bias는 분류가 어려운 문제를 풀기 위해 지속적으로 weak learner를 만들어가기 때문이다. 이 때문에 boosting이 outlier나 anomaly detection 문제를 잘 푸는 이유가 된다. 게다가 몇몇 연구는 SVM의 margin을 최대화 하는 기법이 boosting 과정에서도 수행된다고 하는 연구 결과도 있다.

개인적인 경험으로도 역시 Boosting이 Bagging보다 성능이 좋았다. 물론 이는 엄청난 학습 시간과 파라메터 서치에 소요되는 시간과 노력을 제외하고 이야기하는 것이라서 Boosting 알고리즘에 대해서 상세하게 튜닝하지 못한다면 이런 성능 향상을 경험하기 다소 어려운 점이 있으나 최근 gradient boosting계열의 모형 기반으로 심심치 않게 kaggle 대회에서 우승하는걸 보자면 역시나 도구도 알고 써야 되는 것이란 생각을 해본다.

Boosting

서론이 매우 길었는데, Boosting에 대해서 좀더 알아보도록 하겠다.

예제는 논문의 Fig 7에서 가져왔다.

suppressMessages({
library(randomForest)
library(data.table)
library(gbm)
library(ggplot2)
library(plyr)
library(dplyr)
library(rpart)
})

x <- seq(-2,2,by=0.01)
lenx<- length(x)
y <- 2 + 3*x^2 + rnorm(lenx, 0, 0.5)
y_r <- 2 + 3*x^2
x.y <- data.frame(x=x,y=y, y_r=y_r)

x.y.samp <- x.y %>% sample_frac(0.5)
x.y.samp.test <- x.y %>% sample_frac(0.1)

ggplot(x.y.samp, aes(x,y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=2)

위와 같은 피팅할 대상이 있다고 가정하자면 cart는 아래와 같이 피팅을 하게 된다.

mdl_cart <- rpart(y ~ x,data=x.y.samp)
x.y.samp.test$cart_fit <- predict(mdl_cart,newdata=x.y.samp.test)

ggplot(x.y.samp, aes(x,y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data =  x.y.samp.test, aes(x=x, y=cart_fit))

randomForest는 좀더 모수에 가까운 피팅을 하는 것을 볼 수 있다.

mdl_rf <- randomForest(y ~ x,data=x.y.samp)
x.y.samp.test$rf_fit <- predict(mdl_rf,newdata=x.y.samp.test)

ggplot(x.y.samp, aes(x,y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data =  x.y.samp.test, aes(y=rf_fit))

위 모형의 피팅을 Boosting을 기반으로 수행해 보자!

Boosting 기법은 아래와 같은 방식으로 수행된다.

  1. \(\hat{y_1} = f_1(x)\) 와 같이 모형을 피팅한다.
  2. \(y_{res} = y – v_{1}\hat{y_1}\) 와 같이 잔차를 계산한다.
    • 여기서 \(v\)는 0 < \(v\) < 1 사이의 값을 가지는 shrinkage 파라메터로 모형이 오버피팅을 하는걸 방지하는 효과를 보여주는데, 이는 특정 모형 하나가 너무 많은 부분을 설명하는걸 방지하면서 가능해진다. 따라서 일종의 weak learner가 shrinkage 파라메터로 만들어지며 randomForest처럼 모든 데이터를 사용해 모형을 만들지 않고 부트스트랩 샘플링으로 weak learner를 만들게 함으로써 randomForest 처럼 일반화를 잘 하게 하는 모형을 만들어준는 효과를 다시한번 발휘하게 된다.
  3. 위의 과정을 t=2…T번 반복하는데, 이를 일반화 하면 아래와 같이 표현할 수 있다.
    • \(\hat{y_t} = f_t(x)\)
    • \(y_{res,t} = y_{res,t-1} – v_{t}\hat{y_t}\)
    • \(v_{t}\hat{y_t}\) 부분이 최종 regression 모형으로 결과를 예측하는데 쓰인다.
  4. 결과 모형은 아래와 같다.
    • \(y_{pre}=v_1\hat{y_1} + v_2\hat{y_2} + v_3\hat{y_3}+ … + v_{t-1}\hat{y_{t-1}} + v_{T}\hat{y_{T}}=\sum_{t=1}^T v_tf_t(X)\)

위와 같은 동작 방식을 정확하게 이해하기 위해서는 구현을 간단하게나마 해보는게 도움이 된다. 물론 regression이나 regression tree가 아닌 neural network등 다양한 모형을 weak learner로 사용 가능하다.

shrink <- 0.1

#regression based boosting
y_n <- x.y.samp$y
x <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  lm_fit <- lm(y_n ~ x*I(0 < x))
  v_y <- shrink * predict(lm_fit)
  v_y_l[[i]] <- shrink * predict(lm_fit, newdata=x.y.samp.test)
  resid_n <-  y_n - v_y
  y_n <- resid_n
}

x.y.samp.test$lm_fit <- apply(as.data.table(v_y_l),1,sum)

x.y.samp.test$lm_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)

x.y.samp.test$lm_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)

x.y.samp.test$lm_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)

ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_2),colour='purple',linetype=4) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_3),colour='purple',linetype=4) + geom_line(data=x.y.samp.test,aes(x=x,y=lm_fit_1),colour='purple',linetype=4) 

#cart based boosting
y_n <- x.y.samp$y
x <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  rpart_fit <- rpart(y_n ~ x)
  v_y <- shrink * predict(rpart_fit)
  v_y_l[[i]] <- shrink * predict(rpart_fit, newdata=x.y.samp.test)
  resid_n <-  y_n - v_y
  y_n <- resid_n
}



x.y.samp.test$rpart_fit <- apply(as.data.table(v_y_l),1,sum)

x.y.samp.test$rpart_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)

x.y.samp.test$rpart_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)

x.y.samp.test$rpart_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)


ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_2),colour='purple',linetype=4) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_3),colour='purple',linetype=4) + geom_line(data=x.y.samp.test,aes(x=x,y=rpart_fit_1),colour='purple',linetype=4) 

위 코드에서는 weak learner를 학습할때 전체 학습셋에서 부트스트랩 샘플링을 하지 않았다. 따라서 이 부분을 변경해서 결과를 확인해 보자.

shrink <- 0.1

#regression based boosting
x.y.samp$y_n <- x.y.samp$y
x <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  x.y.samp.sub <- x.y.samp %>% sample_frac(0.2,replace=T)
  lm_fit <- lm(y_n ~ x*I(0 < x),data=x.y.samp.sub)
  v_y <- shrink * predict(lm_fit,newdata=x.y.samp)
  v_y_l[[i]] <- shrink * predict(lm_fit, newdata=x.y.samp.test)
  resid_n <-  x.y.samp$y_n - v_y
  x.y.samp$y_n <- resid_n
}


x.y.samp.test$lm_fit <- apply(as.data.table(v_y_l),1,sum)

x.y.samp.test$lm_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)

x.y.samp.test$lm_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)

x.y.samp.test$lm_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)


ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_2),colour='purple',linetype=4) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_3),colour='purple',linetype=4) + geom_line(data=x.y.samp.test,aes(x=x,y=lm_fit_1),colour='purple',linetype=4) 

#cart based boosting
x.y.samp$y_n <- x.y.samp$y
x <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  x.y.samp.sub <- x.y.samp %>% sample_frac(0.2,replace=T)
  rpart_fit <- rpart(y_n ~ x,data=x.y.samp.sub)
  v_y <- shrink * predict(rpart_fit,newdata=x.y.samp)
  v_y_l[[i]] <- shrink * predict(rpart_fit, newdata=x.y.samp.test)
  resid_n <-  x.y.samp$y_n - v_y
  x.y.samp$y_n <- resid_n
}



x.y.samp.test$rpart_fit <- apply(as.data.table(v_y_l),1,sum)

x.y.samp.test$rpart_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)

x.y.samp.test$rpart_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)

x.y.samp.test$rpart_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)


ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_2),colour='purple',linetype=4)  + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_3, linetype=4), colour='purple', linetype=4)  +  geom_line(data=x.y.samp.test,aes(x=x,y=rpart_fit_1),colour='purple',linetype=4)

개별 모형이 샘플링 기반으로 동작하기 때문에 모형이 학습셋 전체를 사용할때보다 다소 보수적으로 피팅되는 것을 볼 수 있으나 결국 반복을 많이 하게 되면 모수에 근사하는 것을 볼 수 있다. 여기에 올리지는 않았지만 shrinkage를 작게 줘서 피팅을 하게 되면 모수에 근접하는 현상이 다소 느리게 진행되는 것을 확인할 수 있는데, 필자의 경험상 대부분의 안정적이고 성능이 좋은 모형은 shrinkage를 적게 줄 경우에 만들 수 있었다.

위에서 얼마나 반복해야 되는지(Boosting Iteration)와 shrinkage(learning rate)등을 사용자가 정해줘야 되는 부담이 있는데, 이런 적정 파라메터 값은 10-fold-cv와 같은 방법으로 적정 수준을 꼭 찾아야 된다. 찾지 않고 마구잡이로 빌드한 모형과 잘 찾아진 모형의 성능차이는 정말 생각보다 크며, 그 값을 찾지 못한다면 일반적으로 randomForest보다 더 좋은 성능을보이지 못한다.

간단하게 Boosting에 대한 내용을 정리해 봤다. 경험상 randomForest는 테스트 모형 빌드에 쓰고, gbm과 같은 Boosting 모형은 테스트 이후 릴리즈 모형에 사용하는 방식이 맞다고 생각한다. 이는 gbm 최적 파라메터 서치에 많은 시간이 소요되며 데이터가 많아질 경우 간극은 더 커지기 때문이다. 하지만 그 성능에서 이런 노력의 결실을 맺어 주는 걸 확인할 수 있을 것이다.

CC BY-NC 4.0 모델링 그리고 Boosting by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.