Note: Some results may differ from the hard copy book due to the changing of sampling procedures introduced in R 3.6.0. See http://bit.ly/35D1SW7 for more details. Access and run the source code for this notebook here.

Hidden chapter requirements used in the book to set the plotting theme and load packages used in hidden code chunks:

# Set global R options
options(scipen = 999)

# Set the graphical theme
ggplot2::theme_set(ggplot2::theme_light())

# Set global knitr chunk options
knitr::opts_chunk$set(
  warning = FALSE, 
  message = FALSE
)

library(rsample)

Prerequisites

This chapter leverages the following packages:

# Helper packages
library(dplyr)    # for data wrangling
library(ggplot2)  # for awesome graphics

# Modeling packages
library(ranger)   # a c++ implementation of random forest 
library(h2o)      # a java-based implementation of random forest

We’ll continue working with the ames_train data set:

# create Ames training data
set.seed(123)
ames <- AmesHousing::make_ames()
split  <- initial_split(ames, prop = 0.7, strata = "Sale_Price")
ames_train  <- training(split)

Out-of-the-box performance

# number of features
n_features <- length(setdiff(names(ames_train), "Sale_Price"))

# train a default random forest model
ames_rf1 <- ranger(
  Sale_Price ~ ., 
  data = ames_train,
  mtry = floor(n_features / 3),
  respect.unordered.factors = "order",
  seed = 123
)

# get OOB RMSE
(default_rmse <- sqrt(ames_rf1$prediction.error))
[1] 26042.77

Hyperparameters

Number of trees

Figure 11.1:

# number of features
n_features <- ncol(ames_train) - 1

# tuning grid
tuning_grid <- expand.grid(
  trees = seq(10, 1000, by = 20),
  rmse  = NA
)

for(i in seq_len(nrow(tuning_grid))) {

  # Fit a random forest
  fit <- ranger(
    formula = Sale_Price ~ ., 
    data = ames_train, 
    num.trees = tuning_grid$trees[i],
    mtry = floor(n_features / 3),
    respect.unordered.factors = 'order',
    verbose = FALSE,
    seed = 123
  )
  
  # Extract OOB RMSE
  tuning_grid$rmse[i] <- sqrt(fit$prediction.error)
  
}

ggplot(tuning_grid, aes(trees, rmse)) +
  geom_line(size = 1) +
  ylab("OOB Error (RMSE)") +
  xlab("Number of trees")

\(m_{try}\)

Figure 11.2 (has an exceptionally long runtime):

tuning_grid <- expand.grid(
  trees = seq(10, 1000, by = 20),
  mtry  = floor(c(seq(2, 80, length.out = 5), 26)),
  rmse  = NA
)

for(i in seq_len(nrow(tuning_grid))) {
  fit <- ranger(
  formula    = Sale_Price ~ ., 
  data       = ames_train, 
  num.trees  = tuning_grid$trees[i],
  mtry       = tuning_grid$mtry[i],
  respect.unordered.factors = 'order',
  verbose    = FALSE,
  seed       = 123
)
  
  tuning_grid$rmse[i] <- sqrt(fit$prediction.error)
  
}

labels <- tuning_grid %>%
  filter(trees == 990) %>%
  mutate(mtry = as.factor(mtry))

tuning_grid %>%
  mutate(mtry = as.factor(mtry)) %>%
  ggplot(aes(trees, rmse, color = mtry)) +
  geom_line(size = 1, show.legend = FALSE) +
  ggrepel::geom_text_repel(data = labels, aes(trees, rmse, label = mtry), nudge_x = 50, show.legend = FALSE) +
  ylab("OOB Error (RMSE)") +
  xlab("Number of trees")

Tree complexity

Figure 11.3:

tuning_grid <- expand.grid(
  min.node.size = 1:20,
  run_time  = NA,
  rmse = NA
)

for(i in seq_len(nrow(tuning_grid))) {
  fit_time <- system.time({
    fit <- ranger(
    formula    = Sale_Price ~ ., 
    data       = ames_train, 
    num.trees  = 1000,
    mtry       = 26,
    min.node.size = tuning_grid$min.node.size[i],
    respect.unordered.factors = 'order',
    verbose    = FALSE,
    seed       = 123
  )
})
  
  tuning_grid$run_time[i] <- fit_time[[3]]
  tuning_grid$rmse[i] <- sqrt(fit$prediction.error)
  
}

min_node_size <- tuning_grid %>% 
  mutate(
    error_first = first(rmse),
    runtime_first = first(run_time),
    `Error Growth` = (rmse / error_first) - 1,
    `Run Time Reduction` = (run_time / runtime_first) - 1
    )

p1 <-  ggplot(min_node_size, aes(min.node.size, `Error Growth`)) +
  geom_smooth(size = 1, se = FALSE, color = "black") +
  scale_y_continuous("Percent growth in error estimate", labels = scales::percent) +
  xlab("Minimum node size") +
  ggtitle("A) Impact to error estimate")

p2 <-  ggplot(min_node_size, aes(min.node.size, `Run Time Reduction`)) +
  geom_smooth(size = 1, se = FALSE, color = "black") +
  scale_y_continuous("Reduction in run time", labels = scales::percent) +
  xlab("Minimum node size") +
  ggtitle("B) Impact to run time")

gridExtra::grid.arrange(p1, p2, nrow = 1)

Sampling scheme

Figure 11.4:

tuning_grid <- expand.grid(
  sample.fraction = seq(.05, .95, by = .05),
  replace  = c(TRUE, FALSE),
  rmse = NA
)

