Naive Bayes Classification with R


Bayesian rule에 대해서는 이전 theorem post를 참조 한다.

Bayes algorithm을 이용해서 mobile phone spam을 filtering하는 방법

Short Message Service (SMS)를 구분 하는 작업을 수행 한다.



Step 1 - collecting data


naive bayes classifer를 개발하기 위해서 SMS Spamcollection으로 부터 알고리즘을 적용 한다.

http://www.dt.fee.unicamp.br/~tiago/smsspamcollection/


이 스팸 데이터의 포맷은 아래와 같다.



spam 라벨은 junk message를 의미하고, ham 라벨은 legitimate message를 의미한다.


ham massage의 예:

  • Better. Made up for Friday and stuffed myself like a pig yesterday. Now I feel bleh. But at least its not writhing pain kind of bleh.
  • If he started searching he will get job in few days. He have great potential and talent.
  • I got another job! The one at the hospital doing data analysis or something, starts on monday! Not sure when my thesis will got finished


spam message의 예:

  • Congratulations ur awarded 500 of CD vouchers or 125gift guaranteed & Free entry 2 100 wkly draw txt MUSIC to 87066
  • December only! Had your mobile 11mths+? You are entitled to update to the latest colour camera mobile for Free! Call The Mobile Update Co FREE on 08002986906
  • Valentines Day Special! Win over £1000 in our quiz and take your partner on thetrip of a lifetime! Send GO to 83600 now. 150p/msg rcvd.
ham과 spam 메시지와의 차이는 우선 위의 메시지만을 고려해 볼 때,
세개 중 두개의 spam은 "free"라는 메시지를 사용 했다. 

이와 다르게, ham message의 경우 2개의 메시지들은 각각 상세한 날짜를 언급 하고 있다는 점이 다르다.

이러한 데이터에 naive bayes classifier를 적용할 경우, 위에서 언급한 단어의 발생 빈도를 이용해서 SMS message가 spam 인지 ham인지를 구분하게 된다.

단순히 free가 들어간 메시지를 spam으로 처리하는 것은 문제가 될 수 있다. 
왜냐하면, 같은 free라도
"are you free on Sunday?" 는 ham 이고
"free ringtones."는 spam 이다.
따라서 제안된 classifier는 확률을 이용해서 전체 메시지의 단어 빈도를 분석해서 spam 인지 아닌지를 결정 해야 한다.


Step 2 - exploring and preparing the data


text 파일을 처리가 어려움으로 csv 파일로 변경된 것을 사용 한다.

sms_spam.csv

#데이터 읽기
sms_raw <- read.csv("sms_spam.csv",stringsAsFactors =  FALSE)
#구조 분석 파악
> str(sms_raw)
'data.frame':	5559 obs. of  2 variables:
 $ type: chr  "ham" "ham" "ham" "spam" ...
 $ text: chr  "Hope you are having a good week. Just checking in" "K..give back my thanks." "Am also doing in cbe only. But have to pay." "complimentary 4 STAR Ibiza Holiday or 짙10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out"| __truncated__ ...
#데이터 타입 분석
> class(sms_raw)
[1] "data.frame"

feature는 type과 text 두개로 구성되었다.
data frame의 구조를 가지며, 5559개의 열로 구성 되었다.

그리고 text feature에 SMS message의 raw 데이터가 기록 되어 있다.


현재 type은 stringAsFactors =  FALSE 로 설정 했기 때문에 factor가 아니라, character vector로 구성 되어 있다.
하지만, 이 type은 categorical variable 이기 때문에 factor로 변환 하는것이 좋다.
※ 처음 부터 factor로 만들지 않은 것은 text는 factor 타입보다는 character vector에 더 적합하기 때문이다.
#변경을 수행 한다.
> sms_raw$type <- factor(sms_raw$type)
#변경 결과를 확인하면 정상적으로 Factor가 된것을 알 수 있다.
> str(sms_raw)
'data.frame':	5559 obs. of  2 variables:
 $ type: Factor w/ 3 levels "All done, all handed in. Don't know if mega shop in asda counts as celebration but thats what i'm doing!",..: 2 2 2 3 3 2 2 2 3 2 ...
 $ text: chr  "Hope you are having a good week. Just checking in" "K..give back my thanks." "Am also doing in cbe only. But have to pay." "complimentary 4 STAR Ibiza Holiday or 짙10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out"| __truncated__ ...
