This is a little implementation that can filter out spam from sms messages. The dataset is provided and made available to public under SMS Spam Collection, and can be downloaded at http://www.dt.fee.unicamp.br/~tiago/smsspamcollection/ . This dataset contains a bunch of sms messages. Junk messages have been labled as “spam” and legit messages as “ham”. The task is to train a Bayesian machine learning system to filter out “spam” sms.

Step 1: Reading in the dataset

# NOTE: the dataset has been modified slightly so that it can be easier to work with
sms_raw <- read.csv('sms_spam.csv', stringsAsFactors = FALSE)

Step 2: Exploring and preparing data

str(sms_raw) # structure of dataset
'data.frame':   5559 obs. of  2 variables:
 $ type: chr  "ham" "ham" "ham" "spam" ...
 $ text: chr  "Hope you are having a good week. Just checking in" "K..give back my thanks." "Am also doing in cbe only. But have to pay." "complimentary 4 STAR Ibiza Holiday or £10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out"| __truncated__ ...
sms_raw$type <- factor(sms_raw$type) # convert $type into factor variable
str(sms_raw) # structure of dataset
'data.frame':   5559 obs. of  2 variables:
 $ type: Factor w/ 2 levels "ham","spam": 1 1 1 2 2 1 1 1 2 1 ...
 $ text: chr  "Hope you are having a good week. Just checking in" "K..give back my thanks." "Am also doing in cbe only. But have to pay." "complimentary 4 STAR Ibiza Holiday or £10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out"| __truncated__ ...
table(sms_raw$type)

 ham spam 
4812  747 
#install.packages("tm", dependencies = TRUE)
#install.packages("SnowballC") # this package help in stemming words; for ex: it makes "installed"", "installing"", and "installs"" become just "install""
library(tm)
Loading required package: NLP
library(SnowballC)
sms_corpus <- VCorpus(VectorSource(sms_raw$text)) # create a corpus for the text
print(sms_corpus)
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 5559
inspect(sms_corpus[1:2])
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 0
Content:  documents: 2

[[1]]
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 49

[[2]]
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 23
lapply(sms_corpus[1:2], as.character) # print out first 2 sms text
$`1`
[1] "Hope you are having a good week. Just checking in"

$`2`
[1] "K..give back my thanks."
# We begin cleaning and standardizing the text messages in our corpus
# +transform all raw text to lowercase
sms_corpus_clean <- tm_map(sms_corpus, content_transformer(tolower))
lapply(sms_corpus[1:2], as.character) # print out first 2 sms text
$`1`
[1] "Hope you are having a good week. Just checking in"

$`2`
[1] "K..give back my thanks."
lapply(sms_corpus_clean[1:2], as.character) # print out first 2 sms text
$`1`
[1] "hope you are having a good week. just checking in"

$`2`
[1] "k..give back my thanks."
# + remove numbers from our corpus texts
sms_corpus_clean <- tm_map(sms_corpus_clean, removeNumbers)
# +remove stopwords sice they reveal little information about a text
sms_corpus_clean <- tm_map(sms_corpus_clean, removeWords, stopwords("english"))
# + remove punctuation
sms_corpus_clean <- tm_map(sms_corpus_clean, removePunctuation)
# + stems words in sms text to their root
sms_corpus_clean <- tm_map(sms_corpus_clean, stemDocument)
# + now remove whitespaces left behind from the cleaning process above
sms_corpus_clean <- tm_map(sms_corpus_clean, stripWhitespace)
lapply(sms_corpus[1:10], as.character)
$`1`
[1] "Hope you are having a good week. Just checking in"

$`2`
[1] "K..give back my thanks."

$`3`
[1] "Am also doing in cbe only. But have to pay."

$`4`
[1] "complimentary 4 STAR Ibiza Holiday or £10,000 cash needs your URGENT collection. 09066364349 NOW from Landline not to lose out! Box434SK38WP150PPM18+"

$`5`
[1] "okmail: Dear Dave this is your final notice to collect your 4* Tenerife Holiday or #5000 CASH award! Call 09061743806 from landline. TCs SAE Box326 CW25WX 150ppm"

$`6`
[1] "Aiya we discuss later lar... Pick u up at 4 is it?"

$`7`
[1] "Are you this much buzy"

$`8`
[1] "Please ask mummy to call father"

$`9`
[1] "Marvel Mobile Play the official Ultimate Spider-man game (£4.50) on ur mobile right now. Text SPIDER to 83338 for the game & we ll send u a FREE 8Ball wallpaper"

$`10`
[1] "fyi I'm at usf now, swing by the room whenever"
lapply(sms_corpus_clean[1:10], as.character)
$`1`
[1] "hope good week just check"

$`2`
[1] "kgive back thank"

$`3`
[1] " also cbe pay"

$`4`
[1] "complimentari star ibiza holiday â cash need urgent collect now landlin lose boxskwpppm"

$`5`
[1] "okmail dear dave final notic collect tenerif holiday cash award call landlin tcs sae box cwwx ppm"

$`6`
[1] "aiya discuss later lar pick u "

$`7`
[1] " much buzi"

$`8`
[1] "pleas ask mummi call father"

$`9`
[1] "marvel mobil play offici ultim spiderman game â ur mobil right now text spider game ll send u free ball wallpap"

$`10`
[1] "fyi usf now swing room whenev"
We have to create a Document-Term-Matrix which is basically a table that has the text messages as rows and thw words in the corpus as column.
sms_dtm <- DocumentTermMatrix(sms_corpus_clean)
inspect(sms_dtm[1:30,815:825]) # take a peek at the new DTM
<<DocumentTermMatrix (documents: 30, terms: 11)>>
Non-/sparse entries: 4/326
Sparsity           : 99%
Maximal term length: 11
Weighting          : term frequency (tf)

    Terms