for(i in seq_len(nrow(tuning_grid))) {
  fit <- ranger(
    formula    = Sale_Price ~ ., 
    data       = ames_train, 
    num.trees  = 1000,
    mtry       = 26,
    sample.fraction = tuning_grid$sample.fraction[i],
    replace = tuning_grid$replace[i],
    respect.unordered.factors = 'order',
    verbose    = FALSE,
    seed       = 123
  )

  tuning_grid$rmse[i] <- sqrt(fit$prediction.error)
  
}

tuning_grid %>%
  ggplot(aes(sample.fraction, rmse, color = replace)) +
  geom_line(size = 1) +
  scale_x_continuous("Sample Fraction", breaks = seq(.1, .9, by = .1), labels = scales::percent) +
  ylab("OOB Error (RMSE)") +
  scale_color_discrete("Sample with Replacement") +
  theme(legend.position = c(0.8, 0.85),
        legend.key = element_blank(),
        legend.background = element_blank())

Tuning strategies

This grid search takes approximately 2 minutes.

# create hyperparameter grid
hyper_grid <- expand.grid(
  mtry = floor(n_features * c(.05, .15, .25, .333, .4)),
  min.node.size = c(1, 3, 5, 10), 
  replace = c(TRUE, FALSE),                               
  sample.fraction = c(.5, .63, .8),                       
  rmse = NA                                               
)

# execute full cartesian grid search
for(i in seq_len(nrow(hyper_grid))) {
  # fit model for ith hyperparameter combination
  fit <- ranger(
    formula         = Sale_Price ~ ., 
    data            = ames_train, 
    num.trees       = n_features * 10,
    mtry            = hyper_grid$mtry[i],
    min.node.size   = hyper_grid$min.node.size[i],
    replace         = hyper_grid$replace[i],
    sample.fraction = hyper_grid$sample.fraction[i],
    verbose         = FALSE,
    seed            = 123,
    respect.unordered.factors = 'order',
  )
  # export OOB error 
  hyper_grid$rmse[i] <- sqrt(fit$prediction.error)
}

# assess top 10 models
hyper_grid %>%
  arrange(rmse) %>%
  mutate(perc_gain = (default_rmse - rmse) / default_rmse * 100) %>%
  head(10)
h2o.no_progress()
h2o.init(max_mem_size = "5g")

H2O is memory intensive so you should not run the code that follows in RStudio Cloud.

# convert training data to h2o object
train_h2o <- as.h2o(ames_train)

# set the response column to Sale_Price
response <- "Sale_Price"

# set the predictor names
predictors <- setdiff(colnames(ames_train), response)
h2o_rf1 <- h2o.randomForest(
    x = predictors, 
    y = response,
    training_frame = train_h2o, 
    ntrees = n_features * 10,
    seed = 123
)

h2o_rf1
Model Details:
==============

H2ORegressionModel: drf
Model ID:  DRF_model_R_1577101028169_1 
Model Summary: 


H2ORegressionMetrics: drf
** Reported on training data. **
** Metrics reported on Out-Of-Bag training samples **

MSE:  680109951
RMSE:  26078.92
MAE:  15451.62
RMSLE:  0.1349113
Mean Residual Deviance :  680109951
# hyperparameter grid
hyper_grid <- list(
  mtries = floor(n_features * c(.05, .15, .25, .333, .4)),
  min_rows = c(1, 3, 5, 10),
  max_depth = c(10, 20, 30),
  sample_rate = c(.55, .632, .70, .80)
)

# random grid search strategy
search_criteria <- list(
  strategy = "RandomDiscrete",
  stopping_metric = "mse",
  stopping_tolerance = 0.001,   # stop if improvement is < 0.1%
  stopping_rounds = 10,         # over the last 10 models
  max_runtime_secs = 60*5      # or stop search after 5 min.
)

This grid search takes 5 minutes.

# perform grid search 
random_grid <- h2o.grid(
  algorithm = "randomForest",
  grid_id = "rf_random_grid",
  x = predictors, 
  y = response, 
  training_frame = train_h2o,
  hyper_params = hyper_grid,
  ntrees = n_features * 10,
  seed = 123,
  stopping_metric = "RMSE",   
  stopping_rounds = 10,           # stop if last 10 trees added 
  stopping_tolerance = 0.005,     # don't improve RMSE by 0.5%
  search_criteria = search_criteria
)
# collect the results and sort by our model performance metric 
# of choice
random_grid_perf <- h2o.getGrid(
  grid_id = "rf_random_grid", 
  sort_by = "mse", 
  decreasing = FALSE
)
random_grid_perf
H2O Grid Details
================

Grid ID: rf_random_grid 
Used hyper parameters: 
  -  max_depth 
  -  min_rows 
  -  mtries 
  -  sample_rate 
Number of models: 107 
Number of failed models: 0 

Hyper-Parameter Search Summary: ordered by increasing mse

---

Feature interpretation

# re-run model with impurity-based variable importance
rf_impurity <- ranger(
  formula = Sale_Price ~ ., 
  data = ames_train, 
  num.trees = 2000,
  mtry = 32,
  min.node.size = 1,
  sample.fraction = .80,
  replace = FALSE,
  importance = "impurity",
  respect.unordered.factors = "order",
  verbose = FALSE,
  seed  = 123
)