이상하게도 기본적으로 제공되는 csv 파일을 가지고 작업을 수행할 경우 문제가 발생한다.
따라서 아래의 코드 작업을 수행 해야 한다.
# 부족한 데이터를 삽입한다.
sms_raw[1072,"type"] <- factor("ham")
sms_raw[1072,"text"] <- c("All done, all handed in. Don't know if mega shop in asda counts as celebration but thats what i'm doing!")


부족한 데이터 삽입전에는 아래와 같이 요인이 3개가 된다. 하나가 요인 값이랑 text 값이 잘못 되어 있기 때문이다.

> table(sms_raw$type)
All done, all handed in. Don't know if mega shop in asda counts as celebration but thats what i'm doing! 
                                                                                                       1 
                                                                                                     ham 
                                                                                                    4811 
                                                                                                    spam 
                                                                                                     747

잘못된 데이터를 수정 하면 아래와 같이 바뀐다.

> table(sms_raw$type)
 ham spam 
4812  747 


■ Data preparation - processing text data for analysis


Document Classification은 주어진 문서를 하나 이상의 분류로 구분하는 문제이다.

지금 다루는 스팸 인지 아닌지를 구분하는 것이 문서 분류의 가장 흔한 예라고 할 수 있다.

또 다른 예로는 제품 리뷰 글을 보고 해당 리뷰가 제품에 대한 긍정적인 리뷰인지 부정적인 리뷰인지를 구분하는 

감성 분석 (Sentiment Analysis)이 있다.


text 처리를 위해서 특화된 package인 Text Mining (tm)의 사용에 대해서 다루 겠다.

TM은 Ingo Feinerer에 의해서 Vienna University에서 박사학위 논문의 일환으로 만들어진 Package 이다.

전공은 Economics and Business이다. 해당 package에 대해서 좀 더 깊이 있는 학습을 원할경우 아래의 사이트를 참조 하자.

http://tm.r-forge.r-project.org/


install.packages("tm")

library(tm)


우선 corpus function을 이용해서 text 데이터를 오브젝트로 생성 한다.


포멧팅 오류를 막기 위해서, corpus 실행전에 UTF-8 en-coding을 수행 하자.

그다음 corpus 함수를 수행 한다.

아래와 같이 corpus 함수는 text를 정의하고 있는 파라메터를 입력 받는다. 이를 위해서 text를 일련의 vector로 만들었었다. 이것을 넘겨줄때 VectorSource()를 이용해서 vector임을 corpus에게 알려주게 된다.

최종적으로 corpus는 sms_corpus라고 이름 지어진 Object를 반환하게 된다. 이 Object를 이용해서 이제 Text Data 처리를 쉽게 할 수 있다.

# try to encode again to UTF-8
sms_raw$text <- iconv(enc2utf8(sms_raw$text),sub="byte")
sms_corpus <- Corpus(VectorSource(sms_raw$text))
# examine the sms corpus
print(sms_corpus)
inspect(sms_corpus[1:3])
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 3
[[1]]
Metadata:  7
Content:  chars: 49
[[2]]
Metadata:  7
Content:  chars: 23
[[3]]
Metadata:  7
Content:  chars: 43


Corpus는 정말로 유연한 문서 읽기 라이브러리이다. 

PDF나 Microsoft Word 문서 같은 것들을 읽을 수 있다. 좀 더 조사하고 싶다면, Data Input section에 대해서

tm package의 vignette를 이용해서 관련 문서를 확인하다.

command: print(vignette("tm"))


본격적으로 text를 분할해서 단어를 분석하기 전에 기본적으로 

데이터를 클린하는 것이 필요하다.

즉, punctuation과 의미 없는 단어들을 말한다.


각각의 단계는 아래와 같다.

# clean up the corpus using tm_map()
# 소문자로 모두 변경 한다.
corpus_clean <- tm_map(sms_corpus, content_transformer(tolower))
# 숫자를 모두 제거 한다.
corpus_clean <- tm_map(corpus_clean, removeNumbers)
# stop words로 알려진, "to", "and", "but", "or"를 모두 제거 한다.
# 이것을 위해서 stop word를 새로 정의하지 않고, 알려진 리스트인 stopword()를 이용 한다.
corpus_clean <- tm_map(corpus_clean, removeWords, stopwords())
# punctuation을 제거 한다.
corpus_clean <- tm_map(corpus_clean, removePunctuation)
# single space로 모두 만든다. 즉, whitespace(여백)을 모두 제거 한다.
corpus_clean <- tm_map(corpus_clean, stripWhitespace)


각각 변경된 내부 구조를 살펴 보면 아래와 같다.