Docs california call callback callcost callcoz calld calldrov caller callertun callfreefon callin
  1           0    0        0        0       0     0        0      0         0           0      0
  2           0    0        0        0       0     0        0      0         0           0      0
  3           0    0        0        0       0     0        0      0         0           0      0
  4           0    0        0        0       0     0        0      0         0           0      0
  5           0    1        0        0       0     0        0      0         0           0      0
  6           0    0        0        0       0     0        0      0         0           0      0
  7           0    0        0        0       0     0        0      0         0           0      0
  8           0    1        0        0       0     0        0      0         0           0      0
  9           0    0        0        0       0     0        0      0         0           0      0
  10          0    0        0        0       0     0        0      0         0           0      0
  11          0    0        0        0       0     0        0      0         0           0      0
  12          0    0        0        0       0     0        0      0         0           0      0
  13          0    1        0        0       0     0        0      0         0           0      0
  14          0    0        0        0       0     0        0      0         0           0      0
  15          0    0        0        0       0     0        0      0         0           0      0
  16          0    0        0        0       0     0        0      0         0           0      0
  17          0    0        0        0       0     0        0      0         0           0      0
  18          0    0        0        0       0     0        0      0         0           0      0
  19          0    0        0        0       0     0        0      0         0           0      0
  20          0    0        0        0       0     0        0      0         0           0      0
  21          0    0        0        0       0     0        0      0         0           0      0
  22          0    0        0        0       0     0        0      0         0           0      0
  23          0    0        0        0       0     0        0      0         0           0      0
  24          0    0        0        0       0     0        0      0         0           0      0
  25          0    0        0        0       0     0        0      0         0           0      0
  26          0    0        0        0       0     0        0      0         0           0      0
  27          0    1        0        0       0     0        0      0         0           0      0
  28          0    0        0        0       0     0        0      0         0           0      0
  29          0    0        0        0       0     0        0      0         0           0      0
  30          0    0        0        0       0     0        0      0         0           0      0
# split our DocumentTermMatrix into test and training dataset
sms_dtm_train <- sms_dtm[1:4169, ]
sms_dtm_test <- sms_dtm[4170:5559, ]
# also append a labels column from the raw dataframe
sms_train_labels <- sms_raw[1:4169, ]$type
sms_test_labels <- sms_raw[4170:5559, ]$type
prop.table(table(sms_train_labels)) # show percentage of ham and spam in training dataset
sms_train_labels
      ham      spam 
0.8647158 0.1352842 
prop.table(table(sms_test_labels)) # show percentage of ham and spam in test dataset
sms_test_labels
      ham      spam 
0.8683453 0.1316547 
A word clouds is a visualiztion technique that display words randomly or orderedly in a cloud, words with higher frequency will appear lager than the ones with lower frequency
#install.packages("wordcloud")
library(wordcloud)
Loading required package: RColorBrewer
wordcloud(sms_corpus_clean, min.freq = 50, scale = c(4,.4), random.order = FALSE)

It’s also very helpful to compare the wordclouds between spam and ham messages

spam <- subset(sms_raw, type == "spam")
ham <- subset(sms_raw, type == "ham")
wordcloud(spam$text, min.freq = 20, scale = c(4,0.3), random.order = FALSE)

wordcloud(ham$text, min.freq = 40, scale = c(4,0.1), random.order = FALSE)

As of right now, we have over 6500 features, each feature for each word that appear in the DTM. However, some words that appear in less than 5 messages (or 5/5999 = 0.1%) and do not make a strong feature indicator; these are the words that we should exclude out of our DTM to simplify the DTM.
sms_fre_words <- findFreqTerms(sms_dtm_train, 5) # find terms that has at least 5 appearances
str(sms_fre_words) # let's see how many frequent words we have
 chr [1:1139] "â‚“""| __truncated__ "abiola" "abl" "abt" "accept" "access" "account" "across" "act" "activ" ...

So we have reduced the number of features from over 6500 down to 1139 by only accepting words with frequency = 5 of higher. Now we use this chr vector to filter out our DTM

sms_dtm_freq_train <- sms_dtm_train[ , sms_fre_words]
sms_dtm_freq_test <- sms_dtm_test[ , sms_fre_words]

Since Bayes classifier works on categorical dataset and our test and train dataset contains numeric values, we have to covnert these dataset into categorical variable by setting the variables to “yes”" or “no” depending on if it appear in each message.

convert_counts <- function(x){
  x <- ifelse(x >0, "Yes", "No") # if the word appear in a text message, then change its value to Yes, otherwise, No
}
sms_train <- apply(sms_dtm_freq_train, MARGIN = 2, convert_counts)
sms_test <- apply(sms_dtm_freq_test, MARGIN = 2, convert_counts)

Step 3: Training Naive Bayes Classifier

This Naive Bayes classifier will estimate the probability of whether a text message is a spam or ham base on the presence and absence of words in a text. The Naive Bayes classifier used is from ‘e1071’ package developed by Vienna University of Technology

#install.packages('e1071')
library(e1071)
sms_classifier <- naiveBayes(sms_train, sms_train_labels)

Step 4: Evaluating the performance of our Naive Bayes classifier

