Introduction

Would it be nice to classify text message as spam or ham(legit messages) ? This project aims to do exactly that. We will use SMS Spam Collection dataset to build a document classifier (Naive Bayes algorithm) that classify text message, then predict the class of new text (testing dataset), then using confusion matrix to evaluate the model performance.

Library

library(tm)
## Loading required package: NLP
library(caret)
## Loading required package: ggplot2
## 
## Attaching package: 'ggplot2'
## The following object is masked from 'package:NLP':
## 
##     annotate
## Loading required package: lattice
library(e1071)
library(pROC)
## Type 'citation("pROC")' for a citation.
## 
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
library(SnowballC)
library(wordcloud)
## Loading required package: RColorBrewer

Importing Data

The dataset can be downloaded at Kaggle.

The dataset contains text of SMS messages and their respective label as Spam or Ham.

sms_raw <- read.csv("https://raw.githubusercontent.com/yinaS1234/data-607/main/project%204/spamham.csv", header = FALSE, stringsAsFactors = FALSE)

sms_raw <- sms_raw[,1:2]
colnames(sms_raw) <- c("Type", "Text")
sms_raw$Type <- factor(sms_raw$Type)

# Check the structure of the dataset
str(sms_raw)
## 'data.frame':    5573 obs. of  2 variables:
##  $ Type: Factor w/ 3 levels "Category","ham",..: 1 2 2 3 2 2 3 2 2 3 ...
##  $ Text: chr  "Message" "Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat..." "Ok lar... Joking wif u oni..." "Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question("| __truncated__ ...
# Check the number of spam and ham messages
table(sms_raw$Type)
## 
## Category      ham     spam 
##        1     4825      747
prop.table(table(sms_raw$Type))
## 
##     Category          ham         spam 
## 0.0001794366 0.8657814463 0.1340391172

About 86% of the messages are ham while the remaining 14% are spam.

Data Clearning- Creating a corpus

Now that we have the tm package installed and loaded we can create a corpus from the text data. A corpus is a collection of text documents.

—–Use the Vcorpus() function to create a volatile corpus.

—- Transform to lower case and remove punctuation and number

—–Remove stopwords

—–Trim words to their root form through **stemming* stemDocument()

—–Remove extra white spaces

# Create corpus
sms_corpus <- VCorpus(x = VectorSource(sms_raw$Text))

# Print corpus
sms_corpus
## <<VCorpus>>
## Metadata:  corpus specific: 0, document level (indexed): 0
## Content:  documents: 5573
# Check the text in some messages and their type
lapply(sms_corpus[5:8], as.character)
## $`5`
## [1] "U dun say so early hor... U c already then say..."
## 
## $`6`
## [1] "Nah I don't think he goes to usf, he lives around here though"
## 
## $`7`
## [1] "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"
## 
## $`8`
## [1] "Even my brother is not like to speak with me. They treat me like aids patent."
sms_raw$Type[5:8]
## [1] ham  ham  spam ham 
## Levels: Category ham spam
corpus_clean <- sms_corpus

# Remove Numbers
corpus_clean <- tm_map(x = corpus_clean, FUN = removeNumbers)

# Transform all letters to lower case
corpus_clean <- tm_map(x = corpus_clean, content_transformer(tolower))


# Remove punctuation
corpus_clean <- tm_map(x = corpus_clean, FUN = removePunctuation)

