1 캐글 스팸 데이터 1 2

SMS Spam Collection Dataset - Collection of SMS messages tagged as spam or legitimate 캐글 페이지를 통해 SMS 메시지가 스팸인지 아닌지를 판별하는 기계학습 예측 모형을 개발해보자.

1.1 데이터 가져오기

캐글 사이트에서 데이터를 다운로드 받아 압축을 푼다. 그리고 나서 데이터 정제작업을 스팸인지 아닌지를 나타내는 변수를 지정하고 텍스트 메시지 본문에 text 변수명을 부여한다. 그리고 나서 SMS 텍스트 길이를 nchar, str_length 함수로 주요 피처로 산출해낸다.

> library(tidyverse)
> library(stringi)
> spam_df <- read_csv("data/spam.csv", local = locale(encoding = "latin1"))
> 
> spam_df <- spam_df %>% 
+   rename(label = v1, text = v2) %>% 
+   select(label, text) %>% 
+   mutate(label = factor(label, levels=c("ham", "spam")))
> 
> # https://stackoverflow.com/questions/14363085/invalid-multibyte-string-in-read-csv
> spam_df <- spam_df %>% 
+   mutate(text_len = str_length(text))
> 
> skimr::skim(spam_df)
Skim summary statistics
 n obs: 5572 
 n variables: 3 

-- Variable type:character -----------------------------------------------------------------------------------------------------------------------------------------------------
 variable missing complete    n min max empty n_unique
     text       0     5572 5572   2 910     0     5158

-- Variable type:factor --------------------------------------------------------------------------------------------------------------------------------------------------------
 variable missing complete    n n_unique                 top_counts
    label       0     5572 5572        2 ham: 4825, spa: 747, NA: 0
 ordered
   FALSE

-- Variable type:integer -------------------------------------------------------------------------------------------------------------------------------------------------------
 variable missing complete    n  mean    sd p0 p25 p50 p75 p100     hist
 text_len       0     5572 5572 80.08 59.68  2  35  61 121  910 <U+2587><U+2583><U+2581><U+2581><U+2581><U+2581><U+2581><U+2581>

2 탐색적 데이터 분석 3

2.1 스팸여부 변수

스팸은 747개로 약 13.4%를 차지함이 확인된다. SMS 텍스트 길이에 따른 스팸과 정상 메시지를 겹쳐 보게 되면 텍스트 길이도 중요한 피쳐가 될 수 있음을 확인하게 된다.

> spam_df %>% 
+   count(label) %>% 
+   mutate(pcnt = scales::percent(n / sum(n)))
# A tibble: 2 x 3
  label     n pcnt 
  <fct> <int> <chr>
1 ham    4825 86.6%
2 spam    747 13.4%
> ggplot(spam_df, aes(x = text_len, fill = label)) +
+   theme_minimal() +
+   geom_histogram(binwidth = 5, alpha=0.7) +
+   labs(y = "SMS 빈도수", x = "텍스트길이", fill = "SMS구분",
+        title = "스팸여부에 따른 텍스트 길이 분포") +
+   scale_fill_viridis_d() +
+   theme(legend.position = "top")

3 스팸판별 기계학습

3.1 훈련/시험 데이터 분할

caret 팩키지로 훈련/시험 데이터를 7:3으로 나눠 데이터를 준비한다.

> library(caret)
> index <- createDataPartition(spam_df$label, times = 1, p = 0.7, list = FALSE)
> 
> train <- spam_df[index,]
> test  <- spam_df[-index,]

3.2 텍스트 피처

3.2.1 텍스트 데이터 전처리

텍스트 데이터를 기계학습 모형에 사용하려면 토큰화 과정을 거쳐 준비한다. 이를 위해서 먼저 전처리 과정이 필요한데, 이를 위해서 qdap 팩키지를 사용해서 텍스트를 전처리한 후에 quanteda 팩키지 token() 함수의 전처리 기능을 사용해서 토큰화 한다.