# perform prediction on the test dataset using the resulted Naive Bayes classifier
sms_test_pred <- predict(sms_classifier, sms_test)
# elvaluate model's performance with CrossTable
library(gmodels)
CrossTable(sms_test_pred, sms_test_labels, prop.chisq = FALSE, prop.t = FALSE, dnn = c("Predicted", "Actual"))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Row Total |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | Actual 
   Predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1201 |        30 |      1231 | 
             |     0.976 |     0.024 |     0.886 | 
             |     0.995 |     0.164 |           | 
-------------|-----------|-----------|-----------|
        spam |         6 |       153 |       159 | 
             |     0.038 |     0.962 |     0.114 | 
             |     0.005 |     0.836 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|

 

==> As we can see, our Naive Bayes classifier predicted actual spam messages as “spam” with accuracy of 96.2%, and actual ham messages as “ham” with accuracy of 97.6% !!! It did incorrectly predicted text messages 36/1390 = 2.58%. Not a bad record for our little spam text classifier :-)

Step 4: Improving our Naive Bayes classifier performance

# using laplace estimator to improve accuracy
sms_classifier2 <- naiveBayes(sms_train, sms_train_labels, laplace = 1)
sms_test_pred2 <- predict(sms_classifier2, sms_test)
CrossTable(sms_test_pred2, sms_test_labels, prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE, dnn = c("Predicted", "Actual"))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | Actual 
   Predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1202 |        28 |      1230 | 
             |     0.996 |     0.153 |           | 
-------------|-----------|-----------|-----------|
        spam |         5 |       155 |       160 | 
             |     0.004 |     0.847 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|

 

==> By setting laplace estimator to 1, we were able to improve the accuracyof our Naive Bayes classifier. Specifically, the number of ham messages incorrectly identifies as “spam” went down from 30 to 28, and the number of spam messages incorrectly identified as “ham” went dowm from 6 to 5.

Let’s try to increase laplace estimator to see how it changes the accuracy + with laplace estimator = 2

sms_classifier3 <- naiveBayes(sms_train, sms_train_labels, laplace = 2)
sms_test_pred3 <- predict(sms_classifier3, sms_test)
CrossTable(sms_test_pred3, sms_test_labels, prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE, dnn = c("Predicted", "Actual"))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | Actual 
   Predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1204 |        34 |      1238 | 
             |     0.998 |     0.186 |           | 
-------------|-----------|-----------|-----------|
        spam |         3 |       149 |       152 | 
             |     0.002 |     0.814 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|

 
sms_classifier4 <- naiveBayes(sms_train, sms_train_labels, laplace = 3)
sms_test_pred4 <- predict(sms_classifier4, sms_test)
CrossTable(sms_test_pred4, sms_test_labels, prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE, dnn = c("Predicted", "Actual"))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | Actual 
   Predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1202 |        40 |      1242 | 
             |     0.996 |     0.219 |           | 
-------------|-----------|-----------|-----------|
        spam |         5 |       143 |       148 | 
             |     0.004 |     0.781 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|

 
sms_classifier5 <- naiveBayes(sms_train, sms_train_labels, laplace = 4)
sms_test_pred5 <- predict(sms_classifier5, sms_test)
CrossTable(sms_test_pred5, sms_test_labels, prop.chisq = FALSE, prop.t = FALSE, prop.r = FALSE, dnn = c("Predicted", "Actual"))

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Col Total |
|-------------------------|

 
Total Observations in Table:  1390 

 
             | Actual 
   Predicted |       ham |      spam | Row Total | 
-------------|-----------|-----------|-----------|
         ham |      1202 |        46 |      1248 | 
             |     0.996 |     0.251 |           | 
-------------|-----------|-----------|-----------|
        spam |         5 |       137 |       142 | 
             |     0.004 |     0.749 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1207 |       183 |      1390 | 
             |     0.868 |     0.132 |           | 
-------------|-----------|-----------|-----------|

 

==> It seems that with laplace estimator = 1, our Naive Bayes classifier is the most accurate. We also observed that, by increasing laplace estimator, the number of spam messagees incorrectly identified as “ham” increases. This shows that it is important to pick the right laplace estimator so that our Naive Bayes classifier does not filter out spam too aggresively (which leads to ham misclassified as “spam”) or too passively (which causes spam misclassified as “ham”)!

Reference:

Machine Learning with R by Brett Lantz.