# Print stopwords()
stopwords()
##   [1] "i"          "me"         "my"         "myself"     "we"        
##   [6] "our"        "ours"       "ourselves"  "you"        "your"      
##  [11] "yours"      "yourself"   "yourselves" "he"         "him"       
##  [16] "his"        "himself"    "she"        "her"        "hers"      
##  [21] "herself"    "it"         "its"        "itself"     "they"      
##  [26] "them"       "their"      "theirs"     "themselves" "what"      
##  [31] "which"      "who"        "whom"       "this"       "that"      
##  [36] "these"      "those"      "am"         "is"         "are"       
##  [41] "was"        "were"       "be"         "been"       "being"     
##  [46] "have"       "has"        "had"        "having"     "do"        
##  [51] "does"       "did"        "doing"      "would"      "should"    
##  [56] "could"      "ought"      "i'm"        "you're"     "he's"      
##  [61] "she's"      "it's"       "we're"      "they're"    "i've"      
##  [66] "you've"     "we've"      "they've"    "i'd"        "you'd"     
##  [71] "he'd"       "she'd"      "we'd"       "they'd"     "i'll"      
##  [76] "you'll"     "he'll"      "she'll"     "we'll"      "they'll"   
##  [81] "isn't"      "aren't"     "wasn't"     "weren't"    "hasn't"    
##  [86] "haven't"    "hadn't"     "doesn't"    "don't"      "didn't"    
##  [91] "won't"      "wouldn't"   "shan't"     "shouldn't"  "can't"     
##  [96] "cannot"     "couldn't"   "mustn't"    "let's"      "that's"    
## [101] "who's"      "what's"     "here's"     "there's"    "when's"    
## [106] "where's"    "why's"      "how's"      "a"          "an"        
## [111] "the"        "and"        "but"        "if"         "or"        
## [116] "because"    "as"         "until"      "while"      "of"        
## [121] "at"         "by"         "for"        "with"       "about"     
## [126] "against"    "between"    "into"       "through"    "during"    
## [131] "before"     "after"      "above"      "below"      "to"        
## [136] "from"       "up"         "down"       "in"         "out"       
## [141] "on"         "off"        "over"       "under"      "again"     
## [146] "further"    "then"       "once"       "here"       "there"     
## [151] "when"       "where"      "why"        "how"        "all"       
## [156] "any"        "both"       "each"       "few"        "more"      
## [161] "most"       "other"      "some"       "such"       "no"        
## [166] "nor"        "not"        "only"       "own"        "same"      
## [171] "so"         "than"       "too"        "very"
# Remove stop words
corpus_clean <- tm_map(x = corpus_clean, FUN = removeWords, stopwords())

# Test the function
wordStem(words = c("cooks", "cooking", "cooked"))
## [1] "cook" "cook" "cook"
# Stem words in corpus
corpus_clean <- tm_map(x = corpus_clean, FUN = stemDocument)

# Remove extra white spaces
corpus_clean <- tm_map(x = corpus_clean, FUN = stripWhitespace)

Create a Document Term Matrix (DTM)

A DTM is a data structure where each document is represented in its own row and a matrix that represents the frequency of each term (word)

# Create Document Term Matrix
DTM <- DocumentTermMatrix(x = corpus_clean)

DTM
## <<DocumentTermMatrix (documents: 5573, terms: 6956)>>
## Non-/sparse entries: 43715/38722073
## Sparsity           : 100%
## Maximal term length: 40
## Weighting          : term frequency (tf)

Data Exploration with word clouds

Word clouds provide a very intuitive way to visualize the frequency of words in a corpus.

wordcloud(words = corpus_clean, 
          min.freq = 100, # minimum number of times a word must be present before it appears 
          random.order = FALSE, # Arrange most frequent words to be in the center of the word cloud
          color = (colors = c("#4575b4","#74add1","#abd9e9","#e0f3f8","#fee090","#fdae61","#f46d43","#d73027")) # Colour gradient for the font
          )

Train/Test Split

Now split the dataset into a training and test datasets. We will use 80% of the data for training and the remaining 20% for testing.

# Create Training Set
DTM_train <- DTM[1:round(nrow(DTM)*0.80, 0), ]

# Create Test Set
DTM_test <- DTM[(round(nrow(DTM)*0.80, 0)+1):nrow(DTM), ]

# Create vectors with labels for the training and test set
train_labels <- sms_raw[1:round(nrow(sms_raw)*0.80, 0), ]$Type
test_labels <- sms_raw[(round(nrow(sms_raw)*0.80, 0)+1):nrow(DTM), ]$Type

# Check proportion of ham and spam is similar on the training and test set
prop.table(table(train_labels))
## train_labels
##     Category          ham         spam 
## 0.0002243158 0.8647375505 0.1350381337
prop.table(table(test_labels))
## test_labels
##  Category       ham      spam 
## 0.0000000 0.8699552 0.1300448

Model Development

Feature Selection- most frequent words

Our document term matrix has 6956 features, since this is a very high number to use on our Bayesian algorithm we will reduce the number of features by only selecting the most frequent words.

# filter out the words appear less than 0.1% of the time
threshold <- 0.1 # in %
min_freq = round(DTM$nrow*(threshold/100),0) # calculate minimum frequency
min_freq
## [1] 6
# Create vector of most frequent words
frequent_words <- findFreqTerms(x = DTM, lowfreq = min_freq)

