2014. 4. 7. 09:03

dlib를 가지고 SVM(support vector machine) 구현하여 기계 학습(machine learning) 하기 (데이터마이닝, data mining)

우리 삶은 일정한 패턴을 가지고 있습니다. 예를 하나 들어 볼까요?

온도

바람

밖에 나가 놀까?

 5

 

O

 아니요

 10

 

 

 예

 8

 

 

 아니요

 6

O

 

 아니요

 24

 

O

 예

 30

 

 

 예

 33

 

 

 아니요

 32

 

 

 아니요

 30

O

 

 아니요

 15

O

 

 아니요

 16

 

O

 예

[Table 1]

 

저만의 행동 패턴이였습니다(가상입니다!). 한눈으로 봤을 땐, 비가 오면 놀지 않았으며, 너무 추워도, 너무 더워도 놀지 않았습니다. 기껏 11개 밖에 되지 않으니, 한눈으로 확인이 가능한데, 대략 그 개수가 수천, 수만인 경우에는 눈으로 분석하는건 불가능합니다.

 

기계 학습(machine learning)은 위와 같은 데이터를 기계가 통계적으로 분석하고 적응하는 과정이라 생각됩니다. 이러한 기계 학습은 computer science의 AI(artificial intelligence)나 통계학에서 주로 연구가 이뤄지고 있습니다. 물론, 데이터 마이닝(data mining)이라고 알려지기도 합니다.

 

최근 고객 개인 정보 유출이 화두가 되고 있는데, 중국에서 피싱하는 것은 1차적인 분석일듯 합니다. 예를 들어, 10만명의 개인 정보를 잘~ 분석하여 각 10만명 개개인의 "올여름 영국으로 여행갈 확율'을 계산하여, 영국 전문 여행업체에 해당 명단과 전화번호를 넘기는건 정말 smart한 경우일것 같습니다. 물론, 그런일이 있을지는 모르겠지만서도요.

 

최근의 big data, 빅 데이터, ... 가 화두가 되고 있는데, 모두 이러한 기술들이 발전되어 이뤄진 내용입니다. 10만명의 정보를 1차원적으로 분석하는 것이 아니라, 그 속뜻을 분석하는 것이죠. 이것을 사람이 하기는 불가능하기 때문에, 기계가 도움을 주는 것이며, 이는 무척 놀라운 일입니다. 과거 "기저귀와 맥주(네이버로 검색해 보세요)" 사례 이상으로 이제는 데이터 마이닝이 발전했으며, 그 응용도 많아졌습니다. 대표적으로 구글의 Page rank같은 알고리즘(어찌보면, 데이터 마이닝 보다 정보 검색에 가깝지만), adsense, 그리고 번역, 그 이외 우리가 알지 못하는 수많은 서비스가 해당될 겁니다. 애플의 쉬리도 마찮가지 입니다.

 

일반적으로 데이터 마이닝에는 많은 알고리즘들이 있는데, 이는 다음에 설명할 기회가 올 것입니다.

우선, 본 포스트에서는 SVM(support vector machine)을 이용하고자 합니다.

