สร้าง Spam Classifier ดักจับสแปมอีเมล์ด้วย R

บทความนี้แอดอธิบายการสร้างโมเดล machine learning ง่ายๆสำหรับปัญหา binary classification ทำนาย spam e-mail โดยใช้ข้อมูลจริงจาก HP Labs และ algorithm ยอดนิยมที่เราจะสอนวันนี้คือ Naive Bayes

Install Packages

tutorial วันนี้ใช้ 3 packages ในตารางด้านล่าง สามารถติดตั้ง package ง่ายๆใน RStudio ด้วยฟังชั่น install.packages()

packagesใช้ทำอะไร?
kernlabโหลดข้อมูล spam e-mail database
naivebayesฟังชั่น naivebayes() เพื่อสร้าง Naive Bayes Classifier
dplyrทำ data wrangling ง่ายๆกับข้อมูล
# install packages
install.packages(c("kernlab", "naivebayes", "dplyr"))

# load library
library(kernlab)
library(naivebayes)
library(dplyr)

Spam or Ham

ฝรั่งมีศัพท์ slang ไว้ใช้เรียก email ดีว่า “ham” ส่วน email ไม่ดีเรียก “spam” ก่อนจะเริ่มเขียนโมเดล ขั้นตอนแรกของการทำ data analysis ใน R คือ exploratory data analysis เรียกสั้นๆว่า EDA

ขอบคุณรูปสวยๆจาก Unsplash
ชื่อฟังชั่นใช้ทำอะไร?
as.tbl()เปลี่ยน data frame ทั่วไปเป็น tibble ที่แสดงผลใน console ดีขึ้น
glimpse()แสดงชื่อคอลั่มและ data type รวมถึง dimenstion ของ data frame
table()สร้างตารางความถี่ ใช้กับตัวแปร factor ใน R
complete.cases()ตรวจสอบว่า data frame ของเรามี missing value หรือเปล่า?

ปกติแอดจะใช้ฟังชั่นนี้กับข้อมูลใหม่ที่เราต้องวิเคราะห์ เช่น ดูชื่อคอลั่มและ dimension ของ data frame เช็คพวก missing value และดูการกระจายตัวของ column ที่เราต้องการทำนายในตัวอย่างวันนี้คือ spam$type

# load dataset into RStudio
data("spam")

# tibble dataframe
spam <- as.tbl(spam)
glimpse(spam) # 58 variables, 4601 records

# type variable
table(spam$type)
table(spam$type) / nrow(spam) # imbalanced classes

# any missing value
mean(complete.cases(spam)) # no missing value

สำหรับ spam database เป็นข้อมูลที่สะอาดมากพร้อมใช้งาน มีทั้งหมด 4601 rows, 58 columns โดยคอลั่มที่เราต้องกสนทำนายคือ type {nonspam, spam} ส่วนฟีเจอร์ที่เราใช้เป็น predictors คือคำต่างๆที่อยู่ในอีเมล์ เช่น make, all, over, internet, order, mail, credit เป็นต้น

สิ่งที่ต้องโน้ตไว้คือปัญหา spam เป็นแบบ imbalanced classes แปลว่าสัดส่วน ham และ spam ไม่เท่ากับ 50:50 โดยข้อมูลส่วนใหญ่ 60.59% เป็น ham ทำให้ผลทำนายของโมเดลเราจะเอียงไปทาง class ที่ใหญ่กว่านิดหน่อย

Intro to Naive Bayes

Naive Bayes ใช้หลักการ conditional probability และ Bayes theorem ในการสร้าง spam classifier โค้ดด้านล่างใช้ฟังชั่น tapply() คำนวณ conditional probability ของ word ที่อยู่ใน nonspam vs. spam email

  • p(word | spam)
  • p(word | nonspam)
# average frequency of these words split by type (nonspam/ spam)
tapply(spam$make, spam$type, mean)
tapply(spam$email, spam$type, mean)

ใช้ Bayes เพื่อทำ inverse probability เปลี่ยนจาก p(word|spam) เป็น p(spam|word) ด้วยสมการนี้

# Bayes theorem
# p(spam | word) = p(word | spam) * p(spam) / p(word)

เทคนิคสำคัญของ naive bayes algorithm คือการ apply ทฤษฏี Bayes กับ all words ใน spam data เราสามารถคำนวณ conditional probability ของ spam และ non spam ได้แบบนี้

# probablity of spam given all words in email
# p(spam | word1, word2, word3, word4, ...)

# probability of nonspam given all words in email
# p(nonspam | word1, word2, word3, word4, ...)

เสร็จแล้วแค่เปรียบเทียบความน่าจะเป็นว่า email ฉบับนั้นมี % เป็น spam หรือ nonspam แบบไหนมากกว่ากัน? ถ้า p(spam) > p(nonspam) เราจะ predict spam ตรงๆเลย!

Split and Train

