This notebook describes an example of using the caret
package to conduct hyperparameter tuning for the k-Nearest Neighbour classifier.
library(mclust)
library(dplyr)
library(ggplot2)
library(caret)
library(pROC)
Example dataset
The example dataset is the banknote
dataframe found in the mclust
package. It contains six measurements made on 100 genuine and 100 counterfeit old-Swiss 1000-franc bank notes.
data(banknote)
head(banknote)
There are six predictor variables (Length
, Left
, Right
, Bottom
, Top
, Diagonal
) with Status
being the categorical response or class variable having two levels, namely genuine
and counterfeit
.
Exploratory data analysis
Observe that the dataset is balanced with 100 observations against each level of Status
.
banknote %>%
group_by(Status) %>%
summarise(N = n(),
Mean_Length = mean(Length),
Mean_Left = mean(Left),
Mean_Right = mean(Right),
Mean_Bottom = mean(Bottom),
Mean_Top = mean(Top),
Mean_Diagonal = mean(Diagonal),
.groups = "keep")
In most of the measurements of bank notes aside from Length
, genuine and counterfeit notes have quite distinct distributions.
library(tidyr)
banknote %>%
mutate(ID = 1:n()) %>%
pivot_longer(Length:Diagonal,
names_to = "Dimension",
values_to = "Size") %>%
mutate(Dimension = factor(Dimension),
ID = factor(ID)) %>%
ggplot() +
aes(y = Size, fill = Status) +
facet_wrap(~ Dimension, scales = "free") +
geom_boxplot() +
theme(axis.text.x = element_blank(),
axis.ticks.x = element_blank()) +
labs(y = "Size (mm)", title = "Comparison of bank note dimensions")
Below is a visualisation of the distribution of the perimeters of the bank notes.
banknote %>%
mutate(Perimeter = 2*Length + Left + Right) %>%
ggplot() +
aes(x = Perimeter, fill = Status) +
geom_density(alpha = 0.5) +
labs(x = "Perimeter (mm)", y = "Density", title = "Distribution of banknote perimeters")
Split dataset
Create training and testing datasets, preserving the 50/50 class split in each.
set.seed(1)
training_index <- createDataPartition(banknote$Status,
p = 0.8,
list = FALSE)
training_set <- banknote[training_index, ]
testing_set <- banknote[-training_index, ]
We can confirm the class split in the training set:
table(training_set$Status)
##
## counterfeit genuine
## 80 80
Hyper-parameter tuning
Set up the cross-validation for hyperparameter tuning, i.e., 10-fold cross validation repeated 10 times.
The summaryFunction
argument determines which metric to use to determine the performance of a particular hyperparameter setting. Here we shall use defaultSummary
which calculates accuracy and kappa statistic.
training_control <- trainControl(method = "repeatedcv",
summaryFunction = defaultSummary,
classProbs = TRUE,
number = 10,
repeats = 10)
Now use the train()
function to perform the model training/tuning of the k
hyperparameter.
The range of k
is from 3 to 31 in steps of 2, i.e., odd distances only.
set.seed(2)
knn_cv <- train(Status ~ .,
data = training_set,
method = "knn",
trControl = training_control,
metric = "Accuracy",
tuneGrid = data.frame(k = seq(11,85,by = 2)))
knn_cv
## k-Nearest Neighbors
##
## 160 samples
## 6 predictor
## 2 classes: 'counterfeit', 'genuine'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold, repeated 10 times)
## Summary of sample sizes: 144, 144, 144, 144, 144, 144, ...
## Resampling results across tuning parameters:
##
## k Accuracy Kappa
## 11 0.993750 0.98750
## 13 0.993750 0.98750
## 15 0.996875 0.99375
## 17 0.996875 0.99375
## 19 0.995000 0.99000
## 21 0.996875 0.99375
## 23 0.998125 0.99625
## 25 0.998125 0.99625
## 27 1.000000 1.00000
## 29 1.000000 1.00000
## 31 1.000000 1.00000
## 33 1.000000 1.00000
## 35 1.000000 1.00000
## 37 1.000000 1.00000
## 39 1.000000 1.00000
## 41 1.000000 1.00000
## 43 1.000000 1.00000
## 45 1.000000 1.00000
## 47 1.000000 1.00000
## 49 1.000000 1.00000
## 51 1.000000 1.00000
## 53 1.000000 1.00000
## 55 1.000000 1.00000
## 57 1.000000 1.00000
## 59 1.000000 1.00000
## 61 1.000000 1.00000
## 63 1.000000 1.00000
## 65 1.000000 1.00000
## 67 1.000000 1.00000
## 69 1.000000 1.00000
## 71 1.000000 1.00000
## 73 1.000000 1.00000
## 75 0.999375 0.99875
## 77 0.998750 0.99750
## 79 0.998125 0.99625
## 81 0.996875 0.99375
## 83 0.995000 0.99000
## 85 0.991875 0.98375
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was k = 73.
The cross-validation on the training set has tuned a k
parameter of 73.
ROC Curve
Inspecting the probabilities reveals that a cutoff probability around 0.5 give good classification results.
training_set <- training_set %>%
mutate(Predicted_prob = predict(knn_cv, type = "prob")$genuine)
training_set %>%
ggplot() +
aes(x = Predicted_prob, fill = Status) +
geom_histogram(bins = 20) +
labs(x = "Probability", y = "Count", title = "Distribution of predicted probabilities" )
An ROC curve is another way to visualise the results and identify a good cutoff.
pROC_train <- roc(training_set$Status, training_set$Predicted_prob,
quiet = TRUE,
plot = TRUE,
percent = TRUE,
auc.polygon = TRUE,
print.auc = TRUE,
print.thres = TRUE,
print.thres.best.method = "youden")
According to the Youden criterion on the training set, the best threshold is 0.5. Choosing this as the cutoff probability returns a perfect classification result on the training data. Be wary of overfitting the training data however.
kNN classification
Apply the final model, with k = 73 and cutoff = 0.5, to the testing dataset to get an estimate of the true performance of this classifier.
knn_predictions <- predict(knn_cv, newdata = testing_set, type = "prob") %>%
select(probability = genuine) %>%
mutate(class = ifelse(probability > 0.5, "genuine", "counterfeit")) %>%
mutate(class = factor(class))
The results on the testing dataset are evenly split between the two classes which is a good sign!
table(knn_predictions$class)
##
## counterfeit genuine
## 20 20
Since we have the ground truth data, we can use the confusionMatrix()
function to report full set of performance statistics.
knn_cm <- confusionMatrix(knn_predictions$class, testing_set$Status, mode = "everything")
knn_cm
## Confusion Matrix and Statistics
##
## Reference
## Prediction counterfeit genuine
## counterfeit 20 0
## genuine 0 20
##
## Accuracy : 1
## 95% CI : (0.9119, 1)
## No Information Rate : 0.5
## P-Value [Acc > NIR] : 9.095e-13
##
## Kappa : 1
##
## Mcnemar's Test P-Value : NA
##
## Sensitivity : 1.0
## Specificity : 1.0
## Pos Pred Value : 1.0
## Neg Pred Value : 1.0
## Precision : 1.0
## Recall : 1.0
## F1 : 1.0
## Prevalence : 0.5
## Detection Rate : 0.5
## Detection Prevalence : 0.5
## Balanced Accuracy : 1.0
##
## 'Positive' Class : counterfeit
##
Indeed we have achieved perfect classification with this kNN classifier!
LS0tDQp0aXRsZTogJ1R1bmluZyBrTk4gdXNpbmcgYGNhcmV0YCcNCmF1dGhvcjogIlNoaWggQ2hpbmcgRnUiDQpkYXRlOiAiQXVndXN0IDIwMjAiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgZGY6IHBhZ2VkIA0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19kZXB0aDogMw0KICAgIHRvY19mbG9hdDogDQogICAgICBjb2xsYXBzZWQ6IHRydWUNCiAgICAgIHNtb290aF9zY3JvbGw6IHRydWUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUNCiAgICB0aGVtZTogcmVhZGFibGUNCiAgICBoaWdobGlnaHQ6IGhhZGRvY2sNCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQprbml0OiANCiAgKGZ1bmN0aW9uKGlucHV0X2ZpbGUsIGVuY29kaW5nKSB7DQogICAgcm1hcmtkb3duOjpyZW5kZXIoaW5wdXRfZmlsZSwNCiAgICAgICAgICAgICAgICAgICAgICBlbmNvZGluZz1lbmNvZGluZywNCiAgICAgICAgICAgICAgICAgICAgICBvdXRwdXRfZmlsZT1maWxlLnBhdGgoZGlybmFtZShpbnB1dF9maWxlKSwgJ2RvY3MnLCAnaW5kZXguaHRtbCcpKX0pDQotLS0NCg0KVGhpcyBub3RlYm9vayBkZXNjcmliZXMgYW4gZXhhbXBsZSBvZiB1c2luZyB0aGUgYGNhcmV0YFteY2FyZXRdIHBhY2thZ2UgdG8gY29uZHVjdCBoeXBlcnBhcmFtZXRlciB0dW5pbmcgZm9yIHRoZSBrLU5lYXJlc3QgTmVpZ2hib3VyIGNsYXNzaWZpZXIuDQoNCmBgYHtyIGxpYnJhcmllcywgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkobWNsdXN0KQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoY2FyZXQpDQpsaWJyYXJ5KHBST0MpDQpgYGANCg0KW15jYXJldF06ICBNYXggS3VobiAoMjAyMCkuIGNhcmV0OiBDbGFzc2lmaWNhdGlvbiBhbmQgUmVncmVzc2lvbiBUcmFpbmluZy4gUiBwYWNrYWdlIHZlcnNpb24gNi4wLTg2LiBodHRwczovL0NSQU4uUi1wcm9qZWN0Lm9yZy9wYWNrYWdlPWNhcmV0DQoNCiMgRXhhbXBsZSBkYXRhc2V0DQoNClRoZSBleGFtcGxlIGRhdGFzZXQgaXMgdGhlIGBiYW5rbm90ZWAgZGF0YWZyYW1lIGZvdW5kIGluIHRoZSBgbWNsdXN0YFtebWNsdXN0XSBwYWNrYWdlLiBJdCBjb250YWlucyBzaXggbWVhc3VyZW1lbnRzIG1hZGUgb24gMTAwIGdlbnVpbmUgYW5kIDEwMCBjb3VudGVyZmVpdCBvbGQtU3dpc3MgMTAwMC1mcmFuYyBiYW5rIG5vdGVzLg0KDQpbXm1jbHVzdF06IFNjcnVjY2EgTC4sIEZvcCBNLiwgTXVycGh5IFQuIEIuIGFuZCBSYWZ0ZXJ5IEEuIEUuICgyMDE2KSBtY2x1c3QgNTogY2x1c3RlcmluZywgY2xhc3NpZmljYXRpb24gYW5kIGRlbnNpdHkgZXN0aW1hdGlvbiB1c2luZyBHYXVzc2lhbiBmaW5pdGUgbWl4dHVyZSBtb2RlbHMgVGhlIFIgSm91cm5hbCA4LzEsIHBwLiAyODktMzE3DQoNCmBgYHtyfQ0KZGF0YShiYW5rbm90ZSkNCmhlYWQoYmFua25vdGUpDQpgYGANCg0KVGhlcmUgYXJlIHNpeCBwcmVkaWN0b3IgdmFyaWFibGVzIChgTGVuZ3RoYCwgYExlZnRgLCBgUmlnaHRgLCBgQm90dG9tYCwgYFRvcGAsIGBEaWFnb25hbGApIHdpdGggYFN0YXR1c2AgYmVpbmcgdGhlIGNhdGVnb3JpY2FsIHJlc3BvbnNlIG9yIGNsYXNzIHZhcmlhYmxlIGhhdmluZyB0d28gbGV2ZWxzLCBuYW1lbHkgIGBnZW51aW5lYCBhbmQgYGNvdW50ZXJmZWl0YC4NCg0KIyBFeHBsb3JhdG9yeSBkYXRhIGFuYWx5c2lzDQoNCk9ic2VydmUgdGhhdCB0aGUgZGF0YXNldCBpcyBiYWxhbmNlZCB3aXRoIDEwMCBvYnNlcnZhdGlvbnMgYWdhaW5zdCBlYWNoIGxldmVsIG9mIGBTdGF0dXNgLg0KDQpgYGB7cn0NCmJhbmtub3RlICU+JQ0KICBncm91cF9ieShTdGF0dXMpICU+JQ0KICBzdW1tYXJpc2UoTiA9IG4oKSwgDQogICAgICAgICAgICBNZWFuX0xlbmd0aCA9IG1lYW4oTGVuZ3RoKSwNCiAgICAgICAgICAgIE1lYW5fTGVmdCA9IG1lYW4oTGVmdCksDQogICAgICAgICAgICBNZWFuX1JpZ2h0ID0gbWVhbihSaWdodCksDQogICAgICAgICAgICBNZWFuX0JvdHRvbSA9IG1lYW4oQm90dG9tKSwNCiAgICAgICAgICAgIE1lYW5fVG9wID0gbWVhbihUb3ApLA0KICAgICAgICAgICAgTWVhbl9EaWFnb25hbCA9IG1lYW4oRGlhZ29uYWwpLA0KICAgICAgICAgICAgLmdyb3VwcyA9ICJrZWVwIikNCmBgYA0KDQpJbiBtb3N0IG9mIHRoZSBtZWFzdXJlbWVudHMgb2YgYmFuayBub3RlcyBhc2lkZSBmcm9tIGBMZW5ndGhgLCBnZW51aW5lIGFuZCBjb3VudGVyZmVpdCBub3RlcyBoYXZlIHF1aXRlIGRpc3RpbmN0IGRpc3RyaWJ1dGlvbnMuDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5cikNCmJhbmtub3RlICU+JSANCiAgbXV0YXRlKElEID0gMTpuKCkpICU+JQ0KICBwaXZvdF9sb25nZXIoTGVuZ3RoOkRpYWdvbmFsLA0KICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAiRGltZW5zaW9uIiwNCiAgICAgICAgICAgICAgIHZhbHVlc190byA9ICJTaXplIikgJT4lDQogIG11dGF0ZShEaW1lbnNpb24gPSBmYWN0b3IoRGltZW5zaW9uKSwNCiAgICAgICAgIElEID0gZmFjdG9yKElEKSkgJT4lDQogIGdncGxvdCgpICsNCiAgYWVzKHkgPSBTaXplLCBmaWxsID0gU3RhdHVzKSArDQogIGZhY2V0X3dyYXAofiBEaW1lbnNpb24sIHNjYWxlcyA9ICJmcmVlIikgKw0KICBnZW9tX2JveHBsb3QoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBheGlzLnRpY2tzLnggPSBlbGVtZW50X2JsYW5rKCkpICsNCiAgbGFicyh5ID0gIlNpemUgKG1tKSIsIHRpdGxlID0gIkNvbXBhcmlzb24gb2YgYmFuayBub3RlIGRpbWVuc2lvbnMiKQ0KYGBgDQoNCkJlbG93IGlzIGEgdmlzdWFsaXNhdGlvbiBvZiB0aGUgZGlzdHJpYnV0aW9uIG9mIHRoZSBwZXJpbWV0ZXJzIG9mIHRoZSBiYW5rIG5vdGVzLg0KDQpgYGB7cn0NCmJhbmtub3RlICU+JQ0KICBtdXRhdGUoUGVyaW1ldGVyID0gMipMZW5ndGggKyBMZWZ0ICsgUmlnaHQpICU+JQ0KICBnZ3Bsb3QoKSArDQogIGFlcyh4ID0gUGVyaW1ldGVyLCBmaWxsID0gU3RhdHVzKSArDQogIGdlb21fZGVuc2l0eShhbHBoYSA9IDAuNSkgKw0KICBsYWJzKHggPSAiUGVyaW1ldGVyIChtbSkiLCB5ID0gIkRlbnNpdHkiLCB0aXRsZSA9ICJEaXN0cmlidXRpb24gb2YgYmFua25vdGUgcGVyaW1ldGVycyIpDQpgYGANCg0KIyBTcGxpdCBkYXRhc2V0DQoNCkNyZWF0ZSB0cmFpbmluZyBhbmQgdGVzdGluZyBkYXRhc2V0cywgcHJlc2VydmluZyB0aGUgNTAvNTAgY2xhc3Mgc3BsaXQgaW4gZWFjaC4NCg0KYGBge3J9DQpzZXQuc2VlZCgxKQ0KdHJhaW5pbmdfaW5kZXggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihiYW5rbm90ZSRTdGF0dXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwID0gMC44LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0ID0gRkFMU0UpDQoNCnRyYWluaW5nX3NldCA8LSBiYW5rbm90ZVt0cmFpbmluZ19pbmRleCwgXQ0KdGVzdGluZ19zZXQgPC0gYmFua25vdGVbLXRyYWluaW5nX2luZGV4LCBdDQpgYGANCg0KV2UgY2FuIGNvbmZpcm0gdGhlIGNsYXNzIHNwbGl0IGluIHRoZSB0cmFpbmluZyBzZXQ6DQoNCmBgYHtyfQ0KdGFibGUodHJhaW5pbmdfc2V0JFN0YXR1cykNCmBgYA0KDQojIEh5cGVyLXBhcmFtZXRlciB0dW5pbmcNCg0KU2V0IHVwIHRoZSBjcm9zcy12YWxpZGF0aW9uIGZvciBoeXBlcnBhcmFtZXRlciB0dW5pbmcsIGkuZS4sIDEwLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiByZXBlYXRlZCAxMCB0aW1lcy4gDQoNClRoZSBgc3VtbWFyeUZ1bmN0aW9uYCBhcmd1bWVudCBkZXRlcm1pbmVzIHdoaWNoIG1ldHJpYyB0byB1c2UgdG8gZGV0ZXJtaW5lIHRoZSBwZXJmb3JtYW5jZSBvZiBhIHBhcnRpY3VsYXIgaHlwZXJwYXJhbWV0ZXIgc2V0dGluZy4gSGVyZSB3ZSBzaGFsbCB1c2UgYGRlZmF1bHRTdW1tYXJ5YCB3aGljaCBjYWxjdWxhdGVzIGFjY3VyYWN5IGFuZCBrYXBwYSBzdGF0aXN0aWMuDQoNCmBgYHtyIENyb3NzIHZhbGlkYXRpb24gc2V0dGluZ3MgZm9yIGNhcmV0fQ0KdHJhaW5pbmdfY29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gZGVmYXVsdFN1bW1hcnksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDEwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDEwKQ0KYGBgDQoNCk5vdyB1c2UgdGhlIGB0cmFpbigpYCBmdW5jdGlvbiB0byBwZXJmb3JtIHRoZSBtb2RlbCB0cmFpbmluZy90dW5pbmcgb2YgdGhlIGBrYCBoeXBlcnBhcmFtZXRlci4NCg0KVGhlIHJhbmdlIG9mIGBrYCBpcyBmcm9tIDMgdG8gMzEgaW4gc3RlcHMgb2YgMiwgaS5lLiwgb2RkIGRpc3RhbmNlcyBvbmx5Lg0KDQpgYGB7ciBrLW5lYXJlc3QgbmVpZ2hib3Vyc30NCnNldC5zZWVkKDIpDQprbm5fY3YgPC0gdHJhaW4oU3RhdHVzIH4gLiwgDQogICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluaW5nX3NldCwNCiAgICAgICAgICAgICAgICBtZXRob2QgPSAia25uIiwNCiAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSB0cmFpbmluZ19jb250cm9sLA0KICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsDQogICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKGsgPSBzZXEoMTEsODUsYnkgPSAyKSkpDQprbm5fY3YNCmBgYA0KDQpUaGUgY3Jvc3MtdmFsaWRhdGlvbiBvbiB0aGUgdHJhaW5pbmcgc2V0IGhhcyB0dW5lZCBhIGBrYCBwYXJhbWV0ZXIgb2YgYHIga25uX2N2JGZpbmFsTW9kZWwka2AuDQoNCg0KIyMgUk9DIEN1cnZlDQoNCkluc3BlY3RpbmcgdGhlIHByb2JhYmlsaXRpZXMgcmV2ZWFscyB0aGF0IGEgY3V0b2ZmIHByb2JhYmlsaXR5IGFyb3VuZCAwLjUgZ2l2ZSBnb29kIGNsYXNzaWZpY2F0aW9uIHJlc3VsdHMuDQoNCmBgYHtyfQ0KdHJhaW5pbmdfc2V0IDwtIHRyYWluaW5nX3NldCAlPiUNCiAgbXV0YXRlKFByZWRpY3RlZF9wcm9iID0gcHJlZGljdChrbm5fY3YsIHR5cGUgPSAicHJvYiIpJGdlbnVpbmUpDQoNCnRyYWluaW5nX3NldCAlPiUNCiAgZ2dwbG90KCkgKw0KICBhZXMoeCA9IFByZWRpY3RlZF9wcm9iLCBmaWxsID0gU3RhdHVzKSArDQogIGdlb21faGlzdG9ncmFtKGJpbnMgPSAyMCkgKw0KICBsYWJzKHggPSAiUHJvYmFiaWxpdHkiLCB5ID0gIkNvdW50IiwgdGl0bGUgPSAiRGlzdHJpYnV0aW9uIG9mIHByZWRpY3RlZCBwcm9iYWJpbGl0aWVzIiApDQpgYGANCg0KQW4gUk9DIGN1cnZlIGlzIGFub3RoZXIgd2F5IHRvIHZpc3VhbGlzZSB0aGUgcmVzdWx0cyBhbmQgaWRlbnRpZnkgYSBnb29kIGN1dG9mZi4gDQoNCmBgYHtyfQ0KcFJPQ190cmFpbiA8LSByb2ModHJhaW5pbmdfc2V0JFN0YXR1cywgdHJhaW5pbmdfc2V0JFByZWRpY3RlZF9wcm9iLA0KICAgICAgICAgICAgICAgIHF1aWV0ID0gVFJVRSwNCiAgICAgICAgICAgICAgICBwbG90ID0gVFJVRSwgDQogICAgICAgICAgICAgICAgcGVyY2VudCA9IFRSVUUsDQogICAgICAgICAgICAgICAgYXVjLnBvbHlnb24gPSBUUlVFLCANCiAgICAgICAgICAgICAgICBwcmludC5hdWMgPSBUUlVFLCANCiAgICAgICAgICAgICAgICBwcmludC50aHJlcyA9IFRSVUUsDQogICAgICAgICAgICAgICAgcHJpbnQudGhyZXMuYmVzdC5tZXRob2QgPSAieW91ZGVuIikNCmBgYA0KDQpBY2NvcmRpbmcgdG8gdGhlIFlvdWRlbiBjcml0ZXJpb24gb24gdGhlIHRyYWluaW5nIHNldCwgdGhlIGJlc3QgdGhyZXNob2xkIGlzIDAuNS4gQ2hvb3NpbmcgdGhpcyBhcyB0aGUgY3V0b2ZmIHByb2JhYmlsaXR5IHJldHVybnMgYSBwZXJmZWN0IGNsYXNzaWZpY2F0aW9uIHJlc3VsdCBvbiB0aGUgdHJhaW5pbmcgZGF0YS4gQmUgd2FyeSBvZiBvdmVyZml0dGluZyB0aGUgdHJhaW5pbmcgZGF0YSBob3dldmVyLg0KDQoNCiMga05OIGNsYXNzaWZpY2F0aW9uDQoNCkFwcGx5IHRoZSBmaW5hbCBtb2RlbCwgd2l0aCBrID0gNzMgYW5kIGN1dG9mZiA9IDAuNSwgdG8gdGhlIHRlc3RpbmcgZGF0YXNldCB0byBnZXQgYW4gZXN0aW1hdGUgb2YgdGhlIHRydWUgcGVyZm9ybWFuY2Ugb2YgdGhpcyBjbGFzc2lmaWVyLg0KDQpgYGB7cn0NCmtubl9wcmVkaWN0aW9ucyA8LSBwcmVkaWN0KGtubl9jdiwgbmV3ZGF0YSA9IHRlc3Rpbmdfc2V0LCB0eXBlID0gInByb2IiKSAlPiUNCiAgc2VsZWN0KHByb2JhYmlsaXR5ID0gZ2VudWluZSkgJT4lDQogIG11dGF0ZShjbGFzcyA9IGlmZWxzZShwcm9iYWJpbGl0eSA+IDAuNSwgImdlbnVpbmUiLCAiY291bnRlcmZlaXQiKSkgJT4lDQogIG11dGF0ZShjbGFzcyA9IGZhY3RvcihjbGFzcykpDQpgYGANCg0KVGhlIHJlc3VsdHMgb24gdGhlIHRlc3RpbmcgZGF0YXNldCBhcmUgZXZlbmx5IHNwbGl0IGJldHdlZW4gdGhlIHR3byBjbGFzc2VzIHdoaWNoIGlzIGEgZ29vZCBzaWduIQ0KDQpgYGB7cn0NCnRhYmxlKGtubl9wcmVkaWN0aW9ucyRjbGFzcykNCmBgYA0KDQpTaW5jZSB3ZSBoYXZlIHRoZSBncm91bmQgdHJ1dGggZGF0YSwgd2UgY2FuIHVzZSB0aGUgYGNvbmZ1c2lvbk1hdHJpeCgpYCBmdW5jdGlvbiB0byByZXBvcnQgZnVsbCBzZXQgb2YgcGVyZm9ybWFuY2Ugc3RhdGlzdGljcy4NCg0KYGBge3J9DQprbm5fY20gPC0gY29uZnVzaW9uTWF0cml4KGtubl9wcmVkaWN0aW9ucyRjbGFzcywgdGVzdGluZ19zZXQkU3RhdHVzLCBtb2RlID0gImV2ZXJ5dGhpbmciKQ0Ka25uX2NtDQpgYGANCg0KSW5kZWVkIHdlIGhhdmUgYWNoaWV2ZWQgcGVyZmVjdCBjbGFzc2lmaWNhdGlvbiB3aXRoIHRoaXMga05OIGNsYXNzaWZpZXIhDQoNCg0KDQoNCg==