그리고 나서 불용어를 제거하고, 어간추출 기법을 사용해서 어간을 뽑아내서 말뭉치를 정제한다.

> library(qdap)
> 
> qdap_clean <- function(x) {
+   x <- replace_abbreviation(x)
+   x <- replace_contraction(x)
+   x <- replace_number(x)
+   x <- replace_ordinal(x)
+   x <- replace_symbol(x)
+   x <- tolower(x)
+   return(x)
+ }
> 
> train <- train %>% 
+   mutate(text = qdap_clean(text))
> 
> library(quanteda)
> # SMS 메시지 토큰화
> train_tokens <- tokens(train$text, what = "word", 
+                        remove_numbers = TRUE, remove_punct = TRUE,
+                        remove_symbols = TRUE, remove_hyphens = TRUE)
> 
> # 불용어 처리
> train_tokens <- tokens_select(train_tokens, stopwords(), 
+                               selection = "remove")
> 
> # 어간추출(stemming)
> train_tokens <- tokens_wordstem(train_tokens, language = "english")

3.2.2 피쳐 생성

quanteda 팩키지 dfm() 함수를 통해 DTM 행렬을 생성하고 나서 이를 행렬로 변환시킨다. ``

> ## 단어주머니 접근법으로 DTM 행렬로 변환
> train_tokens_dfm <- dfm(train_tokens, tolower = FALSE)
> train_tokens_m <- as.matrix(train_tokens_dfm)

가장 먼저 스팸과 정상 메시지에 가장 출현빈도가 높은 단어를 추출하여 단어구름(wordcloud)을 제작하여 비교한다.

> library(wordcloud)
> train_tokens_df <- train_tokens_m %>% 
+   tbl_df %>% 
+   bind_cols(label = train$label)
> 
> train_tokens_spam_freq <- train_tokens_df %>% 
+   filter(label == "spam") %>% 
+   select(-label) %>% 
+   colSums(.)
> 
> train_tokens_ham_freq <- train_tokens_df %>% 
+   filter(label == "ham") %>% 
+   select(-label) %>% 
+   colSums(.)
> 
> par(mfrow=c(1,2))
> wordcloud(names(train_tokens_spam_freq), train_tokens_spam_freq, min.freq=10, color = "red")
> wordcloud(names(train_tokens_ham_freq), train_tokens_ham_freq, min.freq=10, color = "blue")

3.3 나이브 베이즈 적합 4

나이브 베이즈 모형 적합을 하기 전에 단어 빈도수가 적은 것은 뽑아낸다. 그리고 TF-IDF를 계산해서 이를 스팸 예측을 위한 피쳐로 사용한다.

추가 피처(text_len)을 추가하여 quanteda 팩키지 textmodel_nb에 넣어 모형을 적합시킨다. 그리고 모형성능 예측은 caret 팩키지 confusionMatrix() 함수를 사용한다.

> # library(klaR)  # caret 나이브베이즈 
> train_tokens_dfm <- dfm_trim(train_tokens_dfm, min_termfreq = 5)  
> train_tokens_idf <- dfm_tfidf(train_tokens_dfm) 
> 
> ## 예측모형 행렬
> train_x_df <- cbind(train$text_len, train_tokens_idf)
> 
> ## 나이브베이즈 모형 적합
> spam_nb <- textmodel_nb(train_x_df, train$label)
> 
> ## 훈련데이터 예측모형 성능
> train_pred <- predict(spam_nb, train_x_df)
> nb_pred_train_tbl <- table(predicted = train_pred, actual=train$label)
> 
> confusionMatrix(nb_pred_train_tbl, mode = "everything", positive = "spam")
Confusion Matrix and Statistics

         actual
