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. Do to output size, most of this chapter’s code chunks should not be ran on RStudio Cloud.

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

Prerequisites

This chapter leverages the following packages, with the emphasis on h2o:

# Helper packages
library(rsample)   # for creating our train-test splits
library(recipes)   # for minor feature engineering tasks

# Modeling packages
library(h2o)       # for fitting stacked models
h2o.no_progress()
h2o.init()

To illustrate key concepts we continue with the Ames housing example from previous chapters:

# Load and split the Ames housing data
ames <- AmesHousing::make_ames()
set.seed(123)  # for reproducibility
split <- initial_split(ames, strata = "Sale_Price")
ames_train <- training(split)
ames_test <- testing(split)

# Make sure we have consistent categorical levels
blueprint <- recipe(Sale_Price ~ ., data = ames_train) %>%
  step_other(all_nominal(), threshold = 0.05)

# Create training & test sets for h2o
train_h2o <- prep(blueprint, training = ames_train, retain = TRUE) %>%
  juice() %>%
  as.h2o()
test_h2o <- prep(blueprint, training = ames_train) %>%
  bake(new_data = ames_test) %>%
  as.h2o()

# Get response and feature names
Y <- "Sale_Price"
X <- setdiff(names(ames_train), Y)

Stacking existing models

# Train & cross-validate a GLM model
best_glm <- h2o.glm(
  x = X, y = Y, training_frame = train_h2o, alpha = 0.1,
  remove_collinear_columns = TRUE, nfolds = 10, fold_assignment = "Modulo",
  keep_cross_validation_predictions = TRUE, seed = 123
)

# Train & cross-validate a RF model
best_rf <- h2o.randomForest(
  x = X, y = Y, training_frame = train_h2o, ntrees = 1000, mtries = 20,
  max_depth = 30, min_rows = 1, sample_rate = 0.8, nfolds = 10,
  fold_assignment = "Modulo", keep_cross_validation_predictions = TRUE,
  seed = 123, stopping_rounds = 50, stopping_metric = "RMSE",
  stopping_tolerance = 0
)

# Train & cross-validate a GBM model
best_gbm <- h2o.gbm(
  x = X, y = Y, training_frame = train_h2o, ntrees = 5000, learn_rate = 0.01,
  max_depth = 7, min_rows = 5, sample_rate = 0.8, nfolds = 10,
  fold_assignment = "Modulo", keep_cross_validation_predictions = TRUE,
  seed = 123, stopping_rounds = 50, stopping_metric = "RMSE",
  stopping_tolerance = 0
)

# Train & cross-validate an XGBoost model
best_xgb <- h2o.xgboost(
  x = X, y = Y, training_frame = train_h2o, ntrees = 5000, learn_rate = 0.05,
  max_depth = 3, min_rows = 3, sample_rate = 0.8, categorical_encoding = "Enum",
  nfolds = 10, fold_assignment = "Modulo", 
  keep_cross_validation_predictions = TRUE, seed = 123, stopping_rounds = 50,
  stopping_metric = "RMSE", stopping_tolerance = 0
)
# Train a stacked tree ensemble
ensemble_tree <- h2o.stackedEnsemble(
  x = X, y = Y, training_frame = train_h2o, model_id = "my_tree_ensemble",
  base_models = list(best_glm, best_rf, best_gbm, best_xgb),
  metalearner_algorithm = "drf"
)
# Get results from base learners
get_rmse <- function(model) {
  results <- h2o.performance(model, newdata = test_h2o)
  results@metrics$RMSE
}

list(best_glm, best_rf, best_gbm, best_xgb) %>%
  purrr::map_dbl(get_rmse)
[1] 36834.07 23635.47 19236.09 19725.81
# Stacked results
h2o.performance(ensemble_tree, newdata = test_h2o)@metrics$RMSE
[1] 20446.95
data.frame(
  GLM_pred = as.vector(h2o.getFrame(best_glm@model$cross_validation_holdout_predictions_frame_id$name)),
  RF_pred = as.vector(h2o.getFrame(best_rf@model$cross_validation_holdout_predictions_frame_id$name)),
  GBM_pred = as.vector(h2o.getFrame(best_gbm@model$cross_validation_holdout_predictions_frame_id$name)),
  XGB_pred = as.vector(h2o.getFrame(best_xgb@model$cross_validation_holdout_predictions_frame_id$name))
) %>% cor()
          GLM_pred   RF_pred  GBM_pred  XGB_pred
