연구 질문

SMS 메시지가 스팸인지 아닌지 분류 및 연관 단어 추출

데이터 설명

데이터는 총 5,574개의 SMS 메시지로 구성되며, 각 행은 “ham” 또는 “spam”으로 레이블이 지정되어 있음.

라이브러리 로드

library(readr)
library(tm)
library(wordcloud)
library(caret)
library(e1071)
library(randomForest)

데이터 불러오기

url <- "https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip"
temp <- tempfile(fileext = ".zip")
download.file(url, temp)
sms <- read_delim(unz(temp, "SMSSpamCollection"), delim = "\t", col_names = c("type", "text"))

텍스트 전처리

docs <- VCorpus(VectorSource(sms$text))  
docs <- tm_map(docs, content_transformer(tolower)) #소문자 변환
docs <- tm_map(docs, removePunctuation) #문장부호 제거
docs <- tm_map(docs, removeNumbers)  #숫자 제거
docs <- tm_map(docs, removeWords, stopwords("english"))    #불용어제거
docs <- tm_map(docs, stripWhitespace) #공백 제거
docs <- tm_map(docs, stemDocument) #어간 추출

단어 행렬 생성 (DTM)

dtm <- DocumentTermMatrix(docs)
freq <- colSums(as.matrix(dtm))
head(sort(freq, decreasing = TRUE), 20)  # 가장 많이 등장한 단어 20개 보기
## call  now  get  can will just come dont  ham free ltgt know like  day love want 
##  599  443  413  367  344  338  276  270  270  251  251  249  239  232  232  226 
##  got  ill good time 
##  221  220  219  219

데이터프레임 변환

dtm_df <- as.data.frame(as.matrix(dtm)) #DTM을 데이터프레임으로 변환
dtm_df$type <- as.factor(sms$type)  # 문자데이터를 factor로 변환

많이 나오는 단어 제외하고 제거

freq <- colSums(as.matrix(dtm))
top_terms <- names(sort(freq, decreasing = TRUE))[1:300] #빈도 상위 300개의 단어만 선택
dtm <- dtm[, top_terms]
dtm_df <- as.data.frame(as.matrix(dtm))
dtm_df$type <- as.factor(sms$type) # 열 이름 중복 방지
colnames(dtm_df) <- make.names(colnames(dtm_df), unique = TRUE)

데이터 분할 (학습/테스트)

set.seed(123) 
train_idx <- createDataPartition(dtm_df$type, p = 0.7, list = FALSE) 
train_data <- dtm_df[train_idx, ]
test_data <- dtm_df[-train_idx, ]

결측치 확인 및 제거

sum(is.na(train_data))  #결측치 없음으로 및의 코드는 없어도 상관없음
## [1] 0
train_data <- na.omit(train_data)
test_data <- na.omit(test_data)

모델별 학습

Naive Bayes

model.nb <- naiveBayes(type ~ ., data = train_data)
pred.nb <- predict(model.nb, test_data)
prob.nb <- predict(model.nb, test_data, type = "raw")[, "spam"]
cm.nb <- confusionMatrix(pred.nb, test_data$type, positive = "spam")
print(cm.nb)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham    43    0
##       spam 1193  195
##                                           
##                Accuracy : 0.1663          
##                  95% CI : (0.1474, 0.1866)
##     No Information Rate : 0.8637          
##     P-Value [Acc > NIR] : 1               
##                                           
##                   Kappa : 0.0097          
##                                           
##  Mcnemar's Test P-Value : <2e-16          
##                                           
##             Sensitivity : 1.00000         
##             Specificity : 0.03479         
##          Pos Pred Value : 0.14049         
##          Neg Pred Value : 1.00000         
##              Prevalence : 0.13627         
##          Detection Rate : 0.13627         
##    Detection Prevalence : 0.96995         
##       Balanced Accuracy : 0.51739         
##                                           
##        'Positive' Class : spam            
## 

정확도: 0.1663

민감도(spam을 잘 잡아냈는지):: 1.00000

특이도(정상메일을 스팸으로 잘못 분류하지 않았는지):0.03479

Random Forest