관련해서, dlib(http://dlib.net)를 이용하였습니다.

 

우선, 간략하게 data mining의 과정을 살펴보면,

  1. Data 추출 단계
    ; 자연계에 있는 정보를 Data로 생성합니다. 사람이 직접 입력 할 수 있기 때문에, noisy data, 즉, 맞지 않는 쓰래기 값이 들어갈 수 있습니다.
  2. 전략 단계
    ; 어떻게 정보를 분석하여 mining 할지를 결정합니다. 주어진 데이터로, 새로운 질문에 답을 원한다면, classifier(분류기)에 해당됩니다. 예를들면, "현재의 e-mail 내용이 spam인지? 아닌지?"를 알 고자 한다면 classifier를 만듭니다. 또한, 과거의 날씨 정보를 가지고, 내일의 온도를 예측하고자 한다면, numeric prediction이 됩니다. 혹은, 10만명의 사람들중 유사한 type의 10가지 분류로 나누고자 한다면 clustering을 알아보도록 합니다. 앞선 기저귀와 맥주 사레 같은 규칙을 알고자 한다면, association rule을 선택합니다. 이와 같이, 정확하게 우리가 원하는 전략이 수립되어야만 진행할 수 있습니다.
  3. Feature Selection 단계
    ; Data mining 혹은 machine learning에서 feature는 db의 attribute 혹은 column 명이 될 수 있습니다. data를 분석하는데 모든 feature가 쓰이지 않을 수 있습니다. 어쩌면, 몇몇 feature를 제거하는 것이 결과에 도움을 줄 때가 많습니다.
    혹은, 개별 feature 값을 정규화(normalize)할 수 있습니다. "키"를 본다면, 보통 160 ~ 180일 것인데, 이를 다른 scale로 만드는 것입니다. 즉, 평균키를 0으로 하고, 평균 이하이면 -, 평균 이상이면 +로 변환합니다. 또한, 보통 숫자값이 사용되는데, 만일, "고향"으로 본다면, 서울, 부산, 광주와 같이 숫자가 아닌 기호인 경우도 많습니다. 이를 nominal 이라고 말합니다. 이를 숫자로 표현하는 것이 필요한데, 서울=0, 부산=1, 광주=2, ... 로 변환하게 됩니다. 혹은, "고향은서울", "고향은부산' 처럼 0/1로 표현할 수 있도록 feature를 추가하기도 합니다.
  4. 학습 단계
    ; 이렇게 주어진 Data를 가지고, 학습을 하게 됩니다. 학습은 보통 10-fold cross validation이 사용되며, 학습의 결과로 정확도(accuracy)와 함수(혹은 규칙(rule))이 생성됩니다.
  5. 응용 단계
    ; 이제, 새로운 문제를 4단계의 함수에 적용하여, 그 결과를 확인하는 과정입니다.
  6. 분석
    ; 몇몇 학습의 결과인 모델(model)은 분석 가능합니다. (기저기를 사면 맥주를 산다)와 같은 규칙이 산출되기도 합니다. 이를 의미적으로 분석할 뿐만 아니라, 직접 실행에 옮기기도 하죠.

와 같을 겁니다.

 

"밖에 나가 놀까?" 문제로 다시 돌아옵시다. 만일 아래와 같은 질문에 답할수 있을 까요?

온도

바람

밖에 나가 놀까?

 10

 

 

 ?

 11

 O

 ?

 15

 

 

 ?

 33

 

 

 ?

 32

 

 

 ?

 31

 

 

 ?

 10

O

 

 ?

[Table 2]

 

그럼 이제 dlib의 svm을 이용하여, [Table 1]을 학습한뒤, [Table 2]의 문제를 풀어 볼까요?

dlib_svm_test.zip

 

우선, 위 압축 파일을 풀면, dlib-18.6와 svm_test 경로가 있는데, svm_test의 svm_test.cpp가 main이 됩니다. 그리고, Release 경로의 svm_test.exe를 실행하면 다음과 같습니다.

temperature: 10, rain=NO, win=NO, output=YES(1.668021), probability=0.356986
temperature: 11, rain=YES, win=YES, output=NO(-9.133972), probability=0.188421
temperature: 15, rain=NO, win=NO, output=YES(6.430962), probability=0.449163
temperature: 33, rain=NO, win=NO, output=NO(-2.312452), probability=0.287054
temperature: 32, rain=NO, win=NO, output=NO(-0.999996), probability=0.309212
temperature: 31, rain=NO, win=NO, output=YES(0.257250), probability=0.331295
temperature: 10, rain=YES, win=NO, output=NO(-6.445307), probability=0.223862

10-fold cross validation accuracy : 1.000000
Press a key to exit.
 

 

위 정보를 아래와 같이 표현할 수 있습니다.

온도

바람

밖에 나가 놀까?

 10

 

 

 예

 11

 O

 아니요

 15

 

 

 예

 33

 

 

 아니요

 32

 

 

 아니요

 31

 

 

 예

 10

O

 

 아니요

[Table 3]

 

어떻습니까? 상식적으로든 맞는 답을 한 것 같네요. 비오면 안놀고, 더우면 안놀고...

 

이제 보다 개발적인 내용으로 들어가 봅니다.

 

우선, dlib(http://dlib.net)에 들어가 보시기 바라며,

http://dlib.net/ml.html 에 중간 부분을 클릭하면, http://dlib.net/ml_guide.svg가 나오는데, 여기에 문제의 특성에 따라 알고리즘을 추천해 줍니다. 그러면, dlib의 home에서 Example을 통해 샘플 코드를 볼 수 있습니다. 혹은, dlib 소스 코드를 다운로드하여 test 경로를 보면 많은 샘플 코드가 있으니 참조할 수 있습니다. 혹은, doxygen과 같은 도움말, http://dlib.net/ml.html#rvm_trainer 에서 C++ Example Program을 통해 확인 가능합니다. 매우 훌룡한 사이트이며, license는 boost 입니다 !

 

dlib를 사용하는건 매우 간단한데, 당신의 프로젝트에 단순히 dlib의 /all/source.cpp를 단순히 Add하면 문제없이 compile이 됩니다. 최근의 웬만한 open source들 중에서 가장 편했습니다.

 

여하튼, 자세한건 dlib 사이트를 참고하기 바랍니다. 단, machine learning등에 기본적인 개념이 잡혀 있어야 됩니다. SVM에 대해서는 제 코드(svm_test.cpp)에 충분한 주석을 달아놨으니, 이해하는데 조금 도움이 되리라 봅니다.

 

#include "../dlib-18.6/dlib/svm.h"
#include <iostream>
#include <conio.h>

using namespace dlib;
using namespace std; 

우선, 프로그램의 include는 단순하게 구성됩니다. conio는 마지막 getch 때문에 들어갔으니, 필요하지는 않습니다. dlib는 C++ 코드로, 모두 dlib namespace에 포함되어 있습니다. dlib:: 없이 바로 사용하도록 합니다.

 

/* field */
#define TEMP 0
#define RAIN 1
#define WIND 2

/* value */
#define YES  (1)
#define NO  (-1) 

TEMP, RAIN, WIND는 온도, 비, 그리고 바람을 의미합니다. 마지막을 위한 Yes/No는 +1, -1로 구분합니다. SVM classifier(classifier는 앞선 설명 참고)를 위해서, class는 +1과 -1로 분리되어야 합니다.

 

void main()
{ 

이제 main 함수로 들어가 봅니다.

 

 // Attribute가 3개이다. (온도, 비, 바람)
 // matrix로 표현한다.

 typedef matrix<double, 3, 1> CTrainingNode;

 // non-linear SVM
 // 일반적으로 비-선형 모델이 사용된다.

 typedef radial_basis_kernel<CTrainingNode> ST_NONLINEAR_KERNEL;

 // Training Set에 Node를 하나하나 추가하기 위해 필요하다.
 CTrainingNode cTrainingNode;

 // Training Node의 Vector 형태로 이뤄진 Training Set이다.
 std::vector<CTrainingNode> cArrTrainingSet;

 // training set에서 Play의 Yes/No 여부를 가지는 Label
 std::vector<double> cArrLabelPlay; 

이제는 dlib와 SVM의 특성화된 코드가 시작됩니다. 3개의 attribute, 즉, feature가 선언되었기 때문에 <double, 3, 1>로 이뤄진 matrix를 생성합니다. 비-선형 모델이 일반적이므로 선택하게 됩니다. 자세한건 SVM에 대한 wiki를 참고하시기 바랍니다(사실, SVM에 대한 몇몇 논문들을 먼저 공부해야 합니다). 참고로, 테스트 샘플은 feature selection을 수행하지 않았는데, dlib의 feature selection 부분을 참고하시기 바랍니다.

그리고, 학습 데이터의 instance와 해당 학습 데이터가 mapping되는 label, 즉, 결과값을 지정합니다.

 

 // 각각의 Node를 추가한다.
 
cTrainingNode(TEMP) = 5;
 cTrainingNode(RAIN) = NO;
 cTrainingNode(WIND) = YES;
 cArrTrainingSet.push_back(cTrainingNode);
 cArrLabelPlay.push_back(NO); 

위 코드는 Table 2의 첫번째 데이터를 전달하는 과정입니다.

온도

 비

바람

밖에 나가 놀까?

 5

 

O

 아니요

여기에 대해서는 굳이 설명할 필요는 없으며, 학습 데이터(training data)가 10만개라면 위 과정을 10만번 수행해야 합니다.

 

 // Training set을 normalize하기 위한 class instance를 생성한다.
 vector_normalizer<CTrainingNode> cNormalizer;

 // Training set을 normalize 한다. 굳이 하지 않아도 된다.
 // 필요하지 않다면, cNormalizer에 관련된 모든 line을 삭제하자.
 cNormalizer.train(cArrTrainingSet);

 // 위에서 계산된 정보를 기반으로, Training set의 모든 element들을 normalize한다.
 // 예를 들어,
 // 첫번째 node인,
 // cTrainingNode(TEMP) = 5;
 // cTrainingNode(RAIN) = -1; // NO
 // cTrainingNode(WIND) = 1;  // YES
 // 는,
 // cTrainingNode(TEMP) = -1.2654276706088274;
 // cTrainingNode(RAIN) = -0.58387420812114232;
 // cTrainingNode(WIND) = 1.5569978883230462;
 // 로 변환된다.
 for (i=0; i<cArrTrainingSet.size(); i++)
 {
  cArrTrainingSet[i] = cNormalizer(cArrTrainingSet[i]);
 }

 // Trainer를 생성한다.
 svm_nu_trainer<ST_NONLINEAR_KERNEL> cTrainer;

 // Trainer의 kernel 값을 전달한다.
 // cTrainer.set_kernel(ST_NONLINEAR_KERNEL(0.15625));
 // cTrainer.set_nu(0.15625);

 // 학습 결과 함수(결정 함수, decision function)를 정의한다.
 // 0보다 작은 값은 -1,
 // 0보다 같거나 큰 값은 +1
 // 로 분류된다.
 typedef decision_function<ST_NONLINEAR_KERNEL> ST_TYPE_FUNCTION;

 // 학습시 normalize 정보를 이용하도록 한다.
 typedef normalized_function<ST_TYPE_FUNCTION> ST_TYPE_NORMALIZE_FUNCTION;

 // Normalize를 기본으로한 결과 함수 instance를 생성한다.
 ST_TYPE_NORMALIZE_FUNCTION stFunction;

 // Normalize 정보를 전달한다.
 stFunction.normalizer = cNormalizer;

 // 학습을 시작한다 !!!
 stFunction.function = cTrainer.train(cArrTrainingSet, cArrLabelPlay); 

주석에서 정리된 것 처럼, 해당 테스트에는 normalize를 수행하였습니다. 굳이 필요한 부분은 아니지만, 정밀도를 올리는데 도움이 될 수 있습니다. normalize를 수행하면, 위 예와 같이 해당 값을 명시적으로 변환하도록 합니다. 학습에 주어진 input은 학습 데이터와 Label 정보이며, output은 학습 결과인 함수가 전달됩니다.

중간의 0.15625같은 값은 SVM 알고리즘의 parameter로 각 문제에 맞는 값을 "자동"이 아닌 "수동"으로 입력해야합니다. 몇몇 값을 입력하고 시도하여 정밀도를 높일 수 있습니다. 해당 값은 몇몇 논문에서 설명되어 있습니다.

 

 // 이제, 양수, 음수로 구분된 값이 아닌,
 // 0~1 사이의 확률을 전달하는 결과 함수를 만들어 보고 학습해 보자.
 typedef probabilistic_decision_function<ST_NONLINEAR_KERNEL> ST_TYPE_FUNCTION_PROB;

 // 역시, 이것도 학습시 normalize 정보를 이용한다.
 typedef normalized_function<ST_TYPE_FUNCTION_PROB> ST_TYPE_NORMALIZE_FUNCTION_PROB;

 // Normalize를 기본으로 한 결과 함수 instance를 생성한다. 0~1 사이의 확률값을 전달한다.
 ST_TYPE_NORMALIZE_FUNCTION_PROB stFunctionProb;

 // Normalize 정보를 전달한다.
 stFunctionProb.normalizer = cNormalizer;

 // 학습을 시작한다 !!!
 stFunctionProb.function = train_probabilistic_decision_function(cTrainer, cArrTrainingSet, cArrLabelPlay, 3); 

앞선 학습은, 그 함수를 실행하면 실제 음수, 혹은 양수가 전달되는데, 음수이면 "아니요", 양수이면 "예"에 해당됩니다. 보다 정밀한 결과를 위해, "예"인 확율을 전달 받을 수 있습니다. 즉, 0~1 사이의 값을 전달 받을 때, 보다 쉽게 이해되는 경우가 있습니다. 즉, 단순히 "예",'아니오"아 아니라, "밖에 나가 놀" 확율을 전달 받는 것입니다. 즉, 같은 "예"인 경우라도 0.8과 0.6은 확율적으로 다를 수 있습니다.

 

 CTrainingNode question;

 question(TEMP) = 10;
 question(RAIN) = NO;
 question(WIND) = NO;
 printf("temperature: %d, rain=%s, win=%s, output=%s(%f), probability=%f\r\n",
       (int)question(TEMP),
       Say(question(RAIN)),
       Say(question(WIND)),
       Say(stFunction(question)),
       stFunction(question),     // 학습 결과값 전달. 0 포함 이상 : +, 0 미만 : -
       stFunctionProb(question));// 학습 결과값 전달. 0 ~ 1 사이, +의 확율 

이제 질문에 대한 답을 구할 차례입니다. 앞선 학습을 통해 총 두개의 함수를 전달 받았습니다.

위 질문은 Table 2의 첫번째 질문입니다.

온도

바람

밖에 나가 놀까?

 10

 

 

 ?

즉, question(TEMP)를 10으로 전달하고, RAIN과 WIND는 NO를 전달합니다.

이 question을 stFunction과 stFunctionProb에 전달하면, 그 값을 전달 받을 수 있습니다.

 

printf("10-fold cross validation accuracy : %f\r\n", cross_validate_trainer(reduced2(cTrainer,10), cArrTrainingSet, cArrLabelPlay, 3));

학습을 하게되면, 함수와 동시에 정확도를 계산할 필요가 있습니다. 이를 위해 10-fold cross validation을 수행할 수 있는데, 이는 위와 같이 계산됩니다. 본 예제에서는 1.0이 나옵니다. 즉, 100% 부합되는 것으로, 오류가 발생하지 않는 상황입니다. 너무 높은 정확도는 자칫 over-fit이라 하여, 학습 데이터에만 부합되는 않좋은 상황인 경우도 많습니다. 즉, 서울 사람들만의 data를 분석하여 99%의 정확도가 나왔을 때, 뉴욕 사람의 정보로 질문하면 과연 정확할 까?라고 반문하는 경우도 있습니다. 일단, 위 예에서는 학습 데이터가 작기 때문에 이런 내용은 무시하도록 하겠습니다.

 

다시, 결과를 보여드리면 아래와 같습니다.

temperature: 10, rain=NO, win=NO, output=YES(1.668021), probability=0.356986
temperature: 11, rain=YES, win=YES, output=NO(-9.133972), probability=0.188421
temperature: 15, rain=NO, win=NO, output=YES(6.430962), probability=0.449163
temperature: 33, rain=NO, win=NO, output=NO(-2.312452), probability=0.287054
temperature: 32, rain=NO, win=NO, output=NO(-0.999996), probability=0.309212
temperature: 31, rain=NO, win=NO, output=YES(0.257250), probability=0.331295
temperature: 10, rain=YES, win=NO, output=NO(-6.445307), probability=0.223862

10-fold cross validation accuracy : 1.000000
Press a key to exit.
 

 

그런데, 이상한 것이, 맨 처음 온도가 10도인 경우, YES라 하여 "밖에 나가서 논다"는 결론을 맺었는데, 그 확율은 0.35로 매우 낮게 측정되었습니다. 즉, 50%도 안되는 확율로 YES라 하였는데, 이는 일단, "학습 데이터"의 부족으로 판단되며(저도 아직 SVM에 대한 완벽한 이해를 못했습니다), 11개의 학습 데이터 입력시 아래와 같이 10회 동일한 data로 loop를 돌리면 상황은 다르게 표현됩니다.

for (i=0; i<10; i++)

{

  ...

 cArrTrainingSet.push_back(cTrainingNode);
 cArrLabelPlay.push_back(NO); 

 ...

 cArrTrainingSet.push_back(cTrainingNode);
 cArrLabelPlay.push_back(NO); 

}

 

즉, 110개의 학습데이터(실제로는 동일한 11개가 10번 추가된 경우)인 경우의 실행화면은 다음과 같습니다.

temperature: 10, rain=NO, win=NO, output=YES(0.997262), probability=0.946768
temperature: 11, rain=YES, win=YES, output=NO(-6.267239), probability=0.000000
temperature: 15, rain=NO, win=NO, output=YES(4.697852), probability=0.999995
temperature: 33, rain=NO, win=NO, output=NO(-2.074476), probability=0.007154
temperature: 32, rain=NO, win=NO, output=NO(-1.000003), probability=0.099699
temperature: 31, rain=NO, win=NO, output=YES(0.023777), probability=0.599376
temperature: 10, rain=YES, win=NO, output=NO(-5.000175), probability=0.000004

10-fold cross validation accuracy : 1.000000
Press a key to exit. 

즉, YES/NO 정답은 11개의 학습데이터와 동일한데, 그 확율값은 강화되어 명확해 집니다.

같은 YES이지만, 온도가 31도인 경우, 0.599, 즉, 60% 확율로 나옵니다.

이는, 31도가 되면, 저는 "매우 고민" 끝에 "밖에 나가 논다"가 되는 것입니다. 즉, 15도 일때도 같이 YES이지만, 확율에서 큰 차이가 나기 때문에, 좀더 세밀한 분석이 가능하다는 뜻입니다. 매우 놀랍군요. 저의 갈등까지도 분석해내니깐요.

 

이상으로 SVM에 대해 간략히 살펴보았는데, 데이터 마이닝 혹은 Machine learning 자체의 이론은 쉽지는 않습니다. 그러나 충분히 노력하면 더 좋은 알고리즘을 만들어 낼지도 모르겠군요. 기여도가 높다면 튜닝상도 받을 수 있을 겁니다. 아니면, 로또 번호를 유추, 혹은 주가 지수를 예측(numeric prediction)하여 대박을 노려볼 수 있겠네요. 의학적으로 염기서열(ACGT)을 분석하여, 암발생 확율을 예측하여 의학계에 기여할 수도 있습니다. 여하튼, 너무 깊은 애해는 상아탑에서 진행되며, 우리는 충분히 사용하는데에만 집중하면 됩니다.

 

앞으로는 SVM 이외의 다른 알고리즘에 대해서, "코드적"으로 알아보겠으며, 기회가 된다면, 각 알고리즘에 대해 정리하는 시간도 가져볼까 합니다.