# untuk persiapan data
library(dplyr)
library(rsample)

# untuk text processing
library(tm)
library(SnowballC) 
library(inspectdf)

# untuk keperluan machine learning
library(e1071)
library(caret)
library(ROCR)

Text Classification

Dalam pembelajaran kali ini, ita akan memanfaatkan data free text sebagi inputan data untuk melatihan model kita. Agar kita dapat melakukan hal tersebut kita pertama-tama perlu untuk mempelajari sebuah konsep persiapan data teks yang disebut dengan Text Mining.

Text Mining

Text Mining adalah salah satu metode analisis data yang fokus utamanya adalah mencari informasi dan pola-pola dari data yang tidak terstruktur (unstructured), yaitu data teks.

Data teks disebut tidak terstruktur karena:

  • Satu kalimat terdiri dari beberapa kata yang jumlahnya berbeda-beda tiap kalimat.
  • Adanya typo (salah ketik), penyingkatan kata (you menjadi u), ataupun simbol-simbol tidak bermakna sehingga perlu dilakukan cleansing.
  • Adanya perbedaan bahasa yang digunakan sehingga perlu mencari kosa kata yang cocok.

Study Case: Spam Classifier

Business Question: Berdasarkan kata-kata pada SMS, kita ingin melakukan klasifikasi apakah suatu SMS termasuk spam atau bukan (ham) sehingga nantinya SMS spam akan diletakkan pada folder spam.

  • Kelas positif: …
  • Kelas negatif: …

Read Data

sms_raw <- read.csv("data_input/spam.csv")
sms <- sms_raw %>% 
  mutate(label = as.factor(label))

head(sms)

EDA

Dikarenakan kita ingin melakukan klasifikasi teks yang dianggap Spam atau Ham, sebelum itu, mari kita coba melihat kata-kata apa saja yang biasanya muncul di teks Spam.

# Mengambil 5 data paling atas
sms %>% 
  filter(label == "spam") %>% 
  pull(text) %>% # Mengambil kolom dan mengubah jadi bentuk vector
  head(5)
## [1] "Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's"  
## [2] "FreeMsg Hey there darling it's been 3 week's now and no word back! I'd like some fun you up for it still? Tb ok! XxX std chgs to send,  1.50 to rcv"          
## [3] "WINNER!! As a valued network customer you have been selected to receive a 900 prize reward! To claim call 09061701461. Claim code KL341. Valid 12 hours only."
## [4] "Had your mobile 11 months or more? U R entitled to Update to the latest colour mobiles with camera for Free! Call The Mobile Update Co FREE on 08002986030"   
## [5] "SIX chances to win CASH! From 100 to 20,000 pounds txt> CSH11 and send to 87575. Cost 150p/day, 6days, 16+ TsandCs apply Reply HL 4 info"

Kata-kata yang sering muncul di kalimat Spam:

Text Cleansing

Kenapa kita harus melakukan text cleansing? setiap kata akan menjadi predictor/kolom sehingga dibersihkan terlebih dahulu untuk komputasi yang lebih ringan dan terstandarisasi.

Text to Corpus

Tahapan pertama yang perlu kita lakukan adalah mengubah teks menjadi bentukan corpus. Corpus itu sendiri merupakan bentukan tipe data untuk mengumpulkan beberapa data menjadi satu dokumen.

Disclaimer, hal ini perlul dilakukan karena kita menggunakan library tm dalam proses pembersihan data.

Fungsi yang akan digunakan adalah VectorSource() dilanjutkan dengan fungsi VCorpus()

# ubah format menjadi corpus
sms_corpus <- sms$text %>% VectorSource() %>% VCorpus()
sms_corpus
## <<VCorpus>>
## Metadata:  corpus specific: 0, document level (indexed): 0
## Content:  documents: 5572
# Mengambil konten dari hasil data yang sudah diubah
sms_corpus[[9]]$content
## [1] "WINNER!! As a valued network customer you have been selected to receive a 900 prize reward! To claim call 09061701461. Claim code KL341. Valid 12 hours only."

Remove numbers

Menghapus angka -> Untuk karakter angka umumnya tidak bermakna ketika melakukan klasifikasi data. Oleh karena itu kita menghapus semua angka pada data text dengan removeNumbers()