> lapply(sms_corpus[1:3],as.character)
$`1`
[1] "Hope you are having a good week. Just checking in"
$`2`
[1] "K..give back my thanks."
$`3`
[1] "Am also doing in cbe only. But have to pay."
> lapply(corpus_clean[1:3],as.character)
$`1`
[1] "hope good week just checking "
$`2`
[1] "kgive back thanks"
$`3`
[1] " also cbe pay"


data pre-processing의 마지막 단계는 tokenization 단계이다.

해당 단계에서는 각각의 word를 하나의 컴포넌트로 구분하게 된다.


DocumentTermMatrix() 함수를 이용해서 sparse matrix를 구성 한다.

기본적으로 그냥 생성하면 SMS message를 나타내는 것은 5,559개 이며, term을 나타내는 것은 7,000개 이다.

> sms_dtm <- DocumentTermMatrix(corpus_clean)
> sms_dtm
<>
Non-/sparse entries: 42635/44407129
Sparsity           : 100%
Maximal term length: 40
Weighting          : term frequency (tf)

이제 sparse matrix을 생성 했으므로 word frequency를 수행 할 수 있다.



■ Data preparation - creating training and test datasets 

트레이닝과 테스트로 데이터를 나눠서 나중에 평가를 위해서도 사용 할 수 있도록 한다.


75 vs 25로 분할한다. 그리고 이미 임의로 섞여 있기 때문에 그냥 나누면 된다.


raw data / document-term matrix / corpus 이렇게 세가지 종류의 데이터 셋을 모두 분할 한다.

# creating training and test datasets
sms_raw_train <- sms_raw[1:4169, ]
sms_raw_test  <- sms_raw[4170:5559, ]

sms_dtm_train <- sms_dtm[1:4169, ]
sms_dtm_test  <- sms_dtm[4170:5559, ]

sms_corpus_train <- corpus_clean[1:4169]
sms_corpus_test  <- corpus_clean[4170:5559]

비율이 정상적으로 분할 되었는지는 아래의 명령어를 통해서 간단히 알 수 있다.

> # check that the proportion of spam is similar
> prop.table(table(sms_raw_train$type))
      ham      spam 
0.8647158 0.1352842 
> prop.table(table(sms_raw_test$type))
      ham      spam 
0.8683453 0.1316547 


■ Visualizing text data - word clouds

word cloud는 각 단어의 빈도를 text data로 묘사하게 된다.

여기서 보여지는 단어들의 배치는 랜덤이며 그 글자 크기가 빈도를 나타낸다.

이것을 통해서 경향을 파악할 수 있기 때문에 word cloud는 많이 사용 된다.


spam과 ham을 구분하는 예제에서는 word cloud를 그려 봄으로서 생성하려는 classifier가 성공적으로 동작 할지 안할지를

가늠 할 수 있게 된다.

word cloud는 Ian Fellows가 전문 통계학자로 UCLA에 있을때 만들어 진것이다.

http://cran.r-project.org/web/packages/wordcloud/index.html 에서 더 자세한 정보를 확인 할 수 있다.


install.packages("wordcloud")
library(wordcloud)
wordcloud(sms_corpus_train, min.freq = 30, random.order = FALSE)



random.order=FALSE 면, 높은 빈도일 수록 가운데에 집중 되는 경향을 보인다.

min.freq의 의미는 최소 빈도를 의미한다. 이것을 넘어야 cloud에서 보여지게 된다.

만약 word를 figure로 보지 못한다는 메시지를 본다면, min.freq를 조정 하면 된다.


이것을 spam과 ham에 대해서 각각 나눠서 수행 해 보자.

# subset the training data into spam and ham groups
spam <- subset(sms_raw_train, type == "spam")
ham  <- subset(sms_raw_train, type == "ham")
wordcloud(spam$text, max.words = 40, scale = c(3, 0.5))
wordcloud(ham$text, max.words = 40, scale = c(3, 0.5))



  


오른쪽이 스팸이고, 왼쪽이 Ham이다.

Spam의 경우 urgent, free, mobile, call, claim, stop 같은 것들이 있는것을 알 수 있다.

Ham의 경우 can, sorry, need, time이 나오는 것을 알 수 있다.



■ Data preparation - creating indicator features for frequent words

데이터 가공의 마지막 단계는 이러한 sparse matrix을 naive Bayes classifier가 처리할 수있는 데이터 타입으로 변형해 주는 것이다.

sparse matrix은 7000개의 feature들을 가지고 있다. 하지만 모든 feature들이 유용하지 않다는 것은 당연한 사실 이다.

따라서 이러한 feature들을 줄이는 작업을 수행 한다.


tm package에 있는 findFreqTerms() 함수를 이용 한다.