str(frequent_words)
##  chr [1:1251] "‘ll" "abiola" "abl" "abt" "accept" "access" "account" ...
# Filter DTM to only have most frequent words
DTM_train_most_frequent <- DTM_train[, frequent_words]
DTM_test_most_frequent <- DTM_test[, frequent_words]

# Check dimension of DTM
dim(DTM_train_most_frequent)
## [1] 4458 1251

Convert numeric values into categorical values

In order to use the Naive Bayes classifier we need to convert the numerical features in our Document Term Matrix (DTM) to categorical features.

# Create function  that converts numeric values to "Yes" or "No" if word is present or absent in document
is_present <- function(x) {
  x <- ifelse(test = x > 0, yes = "Yes", no = "No")
}

# Test function
x <- is_present(c(1, 0, 3, 4, 0, 0))
x
## [1] "Yes" "No"  "Yes" "Yes" "No"  "No"
# Apply is_present() function to training and test DTM
DTM_train_most_frequent <- apply(X = DTM_train_most_frequent, 
                                 MARGIN = 2, # Apply function to columns
                                 FUN = is_present) # Specify function to be used

DTM_test_most_frequent <- apply(X = DTM_test_most_frequent, 
                                 MARGIN = 2, # Apply function to columns
                                 FUN = is_present) # Specify function to be used

Train Model

# Create model from the training dataset
spam_classifier <- naiveBayes(x = DTM_train_most_frequent, y = train_labels)

# Print probability tables for some words
spam_classifier$tables$call
##             call
## train_labels         No        Yes
##     Category 1.00000000 0.00000000
##     ham      0.94422827 0.05577173
##     spam     0.57308970 0.42691030
spam_classifier$tables$friend
##             friend
## train_labels         No        Yes
##     Category 1.00000000 0.00000000
##     ham      0.98184176 0.01815824
##     spam     0.98172757 0.01827243
spam_classifier$tables$free
##             free
## train_labels         No        Yes
##     Category 1.00000000 0.00000000
##     ham      0.98806744 0.01193256
##     spam     0.77574751 0.22425249

We can see from the probability tables above that the probability of finding the words “call” and “free” in spam messages are multiple times higher than in legitimate messages. On the other hand the word “friend” is almost equally likely to be found in both ham and spam.

Evaluating the performance of our classifier

# Make predictions on test set
test_predictions <- predict(object = spam_classifier, newdata = DTM_test_most_frequent)


# Create confusion matrix
confusionMatrix(data = test_predictions, reference = test_labels, positive = "spam", dnn = c("Prediction", "Actual"))
## Confusion Matrix and Statistics
## 
##           Actual
## Prediction Category ham spam
##   Category        0   0    0
##   ham             0 965   16
##   spam            0   5  129
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9812          
##                  95% CI : (0.9714, 0.9883)
##     No Information Rate : 0.87            
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.914           
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: Category Class: ham Class: spam
## Sensitivity                       NA     0.9948      0.8897
## Specificity                        1     0.8897      0.9948
## Pos Pred Value                    NA     0.9837      0.9627
## Neg Pred Value                    NA     0.9627      0.9837
## Prevalence                         0     0.8700      0.1300
## Detection Rate                     0     0.8655      0.1157
## Detection Prevalence               0     0.8798      0.1202
## Balanced Accuracy                 NA     0.9423      0.9423

Conclusion

We test the performance of our spam classifier using our testing dataset
As we can see from the output above we obtain an overall accuracy of 98.12%.

The Naive Bayes model we built seems like able to classify spam and ham text effectively.