# re-run model with permutation-based variable importance
rf_permutation <- ranger(
  formula = Sale_Price ~ ., 
  data = ames_train, 
  num.trees = 2000,
  mtry = 32,
  min.node.size = 1,
  sample.fraction = .80,
  replace = FALSE,
  importance = "permutation",
  respect.unordered.factors = "order",
  verbose = FALSE,
  seed  = 123
)
p1 <- vip::vip(rf_impurity, num_features = 25, bar = FALSE)
p2 <- vip::vip(rf_permutation, num_features = 25, bar = FALSE)

gridExtra::grid.arrange(p1, p2, nrow = 1)

LS0tCnRpdGxlOiAiQ2hhcHRlciAxMTogUmFuZG9tIEZvcmVzdHMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCl9fTm90ZV9fOiBTb21lIHJlc3VsdHMgbWF5IGRpZmZlciBmcm9tIHRoZSBoYXJkIGNvcHkgYm9vayBkdWUgdG8gdGhlIGNoYW5naW5nIG9mIHNhbXBsaW5nIHByb2NlZHVyZXMgaW50cm9kdWNlZCBpbiBSIDMuNi4wLiBTZWUgaHR0cDovL2JpdC5seS8zNUQxU1c3IGZvciBtb3JlIGRldGFpbHMuIEFjY2VzcyBhbmQgcnVuIHRoZSBzb3VyY2UgY29kZSBmb3IgdGhpcyBub3RlYm9vayBbaGVyZV0oaHR0cHM6Ly9yc3R1ZGlvLmNsb3VkL3Byb2plY3QvODAxMTg1KS4gCgpIaWRkZW4gY2hhcHRlciByZXF1aXJlbWVudHMgdXNlZCBpbiB0aGUgYm9vayB0byBzZXQgdGhlIHBsb3R0aW5nIHRoZW1lIGFuZCBsb2FkIHBhY2thZ2VzIHVzZWQgaW4gaGlkZGVuIGNvZGUgY2h1bmtzOgoKYGBge3Igc2V0dXB9CiMgU2V0IGdsb2JhbCBSIG9wdGlvbnMKb3B0aW9ucyhzY2lwZW4gPSA5OTkpCgojIFNldCB0aGUgZ3JhcGhpY2FsIHRoZW1lCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9saWdodCgpKQoKIyBTZXQgZ2xvYmFsIGtuaXRyIGNodW5rIG9wdGlvbnMKa25pdHI6Om9wdHNfY2h1bmskc2V0KAogIHdhcm5pbmcgPSBGQUxTRSwgCiAgbWVzc2FnZSA9IEZBTFNFCikKCmxpYnJhcnkocnNhbXBsZSkKYGBgCgojIyBQcmVyZXF1aXNpdGVzCgpUaGlzIGNoYXB0ZXIgbGV2ZXJhZ2VzIHRoZSBmb2xsb3dpbmcgcGFja2FnZXM6CgpgYGB7ciByZi1wa2ctcmVxfQojIEhlbHBlciBwYWNrYWdlcwpsaWJyYXJ5KGRwbHlyKSAgICAjIGZvciBkYXRhIHdyYW5nbGluZwpsaWJyYXJ5KGdncGxvdDIpICAjIGZvciBhd2Vzb21lIGdyYXBoaWNzCgojIE1vZGVsaW5nIHBhY2thZ2VzCmxpYnJhcnkocmFuZ2VyKSAgICMgYSBjKysgaW1wbGVtZW50YXRpb24gb2YgcmFuZG9tIGZvcmVzdCAKbGlicmFyeShoMm8pICAgICAgIyBhIGphdmEtYmFzZWQgaW1wbGVtZW50YXRpb24gb2YgcmFuZG9tIGZvcmVzdApgYGAKCldlJ2xsIGNvbnRpbnVlIHdvcmtpbmcgd2l0aCB0aGUgYGFtZXNfdHJhaW5gIGRhdGEgc2V0OgoKYGBge3IgcmYtYW1lcy10cmFpbiwgZWNobz1UUlVFfQojIGNyZWF0ZSBBbWVzIHRyYWluaW5nIGRhdGEKc2V0LnNlZWQoMTIzKQphbWVzIDwtIEFtZXNIb3VzaW5nOjptYWtlX2FtZXMoKQpzcGxpdCAgPC0gaW5pdGlhbF9zcGxpdChhbWVzLCBwcm9wID0gMC43LCBzdHJhdGEgPSAiU2FsZV9QcmljZSIpCmFtZXNfdHJhaW4gIDwtIHRyYWluaW5nKHNwbGl0KQpgYGAKCiMjIE91dC1vZi10aGUtYm94IHBlcmZvcm1hbmNlCgpgYGB7ciBvdXQtb2YtYm94LXJmfQojIG51bWJlciBvZiBmZWF0dXJlcwpuX2ZlYXR1cmVzIDwtIGxlbmd0aChzZXRkaWZmKG5hbWVzKGFtZXNfdHJhaW4pLCAiU2FsZV9QcmljZSIpKQoKIyB0cmFpbiBhIGRlZmF1bHQgcmFuZG9tIGZvcmVzdCBtb2RlbAphbWVzX3JmMSA8LSByYW5nZXIoCiAgU2FsZV9QcmljZSB+IC4sIAogIGRhdGEgPSBhbWVzX3RyYWluLAogIG10cnkgPSBmbG9vcihuX2ZlYXR1cmVzIC8gMyksCiAgcmVzcGVjdC51bm9yZGVyZWQuZmFjdG9ycyA9ICJvcmRlciIsCiAgc2VlZCA9IDEyMwopCgojIGdldCBPT0IgUk1TRQooZGVmYXVsdF9ybXNlIDwtIHNxcnQoYW1lc19yZjEkcHJlZGljdGlvbi5lcnJvcikpCmBgYAoKIyMgSHlwZXJwYXJhbWV0ZXJzCgojIyMgTnVtYmVyIG9mIHRyZWVzCgpGaWd1cmUgMTEuMToKCmBgYHtyIHR1bmluZy10cmVlcywgZWNobz1UUlVFLCBmaWcuY2FwPSJUaGUgQW1lcyBkYXRhIGhhcyA4MCBmZWF0dXJlcyBhbmQgc3RhcnRpbmcgd2l0aCAxMCB0aW1lcyB0aGUgbnVtYmVyIG9mIGZlYXR1cmVzIHR5cGljYWxseSBlbnN1cmVzIHRoZSBlcnJvciBlc3RpbWF0ZSBjb252ZXJnZXMuIiwgZmlnLmhlaWdodD0zfQojIG51bWJlciBvZiBmZWF0dXJlcwpuX2ZlYXR1cmVzIDwtIG5jb2woYW1lc190cmFpbikgLSAxCgojIHR1bmluZyBncmlkCnR1bmluZ19ncmlkIDwtIGV4cGFuZC5ncmlkKAogIHRyZWVzID0gc2VxKDEwLCAxMDAwLCBieSA9IDIwKSwKICBybXNlICA9IE5BCikKCmZvcihpIGluIHNlcV9sZW4obnJvdyh0dW5pbmdfZ3JpZCkpKSB7CgogICMgRml0IGEgcmFuZG9tIGZvcmVzdAogIGZpdCA8LSByYW5nZXIoCiAgICBmb3JtdWxhID0gU2FsZV9QcmljZSB+IC4sIAogICAgZGF0YSA9IGFtZXNfdHJhaW4sIAogICAgbnVtLnRyZWVzID0gdHVuaW5nX2dyaWQkdHJlZXNbaV0sCiAgICBtdHJ5ID0gZmxvb3Iobl9mZWF0dXJlcyAvIDMpLAogICAgcmVzcGVjdC51bm9yZGVyZWQuZmFjdG9ycyA9ICdvcmRlcicsCiAgICB2ZXJib3NlID0gRkFMU0UsCiAgICBzZWVkID0gMTIzCiAgKQogIAogICMgRXh0cmFjdCBPT0IgUk1TRQogIHR1bmluZ19ncmlkJHJtc2VbaV0gPC0gc3FydChmaXQkcHJlZGljdGlvbi5lcnJvcikKICAKfQoKZ2dwbG90KHR1bmluZ19ncmlkLCBhZXModHJlZXMsIHJtc2UpKSArCiAgZ2VvbV9saW5lKHNpemUgPSAxKSArCiAgeWxhYigiT09CIEVycm9yIChSTVNFKSIpICsKICB4bGFiKCJOdW1iZXIgb2YgdHJlZXMiKQpgYGAKCiMjIyAkbV97dHJ5fSQgCgpGaWd1cmUgMTEuMiAoaGFzIGFuIGV4Y2VwdGlvbmFsbHkgbG9uZyBydW50aW1lKToKCmBgYHtyIHR1bmluZy1tdHJ5LCBlY2hvPVRSVUUsIGZpZy5jYXA9IkZvciB0aGUgQW1lcyBkYXRhLCBhbiBtdHJ5IHZhbHVlIHNsaWdodGx5IGxvd2VyICgyMSkgdGhhbiB0aGUgZGVmYXVsdCAoMjYpIGltcHJvdmVzIHBlcmZvcm1hbmNlLiIsIGZpZy5oZWlnaHQ9My41fQp0dW5pbmdfZ3JpZCA8LSBleHBhbmQuZ3JpZCgKICB0cmVlcyA9IHNlcSgxMCwgMTAwMCwgYnkgPSAyMCksCiAgbXRyeSAgPSBmbG9vcihjKHNlcSgyLCA4MCwgbGVuZ3RoLm91dCA9IDUpLCAyNikpLAogIHJtc2UgID0gTkEKKQoKZm9yKGkgaW4gc2VxX2xlbihucm93KHR1bmluZ19ncmlkKSkpIHsKICBmaXQgPC0gcmFuZ2VyKAogIGZvcm11bGEgICAgPSBTYWxlX1ByaWNlIH4gLiwgCiAgZGF0YSAgICAgICA9IGFtZXNfdHJhaW4sIAogIG51bS50cmVlcyAgPSB0dW5pbmdfZ3JpZCR0cmVlc1tpXSwKICBtdHJ5ICAgICAgID0gdHVuaW5nX2dyaWQkbXRyeVtpXSwKICByZXNwZWN0LnVub3JkZXJlZC5mYWN0b3JzID0gJ29yZGVyJywKICB2ZXJib3NlICAgID0gRkFMU0UsCiAgc2VlZCAgICAgICA9IDEyMwopCiAgCiAgdHVuaW5nX2dyaWQkcm1zZVtpXSA8LSBzcXJ0KGZpdCRwcmVkaWN0aW9uLmVycm9yKQogIAp9CgpsYWJlbHMgPC0gdHVuaW5nX2dyaWQgJT4lCiAgZmlsdGVyKHRyZWVzID09IDk5MCkgJT4lCiAgbXV0YXRlKG10cnkgPSBhcy5mYWN0b3IobXRyeSkpCgp0dW5pbmdfZ3JpZCAlPiUKICBtdXRhdGUobXRyeSA9IGFzLmZhY3RvcihtdHJ5KSkgJT4lCiAgZ2dwbG90KGFlcyh0cmVlcywgcm1zZSwgY29sb3IgPSBtdHJ5KSkgKwogIGdlb21fbGluZShzaXplID0gMSwgc2hvdy5sZWdlbmQgPSBGQUxTRSkgKwogIGdncmVwZWw6Omdlb21fdGV4dF9yZXBlbChkYXRhID0gbGFiZWxzLCBhZXModHJlZXMsIHJtc2UsIGxhYmVsID0gbXRyeSksIG51ZGdlX3ggPSA1MCwgc2hvdy5sZWdlbmQgPSBGQUxTRSkgKwogIHlsYWIoIk9PQiBFcnJvciAoUk1TRSkiKSArCiAgeGxhYigiTnVtYmVyIG9mIHRyZWVzIikKYGBgCgojIyMgVHJlZSBjb21wbGV4aXR5CgpGaWd1cmUgMTEuMzoKCmBgYHtyIHR1bmluZy1ub2RlLXNpemUsIGVjaG89VFJVRSwgZmlnLmNhcD0iSW5jcmVhc2luZyBub2RlIHNpemUgdG8gcmVkdWNlIHRyZWUgY29tcGxleGl0eSB3aWxsIG9mdGVuIGhhdmUgYSBsYXJnZXIgaW1wYWN0IG9uIGNvbXB1dGF0aW9uIHNwZWVkIChyaWdodCkgdGhhbiBvbiB5b3VyIGVycm9yIGVzdGltYXRlLiIsIGZpZy53aWR0aD0xMH0KdHVuaW5nX2dyaWQgPC0gZXhwYW5kLmdyaWQoCiAgbWluLm5vZGUuc2l6ZSA9IDE6MjAsCiAgcnVuX3RpbWUgID0gTkEsCiAgcm1zZSA9IE5BCikKCmZvcihpIGluIHNlcV9sZW4obnJvdyh0dW5pbmdfZ3JpZCkpKSB7CiAgZml0X3RpbWUgPC0gc3lzdGVtLnRpbWUoewogICAgZml0IDwtIHJhbmdlcigKICAgIGZvcm11bGEgICAgPSBTYWxlX1ByaWNlIH4gLiwgCiAgICBkYXRhICAgICAgID0gYW1lc190cmFpbiwgCiAgICBudW0udHJlZXMgID0gMTAwMCwKICAgIG10cnkgICAgICAgPSAyNiwKICAgIG1pbi5ub2RlLnNpemUgPSB0dW5pbmdfZ3JpZCRtaW4ubm9kZS5zaXplW2ldLAogICAgcmVzcGVjdC51bm9yZGVyZWQuZmFjdG9ycyA9ICdvcmRlcicsCiAgICB2ZXJib3NlICAgID0gRkFMU0UsCiAgICBzZWVkICAgICAgID0gMTIzCiAgKQp9KQogIAogIHR1bmluZ19ncmlkJHJ1bl90aW1lW2ldIDwtIGZpdF90aW1lW1szXV0KICB0dW5pbmdfZ3JpZCRybXNlW2ldIDwtIHNxcnQoZml0JHByZWRpY3Rpb24uZXJyb3IpCiAgCn0KCm1pbl9ub2RlX3NpemUgPC0gdHVuaW5nX2dyaWQgJT4lIAogIG11dGF0ZSgKICAgIGVycm9yX2ZpcnN0ID0gZmlyc3Qocm1zZSksCiAgICBydW50aW1lX2ZpcnN0ID0gZmlyc3QocnVuX3RpbWUpLAogICAgYEVycm9yIEdyb3d0aGAgPSAocm1zZSAvIGVycm9yX2ZpcnN0KSAtIDEsCiAgICBgUnVuIFRpbWUgUmVkdWN0aW9uYCA9IChydW5fdGltZSAvIHJ1bnRpbWVfZmlyc3QpIC0gMQogICAgKQoKcDEgPC0gIGdncGxvdChtaW5fbm9kZV9zaXplLCBhZXMobWluLm5vZGUuc2l6ZSwgYEVycm9yIEdyb3d0aGApKSArCiAgZ2VvbV9zbW9vdGgoc2l6ZSA9IDEsIHNlID0gRkFMU0UsIGNvbG9yID0gImJsYWNrIikgKwogIHNjYWxlX3lfY29udGludW91cygiUGVyY2VudCBncm93dGggaW4gZXJyb3IgZXN0aW1hdGUiLCBsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsKICB4bGFiKCJNaW5pbXVtIG5vZGUgc2l6ZSIpICsKICBnZ3RpdGxlKCJBKSBJbXBhY3QgdG8gZXJyb3IgZXN0aW1hdGUiKQoKcDIgPC0gIGdncGxvdChtaW5fbm9kZV9zaXplLCBhZXMobWluLm5vZGUuc2l6ZSwgYFJ1biBUaW1lIFJlZHVjdGlvbmApKSArCiAgZ2VvbV9zbW9vdGgoc2l6ZSA9IDEsIHNlID0gRkFMU0UsIGNvbG9yID0gImJsYWNrIikgKwogIHNjYWxlX3lfY29udGludW91cygiUmVkdWN0aW9uIGluIHJ1biB0aW1lIiwgbGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArCiAgeGxhYigiTWluaW11bSBub2RlIHNpemUiKSArCiAgZ2d0aXRsZSgiQikgSW1wYWN0IHRvIHJ1biB0aW1lIikKCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHAxLCBwMiwgbnJvdyA9IDEpCmBgYAoKIyMjIFNhbXBsaW5nIHNjaGVtZQoKRmlndXJlIDExLjQ6CgpgYGB7ciB0dW5pbmctc2FtcGxpbmctc2NoZW1lLCBlY2hvPVRSVUUsIGZpZy5jYXA9IlRoZSBBbWVzIGRhdGEgaGFzIHNldmVyYWwgaW1iYWxhbmNlZCBjYXRlZ29yaWNhbCBmZWF0dXJlcyBzdWNoIGFzIG5laWdoYm9yaG9vZCwgem9uaW5nLCBvdmVyYWxsIHF1YWxpdHksIGFuZCBtb3JlLiBDb25zZXF1ZW50bHksIHNhbXBsaW5nIHdpdGhvdXQgcmVwbGFjZW1lbnQgYXBwZWFycyB0byBpbXByb3ZlIHBlcmZvcm1hbmNlIGFzIGl0IGxlYWRzIHRvIGxlc3MgYmlhc2VkIHNwbGl0IHZhcmlhYmxlIHNlbGVjdGlvbiBhbmQgbW9yZSB1bmNvcnJlbGF0ZWQgdHJlZXMuIiwgZmlnLmhlaWdodD0zfQp0dW5pbmdfZ3JpZCA8LSBleHBhbmQuZ3JpZCgKICBzYW1wbGUuZnJhY3Rpb24gPSBzZXEoLjA1LCAuOTUsIGJ5ID0gLjA1KSwKICByZXBsYWNlICA9IGMoVFJVRSwgRkFMU0UpLAogIHJtc2UgPSBOQQopCgpmb3IoaSBpbiBzZXFfbGVuKG5yb3codHVuaW5nX2dyaWQpKSkgewogIGZpdCA8LSByYW5nZXIoCiAgICBmb3JtdWxhICAgID0gU2FsZV9QcmljZSB+IC4sIAogICAgZGF0YSAgICAgICA9IGFtZXNfdHJhaW4sIAogICAgbnVtLnRyZWVzICA9IDEwMDAsCiAgICBtdHJ5ICAgICAgID0gMjYsCiAgICBzYW1wbGUuZnJhY3Rpb24gPSB0dW5pbmdfZ3JpZCRzYW1wbGUuZnJhY3Rpb25baV0sCiAgICByZXBsYWNlID0gdHVuaW5nX2dyaWQkcmVwbGFjZVtpXSwKICAgIHJlc3BlY3QudW5vcmRlcmVkLmZhY3RvcnMgPSAnb3JkZXInLAogICAgdmVyYm9zZSAgICA9IEZBTFNFLAogICAgc2VlZCAgICAgICA9IDEyMwogICkKCiAgdHVuaW5nX2dyaWQkcm1zZVtpXSA8LSBzcXJ0KGZpdCRwcmVkaWN0aW9uLmVycm9yKQogIAp9Cgp0dW5pbmdfZ3JpZCAlPiUKICBnZ3Bsb3QoYWVzKHNhbXBsZS5mcmFjdGlvbiwgcm1zZSwgY29sb3IgPSByZXBsYWNlKSkgKwogIGdlb21fbGluZShzaXplID0gMSkgKwogIHNjYWxlX3hfY29udGludW91cygiU2FtcGxlIEZyYWN0aW9uIiwgYnJlYWtzID0gc2VxKC4xLCAuOSwgYnkgPSAuMSksIGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKwogIHlsYWIoIk9PQiBFcnJvciAoUk1TRSkiKSArCiAgc2NhbGVfY29sb3JfZGlzY3JldGUoIlNhbXBsZSB3aXRoIFJlcGxhY2VtZW50IikgKwogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9IGMoMC44LCAwLjg1KSwKICAgICAgICBsZWdlbmQua2V5ID0gZWxlbWVudF9ibGFuaygpLAogICAgICAgIGxlZ2VuZC5iYWNrZ3JvdW5kID0gZWxlbWVudF9ibGFuaygpKQpgYGAKCiMjIFR1bmluZyBzdHJhdGVnaWVzIAoKX19fVGhpcyBncmlkIHNlYXJjaCB0YWtlcyBhcHByb3hpbWF0ZWx5IDIgbWludXRlcy5fX18KCmBgYHtyIHJhbmdlci1ncmlkLXNlYXJjaH0KIyBjcmVhdGUgaHlwZXJwYXJhbWV0ZXIgZ3JpZApoeXBlcl9ncmlkIDwtIGV4cGFuZC5ncmlkKAogIG10cnkgPSBmbG9vcihuX2ZlYXR1cmVzICogYyguMDUsIC4xNSwgLjI1LCAuMzMzLCAuNCkpLAogIG1pbi5ub2RlLnNpemUgPSBjKDEsIDMsIDUsIDEwKSwgCiAgcmVwbGFjZSA9IGMoVFJVRSwgRkFMU0UpLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICBzYW1wbGUuZnJhY3Rpb24gPSBjKC41LCAuNjMsIC44KSwgICAgICAgICAgICAgICAgICAgICAgIAogIHJtc2UgPSBOQSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCikKCiMgZXhlY3V0ZSBmdWxsIGNhcnRlc2lhbiBncmlkIHNlYXJjaApmb3IoaSBpbiBzZXFfbGVuKG5yb3coaHlwZXJfZ3JpZCkpKSB7CiAgIyBmaXQgbW9kZWwgZm9yIGl0aCBoeXBlcnBhcmFtZXRlciBjb21iaW5hdGlvbgogIGZpdCA8LSByYW5nZXIoCiAgICBmb3JtdWxhICAgICAgICAgPSBTYWxlX1ByaWNlIH4gLiwgCiAgICBkYXRhICAgICAgICAgICAgPSBhbWVzX3RyYWluLCAKICAgIG51bS50cmVlcyAgICAgICA9IG5fZmVhdHVyZXMgKiAxMCwKICAgIG10cnkgICAgICAgICAgICA9IGh5cGVyX2dyaWQkbXRyeVtpXSwKICAgIG1pbi5ub2RlLnNpemUgICA9IGh5cGVyX2dyaWQkbWluLm5vZGUuc2l6ZVtpXSwKICAgIHJlcGxhY2UgICAgICAgICA9IGh5cGVyX2dyaWQkcmVwbGFjZVtpXSwKICAgIHNhbXBsZS5mcmFjdGlvbiA9IGh5cGVyX2dyaWQkc2FtcGxlLmZyYWN0aW9uW2ldLAogICAgdmVyYm9zZSAgICAgICAgID0gRkFMU0UsCiAgICBzZWVkICAgICAgICAgICAgPSAxMjMsCiAgICByZXNwZWN0LnVub3JkZXJlZC5mYWN0b3JzID0gJ29yZGVyJywKICApCiAgIyBleHBvcnQgT09CIGVycm9yIAogIGh5cGVyX2dyaWQkcm1zZVtpXSA8LSBzcXJ0KGZpdCRwcmVkaWN0aW9uLmVycm9yKQp9CgojIGFzc2VzcyB0b3AgMTAgbW9kZWxzCmh5cGVyX2dyaWQgJT4lCiAgYXJyYW5nZShybXNlKSAlPiUKICBtdXRhdGUocGVyY19nYWluID0gKGRlZmF1bHRfcm1zZSAtIHJtc2UpIC8gZGVmYXVsdF9ybXNlICogMTAwKSAlPiUKICBoZWFkKDEwKQpgYGAKCmBgYHtyIGgyby1pbml0fQpoMm8ubm9fcHJvZ3Jlc3MoKQpoMm8uaW5pdChtYXhfbWVtX3NpemUgPSAiNWciKQpgYGAKCkgyTyBpcyBtZW1vcnkgaW50ZW5zaXZlIHNvIHlvdSBzaG91bGQgbm90IHJ1biB0aGUgY29kZSB0aGF0IGZvbGxvd3MgaW4gUlN0dWRpbyBDbG91ZC4KCmBgYHtyIGgyby1vYmplY3RzfQojIGNvbnZlcnQgdHJhaW5pbmcgZGF0YSB0byBoMm8gb2JqZWN0CnRyYWluX2gybyA8LSBhcy5oMm8oYW1lc190cmFpbikKCiMgc2V0IHRoZSByZXNwb25zZSBjb2x1bW4gdG8gU2FsZV9QcmljZQpyZXNwb25zZSA8LSAiU2FsZV9QcmljZSIKCiMgc2V0IHRoZSBwcmVkaWN0b3IgbmFtZXMKcHJlZGljdG9ycyA8LSBzZXRkaWZmKGNvbG5hbWVzKGFtZXNfdHJhaW4pLCByZXNwb25zZSkKYGBgCgpgYGB7ciBoMm8tYmFzZWxpbmV9Cmgyb19yZjEgPC0gaDJvLnJhbmRvbUZvcmVzdCgKICAgIHggPSBwcmVkaWN0b3JzLCAKICAgIHkgPSByZXNwb25zZSwKICAgIHRyYWluaW5nX2ZyYW1lID0gdHJhaW5faDJvLCAKICAgIG50cmVlcyA9IG5fZmVhdHVyZXMgKiAxMCwKICAgIHNlZWQgPSAxMjMKKQoKaDJvX3JmMQpgYGAKCmBgYHtyIGgyMC1yYW5kb20tc2VhcmNoLXNldHVwfQojIGh5cGVycGFyYW1ldGVyIGdyaWQKaHlwZXJfZ3JpZCA8LSBsaXN0KAogIG10cmllcyA9IGZsb29yKG5fZmVhdHVyZXMgKiBjKC4wNSwgLjE1LCAuMjUsIC4zMzMsIC40KSksCiAgbWluX3Jvd3MgPSBjKDEsIDMsIDUsIDEwKSwKICBtYXhfZGVwdGggPSBjKDEwLCAyMCwgMzApLAogIHNhbXBsZV9yYXRlID0gYyguNTUsIC42MzIsIC43MCwgLjgwKQopCgojIHJhbmRvbSBncmlkIHNlYXJjaCBzdHJhdGVneQpzZWFyY2hfY3JpdGVyaWEgPC0gbGlzdCgKICBzdHJhdGVneSA9ICJSYW5kb21EaXNjcmV0ZSIsCiAgc3RvcHBpbmdfbWV0cmljID0gIm1zZSIsCiAgc3RvcHBpbmdfdG9sZXJhbmNlID0gMC4wMDEsICAgIyBzdG9wIGlmIGltcHJvdmVtZW50IGlzIDwgMC4xJQogIHN0b3BwaW5nX3JvdW5kcyA9IDEwLCAgICAgICAgICMgb3ZlciB0aGUgbGFzdCAxMCBtb2RlbHMKICBtYXhfcnVudGltZV9zZWNzID0gNjAqNSAgICAgICMgb3Igc3RvcCBzZWFyY2ggYWZ0ZXIgNSBtaW4uCikKYGBgCgpfX19UaGlzIGdyaWQgc2VhcmNoIHRha2VzIDUgbWludXRlcy5fX18KCmBgYHtyIGgyMC1yYW5kb20tc2VhcmNoLWV4ZWN1dGlvbn0KIyBwZXJmb3JtIGdyaWQgc2VhcmNoIApyYW5kb21fZ3JpZCA8LSBoMm8uZ3JpZCgKICBhbGdvcml0aG0gPSAicmFuZG9tRm9yZXN0IiwKICBncmlkX2lkID0gInJmX3JhbmRvbV9ncmlkIiwKICB4ID0gcHJlZGljdG9ycywgCiAgeSA9IHJlc3BvbnNlLCAKICB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywKICBoeXBlcl9wYXJhbXMgPSBoeXBlcl9ncmlkLAogIG50cmVlcyA9IG5fZmVhdHVyZXMgKiAxMCwKICBzZWVkID0gMTIzLAogIHN0b3BwaW5nX21ldHJpYyA9ICJSTVNFIiwgICAKICBzdG9wcGluZ19yb3VuZHMgPSAxMCwgICAgICAgICAgICMgc3RvcCBpZiBsYXN0IDEwIHRyZWVzIGFkZGVkIAogIHN0b3BwaW5nX3RvbGVyYW5jZSA9IDAuMDA1LCAgICAgIyBkb24ndCBpbXByb3ZlIFJNU0UgYnkgMC41JQogIHNlYXJjaF9jcml0ZXJpYSA9IHNlYXJjaF9jcml0ZXJpYQopCmBgYAoKYGBge3IgaDJvLWdyaWQtc2VhcmNoLXJlc3VsdHN9CiMgY29sbGVjdCB0aGUgcmVzdWx0cyBhbmQgc29ydCBieSBvdXIgbW9kZWwgcGVyZm9ybWFuY2UgbWV0cmljIAojIG9mIGNob2ljZQpyYW5kb21fZ3JpZF9wZXJmIDwtIGgyby5nZXRHcmlkKAogIGdyaWRfaWQgPSAicmZfcmFuZG9tX2dyaWQiLCAKICBzb3J0X2J5ID0gIm1zZSIsIAogIGRlY3JlYXNpbmcgPSBGQUxTRQopCnJhbmRvbV9ncmlkX3BlcmYKYGBgCgojIyBGZWF0dXJlIGludGVycHJldGF0aW9uCgpgYGB7ciBmZWF0dXJlLWltcG9ydGFuY2V9CiMgcmUtcnVuIG1vZGVsIHdpdGggaW1wdXJpdHktYmFzZWQgdmFyaWFibGUgaW1wb3J0YW5jZQpyZl9pbXB1cml0eSA8LSByYW5nZXIoCiAgZm9ybXVsYSA9IFNhbGVfUHJpY2UgfiAuLCAKICBkYXRhID0gYW1lc190cmFpbiwgCiAgbnVtLnRyZWVzID0gMjAwMCwKICBtdHJ5ID0gMzIsCiAgbWluLm5vZGUuc2l6ZSA9IDEsCiAgc2FtcGxlLmZyYWN0aW9uID0gLjgwLAogIHJlcGxhY2UgPSBGQUxTRSwKICBpbXBvcnRhbmNlID0gImltcHVyaXR5IiwKICByZXNwZWN0LnVub3JkZXJlZC5mYWN0b3JzID0gIm9yZGVyIiwKICB2ZXJib3NlID0gRkFMU0UsCiAgc2VlZCAgPSAxMjMKKQoKIyByZS1ydW4gbW9kZWwgd2l0aCBwZXJtdXRhdGlvbi1iYXNlZCB2YXJpYWJsZSBpbXBvcnRhbmNlCnJmX3Blcm11dGF0aW9uIDwtIHJhbmdlcigKICBmb3JtdWxhID0gU2FsZV9QcmljZSB+IC4sIAogIGRhdGEgPSBhbWVzX3RyYWluLCAKICBudW0udHJlZXMgPSAyMDAwLAogIG10cnkgPSAzMiwKICBtaW4ubm9kZS5zaXplID0gMSwKICBzYW1wbGUuZnJhY3Rpb24gPSAuODAsCiAgcmVwbGFjZSA9IEZBTFNFLAogIGltcG9ydGFuY2UgPSAicGVybXV0YXRpb24iLAogIHJlc3BlY3QudW5vcmRlcmVkLmZhY3RvcnMgPSAib3JkZXIiLAogIHZlcmJvc2UgPSBGQUxTRSwKICBzZWVkICA9IDEyMwopCmBgYAoKYGBge3IgZmVhdHVyZS1pbXBvcnRhbmNlLXBsb3QsIGZpZy5jYXA9IlRvcCAyNSBtb3N0IGltcG9ydGFudCB2YXJpYWJsZXMgYmFzZWQgb24gaW1wdXJpdHkgKGxlZnQpIGFuZCBwZXJtdXRhdGlvbiAocmlnaHQpLiIsIGZpZy5oZWlnaHQ9NC41LCBmaWcud2lkdGg9MTB9CnAxIDwtIHZpcDo6dmlwKHJmX2ltcHVyaXR5LCBudW1fZmVhdHVyZXMgPSAyNSwgYmFyID0gRkFMU0UpCnAyIDwtIHZpcDo6dmlwKHJmX3Blcm11dGF0aW9uLCBudW1fZmVhdHVyZXMgPSAyNSwgYmFyID0gRkFMU0UpCgpncmlkRXh0cmE6OmdyaWQuYXJyYW5nZShwMSwgcDIsIG5yb3cgPSAxKQpgYGAK