findFreqTerms(sms_dtm_train, 5)

위 command의 뜻은 최소 matrix으로 부터 최소 5번 이상 발생한 word로 구성된 character vector를 만들라는 명령어 이다.


이제 이러한 frequent word로만 구성 시켜서 matrix을 만들기 위해서 다시 DocumentTermMatrix을 수행 한다.

# indicator features for frequent words
sms_dict<- findFreqTerms(sms_dtm_train, 5)
sms_train <- DocumentTermMatrix(sms_corpus_train, list(dictionary = sms_dict))
sms_test  <- DocumentTermMatrix(sms_corpus_test, list(dictionary = sms_dict))

이렇게 수행 하면 최종적으로 7000개의 feature들은 1200개 수준으로 줄어 들게 된다.


naive Bayes classifier는 기본적으로 categorical feature들에 대해서만 학습을 수행 한다.

따라서 지금의 구조는 셀이 빈도를 나타내는 numerical type으로 표현 되므로 이것을 factor variable로 변경 해야 한다.

여기서는 "Yes", "No"와 같은 간단한 두 종류의 factor로 구분한다.

# convert counts to a factor
convert_counts <- function(x) {
  x <- ifelse(x > 0, 1, 0)
  x <- factor(x, levels = c(0, 1), labels = c("No", "Yes"))
}
# apply() convert_counts() to columns of train/test data
sms_train <- apply(sms_train, MARGIN = 2, convert_counts)
sms_test  <- apply(sms_test, MARGIN = 2, convert_counts)

apply를 이용해서 처리한다. margin=2의 의미는 column을 의미한다.

더 자세한 사항은 이전 포스트를 참조하자.




Step 3 - training a model on the data


이제 naive Bayes algorithm을 적용할 때가 왔다.

Machine Learning의 대부분의 시간은 지금과 같이 data 가공에 소모된다. 데이터만 가공된다면 적용하는것은 일도 아니다.

Naive Bayes algorithm의 원리는 이전 포스트의 "Probabilistic Learning - Classification using Naive Bayes

"를 참조 하자.


naive Bayes를 최적으로 구현하는 것은 쉬운 일이 아니므로, 이미 구현된 library를 사용하자.

"e1071" package에는 다양한 Machine Learning algorithm들이 구현되어 있다.

Vienna University of Technology (TU Wien)의 Statistics 학과에서 개발한 package이다.


install.packages("e1071")

library(e1071)


설치가 실패할 경우, 해당 package는 바이너리로 제공이 안되기 때문이다. 수작업으로 설치해 준다.

.libPaths() # 라이브러리 설치 경로 확인

https://cran.r-project.org/web/packages/e1071/index.html에서

Windows binaries항목의 zip파일을 다운 받은 다음 라이브러리가 저장된 폴더에 복사 한다.


사실 다른 package들도 많이 있다.

klaR 또한 naiveBayes를 지원 한다. 어느것을 쓰던 상관없으니 마음대로 찾아서 쓰면된다. 


Naive Bayes는 Learning과 Prediction을 구분해서 수행되는 알고리즘이다.

즉, eager learning 방식이다. 특별한 Model을 생성하고 prediction 하게 된다. 별도의 트레이닝 실행 시간을 필요로 하지만

prediction에 걸리는 시간은 트레이닝을 통해서 생성된 model에 기반하므로 Lazy Learning 방식 (KNN)에 비해서 빠르게 된다.


Building the classifier

m <- naiveBayes (train, class, laplace = 0)

  • train은 data frame 또는 matrix가 가능하다. training data를 포함하고 있다.
  • class는 factor vector 값이다. 각 행에서 class 값을 가지고 있는 것이다.
  • laplace 는 Laplace Estimator 사용을 위한 최소 값을 의미한다. Default 는 0이다.

해당 함수는 prediction을 위한 bayes model을 반환 한다.


Making Prediction

p <- predict (m, test, type = "class")

  • m은 model이다. naiveBayes()에 의해서 학습 되어진
  • test는 data frame 또는 matrix으로 test data set을 포함하고 있다. 당연히 training과 같은 형태와 수의 feature를 가지고 있어야 한다.
  • type은 "class" 또는 "raw"로 설정 할 수 있다. class는 가장 그럴것 같은 class로 예측해 주는것이고, raw는 확률을 반환해 주게 된다.

해당 함수는 type argument에 의해서 예측되어진 class 또는 확률의 vector를 반환 한다.

## Step 3: Training a model on the data ----
library(e1071)
sms_classifier <- naiveBayes(sms_train, sms_raw_train$type)
sms_classifier
$tables$yesterday
                  yesterday