LS0tDQp0aXRsZTogIkZpbHRlcmluZyBNb2JpbGUgUGhvbmUgU3BhbSBVc2luZyBOYWl2ZSBCYXllc3NpYW4gTWV0aG9kIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOiBkZWZhdWx0DQogIGh0bWxfZG9jdW1lbnQ6IGRlZmF1bHQNCi0tLQ0KDQpUaGlzIGlzIGEgbGl0dGxlIGltcGxlbWVudGF0aW9uIHRoYXQgY2FuIGZpbHRlciBvdXQgc3BhbSBmcm9tIHNtcyBtZXNzYWdlcy4gVGhlIGRhdGFzZXQgaXMgcHJvdmlkZWQgYW5kIG1hZGUgYXZhaWxhYmxlIHRvIHB1YmxpYyB1bmRlciBTTVMgU3BhbSBDb2xsZWN0aW9uLCBhbmQgY2FuIGJlIGRvd25sb2FkZWQgYXQgaHR0cDovL3d3dy5kdC5mZWUudW5pY2FtcC5ici9+dGlhZ28vc21zc3BhbWNvbGxlY3Rpb24vIC4gVGhpcyBkYXRhc2V0IGNvbnRhaW5zIGEgYnVuY2ggb2Ygc21zIG1lc3NhZ2VzLiBKdW5rIG1lc3NhZ2VzIGhhdmUgYmVlbiBsYWJsZWQgYXMgInNwYW0iIGFuZCBsZWdpdCBtZXNzYWdlcyBhcyAiaGFtIi4gVGhlIHRhc2sgaXMgdG8gdHJhaW4gYSBCYXllc2lhbiBtYWNoaW5lIGxlYXJuaW5nIHN5c3RlbSB0byBmaWx0ZXIgb3V0ICJzcGFtIiBzbXMuDQoNCjxoND48dT5TdGVwIDE6IFJlYWRpbmcgaW4gdGhlIGRhdGFzZXQ8L3U+PC9oND4NCmBgYHtyfQ0KIyBOT1RFOiB0aGUgZGF0YXNldCBoYXMgYmVlbiBtb2RpZmllZCBzbGlnaHRseSBzbyB0aGF0IGl0IGNhbiBiZSBlYXNpZXIgdG8gd29yayB3aXRoDQpzbXNfcmF3IDwtIHJlYWQuY3N2KCdzbXNfc3BhbS5jc3YnLCBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpDQpgYGANCg0KPGg0Pjx1PlN0ZXAgMjogRXhwbG9yaW5nIGFuZCBwcmVwYXJpbmcgZGF0YTwvdT48L2g0Pg0KYGBge3J9DQpzdHIoc21zX3JhdykgIyBzdHJ1Y3R1cmUgb2YgZGF0YXNldA0Kc21zX3JhdyR0eXBlIDwtIGZhY3RvcihzbXNfcmF3JHR5cGUpICMgY29udmVydCAkdHlwZSBpbnRvIGZhY3RvciB2YXJpYWJsZQ0Kc3RyKHNtc19yYXcpICMgc3RydWN0dXJlIG9mIGRhdGFzZXQNCnRhYmxlKHNtc19yYXckdHlwZSkNCmBgYA0KDQo8aDU+KyBUbyBwcmVwYXJlIGFuZCBzdGFuZHJhZGl6ZSB0ZXh0LCB3ZSBuZWVkIHRvIGluc3RhbGwgYW5kIGxvYWQgPGVtPid0bSc8L2VtPiBwYWNrYWdlOiA8ZW0+J3RtJzwvZW0+IGlzIHRleHQgbWluaW5nIHBhY2thZ2U8L2g1Pg0KYGBge3J9DQojaW5zdGFsbC5wYWNrYWdlcygidG0iLCBkZXBlbmRlbmNpZXMgPSBUUlVFKQ0KI2luc3RhbGwucGFja2FnZXMoIlNub3diYWxsQyIpICMgdGhpcyBwYWNrYWdlIGhlbHAgaW4gc3RlbW1pbmcgd29yZHM7IGZvciBleDogaXQgbWFrZXMgImluc3RhbGxlZCIiLCAiaW5zdGFsbGluZyIiLCBhbmQgImluc3RhbGxzIiIgYmVjb21lIGp1c3QgImluc3RhbGwiIg0KbGlicmFyeSh0bSkNCmxpYnJhcnkoU25vd2JhbGxDKQ0Kc21zX2NvcnB1cyA8LSBWQ29ycHVzKFZlY3RvclNvdXJjZShzbXNfcmF3JHRleHQpKSAjIGNyZWF0ZSBhIGNvcnB1cyBmb3IgdGhlIHRleHQNCnByaW50KHNtc19jb3JwdXMpDQppbnNwZWN0KHNtc19jb3JwdXNbMToyXSkNCmxhcHBseShzbXNfY29ycHVzWzE6Ml0sIGFzLmNoYXJhY3RlcikgIyBwcmludCBvdXQgZmlyc3QgMiBzbXMgdGV4dA0KDQojIFdlIGJlZ2luIGNsZWFuaW5nIGFuZCBzdGFuZGFyZGl6aW5nIHRoZSB0ZXh0IG1lc3NhZ2VzIGluIG91ciBjb3JwdXMNCg0KIyArdHJhbnNmb3JtIGFsbCByYXcgdGV4dCB0byBsb3dlcmNhc2UNCnNtc19jb3JwdXNfY2xlYW4gPC0gdG1fbWFwKHNtc19jb3JwdXMsIGNvbnRlbnRfdHJhbnNmb3JtZXIodG9sb3dlcikpDQpsYXBwbHkoc21zX2NvcnB1c1sxOjJdLCBhcy5jaGFyYWN0ZXIpICMgcHJpbnQgb3V0IGZpcnN0IDIgc21zIHRleHQNCmxhcHBseShzbXNfY29ycHVzX2NsZWFuWzE6Ml0sIGFzLmNoYXJhY3RlcikgIyBwcmludCBvdXQgZmlyc3QgMiBzbXMgdGV4dA0KIyArIHJlbW92ZSBudW1iZXJzIGZyb20gb3VyIGNvcnB1cyB0ZXh0cw0Kc21zX2NvcnB1c19jbGVhbiA8LSB0bV9tYXAoc21zX2NvcnB1c19jbGVhbiwgcmVtb3ZlTnVtYmVycykNCiMgK3JlbW92ZSBzdG9wd29yZHMgc2ljZSB0aGV5IHJldmVhbCBsaXR0bGUgaW5mb3JtYXRpb24gYWJvdXQgYSB0ZXh0DQpzbXNfY29ycHVzX2NsZWFuIDwtIHRtX21hcChzbXNfY29ycHVzX2NsZWFuLCByZW1vdmVXb3Jkcywgc3RvcHdvcmRzKCJlbmdsaXNoIikpDQojICsgcmVtb3ZlIHB1bmN0dWF0aW9uDQpzbXNfY29ycHVzX2NsZWFuIDwtIHRtX21hcChzbXNfY29ycHVzX2NsZWFuLCByZW1vdmVQdW5jdHVhdGlvbikNCiMgKyBzdGVtcyB3b3JkcyBpbiBzbXMgdGV4dCB0byB0aGVpciByb290DQpzbXNfY29ycHVzX2NsZWFuIDwtIHRtX21hcChzbXNfY29ycHVzX2NsZWFuLCBzdGVtRG9jdW1lbnQpDQojICsgbm93IHJlbW92ZSB3aGl0ZXNwYWNlcyBsZWZ0IGJlaGluZCBmcm9tIHRoZSBjbGVhbmluZyBwcm9jZXNzIGFib3ZlDQpzbXNfY29ycHVzX2NsZWFuIDwtIHRtX21hcChzbXNfY29ycHVzX2NsZWFuLCBzdHJpcFdoaXRlc3BhY2UpDQpgYGANCg0KPGg1PisgVGFrZSBsb29rIGF0IHRoZSBkaWZmcmVuY2UgYmV0d2VlbiB0aGUgb3JpZ2luYWwgY29ycHVzIGFuZCBtb2RpZmllZCBjb3JwdXM6PC9oNT4NCmBgYHtyfQ0KbGFwcGx5KHNtc19jb3JwdXNbMToxMF0sIGFzLmNoYXJhY3RlcikNCmxhcHBseShzbXNfY29ycHVzX2NsZWFuWzE6MTBdLCBhcy5jaGFyYWN0ZXIpDQpgYGANCg0KPGg1PisgU3BsaXR0aW5nIGNvcnB1cyBpbnRvIGluZGl2aWR1YWwgd29yZHM6PC9oNT4NCldlIGhhdmUgdG8gY3JlYXRlIGEgRG9jdW1lbnQtVGVybS1NYXRyaXggd2hpY2ggaXMgYmFzaWNhbGx5IGEgdGFibGUgdGhhdCBoYXMgdGhlIHRleHQgbWVzc2FnZXMgYXMgcm93cyBhbmQgdGh3IHdvcmRzIGluIHRoZSBjb3JwdXMgYXMgY29sdW1uLg0KYGBge3J9DQpzbXNfZHRtIDwtIERvY3VtZW50VGVybU1hdHJpeChzbXNfY29ycHVzX2NsZWFuKQ0KaW5zcGVjdChzbXNfZHRtWzE6MzAsODE1OjgyNV0pICMgdGFrZSBhIHBlZWsgYXQgdGhlIG5ldyBEVE0NCmBgYA0KDQo8aDU+KyBOb3cgd2UgY3JlYXRlIGEgdHJhaW5pbmcgYW5kIHRlc3QgZGF0YXNldDo8L2g1Pg0KYGBge3J9DQojIHNwbGl0IG91ciBEb2N1bWVudFRlcm1NYXRyaXggaW50byB0ZXN0IGFuZCB0cmFpbmluZyBkYXRhc2V0DQpzbXNfZHRtX3RyYWluIDwtIHNtc19kdG1bMTo0MTY5LCBdDQpzbXNfZHRtX3Rlc3QgPC0gc21zX2R0bVs0MTcwOjU1NTksIF0NCiMgYWxzbyBhcHBlbmQgYSBsYWJlbHMgY29sdW1uIGZyb20gdGhlIHJhdyBkYXRhZnJhbWUNCnNtc190cmFpbl9sYWJlbHMgPC0gc21zX3Jhd1sxOjQxNjksIF0kdHlwZQ0Kc21zX3Rlc3RfbGFiZWxzIDwtIHNtc19yYXdbNDE3MDo1NTU5LCBdJHR5cGUNCnByb3AudGFibGUodGFibGUoc21zX3RyYWluX2xhYmVscykpICMgc2hvdyBwZXJjZW50YWdlIG9mIGhhbSBhbmQgc3BhbSBpbiB0cmFpbmluZyBkYXRhc2V0DQpwcm9wLnRhYmxlKHRhYmxlKHNtc190ZXN0X2xhYmVscykpICMgc2hvdyBwZXJjZW50YWdlIG9mIGhhbSBhbmQgc3BhbSBpbiB0ZXN0IGRhdGFzZXQNCmBgYA0KDQo8aDU+KyBWaXN1YWxpemluZyB0ZXh0IHVzaW5nIHdvcmQgY2xvdWRzOjwvaDU+DQpBIHdvcmQgY2xvdWRzIGlzIGEgdmlzdWFsaXp0aW9uIHRlY2huaXF1ZSB0aGF0IGRpc3BsYXkgd29yZHMgcmFuZG9tbHkgb3Igb3JkZXJlZGx5IGluIGEgY2xvdWQsIHdvcmRzIHdpdGggaGlnaGVyIGZyZXF1ZW5jeSB3aWxsIGFwcGVhciBsYWdlciB0aGFuIHRoZSBvbmVzIHdpdGggbG93ZXIgZnJlcXVlbmN5DQpgYGB7cn0NCiNpbnN0YWxsLnBhY2thZ2VzKCJ3b3JkY2xvdWQiKQ0KbGlicmFyeSh3b3JkY2xvdWQpDQp3b3JkY2xvdWQoc21zX2NvcnB1c19jbGVhbiwgbWluLmZyZXEgPSA1MCwgc2NhbGUgPSBjKDQsLjQpLCByYW5kb20ub3JkZXIgPSBGQUxTRSkNCmBgYA0KSXQncyBhbHNvIHZlcnkgaGVscGZ1bCB0byBjb21wYXJlIHRoZSB3b3JkY2xvdWRzIGJldHdlZW4gc3BhbSBhbmQgaGFtIG1lc3NhZ2VzDQpgYGB7cn0NCnNwYW0gPC0gc3Vic2V0KHNtc19yYXcsIHR5cGUgPT0gInNwYW0iKQ0KaGFtIDwtIHN1YnNldChzbXNfcmF3LCB0eXBlID09ICJoYW0iKQ0Kd29yZGNsb3VkKHNwYW0kdGV4dCwgbWluLmZyZXEgPSAyMCwgc2NhbGUgPSBjKDQsMC4zKSwgcmFuZG9tLm9yZGVyID0gRkFMU0UpDQp3b3JkY2xvdWQoaGFtJHRleHQsIG1pbi5mcmVxID0gNDAsIHNjYWxlID0gYyg0LDAuMSksIHJhbmRvbS5vcmRlciA9IEZBTFNFKQ0KYGBgDQoNCjxoNT4rIFRoZSBsYXN0IHN0ZXAgb2YgZGF0YSBwcmVwYXJhdGlvbiBpcyB0byBjcmVhdGUgaW5kaWNhdG9yIGZlYXR1cmVzIGZvciB0aGUgbW9zdCBmcmVxdWVudCB3b3Jkczo8L2g1Pg0KQXMgb2YgcmlnaHQgbm93LCB3ZSBoYXZlIG92ZXIgNjUwMCBmZWF0dXJlcywgZWFjaCBmZWF0dXJlIGZvciBlYWNoIHdvcmQgdGhhdCBhcHBlYXIgaW4gdGhlIERUTS4gSG93ZXZlciwgc29tZSB3b3JkcyB0aGF0IGFwcGVhciBpbiBsZXNzIHRoYW4gNSBtZXNzYWdlcyAob3IgNS81OTk5ID0gMC4xJSkgYW5kIGRvIG5vdCBtYWtlIGEgc3Ryb25nIGZlYXR1cmUgaW5kaWNhdG9yOyB0aGVzZSBhcmUgdGhlIHdvcmRzIHRoYXQgd2Ugc2hvdWxkIGV4Y2x1ZGUgb3V0IG9mIG91ciBEVE0gdG8gc2ltcGxpZnkgdGhlIERUTS4NCmBgYHtyfQ0Kc21zX2ZyZV93b3JkcyA8LSBmaW5kRnJlcVRlcm1zKHNtc19kdG1fdHJhaW4sIDUpICMgZmluZCB0ZXJtcyB0aGF0IGhhcyBhdCBsZWFzdCA1IGFwcGVhcmFuY2VzDQpzdHIoc21zX2ZyZV93b3JkcykgIyBsZXQncyBzZWUgaG93IG1hbnkgZnJlcXVlbnQgd29yZHMgd2UgaGF2ZQ0KYGBgDQpTbyB3ZSBoYXZlIHJlZHVjZWQgdGhlIG51bWJlciBvZiBmZWF0dXJlcyBmcm9tIG92ZXIgNjUwMCBkb3duIHRvIDExMzkgYnkgb25seSBhY2NlcHRpbmcgd29yZHMgd2l0aCBmcmVxdWVuY3kgPSA1IG9mIGhpZ2hlci4gTm93IHdlIHVzZSB0aGlzIGNociB2ZWN0b3IgdG8gZmlsdGVyIG91dCBvdXIgRFRNDQpgYGB7cn0NCnNtc19kdG1fZnJlcV90cmFpbiA8LSBzbXNfZHRtX3RyYWluWyAsIHNtc19mcmVfd29yZHNdDQpzbXNfZHRtX2ZyZXFfdGVzdCA8LSBzbXNfZHRtX3Rlc3RbICwgc21zX2ZyZV93b3Jkc10NCmBgYA0KDQpTaW5jZSBCYXllcyBjbGFzc2lmaWVyIHdvcmtzIG9uIGNhdGVnb3JpY2FsIGRhdGFzZXQgYW5kIG91ciB0ZXN0IGFuZCB0cmFpbiBkYXRhc2V0IGNvbnRhaW5zIG51bWVyaWMgdmFsdWVzLCB3ZSBoYXZlIHRvIGNvdm5lcnQgdGhlc2UgZGF0YXNldCBpbnRvIGNhdGVnb3JpY2FsIHZhcmlhYmxlIGJ5IHNldHRpbmcgdGhlIHZhcmlhYmxlcyB0byAieWVzIiIgb3IgIm5vIiBkZXBlbmRpbmcgb24gaWYgaXQgYXBwZWFyIGluIGVhY2ggbWVzc2FnZS4NCmBgYHtyfQ0KY29udmVydF9jb3VudHMgPC0gZnVuY3Rpb24oeCl7DQogIHggPC0gaWZlbHNlKHggPjAsICJZZXMiLCAiTm8iKSAjIGlmIHRoZSB3b3JkIGFwcGVhciBpbiBhIHRleHQgbWVzc2FnZSwgdGhlbiBjaGFuZ2UgaXRzIHZhbHVlIHRvIFllcywgb3RoZXJ3aXNlLCBObw0KfQ0KDQpzbXNfdHJhaW4gPC0gYXBwbHkoc21zX2R0bV9mcmVxX3RyYWluLCBNQVJHSU4gPSAyLCBjb252ZXJ0X2NvdW50cykNCnNtc190ZXN0IDwtIGFwcGx5KHNtc19kdG1fZnJlcV90ZXN0LCBNQVJHSU4gPSAyLCBjb252ZXJ0X2NvdW50cykNCmBgYA0KDQoNCg0KPGg0Pjx1PlN0ZXAgMzogVHJhaW5pbmcgTmFpdmUgQmF5ZXMgQ2xhc3NpZmllcjwvdT48L2g0Pg0KVGhpcyBOYWl2ZSBCYXllcyBjbGFzc2lmaWVyIHdpbGwgZXN0aW1hdGUgdGhlIHByb2JhYmlsaXR5IG9mIHdoZXRoZXIgYSB0ZXh0IG1lc3NhZ2UgaXMgYSBzcGFtIG9yIGhhbSBiYXNlIG9uIHRoZSBwcmVzZW5jZSBhbmQgYWJzZW5jZSBvZiB3b3JkcyBpbiBhIHRleHQuIFRoZSBOYWl2ZSBCYXllcyBjbGFzc2lmaWVyIHVzZWQgaXMgZnJvbSAnZTEwNzEnIHBhY2thZ2UgZGV2ZWxvcGVkIGJ5IFZpZW5uYSBVbml2ZXJzaXR5IG9mDQpUZWNobm9sb2d5DQpgYGB7cn0NCiNpbnN0YWxsLnBhY2thZ2VzKCdlMTA3MScpDQpsaWJyYXJ5KGUxMDcxKQ0Kc21zX2NsYXNzaWZpZXIgPC0gbmFpdmVCYXllcyhzbXNfdHJhaW4sIHNtc190cmFpbl9sYWJlbHMpDQpgYGANCg0KPGg0Pjx1PlN0ZXAgNDogRXZhbHVhdGluZyB0aGUgcGVyZm9ybWFuY2Ugb2Ygb3VyIE5haXZlIEJheWVzIGNsYXNzaWZpZXI8L3U+PC9oND4NCmBgYHtyfQ0KIyBwZXJmb3JtIHByZWRpY3Rpb24gb24gdGhlIHRlc3QgZGF0YXNldCB1c2luZyB0aGUgcmVzdWx0ZWQgTmFpdmUgQmF5ZXMgY2xhc3NpZmllcg0Kc21zX3Rlc3RfcHJlZCA8LSBwcmVkaWN0KHNtc19jbGFzc2lmaWVyLCBzbXNfdGVzdCkNCiMgZWx2YWx1YXRlIG1vZGVsJ3MgcGVyZm9ybWFuY2Ugd2l0aCBDcm9zc1RhYmxlDQpsaWJyYXJ5KGdtb2RlbHMpDQpDcm9zc1RhYmxlKHNtc190ZXN0X3ByZWQsIHNtc190ZXN0X2xhYmVscywgcHJvcC5jaGlzcSA9IEZBTFNFLCBwcm9wLnQgPSBGQUxTRSwgZG5uID0gYygiUHJlZGljdGVkIiwgIkFjdHVhbCIpKQ0KYGBgDQo9PT4gQXMgd2UgY2FuIHNlZSwgb3VyIE5haXZlIEJheWVzIGNsYXNzaWZpZXIgcHJlZGljdGVkIGFjdHVhbCBzcGFtIG1lc3NhZ2VzIGFzICJzcGFtIiB3aXRoIGFjY3VyYWN5IG9mIDk2LjIlLCBhbmQgYWN0dWFsIGhhbSBtZXNzYWdlcyBhcyAiaGFtIiB3aXRoIGFjY3VyYWN5IG9mIDk3LjYlICEhISBJdCBkaWQgaW5jb3JyZWN0bHkgcHJlZGljdGVkIHRleHQgbWVzc2FnZXMgMzYvMTM5MCA9IDIuNTglLiBOb3QgYSBiYWQgcmVjb3JkIGZvciBvdXIgbGl0dGxlIHNwYW0gdGV4dCBjbGFzc2lmaWVyIDotKQ0KDQo8aDQ+PHU+U3RlcCA0OiBJbXByb3Zpbmcgb3VyIE5haXZlIEJheWVzIGNsYXNzaWZpZXIgcGVyZm9ybWFuY2U8L3U+PC9oND4NCmBgYHtyfQ0KIyB1c2luZyBsYXBsYWNlIGVzdGltYXRvciB0byBpbXByb3ZlIGFjY3VyYWN5DQpzbXNfY2xhc3NpZmllcjIgPC0gbmFpdmVCYXllcyhzbXNfdHJhaW4sIHNtc190cmFpbl9sYWJlbHMsIGxhcGxhY2UgPSAxKQ0Kc21zX3Rlc3RfcHJlZDIgPC0gcHJlZGljdChzbXNfY2xhc3NpZmllcjIsIHNtc190ZXN0KQ0KQ3Jvc3NUYWJsZShzbXNfdGVzdF9wcmVkMiwgc21zX3Rlc3RfbGFiZWxzLCBwcm9wLmNoaXNxID0gRkFMU0UsIHByb3AudCA9IEZBTFNFLCBwcm9wLnIgPSBGQUxTRSwgZG5uID0gYygiUHJlZGljdGVkIiwgIkFjdHVhbCIpKQ0KYGBgDQo9PT4gQnkgc2V0dGluZyBsYXBsYWNlIGVzdGltYXRvciB0byAxLCB3ZSB3ZXJlIGFibGUgdG8gaW1wcm92ZSB0aGUgYWNjdXJhY3lvZiBvdXIgTmFpdmUgQmF5ZXMgY2xhc3NpZmllci4gU3BlY2lmaWNhbGx5LCB0aGUgbnVtYmVyIG9mIGhhbSBtZXNzYWdlcyBpbmNvcnJlY3RseSBpZGVudGlmaWVzIGFzICJzcGFtIiB3ZW50IGRvd24gZnJvbSAzMCB0byAyOCwgYW5kIHRoZSBudW1iZXIgb2Ygc3BhbSBtZXNzYWdlcyBpbmNvcnJlY3RseSBpZGVudGlmaWVkIGFzICJoYW0iIHdlbnQgZG93bSBmcm9tIDYgdG8gNS4NCg0KTGV0J3MgdHJ5IHRvIGluY3JlYXNlIGxhcGxhY2UgZXN0aW1hdG9yIHRvIHNlZSBob3cgaXQgY2hhbmdlcyB0aGUgYWNjdXJhY3kNCisgd2l0aCBsYXBsYWNlIGVzdGltYXRvciA9IDINCmBgYHtyfQ0Kc21zX2NsYXNzaWZpZXIzIDwtIG5haXZlQmF5ZXMoc21zX3RyYWluLCBzbXNfdHJhaW5fbGFiZWxzLCBsYXBsYWNlID0gMikNCnNtc190ZXN0X3ByZWQzIDwtIHByZWRpY3Qoc21zX2NsYXNzaWZpZXIzLCBzbXNfdGVzdCkNCkNyb3NzVGFibGUoc21zX3Rlc3RfcHJlZDMsIHNtc190ZXN0X2xhYmVscywgcHJvcC5jaGlzcSA9IEZBTFNFLCBwcm9wLnQgPSBGQUxTRSwgcHJvcC5yID0gRkFMU0UsIGRubiA9IGMoIlByZWRpY3RlZCIsICJBY3R1YWwiKSkNCmBgYA0KDQorIHdpdGggbGFwbGFjZSBlc3RpbWF0b3IgPSAzDQpgYGB7cn0NCnNtc19jbGFzc2lmaWVyNCA8LSBuYWl2ZUJheWVzKHNtc190cmFpbiwgc21zX3RyYWluX2xhYmVscywgbGFwbGFjZSA9IDMpDQpzbXNfdGVzdF9wcmVkNCA8LSBwcmVkaWN0KHNtc19jbGFzc2lmaWVyNCwgc21zX3Rlc3QpDQpDcm9zc1RhYmxlKHNtc190ZXN0X3ByZWQ0LCBzbXNfdGVzdF9sYWJlbHMsIHByb3AuY2hpc3EgPSBGQUxTRSwgcHJvcC50ID0gRkFMU0UsIHByb3AuciA9IEZBTFNFLCBkbm4gPSBjKCJQcmVkaWN0ZWQiLCAiQWN0dWFsIikpDQpgYGANCg0KKyB3aXRoIGxhcGxhY2UgZXN0aW1hdG9yID0gNA0KYGBge3J9DQpzbXNfY2xhc3NpZmllcjUgPC0gbmFpdmVCYXllcyhzbXNfdHJhaW4sIHNtc190cmFpbl9sYWJlbHMsIGxhcGxhY2UgPSA0KQ0Kc21zX3Rlc3RfcHJlZDUgPC0gcHJlZGljdChzbXNfY2xhc3NpZmllcjUsIHNtc190ZXN0KQ0KQ3Jvc3NUYWJsZShzbXNfdGVzdF9wcmVkNSwgc21zX3Rlc3RfbGFiZWxzLCBwcm9wLmNoaXNxID0gRkFMU0UsIHByb3AudCA9IEZBTFNFLCBwcm9wLnIgPSBGQUxTRSwgZG5uID0gYygiUHJlZGljdGVkIiwgIkFjdHVhbCIpKQ0KYGBgDQo9PT4gSXQgc2VlbXMgdGhhdCB3aXRoIGxhcGxhY2UgZXN0aW1hdG9yID0gMSwgb3VyIE5haXZlIEJheWVzIGNsYXNzaWZpZXIgaXMgdGhlIG1vc3QgYWNjdXJhdGUuIFdlIGFsc28gb2JzZXJ2ZWQgdGhhdCwgYnkgaW5jcmVhc2luZyBsYXBsYWNlIGVzdGltYXRvciwgdGhlIG51bWJlciBvZiBzcGFtIG1lc3NhZ2VlcyBpbmNvcnJlY3RseSBpZGVudGlmaWVkIGFzICJoYW0iIGluY3JlYXNlcy4gVGhpcyBzaG93cyB0aGF0IGl0IGlzIGltcG9ydGFudCB0byBwaWNrIHRoZSByaWdodCBsYXBsYWNlIGVzdGltYXRvciBzbyB0aGF0IG91ciBOYWl2ZSBCYXllcyBjbGFzc2lmaWVyIGRvZXMgbm90IGZpbHRlciBvdXQgc3BhbSB0b28gYWdncmVzaXZlbHkgKHdoaWNoIGxlYWRzIHRvIGhhbSBtaXNjbGFzc2lmaWVkIGFzICJzcGFtIikgb3IgdG9vIHBhc3NpdmVseSAod2hpY2ggY2F1c2VzIHNwYW0gbWlzY2xhc3NpZmllZCBhcyAiaGFtIikhDQoNCjxoND5SZWZlcmVuY2U6PC9oND4NCg0KPGVtPk1hY2hpbmUgTGVhcm5pbmcgd2l0aCBSPC9lbT4gYnkgQnJldHQgTGFudHouDQo=