removeNumbers("Nomer HP saya 08123456789")
## [1] "Nomer HP saya "
# Remove numbers
sms_clean <- sms_corpus %>% tm_map(removeNumbers)

sms_clean[[9]]$content
## [1] "WINNER!! As a valued network customer you have been selected to receive a  prize reward! To claim call . Claim code KL. Valid  hours only."

Remove Punctuation

Asalan menghilangkan tanda baca, sama saja dengan alasan menghilangkan angka pada sebuah teks.

Menghilangkan tanda baca menggunakan removePunctuation. Tanda baca yang dihilangkan: ! ’ # S % & ’ ( ) * + , - . / : ; < = > ? @ [ / ] ^ _ { | } ~

removePunctuation("I am very angry!")
## [1] "I am very angry"
# Remove punctuation
sms_clean <- sms_clean %>% tm_map(removePunctuation)

sms_clean[[9]]$content
## [1] "WINNER As a valued network customer you have been selected to receive a  prize reward To claim call  Claim code KL Valid  hours only"

Case-folding

Mengubah semua text menjadi lowercase dengan fungsi tolower()

tolower("WINNER")
## [1] "winner"
# Case-folding to lowercase
sms_clean <- sms_clean %>% tm_map(content_transformer(tolower))

sms_clean[[9]]$content
## [1] "winner as a valued network customer you have been selected to receive a  prize reward to claim call  claim code kl valid  hours only"
  • Gunakan function content_transformer() jika fungsi yang ingin diterapkan bukan dari package tm

Remove Stopwords

Menghapus kata yang sering muncul di corpus dan biasanya tidak meaningful dengan removeWords(). (contoh stopwords bahasa Inggris: “the”, “to”, “was”, etc.)

# Before remove stopwords
sms_clean[[9]]$content
## [1] "winner as a valued network customer you have been selected to receive a  prize reward to claim call  claim code kl valid  hours only"
# Remove stopwords
sms_clean <- sms_clean %>% tm_map(removeWords, stopwords("en"))

sms_clean[[9]]$content
## [1] "winner   valued network customer    selected  receive   prize reward  claim call  claim code kl valid  hours "

Stemming

Stemming atau bisa diartikan sebagai, pemotongan kata menjadi kata dasarnya menggunakan stemDocument.

stemDocument() akan menghapus imbuhan kata “ing”, “e”, “ed”, “er”, “s”, “es”, dll

Mengapa kata imbuhan dihapuskan? Karena yang dibutuhkan dalam text mining ini adalah mendapatkan beberapa kata-kata yang muncul atau tidak muncul dalam suatu sekumpulan text. Kata-kata tersebut akan digunakan sebagai prediktor untuk memprediksi apakah sms masuk kedalam “spam” atau “ham”.

stemDocument("winners")
## [1] "winner"
# stemming
sms_clean <- sms_clean %>% tm_map(stemDocument)

sms_clean[[9]]$content
## [1] "winner valu network custom select receiv prize reward claim call claim code kl valid hour"

Menghapus whitespace berlebih menggunakan stripWhitespace.

Hal ini diperlukan karena pada proses tokenizing (selanjutnya), kata akan dipotong berdasarkan karakter spasi.

sms_clean <- sms_clean %>% tm_map(stripWhitespace)

sms_clean[[9]]$content
## [1] "winner valu network custom select receiv prize reward claim call claim code kl valid hour"

Additional Notes:

  • Step-step di atas merupakan step-step yang umum untuk text processing. Apabila dibutuhkan, kita dapat menambah atau mengurangi beberapa step, tergantung pada kompleksitas data yang dibersihkan.

Summary singkat, secara umum tahapan yang sering dilakukan untuk text cleansing adalah:

  • Case-folding
  • Remove numbers
  • Remove stopwords: kata - kata yang tidak bermakna
  • Remove punctuation
  • Stemming
  • Remove white space

Data Preprocessing

Setelah berhasil melakukan proses pembersihan data, kita akan masuk ke tahapan data preprocessing agar nantinya data yang sudah bersih dapat diolah lebih lanjut oleh model machine learning.

Document-Term Matrix (DTM)

Sampai di tahap ini, data kita masih berupa text. Pertanyaannya bagaimana cara model kita belajar apabila prediktornya masih berupa text?