predicted  ham spam
     ham  3344   13
     spam   34  510
                                         
               Accuracy : 0.988          
                 95% CI : (0.984, 0.9911)
    No Information Rate : 0.8659         
    P-Value [Acc > NIR] : < 2.2e-16      
                                         
                  Kappa : 0.949          
 Mcnemar's Test P-Value : 0.003531       
                                         
            Sensitivity : 0.9751         
            Specificity : 0.9899         
         Pos Pred Value : 0.9375         
         Neg Pred Value : 0.9961         
              Precision : 0.9375         
                 Recall : 0.9751         
                     F1 : 0.9560         
             Prevalence : 0.1341         
         Detection Rate : 0.1307         
   Detection Prevalence : 0.1395         
      Balanced Accuracy : 0.9825         
                                         
       'Positive' Class : spam           
                                         

4 스팸예측 성능

시험데이터를 통해 스팸성능 예측을 해야 스팸예측 모형의 과적합이 방지되어 일반화가 가능한지 파악할 수 있다. 이를 위해서 나이브베이즈 예측모형에 입력값으로 대입된 데이터 형태를 동일하게 유지해야 한다.

  1. qdap 팩키지 텍스트 데이터 전처리 수행
  2. quandeta 팩키지 토큰화 과정
  3. 불용어 처리 및 어간 추출 작업
  4. 토큰을 단어주머니(Bag-of-Words) DTM 변환
  5. dfm_select() 함수로 훈련데이터와 동일한 DTM이 되도록 작업
  6. TF-IDF 변환
  7. 텍스트 길이(text_len) 피처 추가

그리고 나서 predict 함수로 시험데이터 예측을 하고 confusionMatrix로 성능을 평가한다. SMS 스팸여부를 예측하는데 민감도와 특이도, 그리고 정확도 모두 99%에 근접하여 만족도가 높게 나오고 있다.

> test <- test %>% 
+   mutate(text = qdap_clean(text))
> # SMS 메시지 토큰화
> test_tokens <- tokens(test$text, what = "word", 
+                        remove_numbers = TRUE, remove_punct = TRUE,
+                        remove_symbols = TRUE, remove_hyphens = TRUE)
> 
> # 불용어 처리
> test_tokens <- tokens_select(test_tokens, stopwords(), 
+                               selection = "remove")
> 
> # 어간추출(stemming)
> test_tokens <- tokens_wordstem(test_tokens, language = "english")
> 
> # 단어주머니 DTM 변환
> test_tokens_dfm <- dfm(test_tokens, tolower = FALSE)
> 
> # 단어주머니 DTM 변환
> test_tokens_dfm <- dfm_select(test_tokens_dfm, train_tokens_dfm)
> # TF-IDF 변환
> test_tokens_idf <- dfm_tfidf(test_tokens_dfm) 
> 
> # 시험데이터 준비
> test_x_df <- cbind(test$text_len, test_tokens_idf)
> 
> # 시험데이터 예측
> predicted_class <- predict(spam_nb, test_x_df)
> 
> # 성능평가
> nb_pred_test_tbl <- table(predicted = predicted_class, actual=test$label)
> confusionMatrix(nb_pred_test_tbl, mode = "everything", positive = "spam")
Confusion Matrix and Statistics

         actual
predicted  ham spam
     ham  1428   14
     spam   19  210
                                          
               Accuracy : 0.9803          
                 95% CI : (0.9724, 0.9864)
    No Information Rate : 0.8659          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9157          
 Mcnemar's Test P-Value : 0.4862          
                                          
            Sensitivity : 0.9375          
            Specificity : 0.9869          
         Pos Pred Value : 0.9170          
         Neg Pred Value : 0.9903          
              Precision : 0.9170          
                 Recall : 0.9375          
                     F1 : 0.9272          
             Prevalence : 0.1341          
         Detection Rate : 0.1257          
   Detection Prevalence : 0.1370          
      Balanced Accuracy : 0.9622          
                                          
       'Positive' Class : spam