LS0tCnRpdGxlOiAiRGF0YSA2MDcgUHJvamVjdCA0LURvY3VtZW50IENsYXNzaWZpZXIiCmF1dGhvcjogInlpbmEgcWlhbyIKZGF0ZTogIjIwMjMtMDQtMjgiCm91dHB1dDogb3BlbmludHJvOjpsYWJfcmVwb3J0Ci0tLQoKIyBJbnRyb2R1Y3Rpb24KCgoqKldvdWxkIGl0IGJlIG5pY2UgdG8gY2xhc3NpZnkgdGV4dCBtZXNzYWdlIGFzIHNwYW0gb3IgaGFtKGxlZ2l0IG1lc3NhZ2VzKSA/IFRoaXMgcHJvamVjdCBhaW1zIHRvIGRvIGV4YWN0bHkgdGhhdC4gV2Ugd2lsbCB1c2UgU01TIFNwYW0gQ29sbGVjdGlvbiBkYXRhc2V0IHRvIGJ1aWxkIGEgZG9jdW1lbnQgY2xhc3NpZmllciAoTmFpdmUgQmF5ZXMgYWxnb3JpdGhtKSB0aGF0IGNsYXNzaWZ5IHRleHQgbWVzc2FnZSwgdGhlbiBwcmVkaWN0IHRoZSBjbGFzcyBvZiBuZXcgdGV4dCAodGVzdGluZyBkYXRhc2V0KSwgdGhlbiB1c2luZyBjb25mdXNpb24gbWF0cml4IHRvIGV2YWx1YXRlIHRoZSBtb2RlbCBwZXJmb3JtYW5jZS4qKgoKCiMgTGlicmFyeQpgYGB7cn0KbGlicmFyeSh0bSkKbGlicmFyeShjYXJldCkKbGlicmFyeShlMTA3MSkKbGlicmFyeShwUk9DKQpsaWJyYXJ5KFNub3diYWxsQykKbGlicmFyeSh3b3JkY2xvdWQpCmBgYAoKIyBJbXBvcnRpbmcgRGF0YQoKVGhlIGRhdGFzZXQgY2FuIGJlIGRvd25sb2FkZWQgYXQgIFtLYWdnbGVdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vdWNpbWwvc21zLXNwYW0tY29sbGVjdGlvbi1kYXRhc2V0L2RhdGEpLgoKVGhlIGRhdGFzZXQgY29udGFpbnMgdGV4dCBvZiBTTVMgbWVzc2FnZXMgYW5kIHRoZWlyIHJlc3BlY3RpdmUgbGFiZWwgYXMgU3BhbSBvciBIYW0uCgpgYGB7cn0Kc21zX3JhdyA8LSByZWFkLmNzdigiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3lpbmFTMTIzNC9kYXRhLTYwNy9tYWluL3Byb2plY3QlMjA0L3NwYW1oYW0uY3N2IiwgaGVhZGVyID0gRkFMU0UsIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKCnNtc19yYXcgPC0gc21zX3Jhd1ssMToyXQpjb2xuYW1lcyhzbXNfcmF3KSA8LSBjKCJUeXBlIiwgIlRleHQiKQpzbXNfcmF3JFR5cGUgPC0gZmFjdG9yKHNtc19yYXckVHlwZSkKCiMgQ2hlY2sgdGhlIHN0cnVjdHVyZSBvZiB0aGUgZGF0YXNldApzdHIoc21zX3JhdykKCiMgQ2hlY2sgdGhlIG51bWJlciBvZiBzcGFtIGFuZCBoYW0gbWVzc2FnZXMKdGFibGUoc21zX3JhdyRUeXBlKQpwcm9wLnRhYmxlKHRhYmxlKHNtc19yYXckVHlwZSkpCmBgYAoKCkFib3V0IDg2JSBvZiB0aGUgbWVzc2FnZXMgYXJlIGhhbSB3aGlsZSB0aGUgcmVtYWluaW5nIDE0JSBhcmUgc3BhbS4KCgoKIyBEYXRhIENsZWFybmluZy0gQ3JlYXRpbmcgYSBjb3JwdXMKCk5vdyB0aGF0IHdlIGhhdmUgdGhlIHRtIHBhY2thZ2UgaW5zdGFsbGVkIGFuZCBsb2FkZWQgd2UgY2FuIGNyZWF0ZSBhICoqY29ycHVzKiogZnJvbSB0aGUgdGV4dCBkYXRhLiBBIGNvcnB1cyBpcyBhIGNvbGxlY3Rpb24gb2YgdGV4dCBkb2N1bWVudHMuCgotLS0tLVVzZSB0aGUgYFZjb3JwdXMoKWAgZnVuY3Rpb24gdG8gY3JlYXRlIGEgdm9sYXRpbGUgY29ycHVzLlwKCi0tLS0gVHJhbnNmb3JtIHRvIGxvd2VyIGNhc2UgYW5kIHJlbW92ZSBwdW5jdHVhdGlvbiBhbmQgbnVtYmVyXAoKLS0tLS1SZW1vdmUgc3RvcHdvcmRzXAoKLS0tLS1UcmltIHdvcmRzIHRvIHRoZWlyIHJvb3QgZm9ybSB0aHJvdWdoICoqc3RlbW1pbmcqIHN0ZW1Eb2N1bWVudCgpXAoKLS0tLS1SZW1vdmUgZXh0cmEgd2hpdGUgc3BhY2VzXAoKYGBge3J9CiMgQ3JlYXRlIGNvcnB1cwpzbXNfY29ycHVzIDwtIFZDb3JwdXMoeCA9IFZlY3RvclNvdXJjZShzbXNfcmF3JFRleHQpKQoKIyBQcmludCBjb3JwdXMKc21zX2NvcnB1cwoKIyBDaGVjayB0aGUgdGV4dCBpbiBzb21lIG1lc3NhZ2VzIGFuZCB0aGVpciB0eXBlCmxhcHBseShzbXNfY29ycHVzWzU6OF0sIGFzLmNoYXJhY3RlcikKc21zX3JhdyRUeXBlWzU6OF0KCmNvcnB1c19jbGVhbiA8LSBzbXNfY29ycHVzCgojIFJlbW92ZSBOdW1iZXJzCmNvcnB1c19jbGVhbiA8LSB0bV9tYXAoeCA9IGNvcnB1c19jbGVhbiwgRlVOID0gcmVtb3ZlTnVtYmVycykKCiMgVHJhbnNmb3JtIGFsbCBsZXR0ZXJzIHRvIGxvd2VyIGNhc2UKY29ycHVzX2NsZWFuIDwtIHRtX21hcCh4ID0gY29ycHVzX2NsZWFuLCBjb250ZW50X3RyYW5zZm9ybWVyKHRvbG93ZXIpKQoKCiMgUmVtb3ZlIHB1bmN0dWF0aW9uCmNvcnB1c19jbGVhbiA8LSB0bV9tYXAoeCA9IGNvcnB1c19jbGVhbiwgRlVOID0gcmVtb3ZlUHVuY3R1YXRpb24pCgojIFByaW50IHN0b3B3b3JkcygpCnN0b3B3b3JkcygpCgojIFJlbW92ZSBzdG9wIHdvcmRzCmNvcnB1c19jbGVhbiA8LSB0bV9tYXAoeCA9IGNvcnB1c19jbGVhbiwgRlVOID0gcmVtb3ZlV29yZHMsIHN0b3B3b3JkcygpKQoKIyBUZXN0IHRoZSBmdW5jdGlvbgp3b3JkU3RlbSh3b3JkcyA9IGMoImNvb2tzIiwgImNvb2tpbmciLCAiY29va2VkIikpCgojIFN0ZW0gd29yZHMgaW4gY29ycHVzCmNvcnB1c19jbGVhbiA8LSB0bV9tYXAoeCA9IGNvcnB1c19jbGVhbiwgRlVOID0gc3RlbURvY3VtZW50KQoKIyBSZW1vdmUgZXh0cmEgd2hpdGUgc3BhY2VzCmNvcnB1c19jbGVhbiA8LSB0bV9tYXAoeCA9IGNvcnB1c19jbGVhbiwgRlVOID0gc3RyaXBXaGl0ZXNwYWNlKQpgYGAKCgoKIyBDcmVhdGUgYSBEb2N1bWVudCBUZXJtIE1hdHJpeCAoRFRNKQoKQSBEVE0gaXMgYSBkYXRhIHN0cnVjdHVyZSB3aGVyZSBlYWNoIGRvY3VtZW50IGlzIHJlcHJlc2VudGVkIGluIGl0cyBvd24gcm93IGFuZCBhIG1hdHJpeCB0aGF0IHJlcHJlc2VudHMgdGhlIGZyZXF1ZW5jeSBvZiBlYWNoIHRlcm0gKHdvcmQpCgoKYGBge3J9CiMgQ3JlYXRlIERvY3VtZW50IFRlcm0gTWF0cml4CkRUTSA8LSBEb2N1bWVudFRlcm1NYXRyaXgoeCA9IGNvcnB1c19jbGVhbikKCkRUTQpgYGAKCiMgRGF0YSBFeHBsb3JhdGlvbiB3aXRoIHdvcmQgY2xvdWRzCgpXb3JkIGNsb3VkcyBwcm92aWRlIGEgdmVyeSBpbnR1aXRpdmUgd2F5IHRvIHZpc3VhbGl6ZSB0aGUgZnJlcXVlbmN5IG9mIHdvcmRzIGluIGEgY29ycHVzLgoKCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9Cgp3b3JkY2xvdWQod29yZHMgPSBjb3JwdXNfY2xlYW4sIAogICAgICAgICAgbWluLmZyZXEgPSAxMDAsICMgbWluaW11bSBudW1iZXIgb2YgdGltZXMgYSB3b3JkIG11c3QgYmUgcHJlc2VudCBiZWZvcmUgaXQgYXBwZWFycyAKICAgICAgICAgIHJhbmRvbS5vcmRlciA9IEZBTFNFLCAjIEFycmFuZ2UgbW9zdCBmcmVxdWVudCB3b3JkcyB0byBiZSBpbiB0aGUgY2VudGVyIG9mIHRoZSB3b3JkIGNsb3VkCiAgICAgICAgICBjb2xvciA9IChjb2xvcnMgPSBjKCIjNDU3NWI0IiwiIzc0YWRkMSIsIiNhYmQ5ZTkiLCIjZTBmM2Y4IiwiI2ZlZTA5MCIsIiNmZGFlNjEiLCIjZjQ2ZDQzIiwiI2Q3MzAyNyIpKSAjIENvbG91ciBncmFkaWVudCBmb3IgdGhlIGZvbnQKICAgICAgICAgICkKYGBgCgojIFRyYWluL1Rlc3QgU3BsaXQKCk5vdyBzcGxpdCB0aGUgZGF0YXNldCBpbnRvIGEgdHJhaW5pbmcgYW5kIHRlc3QgZGF0YXNldHMuIFdlIHdpbGwgdXNlIDgwJSBvZiB0aGUgZGF0YSBmb3IgdHJhaW5pbmcgYW5kIHRoZSByZW1haW5pbmcgMjAlIGZvciB0ZXN0aW5nLgoKYGBge3J9CgojIENyZWF0ZSBUcmFpbmluZyBTZXQKRFRNX3RyYWluIDwtIERUTVsxOnJvdW5kKG5yb3coRFRNKSowLjgwLCAwKSwgXQoKIyBDcmVhdGUgVGVzdCBTZXQKRFRNX3Rlc3QgPC0gRFRNWyhyb3VuZChucm93KERUTSkqMC44MCwgMCkrMSk6bnJvdyhEVE0pLCBdCgojIENyZWF0ZSB2ZWN0b3JzIHdpdGggbGFiZWxzIGZvciB0aGUgdHJhaW5pbmcgYW5kIHRlc3Qgc2V0CnRyYWluX2xhYmVscyA8LSBzbXNfcmF3WzE6cm91bmQobnJvdyhzbXNfcmF3KSowLjgwLCAwKSwgXSRUeXBlCnRlc3RfbGFiZWxzIDwtIHNtc19yYXdbKHJvdW5kKG5yb3coc21zX3JhdykqMC44MCwgMCkrMSk6bnJvdyhEVE0pLCBdJFR5cGUKCiMgQ2hlY2sgcHJvcG9ydGlvbiBvZiBoYW0gYW5kIHNwYW0gaXMgc2ltaWxhciBvbiB0aGUgdHJhaW5pbmcgYW5kIHRlc3Qgc2V0CnByb3AudGFibGUodGFibGUodHJhaW5fbGFiZWxzKSkKcHJvcC50YWJsZSh0YWJsZSh0ZXN0X2xhYmVscykpCgpgYGAKCiMgTW9kZWwgRGV2ZWxvcG1lbnQKCiMjIEZlYXR1cmUgU2VsZWN0aW9uLSBtb3N0IGZyZXF1ZW50IHdvcmRzCgpPdXIgZG9jdW1lbnQgdGVybSBtYXRyaXggaGFzIGByIERUTSRuY29sYCBmZWF0dXJlcywgc2luY2UgdGhpcyBpcyBhIHZlcnkgaGlnaCBudW1iZXIgdG8gdXNlIG9uIG91ciBCYXllc2lhbiBhbGdvcml0aG0gd2Ugd2lsbCByZWR1Y2UgdGhlIG51bWJlciBvZiBmZWF0dXJlcyBieSBvbmx5IHNlbGVjdGluZyB0aGUgbW9zdCBmcmVxdWVudCB3b3Jkcy4KCgpgYGB7cn0KCiMgZmlsdGVyIG91dCB0aGUgd29yZHMgYXBwZWFyIGxlc3MgdGhhbiAwLjElIG9mIHRoZSB0aW1lCnRocmVzaG9sZCA8LSAwLjEgIyBpbiAlCm1pbl9mcmVxID0gcm91bmQoRFRNJG5yb3cqKHRocmVzaG9sZC8xMDApLDApICMgY2FsY3VsYXRlIG1pbmltdW0gZnJlcXVlbmN5Cm1pbl9mcmVxCgojIENyZWF0ZSB2ZWN0b3Igb2YgbW9zdCBmcmVxdWVudCB3b3JkcwpmcmVxdWVudF93b3JkcyA8LSBmaW5kRnJlcVRlcm1zKHggPSBEVE0sIGxvd2ZyZXEgPSBtaW5fZnJlcSkKCnN0cihmcmVxdWVudF93b3JkcykKCiMgRmlsdGVyIERUTSB0byBvbmx5IGhhdmUgbW9zdCBmcmVxdWVudCB3b3JkcwpEVE1fdHJhaW5fbW9zdF9mcmVxdWVudCA8LSBEVE1fdHJhaW5bLCBmcmVxdWVudF93b3Jkc10KRFRNX3Rlc3RfbW9zdF9mcmVxdWVudCA8LSBEVE1fdGVzdFssIGZyZXF1ZW50X3dvcmRzXQoKIyBDaGVjayBkaW1lbnNpb24gb2YgRFRNCmRpbShEVE1fdHJhaW5fbW9zdF9mcmVxdWVudCkKCmBgYAoKCiMjIENvbnZlcnQgbnVtZXJpYyB2YWx1ZXMgaW50byBjYXRlZ29yaWNhbCB2YWx1ZXMKCkluIG9yZGVyIHRvIHVzZSB0aGUgTmFpdmUgQmF5ZXMgY2xhc3NpZmllciB3ZSBuZWVkIHRvIGNvbnZlcnQgdGhlIG51bWVyaWNhbCBmZWF0dXJlcyBpbiBvdXIgRG9jdW1lbnQgVGVybSBNYXRyaXggKERUTSkgdG8gY2F0ZWdvcmljYWwgZmVhdHVyZXMuCgpgYGB7cn0KIyBDcmVhdGUgZnVuY3Rpb24gIHRoYXQgY29udmVydHMgbnVtZXJpYyB2YWx1ZXMgdG8gIlllcyIgb3IgIk5vIiBpZiB3b3JkIGlzIHByZXNlbnQgb3IgYWJzZW50IGluIGRvY3VtZW50CmlzX3ByZXNlbnQgPC0gZnVuY3Rpb24oeCkgewogIHggPC0gaWZlbHNlKHRlc3QgPSB4ID4gMCwgeWVzID0gIlllcyIsIG5vID0gIk5vIikKfQoKIyBUZXN0IGZ1bmN0aW9uCnggPC0gaXNfcHJlc2VudChjKDEsIDAsIDMsIDQsIDAsIDApKQp4CgojIEFwcGx5IGlzX3ByZXNlbnQoKSBmdW5jdGlvbiB0byB0cmFpbmluZyBhbmQgdGVzdCBEVE0KRFRNX3RyYWluX21vc3RfZnJlcXVlbnQgPC0gYXBwbHkoWCA9IERUTV90cmFpbl9tb3N0X2ZyZXF1ZW50LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTUFSR0lOID0gMiwgIyBBcHBseSBmdW5jdGlvbiB0byBjb2x1bW5zCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IGlzX3ByZXNlbnQpICMgU3BlY2lmeSBmdW5jdGlvbiB0byBiZSB1c2VkCgpEVE1fdGVzdF9tb3N0X2ZyZXF1ZW50IDwtIGFwcGx5KFggPSBEVE1fdGVzdF9tb3N0X2ZyZXF1ZW50LCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgTUFSR0lOID0gMiwgIyBBcHBseSBmdW5jdGlvbiB0byBjb2x1bW5zCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEZVTiA9IGlzX3ByZXNlbnQpICMgU3BlY2lmeSBmdW5jdGlvbiB0byBiZSB1c2VkCgpgYGAKCiMjIFRyYWluIE1vZGVsCgpgYGB7cn0KIyBDcmVhdGUgbW9kZWwgZnJvbSB0aGUgdHJhaW5pbmcgZGF0YXNldApzcGFtX2NsYXNzaWZpZXIgPC0gbmFpdmVCYXllcyh4ID0gRFRNX3RyYWluX21vc3RfZnJlcXVlbnQsIHkgPSB0cmFpbl9sYWJlbHMpCgojIFByaW50IHByb2JhYmlsaXR5IHRhYmxlcyBmb3Igc29tZSB3b3JkcwpzcGFtX2NsYXNzaWZpZXIkdGFibGVzJGNhbGwKc3BhbV9jbGFzc2lmaWVyJHRhYmxlcyRmcmllbmQKc3BhbV9jbGFzc2lmaWVyJHRhYmxlcyRmcmVlCmBgYAoKV2UgY2FuIHNlZSBmcm9tIHRoZSBwcm9iYWJpbGl0eSB0YWJsZXMgYWJvdmUgdGhhdCB0aGUgcHJvYmFiaWxpdHkgb2YgZmluZGluZyB0aGUgd29yZHMgImNhbGwiIGFuZCAiZnJlZSIgaW4gc3BhbSBtZXNzYWdlcyBhcmUgbXVsdGlwbGUgdGltZXMgaGlnaGVyIHRoYW4gaW4gbGVnaXRpbWF0ZSBtZXNzYWdlcy4gT24gdGhlIG90aGVyIGhhbmQgdGhlIHdvcmQgImZyaWVuZCIgaXMgYWxtb3N0IGVxdWFsbHkgbGlrZWx5IHRvIGJlIGZvdW5kIGluIGJvdGggaGFtIGFuZCBzcGFtLgoKIyBFdmFsdWF0aW5nIHRoZSBwZXJmb3JtYW5jZSBvZiBvdXIgY2xhc3NpZmllcgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBNYWtlIHByZWRpY3Rpb25zIG9uIHRlc3Qgc2V0CnRlc3RfcHJlZGljdGlvbnMgPC0gcHJlZGljdChvYmplY3QgPSBzcGFtX2NsYXNzaWZpZXIsIG5ld2RhdGEgPSBEVE1fdGVzdF9tb3N0X2ZyZXF1ZW50KQoKCiMgQ3JlYXRlIGNvbmZ1c2lvbiBtYXRyaXgKY29uZnVzaW9uTWF0cml4KGRhdGEgPSB0ZXN0X3ByZWRpY3Rpb25zLCByZWZlcmVuY2UgPSB0ZXN0X2xhYmVscywgcG9zaXRpdmUgPSAic3BhbSIsIGRubiA9IGMoIlByZWRpY3Rpb24iLCAiQWN0dWFsIikpCgpgYGAKCiMgQ29uY2x1c2lvbgoKV2UgdGVzdCB0aGUgcGVyZm9ybWFuY2Ugb2Ygb3VyIHNwYW0gY2xhc3NpZmllciB1c2luZyBvdXIgdGVzdGluZyBkYXRhc2V0XApBcyB3ZSBjYW4gc2VlIGZyb20gdGhlIG91dHB1dCBhYm92ZSB3ZSBvYnRhaW4gYW4gb3ZlcmFsbCBhY2N1cmFjeSBvZiBgciB4IDwtIGNvbmZ1c2lvbk1hdHJpeChkYXRhID0gdGVzdF9wcmVkaWN0aW9ucywgcmVmZXJlbmNlID0gdGVzdF9sYWJlbHMpOyByb3VuZCh4JG92ZXJhbGxbWyJBY2N1cmFjeSJdXSwgNCkqMTAwYCUuIAoKVGhlIE5haXZlIEJheWVzIG1vZGVsIHdlIGJ1aWx0IHNlZW1zIGxpa2UgYWJsZSB0byBjbGFzc2lmeSBzcGFtIGFuZCBoYW0gdGV4dCBlZmZlY3RpdmVseS4gCgojIFJlZmVyZW5jZXMKMS4gaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9kYXRhc2V0cy91Y2ltbC9zbXMtc3BhbS1jb2xsZWN0aW9uLWRhdGFzZXQKCjIuIGh0dHBzOi8vd3d3LmdlZWtzZm9yZ2Vla3Mub3JnL25haXZlLWJheWVzLWNsYXNzaWZpZXItaW4tci1wcm9ncmFtbWluZy8KCgoK