Kita perlu melakukan transformasi data text menjadi Document-Term Matrix (DTM) melalu proses tokenization. Tokenization adalah proses memecah satu kalimat menjadi beberapa term (bisa berupa 1 kata, pasangan kata, dll). Dalam DTM, 1 kata akan menjadi 1 prediktor dengan nilai berupa frekuensi kemunculan kata tersebut dalam sebuah dokumen.

Gunakan fungsi DocumentTermMatrix() untuk membuat DTM dan fungsi inspect() untuk melihat hasil DTM

# ubah menjadi DTM
sms_dtm <- DocumentTermMatrix(sms_clean)

inspect(sms_dtm)
## <<DocumentTermMatrix (documents: 5572, terms: 6822)>>
## Non-/sparse entries: 43465/37968719
## Sparsity           : 100%
## Maximal term length: 40
## Weighting          : term frequency (tf)
## Sample             :
##       Terms
## Docs   call can come dont free get just ltgt now will
##   1085    0   0    1    1    0   1    0    0   0    9
##   1579    0   0    0    0    0   0    0   18   0    0
##   1863    0   0    0    3    0   0    0    0   0    0
##   2158    0   0    0    0    0   0    0    0   0    0
##   2370    0   0    0    0    1   0    0    0   0    0
##   2380    0   1    0    0    0   0    0    1   0    0
##   2434    0   3    0    0    1   1    0    6   0    0
##   2848    0   0    0    0    0   0    0    0   0    0
##   3016    0   0    0    0    0   0    0    2   0    0
##   5105    0   0    0    0    1   0    0    0   0    0

Istilah:

  • documents: SMS
  • terms: kata yang unique di seluruh SMS kita
  • non-sparse: nilai yang bukan 0 pada matrix
  • sparse: nilai yang 0 pada matrix

Pembuktian

Mari kita amati SMS ke 5105 yang sudah di-cleansing, kemudian dapat kita konfirmasi bahwa kata “free” apakah muncul sebanyak 1 kali?

sms_clean[[5105]]$content
## [1] "boy love gal propsd bt didnt mind gv lv lttrs bt frnds threw thm d boy decid aproach d gal dt time truck speed toward d gal wn hit d girld boy ran like hell n save ask hw cn u run fast d boy repli boost d secret energi n instant d girl shout energi n thi live happili gthr drink boost evrydi moral d stori hv free msgsd gud ni"

Cross Validation

Tahapan selanjutnya adalah kita akan memisahkan ke data train dan data test.

RNGkind(sample.kind = "Rounding")
set.seed(100)

# train-test splitting
index <- sample(nrow(sms_dtm), nrow(sms_dtm)*0.75)

# sms_dtm = DocumentTermMatrix yang tidak ada labelnya
sms_train_x <- sms_dtm[index,]
sms_test_x <- sms_dtm[-index,]

Siapkan juga label untuk targetnya:

# label untuk train dan test, tersimpan pada dataframe sms
sms_train_y <- sms[index, "label"]
sms_test_y <- sms[-index, "label"]

Cek proporsi kelas target pada sms_train_y:

prop.table(table(sms_train_y))
## sms_train_y
##       ham      spam 
## 0.8631251 0.1368749

Remove Infrequent Words

Cek dimensi sms_train yang akan digunakan untuk pembuatan model:

# cek dimensi (dim)
dim(sms_train_x)
## [1] 4179 6822

Dari dimensi yang ditampilkan, prediktor yang digunakan cukup banyak. Ketika prediktor yang sangat banyak, bisa menyebabkan “noise” yang menggangu peforma model, maka dari itu kita bisa mengsedeharnakannya dengan menggunakan prediktor yang setidaknya muncul minimal 20 kali. Fungsi yang digunakan adalah findFreqTerms()

# sms_train_x dari sms_dtm
sms_freq <- findFreqTerms(sms_train_x, lowfreq = 20)
  
length(sms_freq)
## [1] 348

Note: Penentuan lowfreq = 20 tidak mutlak dan dapat diubah-ubah untuk feature selection. Perlu diketahui: Semakin besar lowfreq, semakin sedikit terms yang kita gunakan sebagai feature/predictor.

Mari subset data sms_train hanya untuk kata-kata yang muncul di sms_freq:

sms_train_x <- sms_train_x[, sms_freq] # terms letaknya di kolom
inspect(sms_train_x)
## <<DocumentTermMatrix (documents: 4179, terms: 348)>>
## Non-/sparse entries: 18787/1435505
## Sparsity           : 99%
## Maximal term length: 10
## Weighting          : term frequency (tf)
## Sample             :
##       Terms
## Docs   call can come dont free get just ltgt now will
##   1085    0   0    1    1    0   1    0    0   0    9
##   1579    0   0    0    0    0   0    0   18   0    0
##   1863    0   0    0    3    0   0    0    0   0    0
##   2010    0   0    0    2    0   0    0    0   0    0
##   2134    1   0    0    1    0   0    2    0   0    1
##   2158    0   0    0    0    0   0    0    0   0    0
##   2380    0   1    0    0    0   0    0    1   0    0
##   2434    0   3    0    0    1   1    0    6   0    0
##   2848    0   0    0    0    0   0    0    0   0    0
##   2945    0   1    1    2    0   1    0    1   1    1

Bernoulli Converter

Model Naive Bayes itu lebih baik ketika menghadapi data kategorikal

Nilai pada matrix sms_train masih berupa frekuensi. Untuk perhitungan peluang, frekuensi akan diubah menjadi hanya kondisi muncul (1) atau tidak (0). Salah satu caranya dengan menggunakan Bernoulli Converter.

  • Jika frekuensi > 0, maka bernilai 1 (muncul)
  • Jika frekuensi == 0, maka bernilai 0 (tidak muncul)
bernoulli_conv <- function(x){
  # parameter ifelse: kondisi, Hasil jika Kondisi TRUE, Hasil jika Kondisi FALSE
  x <- as.factor(ifelse(x > 0, 1, 0)) 
  return(x)
}

# testing fungsi
bernoulli_conv(c(3,0,0,1,4,0))
## [1] 1 0 0 1 1 0
## Levels: 0 1

Selanjutnya, terapkan bernoulli_conv ke sms_train dan sms_test:

sms_train_bn <- apply(X = sms_train_x, 
                      MARGIN = 2, 
                      FUN = bernoulli_conv)

sms_test_bn <- apply(X = sms_test_x, 
                     MARGIN = 2, 
                     FUN = bernoulli_conv)
  • MARGIN = 1 -> mengaplikasikan FUN by baris
  • MARGIN = 2 -> mengaplikasikan FUN by kolom, karena kita ingin tetap matrix berupa DocumentTermMatrix

Model

Model Fitting

Salah satu model yang cukup baik dalam melakukan prediksi klasifikasi untuk kasus teks adalah Naive Bayes. Maka dari itu, mari kita coba untuk implementasikan.

# Train Model
model_nb_spam <- naiveBayes(x = sms_train_bn,
                            y = sms_train_y,
                            laplace = 1)

Model Prediction

Prediksi kelas target pada sms_test_bn. Simpan ke objek sms_pred_class, akan digunakan untuk mengevaluasi dengan confusion matrix.

# predict
sms_pred_class <- predict(model_nb_spam,
                          sms_test_bn,
                          type = "class")
sms_pred_class[0:5]
## [1] ham  spam ham  ham  ham 
## Levels: ham spam

Confusion Matrix

Evaluasi model naive_spam menggunakan confusion matrix dan metric-metric yang ada:

Fungsi: confusionMatrix(data, reference)

  • data: Data prediksi -> data test -> sms_pred_class
  • reference: Data aktual -> sms_test_y
  • positive = “spam”
confusionMatrix(data = sms_pred_class, 
                reference = sms_test_y,
                positive = "spam")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  ham spam
##       ham  1202   28
##       spam   16  147
##                                               
##                Accuracy : 0.9684              
##                  95% CI : (0.9578, 0.977)     
##     No Information Rate : 0.8744              
##     P-Value [Acc > NIR] : < 0.0000000000000002
##                                               
##                   Kappa : 0.8519              
##                                               
##  Mcnemar's Test P-Value : 0.09725             
##                                               
##             Sensitivity : 0.8400              
##             Specificity : 0.9869              
##          Pos Pred Value : 0.9018              
##          Neg Pred Value : 0.9772              
##              Prevalence : 0.1256              
##          Detection Rate : 0.1055              
##    Detection Prevalence : 0.1170              
##       Balanced Accuracy : 0.9134              
##                                               
##        'Positive' Class : spam                
##