
Motivations
The upsurge in the volume of unwanted SMS messages called spam has created an intense need for the development of more dependable and robust antispam filters. In addition, Social Network (SN) is an online platform broadly used as communication tool by millions of users in order to build social relationships with others for knowledge point of view, career purposes and many more. Social Networks such as Twitter, Facebook, and LinkedIn have become the most leading tools on the web. Spam, floods the Internet with many copies of the same message and it can be manifest in numerous ways, it includes bulk messages, malicious links, fake friends, fraudulent reviews and personally identifiable information.
Viettel build a algorithm for filtering spam message with accuracy of 90%. You can read here for more information and here about Spam Detection.
This post will present hand-on instructions for modelling and building Random Forest algorithm for filtering spam message. I will use data adapted from the SMS Spam Collection (sms_spam.csv) provided by Center for Machine Learning and Intelligent Systems, the University of California at Irvine and you can download here for practice and reproducibility.
Our Results
Some main metrics for model performance:

- Accuracy Rate for detecting spam = 98.58%, Overall Accuracy Rate = 94.78% when cutoff = 0.35 (Figure 2):

R Codes
# Load data:
library(tidyverse)
library(magrittr)
sms_raw <- read_csv("C:\\Users\\Zbook\\Documents\\sms_spam.csv")
# Extract label (spam or ham) and convert to factor:
label <- sms_raw$type %>% as.factor()
# Word cloud for spams:
par(bg = "black")
set.seed(1709)
library(wordcloud)
wordcloud(sms_raw %>% filter(type == "spam") %>% pull(text),
max.words = 100,
random.order = FALSE,
rot.per = 0.35,
font = 2,
colors = brewer.pal(8, "Dark2"))
# Preapre data for modelling:
library(tm)
sms_corpus <- sms_raw$text %>%
VectorSource() %>%
VCorpus() %>%
tm_map(content_transformer(tolower)) %>%
tm_map(removeNumbers) %>%
tm_map(removeWords, stopwords()) %>%
tm_map(removePunctuation) %>%
tm_map(stripWhitespace)
# Convert to DTM sparse matrix:
dtm <- sms_corpus %>% DocumentTermMatrix()
# List of words that appear more than 20:
at_least20 <- findFreqTerms(dtm, 20)
# Convert sparse matrix to data frame:
inputs <- apply(dtm[, at_least20], 2,
function (x) {case_when(x == 0 ~ "No", TRUE ~ "Yes") %>% as.factor()})
inputs %>%
as.matrix() %>%
as.data.frame() %>%
mutate(Class = label) -> df
# Split data:
library(caret)
set.seed(1)
id <- createDataPartition(y = df$Class, p = 0.7, list = FALSE)
df_train_ml <- df[id, ]
df_test_ml <- df[-id, ]
# Use Parallel computing:
library(doParallel)
registerDoParallel(cores = detectCores() - 1)
# Activate h2o package for using:
library(h2o)
h2o.init(nthreads = -1, max_mem_size = "16g")
# Convert to h2o Frame and identify inputs and output:
h2o.no_progress()
test <- as.h2o(df_test_ml)
train <- as.h2o(df_train_ml)
y <- "Class"
x <- setdiff(names(train), y)
# Set hyperparameter grid:
hyper_grid.h2o <- list(ntrees = seq(50, 500, by = 50),
mtries = seq(3, 5, by = 1),
# max_depth = seq(10, 30, by = 10),
# min_rows = seq(1, 3, by = 1),
# nbins = seq(20, 30, by = 10),
sample_rate = c(0.55, 0.632, 0.75))
# Set random grid search criteria:
search_criteria_2 <- list(strategy = "RandomDiscrete",
stopping_metric = "AUC",
stopping_tolerance = 0.005,
stopping_rounds = 10,
max_runtime_secs = 30*60)
# Turn parameters for RF:
system.time(random_grid <- h2o.grid(algorithm = "randomForest",
grid_id = "rf_grid2",
x = x,
y = y,
seed = 29,
nfolds = 10,
training_frame = train,
hyper_params = hyper_grid.h2o,
search_criteria = search_criteria_2))
# Collect the results and sort by our models:
grid_perf2 <- h2o.getGrid(grid_id = "rf_grid2",
sort_by = "AUC",
decreasing = FALSE)
# Best RF:
best_model2 <- h2o.getModel(grid_perf2@model_ids[[1]])
# ROC curve and AUC:
library(pROC)
# Function calculates AUC:
auc_for_test <- function(model_selected) {
actual <- df_test_ml$Class
pred_prob <- h2o.predict(model_selected, test) %>% as.data.frame() %>% pull(spam)
return(roc(actual, pred_prob))
}
# Use this function:
my_auc <- auc_for_test(best_model2)
# Graph ROC and AUC:
sen_spec_df <- data_frame(TPR = my_auc$sensitivities, FPR = 1 - my_auc$specificities)
theme_set(theme_minimal())
sen_spec_df %>%
ggplot(aes(x = FPR, ymin = 0, ymax = TPR))+
geom_polygon(aes(y = TPR), fill = "red", alpha = 0.3)+
geom_path(aes(y = TPR), col = "firebrick", size = 1.2) +
geom_abline(intercept = 0, slope = 1, color = "gray37", size = 1, linetype = "dashed") +
theme_bw() +
coord_equal() +
labs(x = "FPR (1 - Specificity)",
y = "TPR (Sensitivity)",
title = "Model Performance for RF Classifier",
subtitle = paste0("AUC Value: ", my_auc$auc %>% round(2)))
# Set a range of threshold for classification:
my_threshold <- c(0.10, 0.15, 0.35, 0.4, 0.45, 0.5)
# Use this best model for prediction with some thresholds selected:
my_cm_com_rf_best2 <- function(thre) {
du_bao_prob <- h2o.predict(best_model2, test) %>% as.data.frame() %>% pull(spam)
du_bao <- case_when(du_bao_prob >= thre ~ "spam", du_bao_prob < thre ~ "ham") %>% as.factor()
cm <- confusionMatrix(du_bao, df_test_ml$Class, positive = "spam")
return(cm)
}
# Model Performance by cutoff selected:
results_list_rf <- lapply(my_threshold, my_cm_com_rf_best2)
# Function for presenting prediction power by class:
vis_detection_rate_rf <- function(x) {
results_list_rf[[x]]$table %>% as.data.frame() -> m
my_acc <- results_list_rf[[x]]$table %>% as.vector()
k <- my_acc[4] / sum(my_acc[c(2, 4)])
rate <- round((100*my_acc[4] / sum(my_acc[c(2, 4)])) , 2)
acc <- round(100*sum(m$Freq[c(1, 4)]) / sum(m$Freq), 2)
acc <- paste0(acc, "%")
m %>%
ggplot(aes(Prediction, Freq, fill = Reference)) +
geom_col(position = "fill") +
scale_fill_manual(values = c("#e41a1c", "#377eb8"), name = "") +
theme(panel.grid.minor.y = element_blank()) +
theme(panel.grid.minor.x = element_blank()) +
scale_y_continuous(labels = scales::percent) +
labs(x = NULL, y = NULL,
title = paste0("Model Performance when Threshold = ", my_threshold[x]),
subtitle = paste0("Accuracy for Spam Cases: ", rate, "%", ", ", "Accuracy: ", acc))
}
# Use this function:
gridExtra::grid.arrange(vis_detection_rate_rf(1),
vis_detection_rate_rf(2),
vis_detection_rate_rf(3),
vis_detection_rate_rf(4),
vis_detection_rate_rf(5),
vis_detection_rate_rf(6))
# Model performance when cutoff = 0.35:
results_list_rf[[3]]$table
LS0tDQp0aXRsZTogIlNwYW0gRGV0ZWN0aW9uIFVzaW5nIFJhbmRvbSBGb3Jlc3QgQWxnb3JpdGhtIg0KYXV0aG9yOiAiTmd1eWVuIENoaSBEdW5nIg0Kc3VidGl0bGU6ICJEYXRhIFNjaWVuY2UgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgIHRoZW1lOiBmbGF0bHkNCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogIHdvcmRfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgZmlnLnJldGluYT0yKQ0KYGBgDQoNCiFbXShDOlxcVXNlcnNcXFpib29rXFxEZXNrdG9wXFxwaWNcXHBpY18xLmpwZykNCg0KIyBNb3RpdmF0aW9ucw0KDQpUaGUgdXBzdXJnZSBpbiB0aGUgdm9sdW1lIG9mIHVud2FudGVkIFNNUyBtZXNzYWdlcyBjYWxsZWQgc3BhbSBoYXMgY3JlYXRlZCBhbiBpbnRlbnNlIG5lZWQgZm9yIHRoZSBkZXZlbG9wbWVudCBvZiBtb3JlIGRlcGVuZGFibGUgYW5kIHJvYnVzdCBhbnRpc3BhbSBmaWx0ZXJzLiBJbiBhZGRpdGlvbiwgU29jaWFsIE5ldHdvcmsgKFNOKSBpcyBhbiBvbmxpbmUgcGxhdGZvcm0gYnJvYWRseSB1c2VkIGFzIGNvbW11bmljYXRpb24gdG9vbCBieSBtaWxsaW9ucyBvZiB1c2VycyBpbiBvcmRlciB0byBidWlsZCBzb2NpYWwgcmVsYXRpb25zaGlwcyB3aXRoIG90aGVycyBmb3Iga25vd2xlZGdlIHBvaW50IG9mIHZpZXcsIGNhcmVlciBwdXJwb3NlcyBhbmQgbWFueSBtb3JlLiBTb2NpYWwgTmV0d29ya3Mgc3VjaCBhcyBUd2l0dGVyLCBGYWNlYm9vaywgYW5kIExpbmtlZEluIGhhdmUgYmVjb21lIHRoZSBtb3N0IGxlYWRpbmcgdG9vbHMgb24gdGhlIHdlYi4gU3BhbSwgZmxvb2RzIHRoZSBJbnRlcm5ldCB3aXRoIG1hbnkgY29waWVzIG9mIHRoZSBzYW1lIG1lc3NhZ2UgYW5kIGl0IGNhbiBiZSBtYW5pZmVzdCBpbiBudW1lcm91cyB3YXlzLCBpdCBpbmNsdWRlcyBidWxrIG1lc3NhZ2VzLCBtYWxpY2lvdXMgbGlua3MsIGZha2UgZnJpZW5kcywgZnJhdWR1bGVudCByZXZpZXdzIGFuZCBwZXJzb25hbGx5IGlkZW50aWZpYWJsZSBpbmZvcm1hdGlvbi4NCg0KDQpWaWV0dGVsIGJ1aWxkIGEgYWxnb3JpdGhtIGZvciBmaWx0ZXJpbmcgc3BhbSBtZXNzYWdlIHdpdGggYWNjdXJhY3kgb2YgOTAlLiBZb3UgY2FuIHJlYWQgW2hlcmVdKGh0dHA6Ly80Z3ZpZXR0ZWwuaW5mby92aWV0dGVsLWFudGktc3BhbS8pIGZvciBtb3JlIGluZm9ybWF0aW9uIGFuZCBbaGVyZV0oaHR0cDovL3d3dy5kdC5mZWUudW5pY2FtcC5ici9+dGlhZ28vc21zc3BhbWNvbGxlY3Rpb24vaWNtbGExMi5wZGYpIGFib3V0IFNwYW0gRGV0ZWN0aW9uLiANCg0KVGhpcyBwb3N0IHdpbGwgcHJlc2VudCBoYW5kLW9uIGluc3RydWN0aW9ucyBmb3IgbW9kZWxsaW5nIGFuZCBidWlsZGluZyBSYW5kb20gRm9yZXN0IGFsZ29yaXRobSBmb3IgZmlsdGVyaW5nIHNwYW0gbWVzc2FnZS4gSSB3aWxsIHVzZSBkYXRhIGFkYXB0ZWQgZnJvbSB0aGUgU01TIFNwYW0gQ29sbGVjdGlvbiAoKipzbXNfc3BhbS5jc3YqKikgcHJvdmlkZWQgYnkgQ2VudGVyIGZvciBNYWNoaW5lIExlYXJuaW5nIGFuZCBJbnRlbGxpZ2VudCBTeXN0ZW1zLCB0aGUgVW5pdmVyc2l0eSBvZiBDYWxpZm9ybmlhIGF0IElydmluZSBhbmQgeW91IGNhbiBkb3dubG9hZCBbaGVyZV0oaHR0cDovL3d3dy5tZWRpYWZpcmUuY29tL2ZpbGUvM2xnOGJzZmJ1NmNzcThkL0tpbmhUZUx1b25nVW5nRHVuZ1ZvaVIucmFyL2ZpbGUpIGZvciBwcmFjdGljZSBhbmQgcmVwcm9kdWNpYmlsaXR5LiANCg0KDQojIE91ciBSZXN1bHRzDQoNClNvbWUgbWFpbiBtZXRyaWNzIGZvciBtb2RlbCBwZXJmb3JtYW5jZTogDQoNCi0gQVVDID0gOTclIChGaWd1cmUgMSk6IA0KDQohW10oQzpcXFVzZXJzXFxaYm9va1xcRGVza3RvcFxccGljXFxzcGFtMS5qcGcpDQoNCi0gQWNjdXJhY3kgUmF0ZSBmb3IgZGV0ZWN0aW5nIHNwYW0gPSA5OC41OCUsIE92ZXJhbGwgQWNjdXJhY3kgUmF0ZSA9IDk0Ljc4JSB3aGVuIGN1dG9mZiA9IDAuMzUgKEZpZ3VyZSAyKTogDQoNCiFbXShDOlxcVXNlcnNcXFpib29rXFxEZXNrdG9wXFxwaWNcXHNwYW0yLmpwZykNCg0KDQojIFIgQ29kZXMNCg0KIA0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCiMgTG9hZCBkYXRhOiANCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShtYWdyaXR0cikNCnNtc19yYXcgPC0gcmVhZF9jc3YoIkM6XFxVc2Vyc1xcWmJvb2tcXERvY3VtZW50c1xcc21zX3NwYW0uY3N2IikNCg0KIyBFeHRyYWN0IGxhYmVsIChzcGFtIG9yIGhhbSkgYW5kIGNvbnZlcnQgdG8gZmFjdG9yOiANCmxhYmVsIDwtIHNtc19yYXckdHlwZSAlPiUgYXMuZmFjdG9yKCkNCg0KIyBXb3JkIGNsb3VkIGZvciBzcGFtczogDQoNCnBhcihiZyA9ICJibGFjayIpIA0Kc2V0LnNlZWQoMTcwOSkNCmxpYnJhcnkod29yZGNsb3VkKQ0KDQp3b3JkY2xvdWQoc21zX3JhdyAlPiUgZmlsdGVyKHR5cGUgPT0gInNwYW0iKSAlPiUgcHVsbCh0ZXh0KSwgDQogICAgICAgICAgbWF4LndvcmRzID0gMTAwLCANCiAgICAgICAgICByYW5kb20ub3JkZXIgPSBGQUxTRSwgDQogICAgICAgICAgcm90LnBlciA9IDAuMzUsIA0KICAgICAgICAgIGZvbnQgPSAyLA0KICAgICAgICAgIGNvbG9ycyA9IGJyZXdlci5wYWwoOCwgIkRhcmsyIikpDQoNCg0KIyBQcmVhcHJlIGRhdGEgZm9yIG1vZGVsbGluZzogDQoNCmxpYnJhcnkodG0pDQoNCnNtc19jb3JwdXMgPC0gc21zX3JhdyR0ZXh0ICU+JSANCiAgVmVjdG9yU291cmNlKCkgJT4lIA0KICBWQ29ycHVzKCkgJT4lIA0KICB0bV9tYXAoY29udGVudF90cmFuc2Zvcm1lcih0b2xvd2VyKSkgJT4lIA0KICB0bV9tYXAocmVtb3ZlTnVtYmVycykgJT4lIA0KICB0bV9tYXAocmVtb3ZlV29yZHMsIHN0b3B3b3JkcygpKSAlPiUgDQogIHRtX21hcChyZW1vdmVQdW5jdHVhdGlvbikgJT4lIA0KICB0bV9tYXAoc3RyaXBXaGl0ZXNwYWNlKQ0KDQojIENvbnZlcnQgdG8gRFRNIHNwYXJzZSBtYXRyaXg6IA0KDQpkdG0gPC0gc21zX2NvcnB1cyAlPiUgRG9jdW1lbnRUZXJtTWF0cml4KCkNCg0KIyBMaXN0IG9mIHdvcmRzIHRoYXQgYXBwZWFyIG1vcmUgdGhhbiAyMDogDQoNCmF0X2xlYXN0MjAgPC0gZmluZEZyZXFUZXJtcyhkdG0sIDIwKQ0KDQojIENvbnZlcnQgc3BhcnNlIG1hdHJpeCB0byBkYXRhIGZyYW1lOiANCg0KaW5wdXRzIDwtIGFwcGx5KGR0bVssIGF0X2xlYXN0MjBdLCAyLCANCiAgICAgICAgICAgICAgICBmdW5jdGlvbiAoeCkge2Nhc2Vfd2hlbih4ID09IDAgfiAiTm8iLCBUUlVFIH4gIlllcyIpICU+JSBhcy5mYWN0b3IoKX0pDQoNCmlucHV0cyAlPiUgDQogIGFzLm1hdHJpeCgpICU+JSANCiAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgbXV0YXRlKENsYXNzID0gbGFiZWwpIC0+IGRmDQoNCiMgU3BsaXQgZGF0YTogDQoNCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgxKQ0KaWQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5ID0gZGYkQ2xhc3MsIHAgPSAwLjcsIGxpc3QgPSBGQUxTRSkNCmRmX3RyYWluX21sIDwtIGRmW2lkLCBdDQpkZl90ZXN0X21sIDwtIGRmWy1pZCwgXQ0KDQoNCiMgVXNlIFBhcmFsbGVsIGNvbXB1dGluZzogDQoNCmxpYnJhcnkoZG9QYXJhbGxlbCkNCnJlZ2lzdGVyRG9QYXJhbGxlbChjb3JlcyA9IGRldGVjdENvcmVzKCkgLSAxKQ0KDQojIEFjdGl2YXRlIGgybyBwYWNrYWdlIGZvciB1c2luZzogDQoNCmxpYnJhcnkoaDJvKQ0KaDJvLmluaXQobnRocmVhZHMgPSAtMSwgbWF4X21lbV9zaXplID0gIjE2ZyIpDQoNCiMgQ29udmVydCB0byBoMm8gRnJhbWUgYW5kIGlkZW50aWZ5IGlucHV0cyBhbmQgb3V0cHV0OiANCg0KaDJvLm5vX3Byb2dyZXNzKCkNCnRlc3QgPC0gYXMuaDJvKGRmX3Rlc3RfbWwpDQp0cmFpbiA8LSBhcy5oMm8oZGZfdHJhaW5fbWwpDQoNCnkgPC0gIkNsYXNzIg0KeCA8LSBzZXRkaWZmKG5hbWVzKHRyYWluKSwgeSkNCg0KDQojIFNldCBoeXBlcnBhcmFtZXRlciBncmlkOiANCg0KaHlwZXJfZ3JpZC5oMm8gPC0gbGlzdChudHJlZXMgPSBzZXEoNTAsIDUwMCwgYnkgPSA1MCksDQogICAgICAgICAgICAgICAgICAgICAgIG10cmllcyA9IHNlcSgzLCA1LCBieSA9IDEpLA0KICAgICAgICAgICAgICAgICAgICAgICAjIG1heF9kZXB0aCA9IHNlcSgxMCwgMzAsIGJ5ID0gMTApLA0KICAgICAgICAgICAgICAgICAgICAgICAjIG1pbl9yb3dzID0gc2VxKDEsIDMsIGJ5ID0gMSksDQogICAgICAgICAgICAgICAgICAgICAgICMgbmJpbnMgPSBzZXEoMjAsIDMwLCBieSA9IDEwKSwNCiAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxlX3JhdGUgPSBjKDAuNTUsIDAuNjMyLCAwLjc1KSkNCg0KDQoNCiMgU2V0IHJhbmRvbSBncmlkIHNlYXJjaCBjcml0ZXJpYTogDQoNCnNlYXJjaF9jcml0ZXJpYV8yIDwtIGxpc3Qoc3RyYXRlZ3kgPSAiUmFuZG9tRGlzY3JldGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19tZXRyaWMgPSAiQVVDIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcHBpbmdfdG9sZXJhbmNlID0gMC4wMDUsDQogICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX3JvdW5kcyA9IDEwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhfcnVudGltZV9zZWNzID0gMzAqNjApDQoNCg0KIyBUdXJuIHBhcmFtZXRlcnMgZm9yIFJGOiANCg0Kc3lzdGVtLnRpbWUocmFuZG9tX2dyaWQgPC0gaDJvLmdyaWQoYWxnb3JpdGhtID0gInJhbmRvbUZvcmVzdCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBncmlkX2lkID0gInJmX2dyaWQyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHggPSB4LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSB5LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSAyOSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZm9sZHMgPSAxMCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbmluZ19mcmFtZSA9IHRyYWluLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHlwZXJfcGFyYW1zID0gaHlwZXJfZ3JpZC5oMm8sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWFyY2hfY3JpdGVyaWEgPSBzZWFyY2hfY3JpdGVyaWFfMikpDQoNCg0KIyBDb2xsZWN0IHRoZSByZXN1bHRzIGFuZCBzb3J0IGJ5IG91ciBtb2RlbHM6IA0KZ3JpZF9wZXJmMiA8LSBoMm8uZ2V0R3JpZChncmlkX2lkID0gInJmX2dyaWQyIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNvcnRfYnkgPSAiQVVDIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgIGRlY3JlYXNpbmcgPSBGQUxTRSkNCg0KIyBCZXN0IFJGOiANCmJlc3RfbW9kZWwyIDwtIGgyby5nZXRNb2RlbChncmlkX3BlcmYyQG1vZGVsX2lkc1tbMV1dKQ0KDQoNCiMgUk9DIGN1cnZlIGFuZCBBVUM6IA0KbGlicmFyeShwUk9DKSANCg0KIyBGdW5jdGlvbiBjYWxjdWxhdGVzIEFVQzogDQoNCmF1Y19mb3JfdGVzdCA8LSBmdW5jdGlvbihtb2RlbF9zZWxlY3RlZCkgew0KICBhY3R1YWwgPC0gZGZfdGVzdF9tbCRDbGFzcw0KICBwcmVkX3Byb2IgPC0gaDJvLnByZWRpY3QobW9kZWxfc2VsZWN0ZWQsIHRlc3QpICU+JSBhcy5kYXRhLmZyYW1lKCkgJT4lIHB1bGwoc3BhbSkNCiAgcmV0dXJuKHJvYyhhY3R1YWwsIHByZWRfcHJvYikpDQp9DQoNCiMgVXNlIHRoaXMgZnVuY3Rpb246IA0KbXlfYXVjIDwtIGF1Y19mb3JfdGVzdChiZXN0X21vZGVsMikNCg0KIyBHcmFwaCBST0MgYW5kIEFVQzogDQoNCnNlbl9zcGVjX2RmIDwtIGRhdGFfZnJhbWUoVFBSID0gbXlfYXVjJHNlbnNpdGl2aXRpZXMsIEZQUiA9IDEgLSBteV9hdWMkc3BlY2lmaWNpdGllcykNCg0KdGhlbWVfc2V0KHRoZW1lX21pbmltYWwoKSkNCg0Kc2VuX3NwZWNfZGYgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBGUFIsIHltaW4gPSAwLCB5bWF4ID0gVFBSKSkrDQogIGdlb21fcG9seWdvbihhZXMoeSA9IFRQUiksIGZpbGwgPSAicmVkIiwgYWxwaGEgPSAwLjMpKw0KICBnZW9tX3BhdGgoYWVzKHkgPSBUUFIpLCBjb2wgPSAiZmlyZWJyaWNrIiwgc2l6ZSA9IDEuMikgKw0KICBnZW9tX2FibGluZShpbnRlcmNlcHQgPSAwLCBzbG9wZSA9IDEsIGNvbG9yID0gImdyYXkzNyIsIHNpemUgPSAxLCBsaW5ldHlwZSA9ICJkYXNoZWQiKSArIA0KICB0aGVtZV9idygpICsNCiAgY29vcmRfZXF1YWwoKSArDQogIGxhYnMoeCA9ICJGUFIgKDEgLSBTcGVjaWZpY2l0eSkiLCANCiAgICAgICB5ID0gIlRQUiAoU2Vuc2l0aXZpdHkpIiwgDQogICAgICAgdGl0bGUgPSAiTW9kZWwgUGVyZm9ybWFuY2UgZm9yIFJGIENsYXNzaWZpZXIiLCANCiAgICAgICBzdWJ0aXRsZSA9IHBhc3RlMCgiQVVDIFZhbHVlOiAiLCBteV9hdWMkYXVjICU+JSByb3VuZCgyKSkpDQoNCg0KDQoNCiMgU2V0IGEgcmFuZ2Ugb2YgdGhyZXNob2xkIGZvciBjbGFzc2lmaWNhdGlvbjogDQoNCm15X3RocmVzaG9sZCA8LSBjKDAuMTAsIDAuMTUsIDAuMzUsIDAuNCwgMC40NSwgMC41KQ0KDQojIFVzZSB0aGlzIGJlc3QgbW9kZWwgZm9yIHByZWRpY3Rpb24gd2l0aCBzb21lIHRocmVzaG9sZHMgc2VsZWN0ZWQ6IA0KDQpteV9jbV9jb21fcmZfYmVzdDIgPC0gZnVuY3Rpb24odGhyZSkgew0KDQogIGR1X2Jhb19wcm9iIDwtIGgyby5wcmVkaWN0KGJlc3RfbW9kZWwyLCB0ZXN0KSAlPiUgYXMuZGF0YS5mcmFtZSgpICU+JSBwdWxsKHNwYW0pDQogIGR1X2JhbyA8LSBjYXNlX3doZW4oZHVfYmFvX3Byb2IgPj0gdGhyZSB+ICJzcGFtIiwgZHVfYmFvX3Byb2IgPCB0aHJlIH4gImhhbSIpICU+JSBhcy5mYWN0b3IoKQ0KICBjbSA8LSBjb25mdXNpb25NYXRyaXgoZHVfYmFvLCBkZl90ZXN0X21sJENsYXNzLCBwb3NpdGl2ZSA9ICJzcGFtIikNCiAgcmV0dXJuKGNtKQ0KICANCn0NCg0KIyBNb2RlbCBQZXJmb3JtYW5jZSBieSBjdXRvZmYgc2VsZWN0ZWQ6IA0KDQpyZXN1bHRzX2xpc3RfcmYgPC0gbGFwcGx5KG15X3RocmVzaG9sZCwgbXlfY21fY29tX3JmX2Jlc3QyKQ0KDQojIEZ1bmN0aW9uIGZvciBwcmVzZW50aW5nIHByZWRpY3Rpb24gcG93ZXIgYnkgY2xhc3M6ICANCg0KdmlzX2RldGVjdGlvbl9yYXRlX3JmIDwtIGZ1bmN0aW9uKHgpIHsNCiAgDQogIHJlc3VsdHNfbGlzdF9yZltbeF1dJHRhYmxlICU+JSBhcy5kYXRhLmZyYW1lKCkgLT4gbQ0KICBteV9hY2MgPC0gcmVzdWx0c19saXN0X3JmW1t4XV0kdGFibGUgJT4lIGFzLnZlY3RvcigpDQogIGsgPC0gbXlfYWNjWzRdIC8gc3VtKG15X2FjY1tjKDIsIDQpXSkNCiAgcmF0ZSA8LSByb3VuZCgoMTAwKm15X2FjY1s0XSAvIHN1bShteV9hY2NbYygyLCA0KV0pKSAsIDIpDQogIGFjYyA8LSByb3VuZCgxMDAqc3VtKG0kRnJlcVtjKDEsIDQpXSkgLyBzdW0obSRGcmVxKSwgMikNCiAgYWNjIDwtIHBhc3RlMChhY2MsICIlIikNCiAgDQogIG0gJT4lIA0KICAgIGdncGxvdChhZXMoUHJlZGljdGlvbiwgRnJlcSwgZmlsbCA9IFJlZmVyZW5jZSkpICsNCiAgICBnZW9tX2NvbChwb3NpdGlvbiA9ICJmaWxsIikgKyANCiAgICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXMgPSBjKCIjZTQxYTFjIiwgIiMzNzdlYjgiKSwgbmFtZSA9ICIiKSArIA0KICAgIHRoZW1lKHBhbmVsLmdyaWQubWlub3IueSA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgICB0aGVtZShwYW5lbC5ncmlkLm1pbm9yLnggPSBlbGVtZW50X2JsYW5rKCkpICsgDQogICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKyANCiAgICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgDQogICAgICAgICB0aXRsZSA9IHBhc3RlMCgiTW9kZWwgUGVyZm9ybWFuY2Ugd2hlbiBUaHJlc2hvbGQgPSAiLCBteV90aHJlc2hvbGRbeF0pLCANCiAgICAgICAgIHN1YnRpdGxlID0gcGFzdGUwKCJBY2N1cmFjeSBmb3IgU3BhbSBDYXNlczogIiwgcmF0ZSwgIiUiLCAiLCAiLCAiQWNjdXJhY3k6ICIsIGFjYykpDQp9DQoNCiMgVXNlIHRoaXMgZnVuY3Rpb246IA0KDQpncmlkRXh0cmE6OmdyaWQuYXJyYW5nZSh2aXNfZGV0ZWN0aW9uX3JhdGVfcmYoMSksIA0KICAgICAgICAgICAgICAgICAgICAgICAgdmlzX2RldGVjdGlvbl9yYXRlX3JmKDIpLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHZpc19kZXRlY3Rpb25fcmF0ZV9yZigzKSwgDQogICAgICAgICAgICAgICAgICAgICAgICB2aXNfZGV0ZWN0aW9uX3JhdGVfcmYoNCksIA0KICAgICAgICAgICAgICAgICAgICAgICAgdmlzX2RldGVjdGlvbl9yYXRlX3JmKDUpLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHZpc19kZXRlY3Rpb25fcmF0ZV9yZig2KSkNCg0KDQoNCg0KIyBNb2RlbCBwZXJmb3JtYW5jZSB3aGVuIGN1dG9mZiA9IDAuMzU6IA0KcmVzdWx0c19saXN0X3JmW1szXV0kdGFibGUNCmBgYA0KDQoNCg==