This notebook describes an example of using the caret1 package to conduct hyperparameter tuning for the k-Nearest Neighbour classifier.

library(mclust)
library(dplyr)
library(ggplot2)
library(caret)
library(pROC)

1 Example dataset

The example dataset is the banknote dataframe found in the mclust2 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.

2 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")

3 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

4 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.

4.1 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.

5 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!


  1. Max Kuhn (2020). caret: Classification and Regression Training. R package version 6.0-86. https://CRAN.R-project.org/package=caret↩︎

  2. Scrucca L., Fop M., Murphy T. B. and Raftery A. E. (2016) mclust 5: clustering, classification and density estimation using Gaussian finite mixture models The R Journal 8/1, pp. 289-317↩︎

LS0tDQp0aXRsZTogJ1R1bmluZyBrTk4gdXNpbmcgYGNhcmV0YCcNCmF1dGhvcjogIlNoaWggQ2hpbmcgRnUiDQpkYXRlOiAiQXVndXN0IDIwMjAiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgZGY6IHBhZ2VkIA0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19kZXB0aDogMw0KICAgIHRvY19mbG9hdDogDQogICAgICBjb2xsYXBzZWQ6IHRydWUNCiAgICAgIHNtb290aF9zY3JvbGw6IHRydWUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUNCiAgICB0aGVtZTogcmVhZGFibGUNCiAgICBoaWdobGlnaHQ6IGhhZGRvY2sNCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQprbml0OiANCiAgKGZ1bmN0aW9uKGlucHV0X2ZpbGUsIGVuY29kaW5nKSB7DQogICAgcm1hcmtkb3duOjpyZW5kZXIoaW5wdXRfZmlsZSwNCiAgICAgICAgICAgICAgICAgICAgICBlbmNvZGluZz1lbmNvZGluZywNCiAgICAgICAgICAgICAgICAgICAgICBvdXRwdXRfZmlsZT1maWxlLnBhdGgoZGlybmFtZShpbnB1dF9maWxlKSwgJ2RvY3MnLCAnaW5kZXguaHRtbCcpKX0pDQotLS0NCg0KVGhpcyBub3RlYm9vayBkZXNjcmliZXMgYW4gZXhhbXBsZSBvZiB1c2luZyB0aGUgYGNhcmV0YFteY2FyZXRdIHBhY2thZ2UgdG8gY29uZHVjdCBoeXBlcnBhcmFtZXRlciB0dW5pbmcgZm9yIHRoZSBrLU5lYXJlc3QgTmVpZ2hib3VyIGNsYXNzaWZpZXIuDQoNCmBgYHtyIGxpYnJhcmllcywgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkobWNsdXN0KQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkoY2FyZXQpDQpsaWJyYXJ5KHBST0MpDQpgYGANCg0KW15jYXJldF06ICBNYXggS3VobiAoMjAyMCkuIGNhcmV0OiBDbGFzc2lmaWNhdGlvbiBhbmQgUmVncmVzc2lvbiBUcmFpbmluZy4gUiBwYWNrYWdlIHZlcnNpb24gNi4wLTg2LiBodHRwczovL0NSQU4uUi1wcm9qZWN0Lm9yZy9wYWNrYWdlPWNhcmV0DQoNCiMgRXhhbXBsZSBkYXRhc2V0DQoNClRoZSBleGFtcGxlIGRhdGFzZXQgaXMgdGhlIGBiYW5rbm90ZWAgZGF0YWZyYW1lIGZvdW5kIGluIHRoZSBgbWNsdXN0YFtebWNsdXN0XSBwYWNrYWdlLiBJdCBjb250YWlucyBzaXggbWVhc3VyZW1lbnRzIG1hZGUgb24gMTAwIGdlbnVpbmUgYW5kIDEwMCBjb3VudGVyZmVpdCBvbGQtU3dpc3MgMTAwMC1mcmFuYyBiYW5rIG5vdGVzLg0KDQpbXm1jbHVzdF06IFNjcnVjY2EgTC4sIEZvcCBNLiwgTXVycGh5IFQuIEIuIGFuZCBSYWZ0ZXJ5IEEuIEUuICgyMDE2KSBtY2x1c3QgNTogY2x1c3RlcmluZywgY2xhc3NpZmljYXRpb24gYW5kIGRlbnNpdHkgZXN0aW1hdGlvbiB1c2luZyBHYXVzc2lhbiBmaW5pdGUgbWl4dHVyZSBtb2RlbHMgVGhlIFIgSm91cm5hbCA4LzEsIHBwLiAyODktMzE3DQoNCmBgYHtyfQ0KZGF0YShiYW5rbm90ZSkNCmhlYWQoYmFua25vdGUpDQpgYGANCg0KVGhlcmUgYXJlIHNpeCBwcmVkaWN0b3IgdmFyaWFibGVzIChgTGVuZ3RoYCwgYExlZnRgLCBgUmlnaHRgLCBgQm90dG9tYCwgYFRvcGAsIGBEaWFnb25hbGApIHdpdGggYFN0YXR1c2AgYmVpbmcgdGhlIGNhdGVnb3JpY2FsIHJlc3BvbnNlIG9yIGNsYXNzIHZhcmlhYmxlIGhhdmluZyB0d28gbGV2ZWxzLCBuYW1lbHkgIGBnZW51aW5lYCBhbmQgYGNvdW50ZXJmZWl0YC4NCg0KIyBFeHBsb3JhdG9yeSBkYXRhIGFuYWx5c2lzDQoNCk9ic2VydmUgdGhhdCB0aGUgZGF0YXNldCBpcyBiYWxhbmNlZCB3aXRoIDEwMCBvYnNlcnZhdGlvbnMgYWdhaW5zdCBlYWNoIGxldmVsIG9mIGBTdGF0dXNgLg0KDQpgYGB7cn0NCmJhbmtub3RlICU+JQ0KICBncm91cF9ieShTdGF0dXMpICU+JQ0KICBzdW1tYXJpc2UoTiA9IG4oKSwgDQogICAgICAgICAgICBNZWFuX0xlbmd0aCA9IG1lYW4oTGVuZ3RoKSwNCiAgICAgICAgICAgIE1lYW5fTGVmdCA9IG1lYW4oTGVmdCksDQogICAgICAgICAgICBNZWFuX1JpZ2h0ID0gbWVhbihSaWdodCksDQogICAgICAgICAgICBNZWFuX0JvdHRvbSA9IG1lYW4oQm90dG9tKSwNCiAgICAgICAgICAgIE1lYW5fVG9wID0gbWVhbihUb3ApLA0KICAgICAgICAgICAgTWVhbl9EaWFnb25hbCA9IG1lYW4oRGlhZ29uYWwpLA0KICAgICAgICAgICAgLmdyb3VwcyA9ICJrZWVwIikNCmBgYA0KDQpJbiBtb3N0IG9mIHRoZSBtZWFzdXJlbWVudHMgb2YgYmFuayBub3RlcyBhc2lkZSBmcm9tIGBMZW5ndGhgLCBnZW51aW5lIGFuZCBjb3VudGVyZmVpdCBub3RlcyBoYXZlIHF1aXRlIGRpc3RpbmN0IGRpc3RyaWJ1dGlvbnMuDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5cikNCmJhbmtub3RlICU+JSANCiAgbXV0YXRlKElEID0gMTpuKCkpICU+JQ0KICBwaXZvdF9sb25nZXIoTGVuZ3RoOkRpYWdvbmFsLA0KICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAiRGltZW5zaW9uIiwNCiAgICAgICAgICAgICAgIHZhbHVlc190byA9ICJTaXplIikgJT4lDQogIG11dGF0ZShEaW1lbnNpb24gPSBmYWN0b3IoRGltZW5zaW9uKSwNCiAgICAgICAgIElEID0gZmFjdG9yKElEKSkgJT4lDQogIGdncGxvdCgpICsNCiAgYWVzKHkgPSBTaXplLCBmaWxsID0gU3RhdHVzKSArDQogIGZhY2V0X3dyYXAofiBEaW1lbnNpb24sIHNjYWxlcyA9ICJmcmVlIikgKw0KICBnZW9tX2JveHBsb3QoKSArDQogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBheGlzLnRpY2tzLnggPSBlbGVtZW50X2JsYW5rKCkpICsNCiAgbGFicyh5ID0gIlNpemUgKG1tKSIsIHRpdGxlID0gIkNvbXBhcmlzb24gb2YgYmFuayBub3RlIGRpbWVuc2lvbnMiKQ0KYGBgDQoNCkJlbG93IGlzIGEgdmlzdWFsaXNhdGlvbiBvZiB0aGUgZGlzdHJpYnV0aW9uIG9mIHRoZSBwZXJpbWV0ZXJzIG9mIHRoZSBiYW5rIG5vdGVzLg0KDQpgYGB7cn0NCmJhbmtub3RlICU+JQ0KICBtdXRhdGUoUGVyaW1ldGVyID0gMipMZW5ndGggKyBMZWZ0ICsgUmlnaHQpICU+JQ0KICBnZ3Bsb3QoKSArDQogIGFlcyh4ID0gUGVyaW1ldGVyLCBmaWxsID0gU3RhdHVzKSArDQogIGdlb21fZGVuc2l0eShhbHBoYSA9IDAuNSkgKw0KICBsYWJzKHggPSAiUGVyaW1ldGVyIChtbSkiLCB5ID0gIkRlbnNpdHkiLCB0aXRsZSA9ICJEaXN0cmlidXRpb24gb2YgYmFua25vdGUgcGVyaW1ldGVycyIpDQpgYGANCg0KIyBTcGxpdCBkYXRhc2V0DQoNCkNyZWF0ZSB0cmFpbmluZyBhbmQgdGVzdGluZyBkYXRhc2V0cywgcHJlc2VydmluZyB0aGUgNTAvNTAgY2xhc3Mgc3BsaXQgaW4gZWFjaC4NCg0KYGBge3J9DQpzZXQuc2VlZCgxKQ0KdHJhaW5pbmdfaW5kZXggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihiYW5rbm90ZSRTdGF0dXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwID0gMC44LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsaXN0ID0gRkFMU0UpDQoNCnRyYWluaW5nX3NldCA8LSBiYW5rbm90ZVt0cmFpbmluZ19pbmRleCwgXQ0KdGVzdGluZ19zZXQgPC0gYmFua25vdGVbLXRyYWluaW5nX2luZGV4LCBdDQpgYGANCg0KV2UgY2FuIGNvbmZpcm0gdGhlIGNsYXNzIHNwbGl0IGluIHRoZSB0cmFpbmluZyBzZXQ6DQoNCmBgYHtyfQ0KdGFibGUodHJhaW5pbmdfc2V0JFN0YXR1cykNCmBgYA0KDQojIEh5cGVyLXBhcmFtZXRlciB0dW5pbmcNCg0KU2V0IHVwIHRoZSBjcm9zcy12YWxpZGF0aW9uIGZvciBoeXBlcnBhcmFtZXRlciB0dW5pbmcsIGkuZS4sIDEwLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiByZXBlYXRlZCAxMCB0aW1lcy4gDQoNClRoZSBgc3VtbWFyeUZ1bmN0aW9uYCBhcmd1bWVudCBkZXRlcm1pbmVzIHdoaWNoIG1ldHJpYyB0byB1c2UgdG8gZGV0ZXJtaW5lIHRoZSBwZXJmb3JtYW5jZSBvZiBhIHBhcnRpY3VsYXIgaHlwZXJwYXJhbWV0ZXIgc2V0dGluZy4gSGVyZSB3ZSBzaGFsbCB1c2UgYGRlZmF1bHRTdW1tYXJ5YCB3aGljaCBjYWxjdWxhdGVzIGFjY3VyYWN5IGFuZCBrYXBwYSBzdGF0aXN0aWMuDQoNCmBgYHtyIENyb3NzIHZhbGlkYXRpb24gc2V0dGluZ3MgZm9yIGNhcmV0fQ0KdHJhaW5pbmdfY29udHJvbCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gInJlcGVhdGVkY3YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gZGVmYXVsdFN1bW1hcnksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IDEwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVwZWF0cyA9IDEwKQ0KYGBgDQoNCk5vdyB1c2UgdGhlIGB0cmFpbigpYCBmdW5jdGlvbiB0byBwZXJmb3JtIHRoZSBtb2RlbCB0cmFpbmluZy90dW5pbmcgb2YgdGhlIGBrYCBoeXBlcnBhcmFtZXRlci4NCg0KVGhlIHJhbmdlIG9mIGBrYCBpcyBmcm9tIDMgdG8gMzEgaW4gc3RlcHMgb2YgMiwgaS5lLiwgb2RkIGRpc3RhbmNlcyBvbmx5Lg0KDQpgYGB7ciBrLW5lYXJlc3QgbmVpZ2hib3Vyc30NCnNldC5zZWVkKDIpDQprbm5fY3YgPC0gdHJhaW4oU3RhdHVzIH4gLiwgDQogICAgICAgICAgICAgICAgZGF0YSA9IHRyYWluaW5nX3NldCwNCiAgICAgICAgICAgICAgICBtZXRob2QgPSAia25uIiwNCiAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSB0cmFpbmluZ19jb250cm9sLA0KICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsDQogICAgICAgICAgICAgICAgdHVuZUdyaWQgPSBkYXRhLmZyYW1lKGsgPSBzZXEoMTEsODUsYnkgPSAyKSkpDQprbm5fY3YNCmBgYA0KDQpUaGUgY3Jvc3MtdmFsaWRhdGlvbiBvbiB0aGUgdHJhaW5pbmcgc2V0IGhhcyB0dW5lZCBhIGBrYCBwYXJhbWV0ZXIgb2YgYHIga25uX2N2JGZpbmFsTW9kZWwka2AuDQoNCg0KIyMgUk9DIEN1cnZlDQoNCkluc3BlY3RpbmcgdGhlIHByb2JhYmlsaXRpZXMgcmV2ZWFscyB0aGF0IGEgY3V0b2ZmIHByb2JhYmlsaXR5IGFyb3VuZCAwLjUgZ2l2ZSBnb29kIGNsYXNzaWZpY2F0aW9uIHJlc3VsdHMuDQoNCmBgYHtyfQ0KdHJhaW5pbmdfc2V0IDwtIHRyYWluaW5nX3NldCAlPiUNCiAgbXV0YXRlKFByZWRpY3RlZF9wcm9iID0gcHJlZGljdChrbm5fY3YsIHR5cGUgPSAicHJvYiIpJGdlbnVpbmUpDQoNCnRyYWluaW5nX3NldCAlPiUNCiAgZ2dwbG90KCkgKw0KICBhZXMoeCA9IFByZWRpY3RlZF9wcm9iLCBmaWxsID0gU3RhdHVzKSArDQogIGdlb21faGlzdG9ncmFtKGJpbnMgPSAyMCkgKw0KICBsYWJzKHggPSAiUHJvYmFiaWxpdHkiLCB5ID0gIkNvdW50IiwgdGl0bGUgPSAiRGlzdHJpYnV0aW9uIG9mIHByZWRpY3RlZCBwcm9iYWJpbGl0aWVzIiApDQpgYGANCg0KQW4gUk9DIGN1cnZlIGlzIGFub3RoZXIgd2F5IHRvIHZpc3VhbGlzZSB0aGUgcmVzdWx0cyBhbmQgaWRlbnRpZnkgYSBnb29kIGN1dG9mZi4gDQoNCmBgYHtyfQ0KcFJPQ190cmFpbiA8LSByb2ModHJhaW5pbmdfc2V0JFN0YXR1cywgdHJhaW5pbmdfc2V0JFByZWRpY3RlZF9wcm9iLA0KICAgICAgICAgICAgICAgIHF1aWV0ID0gVFJVRSwNCiAgICAgICAgICAgICAgICBwbG90ID0gVFJVRSwgDQogICAgICAgICAgICAgICAgcGVyY2VudCA9IFRSVUUsDQogICAgICAgICAgICAgICAgYXVjLnBvbHlnb24gPSBUUlVFLCANCiAgICAgICAgICAgICAgICBwcmludC5hdWMgPSBUUlVFLCANCiAgICAgICAgICAgICAgICBwcmludC50aHJlcyA9IFRSVUUsDQogICAgICAgICAgICAgICAgcHJpbnQudGhyZXMuYmVzdC5tZXRob2QgPSAieW91ZGVuIikNCmBgYA0KDQpBY2NvcmRpbmcgdG8gdGhlIFlvdWRlbiBjcml0ZXJpb24gb24gdGhlIHRyYWluaW5nIHNldCwgdGhlIGJlc3QgdGhyZXNob2xkIGlzIDAuNS4gQ2hvb3NpbmcgdGhpcyBhcyB0aGUgY3V0b2ZmIHByb2JhYmlsaXR5IHJldHVybnMgYSBwZXJmZWN0IGNsYXNzaWZpY2F0aW9uIHJlc3VsdCBvbiB0aGUgdHJhaW5pbmcgZGF0YS4gQmUgd2FyeSBvZiBvdmVyZml0dGluZyB0aGUgdHJhaW5pbmcgZGF0YSBob3dldmVyLg0KDQoNCiMga05OIGNsYXNzaWZpY2F0aW9uDQoNCkFwcGx5IHRoZSBmaW5hbCBtb2RlbCwgd2l0aCBrID0gNzMgYW5kIGN1dG9mZiA9IDAuNSwgdG8gdGhlIHRlc3RpbmcgZGF0YXNldCB0byBnZXQgYW4gZXN0aW1hdGUgb2YgdGhlIHRydWUgcGVyZm9ybWFuY2Ugb2YgdGhpcyBjbGFzc2lmaWVyLg0KDQpgYGB7cn0NCmtubl9wcmVkaWN0aW9ucyA8LSBwcmVkaWN0KGtubl9jdiwgbmV3ZGF0YSA9IHRlc3Rpbmdfc2V0LCB0eXBlID0gInByb2IiKSAlPiUNCiAgc2VsZWN0KHByb2JhYmlsaXR5ID0gZ2VudWluZSkgJT4lDQogIG11dGF0ZShjbGFzcyA9IGlmZWxzZShwcm9iYWJpbGl0eSA+IDAuNSwgImdlbnVpbmUiLCAiY291bnRlcmZlaXQiKSkgJT4lDQogIG11dGF0ZShjbGFzcyA9IGZhY3RvcihjbGFzcykpDQpgYGANCg0KVGhlIHJlc3VsdHMgb24gdGhlIHRlc3RpbmcgZGF0YXNldCBhcmUgZXZlbmx5IHNwbGl0IGJldHdlZW4gdGhlIHR3byBjbGFzc2VzIHdoaWNoIGlzIGEgZ29vZCBzaWduIQ0KDQpgYGB7cn0NCnRhYmxlKGtubl9wcmVkaWN0aW9ucyRjbGFzcykNCmBgYA0KDQpTaW5jZSB3ZSBoYXZlIHRoZSBncm91bmQgdHJ1dGggZGF0YSwgd2UgY2FuIHVzZSB0aGUgYGNvbmZ1c2lvbk1hdHJpeCgpYCBmdW5jdGlvbiB0byByZXBvcnQgZnVsbCBzZXQgb2YgcGVyZm9ybWFuY2Ugc3RhdGlzdGljcy4NCg0KYGBge3J9DQprbm5fY20gPC0gY29uZnVzaW9uTWF0cml4KGtubl9wcmVkaWN0aW9ucyRjbGFzcywgdGVzdGluZ19zZXQkU3RhdHVzLCBtb2RlID0gImV2ZXJ5dGhpbmciKQ0Ka25uX2NtDQpgYGANCg0KSW5kZWVkIHdlIGhhdmUgYWNoaWV2ZWQgcGVyZmVjdCBjbGFzc2lmaWNhdGlvbiB3aXRoIHRoaXMga05OIGNsYXNzaWZpZXIhDQoNCg0KDQoNCg==