model.rf <- randomForest(type ~ ., data = train_data)
pred.rf <- predict(model.rf, test_data)
prob.rf <- predict(model.rf, test_data, type = "prob")[, "spam"]  # spam 확률
cm.rf <- confusionMatrix(pred.rf, test_data$type, positive = "spam")
print(cm.rf)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham  1225   39
##       spam   11  156
##                                          
##                Accuracy : 0.9651         
##                  95% CI : (0.9542, 0.974)
##     No Information Rate : 0.8637         
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.842          
##                                          
##  Mcnemar's Test P-Value : 0.0001343      
##                                          
##             Sensitivity : 0.8000         
##             Specificity : 0.9911         
##          Pos Pred Value : 0.9341         
##          Neg Pred Value : 0.9691         
##              Prevalence : 0.1363         
##          Detection Rate : 0.1090         
##    Detection Prevalence : 0.1167         
##       Balanced Accuracy : 0.8956         
##                                          
##        'Positive' Class : spam           
## 

정확도: 0.9651

민감도(spam을 잘 잡아냈는지): 0.8051

특이도(정상메일을 스팸으로 잘못 분류하지 않았는지): 0.9903

SVM

model.svm <- svm(type ~ ., data = train_data, probability = TRUE)
pred.svm <- predict(model.svm, test_data) # 확률 추출
prob_attr <- predict(model.svm, test_data, probability = TRUE)
prob.svm <- attr(prob_attr, "probabilities")[, "spam"]

cm.svm <- confusionMatrix(pred.svm, test_data$type, positive = "spam")
print(cm.svm)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham  1235   43
##       spam    1  152
##                                           
##                Accuracy : 0.9693          
##                  95% CI : (0.9589, 0.9776)
##     No Information Rate : 0.8637          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.8564          
##                                           
##  Mcnemar's Test P-Value : 6.37e-10        
##                                           
##             Sensitivity : 0.7795          
##             Specificity : 0.9992          
##          Pos Pred Value : 0.9935          
##          Neg Pred Value : 0.9664          
##              Prevalence : 0.1363          
##          Detection Rate : 0.1062          
##    Detection Prevalence : 0.1069          
##       Balanced Accuracy : 0.8893          
##                                           
##        'Positive' Class : spam            
## 

정확도: 0.9693

민감도(spam을 잘 잡아냈는지): 0.7795

특이도(정상메일을 스팸으로 잘못 분류하지 않았는지): 0.9992

중요 단어 시각화

freq <- colSums(as.matrix(DocumentTermMatrix(VCorpus(VectorSource(sms$text[train_idx])))))  #학습데이터 텍스트를 DTM으로 다시 변환
wordcloud(names(freq), freq, max.words = 100, colors = brewer.pal(8, "Dark2"))

freq_sorted <- sort(freq, decreasing = TRUE)
head(freq_sorted, 20)
##  you  the  and  for your have call that  are  ham  not  but  get with will  i'm 
## 1305  865  655  451  439  388  371  322  317  270  268  267  261  261  253  252 
##  can just this when 
##  241  233  202  188
top_20 <- names(head(freq_sorted, 20))  #빈도 상위 20개 단어 출력
wordcloud_words <- names(freq)[order(freq, decreasing = TRUE)][1:100]
top_20 %in% wordcloud_words  #워드클라우드에 포함된 단어에 상위 20개 단어가 포함되었는지 확인
##  [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
## [16] TRUE TRUE TRUE TRUE TRUE

주요 결과 및 결론

- Naive Bayes는 속도가 빠르지만 다른 모델에 비해서는 정확도가 낮은편

- Random Forest, SVM은 비교적 높은 정확도

- 스팸 메시지는 특정 키워드(예: “you”, “call”, “can”, “get)와 연관이 많음.

- 대체로 얻다, 지금 등 받는 사람으로 하여금 바로 행동을 하도록 유도하는 듯한 패턴이 드러남.

- 워드클라우드 시각화는 스팸 메시지에서 자주 등장하는 단어를 파악하는 데 유용함.

결론

머신러닝 기법을 사용해 문자 메시지를 spam, ham으로 분류하는 시스템을 구축할 수 있으며, 비교적 간단한 모델(Naive Bayes)로도 결과를 도출해낼 수는 있음.