GLM_pred 1.0000000 0.9600988 0.9537208 0.9539898
RF_pred  0.9600988 1.0000000 0.9936554 0.9851487
GBM_pred 0.9537208 0.9936554 1.0000000 0.9915265
XGB_pred 0.9539898 0.9851487 0.9915265 1.0000000

Automated machine learning

# Use AutoML to find a list of candidate models (i.e., leaderboard)
auto_ml <- h2o.automl(
  x = X, y = Y, training_frame = train_h2o, nfolds = 5, 
  max_runtime_secs = 60 * 120, max_models = 50,
  keep_cross_validation_predictions = TRUE, sort_metric = "RMSE", seed = 123,
  stopping_rounds = 50, stopping_metric = "RMSE", stopping_tolerance = 0
)
# Assess the leader board; the following truncates the results to show the top 
# 25 models. You can get the top model with auto_ml@leader
auto_ml@leaderboard %>% 
  as.data.frame() %>%
  dplyr::select(model_id, rmse) %>%
  dplyr::slice(1:25)
h2o.shutdown(prompt = FALSE)
[1] TRUE
LS0tCnRpdGxlOiAiQ2hhcHRlciAxNTogU3RhY2tlZCBNb2RlbHMiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCl9fTm90ZV9fOiBTb21lIHJlc3VsdHMgbWF5IGRpZmZlciBmcm9tIHRoZSBoYXJkIGNvcHkgYm9vayBkdWUgdG8gdGhlIGNoYW5naW5nIG9mIHNhbXBsaW5nIHByb2NlZHVyZXMgaW50cm9kdWNlZCBpbiBSIDMuNi4wLiBTZWUgaHR0cDovL2JpdC5seS8zNUQxU1c3IGZvciBtb3JlIGRldGFpbHMuIEFjY2VzcyBhbmQgcnVuIHRoZSBzb3VyY2UgY29kZSBmb3IgdGhpcyBub3RlYm9vayBbaGVyZV0oaHR0cHM6Ly9yc3R1ZGlvLmNsb3VkL3Byb2plY3QvODAxMTg1KS4gRG8gdG8gb3V0cHV0IHNpemUsIG1vc3Qgb2YgdGhpcwpjaGFwdGVyJ3MgY29kZSBjaHVua3Mgc2hvdWxkIG5vdCBiZSByYW4gb24gUlN0dWRpbyBDbG91ZC4KCkhpZGRlbiBjaGFwdGVyIHJlcXVpcmVtZW50cyB1c2VkIGluIHRoZSBib29rIHRvIHNldCB0aGUgcGxvdHRpbmcgdGhlbWUgYW5kIGxvYWQgcGFja2FnZXMgdXNlZCBpbiBoaWRkZW4gY29kZSBjaHVua3M6CgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KAogIG1lc3NhZ2UgPSBGQUxTRSwgCiAgd2FybmluZyA9IEZBTFNFLCAKICBjYWNoZSA9IEZBTFNFCikKYGBgCgojIyBQcmVyZXF1aXNpdGVzCgpUaGlzIGNoYXB0ZXIgbGV2ZXJhZ2VzIHRoZSBmb2xsb3dpbmcgcGFja2FnZXMsIHdpdGggdGhlIGVtcGhhc2lzIG9uIF9faDJvX186CgpgYGB7ciBwa2ctcmVxLTEyfQojIEhlbHBlciBwYWNrYWdlcwpsaWJyYXJ5KHJzYW1wbGUpICAgIyBmb3IgY3JlYXRpbmcgb3VyIHRyYWluLXRlc3Qgc3BsaXRzCmxpYnJhcnkocmVjaXBlcykgICAjIGZvciBtaW5vciBmZWF0dXJlIGVuZ2luZWVyaW5nIHRhc2tzCgojIE1vZGVsaW5nIHBhY2thZ2VzCmxpYnJhcnkoaDJvKSAgICAgICAjIGZvciBmaXR0aW5nIHN0YWNrZWQgbW9kZWxzCmBgYAoKYGBge3J9Cmgyby5ub19wcm9ncmVzcygpCmgyby5pbml0KCkKYGBgCgpUbyBpbGx1c3RyYXRlIGtleSBjb25jZXB0cyB3ZSBjb250aW51ZSB3aXRoIHRoZSBBbWVzIGhvdXNpbmcgZXhhbXBsZSBmcm9tIHByZXZpb3VzIGNoYXB0ZXJzOgoKYGBge3IgZGF0YS1yZXEtMTJ9CiMgTG9hZCBhbmQgc3BsaXQgdGhlIEFtZXMgaG91c2luZyBkYXRhCmFtZXMgPC0gQW1lc0hvdXNpbmc6Om1ha2VfYW1lcygpCnNldC5zZWVkKDEyMykgICMgZm9yIHJlcHJvZHVjaWJpbGl0eQpzcGxpdCA8LSBpbml0aWFsX3NwbGl0KGFtZXMsIHN0cmF0YSA9ICJTYWxlX1ByaWNlIikKYW1lc190cmFpbiA8LSB0cmFpbmluZyhzcGxpdCkKYW1lc190ZXN0IDwtIHRlc3Rpbmcoc3BsaXQpCgojIE1ha2Ugc3VyZSB3ZSBoYXZlIGNvbnNpc3RlbnQgY2F0ZWdvcmljYWwgbGV2ZWxzCmJsdWVwcmludCA8LSByZWNpcGUoU2FsZV9QcmljZSB+IC4sIGRhdGEgPSBhbWVzX3RyYWluKSAlPiUKICBzdGVwX290aGVyKGFsbF9ub21pbmFsKCksIHRocmVzaG9sZCA9IDAuMDUpCgojIENyZWF0ZSB0cmFpbmluZyAmIHRlc3Qgc2V0cyBmb3IgaDJvCnRyYWluX2gybyA8LSBwcmVwKGJsdWVwcmludCwgdHJhaW5pbmcgPSBhbWVzX3RyYWluLCByZXRhaW4gPSBUUlVFKSAlPiUKICBqdWljZSgpICU+JQogIGFzLmgybygpCnRlc3RfaDJvIDwtIHByZXAoYmx1ZXByaW50LCB0cmFpbmluZyA9IGFtZXNfdHJhaW4pICU+JQogIGJha2UobmV3X2RhdGEgPSBhbWVzX3Rlc3QpICU+JQogIGFzLmgybygpCgojIEdldCByZXNwb25zZSBhbmQgZmVhdHVyZSBuYW1lcwpZIDwtICJTYWxlX1ByaWNlIgpYIDwtIHNldGRpZmYobmFtZXMoYW1lc190cmFpbiksIFkpCmBgYAoKIyMgU3RhY2tpbmcgZXhpc3RpbmcgbW9kZWxzCgpgYGB7cn0KIyBUcmFpbiAmIGNyb3NzLXZhbGlkYXRlIGEgR0xNIG1vZGVsCmJlc3RfZ2xtIDwtIGgyby5nbG0oCiAgeCA9IFgsIHkgPSBZLCB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywgYWxwaGEgPSAwLjEsCiAgcmVtb3ZlX2NvbGxpbmVhcl9jb2x1bW5zID0gVFJVRSwgbmZvbGRzID0gMTAsIGZvbGRfYXNzaWdubWVudCA9ICJNb2R1bG8iLAogIGtlZXBfY3Jvc3NfdmFsaWRhdGlvbl9wcmVkaWN0aW9ucyA9IFRSVUUsIHNlZWQgPSAxMjMKKQoKIyBUcmFpbiAmIGNyb3NzLXZhbGlkYXRlIGEgUkYgbW9kZWwKYmVzdF9yZiA8LSBoMm8ucmFuZG9tRm9yZXN0KAogIHggPSBYLCB5ID0gWSwgdHJhaW5pbmdfZnJhbWUgPSB0cmFpbl9oMm8sIG50cmVlcyA9IDEwMDAsIG10cmllcyA9IDIwLAogIG1heF9kZXB0aCA9IDMwLCBtaW5fcm93cyA9IDEsIHNhbXBsZV9yYXRlID0gMC44LCBuZm9sZHMgPSAxMCwKICBmb2xkX2Fzc2lnbm1lbnQgPSAiTW9kdWxvIiwga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zID0gVFJVRSwKICBzZWVkID0gMTIzLCBzdG9wcGluZ19yb3VuZHMgPSA1MCwgc3RvcHBpbmdfbWV0cmljID0gIlJNU0UiLAogIHN0b3BwaW5nX3RvbGVyYW5jZSA9IDAKKQoKIyBUcmFpbiAmIGNyb3NzLXZhbGlkYXRlIGEgR0JNIG1vZGVsCmJlc3RfZ2JtIDwtIGgyby5nYm0oCiAgeCA9IFgsIHkgPSBZLCB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywgbnRyZWVzID0gNTAwMCwgbGVhcm5fcmF0ZSA9IDAuMDEsCiAgbWF4X2RlcHRoID0gNywgbWluX3Jvd3MgPSA1LCBzYW1wbGVfcmF0ZSA9IDAuOCwgbmZvbGRzID0gMTAsCiAgZm9sZF9hc3NpZ25tZW50ID0gIk1vZHVsbyIsIGtlZXBfY3Jvc3NfdmFsaWRhdGlvbl9wcmVkaWN0aW9ucyA9IFRSVUUsCiAgc2VlZCA9IDEyMywgc3RvcHBpbmdfcm91bmRzID0gNTAsIHN0b3BwaW5nX21ldHJpYyA9ICJSTVNFIiwKICBzdG9wcGluZ190b2xlcmFuY2UgPSAwCikKCiMgVHJhaW4gJiBjcm9zcy12YWxpZGF0ZSBhbiBYR0Jvb3N0IG1vZGVsCmJlc3RfeGdiIDwtIGgyby54Z2Jvb3N0KAogIHggPSBYLCB5ID0gWSwgdHJhaW5pbmdfZnJhbWUgPSB0cmFpbl9oMm8sIG50cmVlcyA9IDUwMDAsIGxlYXJuX3JhdGUgPSAwLjA1LAogIG1heF9kZXB0aCA9IDMsIG1pbl9yb3dzID0gMywgc2FtcGxlX3JhdGUgPSAwLjgsIGNhdGVnb3JpY2FsX2VuY29kaW5nID0gIkVudW0iLAogIG5mb2xkcyA9IDEwLCBmb2xkX2Fzc2lnbm1lbnQgPSAiTW9kdWxvIiwgCiAga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zID0gVFJVRSwgc2VlZCA9IDEyMywgc3RvcHBpbmdfcm91bmRzID0gNTAsCiAgc3RvcHBpbmdfbWV0cmljID0gIlJNU0UiLCBzdG9wcGluZ190b2xlcmFuY2UgPSAwCikKYGBgCgpgYGB7cn0KIyBUcmFpbiBhIHN0YWNrZWQgdHJlZSBlbnNlbWJsZQplbnNlbWJsZV90cmVlIDwtIGgyby5zdGFja2VkRW5zZW1ibGUoCiAgeCA9IFgsIHkgPSBZLCB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywgbW9kZWxfaWQgPSAibXlfdHJlZV9lbnNlbWJsZSIsCiAgYmFzZV9tb2RlbHMgPSBsaXN0KGJlc3RfZ2xtLCBiZXN0X3JmLCBiZXN0X2dibSwgYmVzdF94Z2IpLAogIG1ldGFsZWFybmVyX2FsZ29yaXRobSA9ICJkcmYiCikKYGBgCgpgYGB7cn0KIyBHZXQgcmVzdWx0cyBmcm9tIGJhc2UgbGVhcm5lcnMKZ2V0X3Jtc2UgPC0gZnVuY3Rpb24obW9kZWwpIHsKICByZXN1bHRzIDwtIGgyby5wZXJmb3JtYW5jZShtb2RlbCwgbmV3ZGF0YSA9IHRlc3RfaDJvKQogIHJlc3VsdHNAbWV0cmljcyRSTVNFCn0KCmxpc3QoYmVzdF9nbG0sIGJlc3RfcmYsIGJlc3RfZ2JtLCBiZXN0X3hnYikgJT4lCiAgcHVycnI6Om1hcF9kYmwoZ2V0X3Jtc2UpCgojIFN0YWNrZWQgcmVzdWx0cwpoMm8ucGVyZm9ybWFuY2UoZW5zZW1ibGVfdHJlZSwgbmV3ZGF0YSA9IHRlc3RfaDJvKUBtZXRyaWNzJFJNU0UKYGBgCgpgYGB7cn0KZGF0YS5mcmFtZSgKICBHTE1fcHJlZCA9IGFzLnZlY3RvcihoMm8uZ2V0RnJhbWUoYmVzdF9nbG1AbW9kZWwkY3Jvc3NfdmFsaWRhdGlvbl9ob2xkb3V0X3ByZWRpY3Rpb25zX2ZyYW1lX2lkJG5hbWUpKSwKICBSRl9wcmVkID0gYXMudmVjdG9yKGgyby5nZXRGcmFtZShiZXN0X3JmQG1vZGVsJGNyb3NzX3ZhbGlkYXRpb25faG9sZG91dF9wcmVkaWN0aW9uc19mcmFtZV9pZCRuYW1lKSksCiAgR0JNX3ByZWQgPSBhcy52ZWN0b3IoaDJvLmdldEZyYW1lKGJlc3RfZ2JtQG1vZGVsJGNyb3NzX3ZhbGlkYXRpb25faG9sZG91dF9wcmVkaWN0aW9uc19mcmFtZV9pZCRuYW1lKSksCiAgWEdCX3ByZWQgPSBhcy52ZWN0b3IoaDJvLmdldEZyYW1lKGJlc3RfeGdiQG1vZGVsJGNyb3NzX3ZhbGlkYXRpb25faG9sZG91dF9wcmVkaWN0aW9uc19mcmFtZV9pZCRuYW1lKSkKKSAlPiUgY29yKCkKYGBgCgojIyBTdGFja2luZyBhIGdyaWQgc2VhcmNoCgpgYGB7cn0KIyBEZWZpbmUgR0JNIGh5cGVycGFyYW1ldGVyIGdyaWQKaHlwZXJfZ3JpZCA8LSBsaXN0KAogIG1heF9kZXB0aCA9IGMoMSwgMywgNSksCiAgbWluX3Jvd3MgPSBjKDEsIDUsIDEwKSwKICBsZWFybl9yYXRlID0gYygwLjAxLCAwLjA1LCAwLjEpLAogIGxlYXJuX3JhdGVfYW5uZWFsaW5nID0gYygwLjk5LCAxKSwKICBzYW1wbGVfcmF0ZSA9IGMoMC41LCAwLjc1LCAxKSwKICBjb2xfc2FtcGxlX3JhdGUgPSBjKDAuOCwgMC45LCAxKQopCgojIERlZmluZSByYW5kb20gZ3JpZCBzZWFyY2ggY3JpdGVyaWEKc2VhcmNoX2NyaXRlcmlhIDwtIGxpc3QoCiAgc3RyYXRlZ3kgPSAiUmFuZG9tRGlzY3JldGUiLAogIG1heF9tb2RlbHMgPSAyNQopCgojIEJ1aWxkIHJhbmRvbSBncmlkIHNlYXJjaCAKcmFuZG9tX2dyaWQgPC0gaDJvLmdyaWQoCiAgYWxnb3JpdGhtID0gImdibSIsIGdyaWRfaWQgPSAiZ2JtX2dyaWQiLCB4ID0gWCwgeSA9IFksCiAgdHJhaW5pbmdfZnJhbWUgPSB0cmFpbl9oMm8sIGh5cGVyX3BhcmFtcyA9IGh5cGVyX2dyaWQsCiAgc2VhcmNoX2NyaXRlcmlhID0gc2VhcmNoX2NyaXRlcmlhLCBudHJlZXMgPSA1MDAwLCBzdG9wcGluZ19tZXRyaWMgPSAiUk1TRSIsICAgICAKICBzdG9wcGluZ19yb3VuZHMgPSAxMCwgc3RvcHBpbmdfdG9sZXJhbmNlID0gMCwgbmZvbGRzID0gMTAsIAogIGZvbGRfYXNzaWdubWVudCA9ICJNb2R1bG8iLCBrZWVwX2Nyb3NzX3ZhbGlkYXRpb25fcHJlZGljdGlvbnMgPSBUUlVFLAogIHNlZWQgPSAxMjMKKQpgYGAKCmBgYHtyfQojIFNvcnQgcmVzdWx0cyBieSBSTVNFCmgyby5nZXRHcmlkKAogIGdyaWRfaWQgPSAiZ2JtX2dyaWQiLCAKICBzb3J0X2J5ID0gInJtc2UiCikKYGBgCgpgYGB7cn0KIyBHcmFiIHRoZSBtb2RlbF9pZCBmb3IgdGhlIHRvcCBtb2RlbCwgY2hvc2VuIGJ5IHZhbGlkYXRpb24gZXJyb3IKYmVzdF9tb2RlbF9pZCA8LSByYW5kb21fZ3JpZEBtb2RlbF9pZHNbWzFdXQpiZXN0X21vZGVsIDwtIGgyby5nZXRNb2RlbChiZXN0X21vZGVsX2lkKQpoMm8ucGVyZm9ybWFuY2UoYmVzdF9tb2RlbCwgbmV3ZGF0YSA9IHRlc3RfaDJvKQpgYGAKCmBgYHtyfQojIFRyYWluIGEgc3RhY2tlZCBlbnNlbWJsZSB1c2luZyB0aGUgR0JNIGdyaWQKZW5zZW1ibGUgPC0gaDJvLnN0YWNrZWRFbnNlbWJsZSgKICB4ID0gWCwgeSA9IFksIHRyYWluaW5nX2ZyYW1lID0gdHJhaW5faDJvLCBtb2RlbF9pZCA9ICJlbnNlbWJsZV9nYm1fZ3JpZCIsCiAgYmFzZV9tb2RlbHMgPSByYW5kb21fZ3JpZEBtb2RlbF9pZHMsIG1ldGFsZWFybmVyX2FsZ29yaXRobSA9ICJnYm0iCikKCiMgRXZhbCBlbnNlbWJsZSBwZXJmb3JtYW5jZSBvbiBhIHRlc3Qgc2V0Cmgyby5wZXJmb3JtYW5jZShlbnNlbWJsZSwgbmV3ZGF0YSA9IHRlc3RfaDJvKQpgYGAKCiMjIEF1dG9tYXRlZCBtYWNoaW5lIGxlYXJuaW5nCgpgYGB7cn0KIyBVc2UgQXV0b01MIHRvIGZpbmQgYSBsaXN0IG9mIGNhbmRpZGF0ZSBtb2RlbHMgKGkuZS4sIGxlYWRlcmJvYXJkKQphdXRvX21sIDwtIGgyby5hdXRvbWwoCiAgeCA9IFgsIHkgPSBZLCB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2gybywgbmZvbGRzID0gNSwgCiAgbWF4X3J1bnRpbWVfc2VjcyA9IDYwICogMTIwLCBtYXhfbW9kZWxzID0gNTAsCiAga2VlcF9jcm9zc192YWxpZGF0aW9uX3ByZWRpY3Rpb25zID0gVFJVRSwgc29ydF9tZXRyaWMgPSAiUk1TRSIsIHNlZWQgPSAxMjMsCiAgc3RvcHBpbmdfcm91bmRzID0gNTAsIHN0b3BwaW5nX21ldHJpYyA9ICJSTVNFIiwgc3RvcHBpbmdfdG9sZXJhbmNlID0gMAopCmBgYAoKYGBge3J9CiMgQXNzZXNzIHRoZSBsZWFkZXIgYm9hcmQ7IHRoZSBmb2xsb3dpbmcgdHJ1bmNhdGVzIHRoZSByZXN1bHRzIHRvIHNob3cgdGhlIHRvcCAKIyAyNSBtb2RlbHMuIFlvdSBjYW4gZ2V0IHRoZSB0b3AgbW9kZWwgd2l0aCBhdXRvX21sQGxlYWRlcgphdXRvX21sQGxlYWRlcmJvYXJkICU+JSAKICBhcy5kYXRhLmZyYW1lKCkgJT4lCiAgZHBseXI6OnNlbGVjdChtb2RlbF9pZCwgcm1zZSkgJT4lCiAgZHBseXI6OnNsaWNlKDE6MjUpCmBgYAoKYGBge3J9Cmgyby5zaHV0ZG93bihwcm9tcHQgPSBGQUxTRSkKYGBgCgo=