ขั้นตอนแรกก่อนที่จะสร้างโมเดลคือการแบ่ง spam data เป็นสองส่วนคือ train_set 80% และ test_set 20% แล้วค่อย fit model กับ train_set ด้วยฟังชั่น naive_bayes(type ~ .)

"type ~ ." เรียกว่า formula ในภาษา R โดยตัวแปรที่เราต้องการทำนายคือ type เป็นฟังชั่นของตัวแปรทั้งหมดที่อยู่ใน data frame ( . เป็น shortcut ใช้แทน x1 + x2 + x3 + ... + xn ใน dataframe)

ขอบคุณรูปสวยๆจาก Unsplash
# split dataset
set.seed(99)
id <- sample(nrow(spam), 0.8 * nrow(spam))
train_set <- spam[id, ]
test_set <- spam[-id, ]

# train model
nb_model <- naive_bayes(type ~ ., data = train_set, laplace = 2)

พอสร้างโมเดลเสร็จแล้ว ขั้นตอนต่อไปคือเอา nb_model ไปลองทำนาย train_set และ test_set เพื่อดู model accuracy โดยเราจะวัดผลโมเดลของเราด้วยข้อมูล test_set ที่เราเตรียมไว้

Functional Programming in R

หัวใจของภาษา R คือการเขียนฟังชั่น หรือที่โปรแกรมเมอร์เรียกกันว่า “Functional Programming” แทนที่เราจะนั่งเขียนโค้ดแล้วกดรันทีละบรรทัด เราสามารถ wrap โค้ดทั้งหมดของเราใน user defined function ได้เลย

function ที่เราจะเขียนชื่อว่า nb_summary() รับได้ 3 arguments ประกอบด้วย

  • nb_model คือโมเดลที่เราสร้างจากฟังชั่น naive_bayes()
  • train_set
  • test_set

output ที่ได้จากฟังชั่น nb_summary() คือ model accuracy ของทั้ง train_set และ test_set ดูว่าโมเดลเราทำนาย spam email ได้แม่นยำขนาดไหน โค้ดด้านล่างเราใช้ฟังชั่น cat() เพื่อแสดงผลใน console

nb_summary <- function(nb_model, train_set, test_set){
   # compute train and test accuracy
   train_acc <- mean(predict(nb_model) == train_set$type)
   test_acc <- mean(predict(nb_model, newdata = test_set) == test_set$type)

   # print accuracy
   cat("===== Model Summary =====\n")
   cat("train accuracy: ", round(train_acc*100, 2), "%", sep = "")
   cat("\n")
   cat(" test accuracy: ", round(test_acc*100, 2), "%", sep = "")
}

Test Our Function

ทดสอบฟังชั่น nb_summary() กับโมเดลและข้อมูล train, test ที่เราเตรียมไว้

ขอบคุณรูปสวยๆจาก Unsplash
# see the accuracy of train and test data
nb_summary(nb_model, train_set, test_set)

ตารางด้านล่างแสดงผล model accuracy ที่ได้จากฟังชั่น nb_summary() ทั้ง train_set และ test_set ได้ accuracy อยู่ที่ประมาณ ~70% ถือว่ากลางๆ ยังมีอีกหลายโมเดลที่ทำงานได้ดีกว่านี้ เช่น decision tree, random forest หรือ logistic regression

Accuracy
train accuracy70.54%
test accuracy69.71%

Optional Reading

ถ้าใครยังไม่คุ้นเคยกับ Bayes Theorem เท่าไร ลองอ่านบทความแนะนำของเราได้ที่นี่

Full Script

สำหรับเพื่อนๆที่อยากได้ R Script ไปศึกษาต่อแบบเต็มๆ ลองดูได้ที่ gist ด้านล่างเลย

# spam or ham
# load library
library(kernlab)
library(naivebayes)
library(dplyr)
# load dataset into RStudio
data("spam")
# tibble dataframe
spam <- as.tbl(spam)
glimpse(spam) # 58 variables, 4601 records
# type variable
table(spam$type)
table(spam$type) / nrow(spam) # imbalanced classes
# any missing value
mean(complete.cases(spam)) # no missing value
# split dataset
set.seed(99)
id <- sample(nrow(spam), .8*nrow(spam))
train_set <- spam[id, ]; nrow(train_set)
test_set <- spam[-id, ]; nrow(test_set)
# train model
nb_model <- naive_bayes(type ~ ., data = train_set, laplace = 2)
# create a summary function
nb_summary <- function(nb_model, train_set, test_set){
train_acc <- mean(predict(nb_model) == train_set$type)
test_acc <- mean(predict(nb_model, newdata = test_set) == test_set$type)
cat("===== Model Summary =====\n")
cat("train accuracy: ", round(train_acc*100, 2), "%", sep = "")
cat("\n")
cat(" test accuracy: ", round(test_acc*100, 2), "%", sep = "")
}
# see the accuracy of train and test data
nb_summary(nb_model, train_set, test_set)
view raw spam_or_ham.R hosted with ❤ by GitHub

Leave a Reply