sms_raw_train$type          No         Yes
              ham  0.995284327 0.004715673
              spam 0.996453901 0.003546099


데이터를 출력해서 보면 각각의 feature들에 대한 확률이 모두 계산되어진 것을 알 수 있다.

위와 같이 각 feature에 대한 조건 확률 들을 모두 계산해 두면 추후에 어떤 message에 대한 spam과 ham의 확률을 쉽게 구할 수 있다.

이것을 통해서 prediction을 즉각적으로 수행 하게 된다.




Step 4 - evaluating model performance 


테스팅을 위해서는 training에 사용하지 않는 message를 가지고 예측을 수행 하면 된다.


평가를 위해서는 이것저것 이전에 생성한 데이터 구조들을 사용 하게 된다.


sms_test 에는 보지 않은 데이터들의 feature들이 Yes와 No로써 저장되어 있다.

sms_raw_test에는 각 message가 "ham"인지 "spam"인지에 대한 참(true) 값이 저장 되어 있다.


학습을 통해서 생성한 classifier는 sms_classifier 이다. 그런 다음 생성한 prediction을 이용해서 true value와 비교하는 작업을 수행 한다.


prediction을 위해서 predict() 함수가 사용 되어 진다. 그리고 이러한 결과는 sms_test_pred vector에 저장 된다.

## Step 4: Evaluating model performance ----
sms_test_pred <- predict(sms_classifier, sms_test)

검증은 confusion matrix을 만들어서 수행 한다.

이를 위해서 CrossTable() 함수를 사용 한다.

> library(gmodels)
> CrossTable(sms_test_pred, sms_raw_test$type,
+            prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE,
+            dnn = c('predicted', 'actual'))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | actual 
   predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1202 |        31 |      1233 | 
             |     0.996 |     0.169 |           | 
-------------|-----------|-----------|-----------|
        spam |         5 |       152 |       157 | 
             |     0.004 |     0.831 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|


위 confusion matrix을 통해서 괭장히 잘 prediction된 것을 알 수 있다.

오직 5개의 message만이 incorrectly 분류되었다 spam으로 이것은 전체 비중에서 0.4% 수준인 것이다.

31개 정도만이 그리고 spam인데 ham으로 prediction 되었다.

project에 투자한 노력에 비해서 상당히 우수한 prediction 실험 결과를 얻어낸 것을 알 수 있다.


하지만 문제는, 단 5개의 message만이 원래 ham인데 spam으로 분류 되었다 할지라도 이것은 심각한 문제를 야기할 수 있다.

즉 spam인데 ham으로 분류되는것은 단순히 불편함을 주지만, 중요한 메시지를 spam으로 분류해서 문제를 발생 시킨다면

그것이 99%의 정확도를 가질지라도 당장 해당 제품은 abandon 되어진다.

즉, False Negative 보다는 절대적으로 False Positive를 줄어야 하는 것이다.



Step 5 - improving model performance


오버피팅 문제를 야기할 수도 있다. 지금까지 생성한 모델은

왜냐하면 모델을 생성하기 위해서 사용한 트레이닝 데이터 셋에 "ringtone"이라는 feature가 오직 spam에서만 존재한다면

해당 경우에는 100% 확률로 spam으로 예측 된다.

의심할 여지 없이 문제가 있는 모델이다. 해당 트레이닝 데이터에서 최고의 정확도를 자랑하지만, 트레이닝 데이터가 현실의 모든 데이터를 반영하지 못하기 때문에 그것이 문제가 있다는 것은 직관적으로 알 수 있다.


laplace = 1로 설정해서 수행해 보자.

## Step 5: Improving model performance ----
sms_classifier2 <- naiveBayes(sms_train, sms_raw_train$type, laplace = 1)
sms_test_pred2 <- predict(sms_classifier2, sms_test)
CrossTable(sms_test_pred2, sms_raw_test$type,
           prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE,
           dnn = c('predicted', 'actual'))
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | actual 
   predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1203 |        31 |      1234 | 
             |     0.997 |     0.169 |           | 
-------------|-----------|-----------|-----------|
        spam |         4 |       152 |       156 | 
             |     0.003 |     0.831 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|


실험 결과 false positive가 5개에서 4개로 1개 줄어든 것을 알 수 있다.

비록 적은 향상이지만 이러한 false positive는 spam filtering product가 use 될지 abandon될지를 결정할 중요한 factor이므로 무시할 수는 없다.

그리고 여전히 false positive로 분류된 4개의 message에 대해서도 분석을 해봐야 할 것이다.








+ Recent posts