SMS Spam Collection Dataset - Collection of SMS messages tagged as spam or legitimate 캐글 페이지를 통해 SMS 메시지가 스팸인지 아닌지를 판별하는 기계학습 예측 모형을 개발해보자.
캐글 사이트에서 데이터를 다운로드 받아 압축을 푼다. 그리고 나서 데이터 정제작업을 스팸인지 아닌지를 나타내는 변수를 지정하고 텍스트 메시지 본문에 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>
스팸은 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")
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,]
텍스트 데이터를 기계학습 모형에 사용하려면 토큰화 과정을 거쳐 준비한다. 이를 위해서 먼저 전처리 과정이 필요한데, 이를 위해서 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")
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")
나이브 베이즈 모형 적합을 하기 전에 단어 빈도수가 적은 것은 뽑아낸다. 그리고 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
시험데이터를 통해 스팸성능 예측을 해야 스팸예측 모형의 과적합이 방지되어 일반화가 가능한지 파악할 수 있다. 이를 위해서 나이브베이즈 예측모형에 입력값으로 대입된 데이터 형태를 동일하게 유지해야 한다.
qdap
팩키지 텍스트 데이터 전처리 수행quandeta
팩키지 토큰화 과정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