In this notebook, we are going to continue using our Ames Housing regression data focused on predicting home sales prices. However, our focus in this notebook is to illustrate:
- Variability in model performance will exists for two reasons
- How to apply different validation procedures
Package requirements
library(keras) # for deep learning
library(testthat) # unit testing
library(tidyverse) # for dplyr, ggplot2, etc.
library(rsample) # for data splitting
library(recipes) # for feature engineering
The Ames housing dataset
For this case study we will use the Ames housing dataset provided by the AmesHousing package.
ames <- AmesHousing::make_ames()
dim(ames)
[1] 2930 81
Create train & test splits
Let’s create our own training and testing samples, which we can do with the rsample package.
set.seed(123)
ames_split <- initial_split(ames, prop = 0.7)
ames_train <- analysis(ames_split)
ames_test <- assessment(ames_split)
dim(ames_train)
[1] 2051 81
dim(ames_test)
[1] 879 81
Preparing the data
The first thing we need to do is prepare our data by:
- removing any zero-variance (or near zero-variance) features
- condensing unique levels of categorical features to “other”
- ordinal encoding the quality features
- normalize numeric feature distributions
- standardizing numeric features to mean = 0, std dev = 1
- one-hot encoding remaining categorical features
This is the same procedure we used in the first case study.
blueprint <- recipe(Sale_Price ~ ., data = ames_train) %>%
step_nzv(all_nominal()) %>%
step_other(all_nominal(), threshold = .01, other = "other") %>%
step_integer(matches("(Qual|Cond|QC|Qu)$")) %>%
step_YeoJohnson(all_numeric(), -all_outcomes()) %>%
step_center(all_numeric(), -all_outcomes()) %>%
step_scale(all_numeric(), -all_outcomes()) %>%
step_dummy(all_nominal(), -all_outcomes(), one_hot = TRUE)
prepare <- prep(blueprint, training = ames_train)
baked_train <- bake(prepare, new_data = ames_train)
baked_test <- bake(prepare, new_data = ames_test)
# unit testing to ensure all columns are numeric
expect_equal(map_lgl(baked_train, ~ !is.numeric(.)) %>% sum(), 0)
expect_equal(map_lgl(baked_test, ~ !is.numeric(.)) %>% sum(), 0)
baked_train
Next, we create our features and labels dataset for training and testing purposes.
x_train <- select(baked_train, -Sale_Price) %>% as.matrix()
y_train <- baked_train %>% pull(Sale_Price)
x_test <- select(baked_test, -Sale_Price) %>% as.matrix()
y_test <- baked_test %>% pull(Sale_Price)
# unit testing to x & y tensors have same number of observations
expect_equal(nrow(x_train), length(y_train))
expect_equal(nrow(x_test), length(y_test))
Our final feature set now has 188 input variables:
dim(x_train)
[1] 2051 188
dim(x_test)
[1] 879 188
Two identical models
Let’s create two models that have the exact same architecture, compilation, and training attributes:
# First model
model1_results <- keras_model_sequential() %>%
layer_dense(units = 1024, activation = "relu", input_shape = ncol(x_train)) %>%
layer_dense(units = 512, activation = "relu") %>%
layer_dense(units = 1) %>%
compile(
optimizer = optimizer_rmsprop(lr = 0.01),
loss = "msle",
metrics = c("mae")
) %>%
fit(
x_train,
y_train,
batch_size = 32,
epochs = 50,
validation_split = 0.2, # supply our validation data
callbacks = list(
callback_early_stopping(patience = 10, restore_best_weights = TRUE),
callback_reduce_lr_on_plateau(factor = 0.2, patience = 4)
)
)
# Second model
model2_results <- keras_model_sequential() %>%
layer_dense(units = 1024, activation = "relu", input_shape = ncol(x_train)) %>%
layer_dense(units = 512, activation = "relu") %>%
layer_dense(units = 1) %>%
compile(
optimizer = optimizer_rmsprop(lr = 0.01),
loss = "msle",
metrics = c("mae")
) %>%
fit(
x_train,
y_train,
batch_size = 32,
epochs = 50,
validation_split = 0.2, # supply our validation data
callbacks = list(
callback_early_stopping(patience = 10, restore_best_weights = TRUE),
callback_reduce_lr_on_plateau(factor = 0.2, patience = 4)
)
)
You will notice that our results slightly differ. This is because we have variability within our model.
# Model 1 results
model1_results
Trained on 1,640 samples (batch_size=32, epochs=20)
Final epoch (plot to see history):
loss: 0.01198
mae: 12,824
val_loss: 0.01364
val_mae: 14,788
lr: 0.00008
# Model 2 results
model2_results
Trained on 1,640 samples (batch_size=32, epochs=22)
Final epoch (plot to see history):
loss: 0.01029
mae: 12,280
val_loss: 0.01418
val_mae: 14,682
lr: 0.0004
This variability is a result of our model weights being randomly initialized. And since the weights in our two models have different starting points, the gradient descent process will result in the final weights differing as well. For larger datasets, the variability in your final results will often be negligible.
However, for smaller datasets, this variability can be greater and can also lead to skewed inferences. Typically, for tabular datasets less than 10,000 observations, I will often perform k-fold cross validation to have a more robust understanding of variability in the loss score. ℹ️.
Validation procedures
To demonstrate how to perform k-fold cross validation, let’s first discuss another way to perform validation within the keras::fit()
function. So far we have performed model validation by using validation_split
. Sometimes this may not be appropriate. validation_split
selects the last XX% samples in the x and y data provided. So, if our data is ordered than this could skew our results.
An alternative is to create our own validation data and supply it via validation_data
. First we extract our own train vs. validation data sets:
set.seed(123)
index <- sample(1:nrow(x_train), size = floor(nrow(x_train) * 0.8))
x_train_sub <- x_train[index,]
y_train_sub <- y_train[index]
x_val <- x_train[-index,]
y_val <- y_train[-index]
length(y_train_sub)
[1] 1640
length(y_val)
[1] 411
Now, we can supply our validation data to validation_data
. Note how we supply our validation features and labels datasets as a list to validation_data
.
network <- keras_model_sequential() %>%
layer_dense(units = 1024, activation = "relu", input_shape = ncol(x_train)) %>%
layer_dense(units = 512, activation = "relu") %>%
layer_dense(units = 1) %>%
compile(
optimizer = optimizer_rmsprop(lr = 0.01),
loss = "msle",
metrics = c("mae")
)
history <- network %>% fit(
x_train_sub, # supply our new training features data
y_train_sub, # supply our new training labels data
epochs = 50,
batch_size = 32,
validation_data = list(x_val, y_val), # supply our validation data
callbacks = list(
callback_early_stopping(patience = 10, restore_best_weights = FALSE),
callback_reduce_lr_on_plateau(factor = 0.2, patience = 5)
)
)
history
Trained on 1,640 samples (batch_size=32, epochs=30)
Final epoch (plot to see history):
loss: 0.007029
mae: 10,360
val_loss: 0.02284
val_mae: 14,432
lr: 0.0004
k-fold cross validation
As the number of observations in our data increases, variance in our loss score will decrease. However, we do not always have the option to just go out and get more data. So, if we want to gain a more accurate understanding of the loss score and its variance we can perform k-fold cross validation.
First, we need to create k folds. This example creates 10 folds by dividing the randomly sampled index into 10 approximately equal “cuts”. Consequently, folds
in this example is simply a vector that is equal length to our training observations; stating that observation 1 is assigned to the 10th fold, observation 2 is assigned to the 3rd fold, observation 3 is assigned to the 1st fold, etc.
# number of folds
k <- 10
# randomize data before making folds
set.seed(123)
indices <- sample(1:nrow(x_train))
# divide the ordered indices into k intervals, labeled 1:k.
folds <- cut(indices, breaks = k, labels = FALSE)
str(folds)
int [1:2051] 3 1 9 6 7 7 7 6 7 4 ...
If we look at all the folds, we’ll see that we have nearly equal number of observations across all folds:
table(folds)
folds
1 2 3 4 5 6 7 8 9 10
206 205 205 205 205 205 205 205 205 205
Now we can apply a for
loop to iterate through the training data and perform k-fold cross validation. This works by:
- Assigning fold
i
to the validation set and the remaining folds to the training set,
- Training our model using
validation_data
to supply our validation set,
- Save our results for that iteration,
- Repeat for all 10 folds.
As this code executes, the minimum validation loss score will be printing out for each fold and you will see the variability across the folds.
# create a data frame to store results
results <- data.frame()
for (i in seq_len(k)) {
cat("processing fold", paste0(i, ": "))
# Prepare the training and validation data for each fold
val_indices <- which(folds == i, arr.ind = TRUE)
# validation set: the ith partition
x_val <- x_train[val_indices,]
y_val <- y_train[val_indices]
# Training set: all other partitions
x_train_sub <- x_train[-val_indices,]
y_train_sub <- y_train[-val_indices]
# Create our model blueprint
network <- keras_model_sequential() %>%
layer_dense(units = 1024, activation = "relu", input_shape = ncol(x_train)) %>%
layer_dense(units = 512, activation = "relu") %>%
layer_dense(units = 1) %>%
compile(
optimizer = optimizer_rmsprop(lr = 0.01),
loss = "msle",
metrics = c("mae")
)
# Train our model with and supply train / validation data
history <- network %>% fit(
x_train_sub,
y_train_sub,
epochs = 50,
batch_size = 32,
validation_data = list(x_val, y_val),
verbose = FALSE,
callbacks = callback_reduce_lr_on_plateau(factor = 0.2, patience = 5)
)
# Extract the performance data
model_performance <- as.data.frame(history) %>% mutate(fold = i)
results <- rbind(results, model_performance)
# append loop message with min loss for ith fold
min_loss <- round(min(history$metrics$val_loss), 4)
cat(min_loss, "\n", append = TRUE)
}
processing fold 1: 0.0372
processing fold 2: 0.025
processing fold 3: 0.0158
processing fold 4: 0.0136
processing fold 5: 0.027
processing fold 6: 0.0162
processing fold 7: 0.0159
processing fold 8: 0.0132
processing fold 9: 0.0136
processing fold 10: 0.0155
We can plot the results; however, the difference between each folds validation loss score is not obvious.
ggplot(results, aes(epoch, value, color = data)) +
geom_point(alpha = 0.5) +
geom_smooth() +
facet_wrap(~ metric, ncol = 1, scales = "free_y")
But if we zoom in on the validation loss we can see the variance that exists:
results %>%
filter(data == 'validation', metric == 'loss') %>%
ggplot(aes(epoch, value)) +
geom_point(alpha = 0.5) +
stat_summary(fun.data = "mean_cl_boot", colour = "red") +
geom_smooth() +
scale_y_log10()
If we pick the epoch with the lowest average validation loss, we can see that our validation loss is about…
# which epic has lowest avg loss
best_epoch<- results %>%
group_by(epoch) %>%
filter(metric == 'loss', data == 'validation') %>%
summarise(avg_loss = mean(value),
std_loss = sd(value)) %>%
top_n(-1, wt = avg_loss)
best_epoch
If we re-train our model and use the best epoch, we should see similar results within reason when scoring on new data:
network <- keras_model_sequential() %>%
layer_dense(units = 1024, activation = "relu", input_shape = ncol(x_train)) %>%
layer_dense(units = 512, activation = "relu") %>%
layer_dense(units = 1) %>%
compile(
optimizer = optimizer_rmsprop(lr = 0.01),
loss = "msle",
metrics = c("mae")
)
history <- network %>% fit(
x_train,
y_train,
epochs = best_epoch$epoch,
batch_size = 32,
validation_split = 0.2,
callbacks = callback_reduce_lr_on_plateau(factor = 0.2, patience = 5),
verbose = FALSE
)
network %>% evaluate(x_test, y_test, verbose = FALSE)
$loss
[1] 0.01642946
$mae
[1] 15433.96
LS0tCnRpdGxlOiAiRGlmZmVyZW50IHZhbGlkYXRpb24gcHJvY2VkdXJlcyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkKZ2dwbG90Mjo6dGhlbWVfc2V0KGdncGxvdDI6OnRoZW1lX21pbmltYWwoKSkKYGBgCgpJbiB0aGlzIG5vdGVib29rLCB3ZSBhcmUgZ29pbmcgdG8gY29udGludWUgdXNpbmcgb3VyIEFtZXMgSG91c2luZyByZWdyZXNzaW9uCmRhdGEgZm9jdXNlZCBvbiBwcmVkaWN0aW5nIGhvbWUgc2FsZXMgcHJpY2VzLiBIb3dldmVyLCBvdXIgZm9jdXMgaW4gdGhpcwpub3RlYm9vayBpcyB0byBpbGx1c3RyYXRlOgoKKiBWYXJpYWJpbGl0eSBpbiBtb2RlbCBwZXJmb3JtYW5jZSB3aWxsIGV4aXN0cyBmb3IgdHdvIHJlYXNvbnMKKiBIb3cgdG8gYXBwbHkgZGlmZmVyZW50IHZhbGlkYXRpb24gcHJvY2VkdXJlcyAKCiMgUGFja2FnZSByZXF1aXJlbWVudHMKCmBgYHtyIGxvYWQtcGtncywgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShrZXJhcykgICAgICMgZm9yIGRlZXAgbGVhcm5pbmcKbGlicmFyeSh0ZXN0dGhhdCkgICMgdW5pdCB0ZXN0aW5nCmxpYnJhcnkodGlkeXZlcnNlKSAjIGZvciBkcGx5ciwgZ2dwbG90MiwgZXRjLgpsaWJyYXJ5KHJzYW1wbGUpICAgIyBmb3IgZGF0YSBzcGxpdHRpbmcKbGlicmFyeShyZWNpcGVzKSAgICMgZm9yIGZlYXR1cmUgZW5naW5lZXJpbmcKYGBgCgoKIyBUaGUgQW1lcyBob3VzaW5nIGRhdGFzZXQKCkZvciB0aGlzIGNhc2Ugc3R1ZHkgd2Ugd2lsbCB1c2UgdGhlIFtBbWVzIGhvdXNpbmcgZGF0YXNldF0oaHR0cDovL2pzZS5hbXN0YXQub3JnL3YxOW4zL2RlY29jay5wZGYpIApwcm92aWRlZCBieSB0aGUgX19BbWVzSG91c2luZ19fIHBhY2thZ2UuCgpgYGB7ciBnZXQtZGF0YSwgd2FybmluZz1GQUxTRX0KYW1lcyA8LSBBbWVzSG91c2luZzo6bWFrZV9hbWVzKCkKZGltKGFtZXMpCmBgYAoKIyBDcmVhdGUgdHJhaW4gJiB0ZXN0IHNwbGl0cwoKTGV0J3MgY3JlYXRlIG91ciBvd24gdHJhaW5pbmcgYW5kIHRlc3Rpbmcgc2FtcGxlcywgd2hpY2ggd2UgY2FuIGRvIHdpdGggdGhlIApyc2FtcGxlIHBhY2thZ2UuCgpgYGB7cn0Kc2V0LnNlZWQoMTIzKQphbWVzX3NwbGl0IDwtIGluaXRpYWxfc3BsaXQoYW1lcywgcHJvcCA9IDAuNykKYW1lc190cmFpbiA8LSBhbmFseXNpcyhhbWVzX3NwbGl0KQphbWVzX3Rlc3QgPC0gYXNzZXNzbWVudChhbWVzX3NwbGl0KQoKZGltKGFtZXNfdHJhaW4pCmRpbShhbWVzX3Rlc3QpCmBgYAoKCiMgUHJlcGFyaW5nIHRoZSBkYXRhCgpUaGUgZmlyc3QgdGhpbmcgd2UgbmVlZCB0byBkbyBpcyBwcmVwYXJlIG91ciBkYXRhIGJ5OgoKLSByZW1vdmluZyBhbnkgemVyby12YXJpYW5jZSAob3IgbmVhciB6ZXJvLXZhcmlhbmNlKSBmZWF0dXJlcwotIGNvbmRlbnNpbmcgdW5pcXVlIGxldmVscyBvZiBjYXRlZ29yaWNhbCBmZWF0dXJlcyB0byAib3RoZXIiCi0gb3JkaW5hbCBlbmNvZGluZyB0aGUgcXVhbGl0eSBmZWF0dXJlcwotIG5vcm1hbGl6ZSBudW1lcmljIGZlYXR1cmUgZGlzdHJpYnV0aW9ucwotIHN0YW5kYXJkaXppbmcgbnVtZXJpYyBmZWF0dXJlcyB0byBtZWFuID0gMCwgc3RkIGRldiA9IDEKLSBvbmUtaG90IGVuY29kaW5nIHJlbWFpbmluZyBjYXRlZ29yaWNhbCBmZWF0dXJlcwoKVGhpcyBpcyB0aGUgc2FtZSBwcm9jZWR1cmUgd2UgdXNlZCBpbiB0aGUgW2ZpcnN0IGNhc2Ugc3R1ZHldKGh0dHBzOi8vcnN0dWRpby1jb25mLTIwMjAuZ2l0aHViLmlvL2RsLWtlcmFzLXRmL25vdGVib29rcy8wMS1hbWVzLm5iLmh0bWwpLgoKYGBge3J9CmJsdWVwcmludCA8LSByZWNpcGUoU2FsZV9QcmljZSB+IC4sIGRhdGEgPSBhbWVzX3RyYWluKSAlPiUKICBzdGVwX256dihhbGxfbm9taW5hbCgpKSAlPiUKICBzdGVwX290aGVyKGFsbF9ub21pbmFsKCksIHRocmVzaG9sZCA9IC4wMSwgb3RoZXIgPSAib3RoZXIiKSAlPiUKICBzdGVwX2ludGVnZXIobWF0Y2hlcygiKFF1YWx8Q29uZHxRQ3xRdSkkIikpICU+JQogIHN0ZXBfWWVvSm9obnNvbihhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lCiAgc3RlcF9zY2FsZShhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwgLWFsbF9vdXRjb21lcygpLCBvbmVfaG90ID0gVFJVRSkKCnByZXBhcmUgPC0gcHJlcChibHVlcHJpbnQsIHRyYWluaW5nID0gYW1lc190cmFpbikKCmJha2VkX3RyYWluIDwtIGJha2UocHJlcGFyZSwgbmV3X2RhdGEgPSBhbWVzX3RyYWluKQpiYWtlZF90ZXN0IDwtIGJha2UocHJlcGFyZSwgbmV3X2RhdGEgPSBhbWVzX3Rlc3QpCgojIHVuaXQgdGVzdGluZyB0byBlbnN1cmUgYWxsIGNvbHVtbnMgYXJlIG51bWVyaWMKZXhwZWN0X2VxdWFsKG1hcF9sZ2woYmFrZWRfdHJhaW4sIH4gIWlzLm51bWVyaWMoLikpICU+JSBzdW0oKSwgMCkKZXhwZWN0X2VxdWFsKG1hcF9sZ2woYmFrZWRfdGVzdCwgfiAhaXMubnVtZXJpYyguKSkgJT4lIHN1bSgpLCAwKQoKYmFrZWRfdHJhaW4KYGBgCgpOZXh0LCB3ZSBjcmVhdGUgb3VyIGZlYXR1cmVzIGFuZCBsYWJlbHMgZGF0YXNldCBmb3IgdHJhaW5pbmcgYW5kIHRlc3RpbmcKcHVycG9zZXMuCgpgYGB7cn0KeF90cmFpbiA8LSBzZWxlY3QoYmFrZWRfdHJhaW4sIC1TYWxlX1ByaWNlKSAlPiUgYXMubWF0cml4KCkKeV90cmFpbiA8LSBiYWtlZF90cmFpbiAlPiUgcHVsbChTYWxlX1ByaWNlKQoKeF90ZXN0IDwtIHNlbGVjdChiYWtlZF90ZXN0LCAtU2FsZV9QcmljZSkgJT4lIGFzLm1hdHJpeCgpCnlfdGVzdCA8LSBiYWtlZF90ZXN0ICU+JSBwdWxsKFNhbGVfUHJpY2UpCgojIHVuaXQgdGVzdGluZyB0byB4ICYgeSB0ZW5zb3JzIGhhdmUgc2FtZSBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zCmV4cGVjdF9lcXVhbChucm93KHhfdHJhaW4pLCBsZW5ndGgoeV90cmFpbikpCmV4cGVjdF9lcXVhbChucm93KHhfdGVzdCksIGxlbmd0aCh5X3Rlc3QpKQpgYGAKCk91ciBmaW5hbCBmZWF0dXJlIHNldCBub3cgaGFzIDE4OCBpbnB1dCB2YXJpYWJsZXM6CgpgYGB7cn0KZGltKHhfdHJhaW4pCmRpbSh4X3Rlc3QpCmBgYAoKIyBUd28gaWRlbnRpY2FsIG1vZGVscwoKTGV0J3MgY3JlYXRlIHR3byBtb2RlbHMgdGhhdCBoYXZlIHRoZSBleGFjdCBzYW1lIGFyY2hpdGVjdHVyZSwgY29tcGlsYXRpb24sIGFuZAp0cmFpbmluZyBhdHRyaWJ1dGVzOgoKYGBge3J9CiMgRmlyc3QgbW9kZWwKbW9kZWwxX3Jlc3VsdHMgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEpICU+JSAKICBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjAxKSwKICAgIGxvc3MgPSAibXNsZSIsCiAgICBtZXRyaWNzID0gYygibWFlIikKICApICU+JSAKICBmaXQoCiAgICB4X3RyYWluLAogICAgeV90cmFpbiwKICAgIGJhdGNoX3NpemUgPSAzMiwKICAgIGVwb2NocyA9IDUwLAogICAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwgICMgc3VwcGx5IG91ciB2YWxpZGF0aW9uIGRhdGEKICAgIGNhbGxiYWNrcyA9IGxpc3QoCiAgICAgICAgICBjYWxsYmFja19lYXJseV9zdG9wcGluZyhwYXRpZW5jZSA9IDEwLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUpLAogICAgICAgICAgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUoZmFjdG9yID0gMC4yLCBwYXRpZW5jZSA9IDQpCiAgICAgICkKKQoKIyBTZWNvbmQgbW9kZWwKbW9kZWwyX3Jlc3VsdHMgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEpICU+JSAKICBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjAxKSwKICAgIGxvc3MgPSAibXNsZSIsCiAgICBtZXRyaWNzID0gYygibWFlIikKICApICU+JSAKICBmaXQoCiAgICB4X3RyYWluLAogICAgeV90cmFpbiwKICAgIGJhdGNoX3NpemUgPSAzMiwKICAgIGVwb2NocyA9IDUwLAogICAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwgICMgc3VwcGx5IG91ciB2YWxpZGF0aW9uIGRhdGEKICAgIGNhbGxiYWNrcyA9IGxpc3QoCiAgICAgICAgICBjYWxsYmFja19lYXJseV9zdG9wcGluZyhwYXRpZW5jZSA9IDEwLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUpLAogICAgICAgICAgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUoZmFjdG9yID0gMC4yLCBwYXRpZW5jZSA9IDQpCiAgICAgICkKKQpgYGAKCllvdSB3aWxsIG5vdGljZSB0aGF0IG91ciByZXN1bHRzIHNsaWdodGx5IGRpZmZlci4gVGhpcyBpcyBiZWNhdXNlIHdlIGhhdmUgCnZhcmlhYmlsaXR5IHdpdGhpbiBvdXIgbW9kZWwuIAoKYGBge3J9CiMgTW9kZWwgMSByZXN1bHRzCm1vZGVsMV9yZXN1bHRzCgojIE1vZGVsIDIgcmVzdWx0cwptb2RlbDJfcmVzdWx0cwpgYGAKClRoaXMgdmFyaWFiaWxpdHkgaXMgYSByZXN1bHQgb2Ygb3VyIG1vZGVsIHdlaWdodHMgYmVpbmcgcmFuZG9tbHkgaW5pdGlhbGl6ZWQuIApBbmQgc2luY2UgdGhlIHdlaWdodHMgaW4gb3VyIHR3byBtb2RlbHMgaGF2ZSBkaWZmZXJlbnQgc3RhcnRpbmcgcG9pbnRzLCB0aGUgCmdyYWRpZW50IGRlc2NlbnQgcHJvY2VzcyB3aWxsIHJlc3VsdCBpbiB0aGUgZmluYWwgd2VpZ2h0cyBkaWZmZXJpbmcgYXMgd2VsbC4gRm9yCmxhcmdlciBkYXRhc2V0cywgdGhlIHZhcmlhYmlsaXR5IGluIHlvdXIgZmluYWwgcmVzdWx0cyB3aWxsIG9mdGVuIGJlIG5lZ2xpZ2libGUuCgpIb3dldmVyLCBmb3Igc21hbGxlciBkYXRhc2V0cywgdGhpcyB2YXJpYWJpbGl0eSBjYW4gYmUgZ3JlYXRlciBhbmQgY2FuIGFsc28gbGVhZAp0byBza2V3ZWQgaW5mZXJlbmNlcy4gVHlwaWNhbGx5LCBmb3IgdGFidWxhciBkYXRhc2V0cyBsZXNzIHRoYW4gMTAsMDAwIG9ic2VydmF0aW9ucywKSSB3aWxsIG9mdGVuIHBlcmZvcm0gay1mb2xkIGNyb3NzIHZhbGlkYXRpb24gdG8gaGF2ZSBhIG1vcmUgcm9idXN0IHVuZGVyc3RhbmRpbmcgCm9mIHZhcmlhYmlsaXR5IGluIHRoZSBsb3NzIHNjb3JlLiBb4oS577iPXShodHRwczovL2JyYWRsZXlib2VobWtlLmdpdGh1Yi5pby9IT01ML3Byb2Nlc3MuaHRtbCNyZXNhbXBsaW5nKS4KCiMgVmFsaWRhdGlvbiBwcm9jZWR1cmVzCgpUbyBkZW1vbnN0cmF0ZSBob3cgdG8gcGVyZm9ybSBrLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiwgbGV0J3MgZmlyc3QgZGlzY3VzcyAKYW5vdGhlciB3YXkgdG8gcGVyZm9ybSB2YWxpZGF0aW9uIHdpdGhpbiB0aGUgYGtlcmFzOjpmaXQoKWAgZnVuY3Rpb24uIFNvIGZhciB3ZSAKaGF2ZSBwZXJmb3JtZWQgbW9kZWwgdmFsaWRhdGlvbiBieSB1c2luZyBgdmFsaWRhdGlvbl9zcGxpdGAuIFNvbWV0aW1lcyB0aGlzIG1heSAKbm90IGJlIGFwcHJvcHJpYXRlLiBgdmFsaWRhdGlvbl9zcGxpdGAgc2VsZWN0cyB0aGUgbGFzdCBYWCUgc2FtcGxlcyBpbiB0aGUgeCBhbmQgCnkgZGF0YSBwcm92aWRlZC4gU28sIGlmIG91ciBkYXRhIGlzIG9yZGVyZWQgdGhhbiB0aGlzIGNvdWxkIHNrZXcgb3VyIHJlc3VsdHMuICAKCkFuIGFsdGVybmF0aXZlIGlzIHRvIGNyZWF0ZSBvdXIgb3duIHZhbGlkYXRpb24gZGF0YSBhbmQgc3VwcGx5IGl0IHZpYSAKYHZhbGlkYXRpb25fZGF0YWAuIEZpcnN0IHdlIGV4dHJhY3Qgb3VyIG93biB0cmFpbiB2cy4gdmFsaWRhdGlvbiBkYXRhIHNldHM6CgpgYGB7ciBjcmVhdGUtdmFsaWRhdGlvbn0Kc2V0LnNlZWQoMTIzKQppbmRleCA8LSBzYW1wbGUoMTpucm93KHhfdHJhaW4pLCBzaXplID0gZmxvb3IobnJvdyh4X3RyYWluKSAqIDAuOCkpCgp4X3RyYWluX3N1YiA8LSB4X3RyYWluW2luZGV4LF0KeV90cmFpbl9zdWIgPC0geV90cmFpbltpbmRleF0KCnhfdmFsIDwtIHhfdHJhaW5bLWluZGV4LF0KeV92YWwgPC0geV90cmFpblstaW5kZXhdCgpsZW5ndGgoeV90cmFpbl9zdWIpCmxlbmd0aCh5X3ZhbCkKYGBgCgpOb3csIHdlIGNhbiBzdXBwbHkgb3VyIHZhbGlkYXRpb24gZGF0YSB0byBgdmFsaWRhdGlvbl9kYXRhYC4gTm90ZSBob3cgd2Ugc3VwcGx5Cm91ciB2YWxpZGF0aW9uIGZlYXR1cmVzIGFuZCBsYWJlbHMgZGF0YXNldHMgYXMgYSBsaXN0IHRvIGB2YWxpZGF0aW9uX2RhdGFgLgoKYGBge3IgdHJhaW4td2l0aC12YWxpZGF0aW9ufQpuZXR3b3JrIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMDI0LCBhY3RpdmF0aW9uID0gInJlbHUiLCBpbnB1dF9zaGFwZSA9IG5jb2woeF90cmFpbikpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKSAlPiUKICBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjAxKSwKICAgIGxvc3MgPSAibXNsZSIsCiAgICBtZXRyaWNzID0gYygibWFlIikKICApCgpoaXN0b3J5IDwtIG5ldHdvcmsgJT4lIGZpdCgKICB4X3RyYWluX3N1YiwgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHN1cHBseSBvdXIgbmV3IHRyYWluaW5nIGZlYXR1cmVzIGRhdGEKICB5X3RyYWluX3N1YiwgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHN1cHBseSBvdXIgbmV3IHRyYWluaW5nIGxhYmVscyBkYXRhCiAgZXBvY2hzID0gNTAsCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIHZhbGlkYXRpb25fZGF0YSA9IGxpc3QoeF92YWwsIHlfdmFsKSwgICMgc3VwcGx5IG91ciB2YWxpZGF0aW9uIGRhdGEKICBjYWxsYmFja3MgPSBsaXN0KAogICAgICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMTAsIHJlc3RvcmVfYmVzdF93ZWlnaHRzID0gRkFMU0UpLAogICAgICAgIGNhbGxiYWNrX3JlZHVjZV9scl9vbl9wbGF0ZWF1KGZhY3RvciA9IDAuMiwgcGF0aWVuY2UgPSA1KQogICAgKQopCmBgYAoKYGBge3IgdmFsaWRhdGlvbi1tb2RlbC1wZXJmb3JtYW5jZX0KaGlzdG9yeQpgYGAKCgojIGstZm9sZCBjcm9zcyB2YWxpZGF0aW9uCgpBcyB0aGUgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyBpbiBvdXIgZGF0YSBpbmNyZWFzZXMsIHZhcmlhbmNlIGluIG91ciBsb3NzIHNjb3JlIAp3aWxsIGRlY3JlYXNlLiBIb3dldmVyLCB3ZSBkbyBub3QgYWx3YXlzIGhhdmUgdGhlIG9wdGlvbiB0byBqdXN0IGdvIG91dCBhbmQgZ2V0IAptb3JlIGRhdGEuIFNvLCBpZiB3ZSB3YW50IHRvIGdhaW4gYSBtb3JlIGFjY3VyYXRlIHVuZGVyc3RhbmRpbmcgb2YgdGhlIGxvc3Mgc2NvcmUgCmFuZCBpdHMgdmFyaWFuY2Ugd2UgY2FuIHBlcmZvcm0gX2stZm9sZCBjcm9zcyB2YWxpZGF0aW9uXy4gCgpGaXJzdCwgd2UgbmVlZCB0byBjcmVhdGUgayBmb2xkcy4gVGhpcyBleGFtcGxlIGNyZWF0ZXMgMTAgZm9sZHMgYnkgZGl2aWRpbmcgdGhlCnJhbmRvbWx5IHNhbXBsZWQgaW5kZXggaW50byAxMCBhcHByb3hpbWF0ZWx5IGVxdWFsICJjdXRzIi4gQ29uc2VxdWVudGx5LCBgZm9sZHNgCmluIHRoaXMgZXhhbXBsZSBpcyBzaW1wbHkgYSB2ZWN0b3IgdGhhdCBpcyBlcXVhbCBsZW5ndGggdG8gb3VyIHRyYWluaW5nCm9ic2VydmF0aW9uczsgc3RhdGluZyB0aGF0IG9ic2VydmF0aW9uIDEgaXMgYXNzaWduZWQgdG8gdGhlIDEwdGggZm9sZCwgb2JzZXJ2YXRpb24KMiBpcyBhc3NpZ25lZCB0byB0aGUgM3JkIGZvbGQsIG9ic2VydmF0aW9uIDMgaXMgYXNzaWduZWQgdG8gdGhlIDFzdCBmb2xkLCBldGMuCgpgYGB7ciBjcmVhdGUtZm9sZHN9CiMgbnVtYmVyIG9mIGZvbGRzCmsgPC0gMTAKCiMgcmFuZG9taXplIGRhdGEgYmVmb3JlIG1ha2luZyBmb2xkcwpzZXQuc2VlZCgxMjMpCmluZGljZXMgPC0gc2FtcGxlKDE6bnJvdyh4X3RyYWluKSkKCiMgZGl2aWRlIHRoZSBvcmRlcmVkIGluZGljZXMgaW50byBrIGludGVydmFscywgbGFiZWxlZCAxOmsuCmZvbGRzIDwtIGN1dChpbmRpY2VzLCBicmVha3MgPSBrLCBsYWJlbHMgPSBGQUxTRSkKc3RyKGZvbGRzKQpgYGAKCklmIHdlIGxvb2sgYXQgYWxsIHRoZSBmb2xkcywgd2UnbGwgc2VlIHRoYXQgd2UgaGF2ZSBuZWFybHkgZXF1YWwgbnVtYmVyIG9mCm9ic2VydmF0aW9ucyBhY3Jvc3MgYWxsIGZvbGRzOgoKYGBge3J9CnRhYmxlKGZvbGRzKQpgYGAKCk5vdyB3ZSBjYW4gYXBwbHkgYSBgZm9yYCBsb29wIHRvIGl0ZXJhdGUgdGhyb3VnaCB0aGUgdHJhaW5pbmcgZGF0YSBhbmQgcGVyZm9ybSAKay1mb2xkIGNyb3NzIHZhbGlkYXRpb24uIFRoaXMgd29ya3MgYnk6CgoxLiBBc3NpZ25pbmcgZm9sZCBgaWAgdG8gdGhlIHZhbGlkYXRpb24gc2V0IGFuZCB0aGUgcmVtYWluaW5nIGZvbGRzIHRvIHRoZQp0cmFpbmluZyBzZXQsCjIuIFRyYWluaW5nIG91ciBtb2RlbCB1c2luZyBgdmFsaWRhdGlvbl9kYXRhYCB0byBzdXBwbHkgb3VyIHZhbGlkYXRpb24gc2V0LAozLiBTYXZlIG91ciByZXN1bHRzIGZvciB0aGF0IGl0ZXJhdGlvbiwKNC4gUmVwZWF0IGZvciBhbGwgMTAgZm9sZHMuCgpBcyB0aGlzIGNvZGUgZXhlY3V0ZXMsIHRoZSBtaW5pbXVtIHZhbGlkYXRpb24gbG9zcyBzY29yZSB3aWxsIGJlIHByaW50aW5nIG91dApmb3IgZWFjaCBmb2xkIGFuZCB5b3Ugd2lsbCBzZWUgdGhlIHZhcmlhYmlsaXR5IGFjcm9zcyB0aGUgZm9sZHMuCgpgYGB7ciBwZXJmb3JtLWtmb2xkLWN2fQojIGNyZWF0ZSBhIGRhdGEgZnJhbWUgdG8gc3RvcmUgcmVzdWx0cwpyZXN1bHRzIDwtIGRhdGEuZnJhbWUoKQoKZm9yIChpIGluIHNlcV9sZW4oaykpIHsKICBjYXQoInByb2Nlc3NpbmcgZm9sZCIsIHBhc3RlMChpLCAiOiAiKSkKICAKICAjIFByZXBhcmUgdGhlIHRyYWluaW5nIGFuZCB2YWxpZGF0aW9uIGRhdGEgZm9yIGVhY2ggZm9sZAogIHZhbF9pbmRpY2VzIDwtIHdoaWNoKGZvbGRzID09IGksIGFyci5pbmQgPSBUUlVFKSAKICAKICAjIHZhbGlkYXRpb24gc2V0OiB0aGUgaXRoIHBhcnRpdGlvbgogIHhfdmFsIDwtIHhfdHJhaW5bdmFsX2luZGljZXMsXQogIHlfdmFsIDwtIHlfdHJhaW5bdmFsX2luZGljZXNdCiAgCiAgIyBUcmFpbmluZyBzZXQ6IGFsbCBvdGhlciBwYXJ0aXRpb25zCiAgeF90cmFpbl9zdWIgPC0geF90cmFpblstdmFsX2luZGljZXMsXQogIHlfdHJhaW5fc3ViIDwtIHlfdHJhaW5bLXZhbF9pbmRpY2VzXQogIAogICMgQ3JlYXRlIG91ciBtb2RlbCBibHVlcHJpbnQKICBuZXR3b3JrIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lIAogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKSAlPiUKICAgIGNvbXBpbGUoCiAgICAgIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKGxyID0gMC4wMSksCiAgICAgIGxvc3MgPSAibXNsZSIsCiAgICAgIG1ldHJpY3MgPSBjKCJtYWUiKQogICAgKQoKICAjIFRyYWluIG91ciBtb2RlbCB3aXRoIGFuZCBzdXBwbHkgdHJhaW4gLyB2YWxpZGF0aW9uIGRhdGEKICBoaXN0b3J5IDwtIG5ldHdvcmsgJT4lIGZpdCgKICAgIHhfdHJhaW5fc3ViLCAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICB5X3RyYWluX3N1YiwgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgIGVwb2NocyA9IDUwLAogICAgYmF0Y2hfc2l6ZSA9IDMyLAogICAgdmFsaWRhdGlvbl9kYXRhID0gbGlzdCh4X3ZhbCwgeV92YWwpLAogICAgdmVyYm9zZSA9IEZBTFNFLAogICAgY2FsbGJhY2tzID0gY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUoZmFjdG9yID0gMC4yLCBwYXRpZW5jZSA9IDUpCiAgICApCiAgIAogICMgRXh0cmFjdCB0aGUgcGVyZm9ybWFuY2UgZGF0YSAgICAgICAgICAgIAogIG1vZGVsX3BlcmZvcm1hbmNlIDwtIGFzLmRhdGEuZnJhbWUoaGlzdG9yeSkgJT4lIG11dGF0ZShmb2xkID0gaSkKICByZXN1bHRzIDwtIHJiaW5kKHJlc3VsdHMsIG1vZGVsX3BlcmZvcm1hbmNlKQogIAogICMgYXBwZW5kIGxvb3AgbWVzc2FnZSB3aXRoIG1pbiBsb3NzIGZvciBpdGggZm9sZAogIG1pbl9sb3NzIDwtIHJvdW5kKG1pbihoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MpLCA0KQogIGNhdChtaW5fbG9zcywgIlxuIiwgYXBwZW5kID0gVFJVRSkKfSAKYGBgCgpXZSBjYW4gcGxvdCB0aGUgcmVzdWx0czsgaG93ZXZlciwgdGhlIGRpZmZlcmVuY2UgYmV0d2VlbiBlYWNoIGZvbGRzIHZhbGlkYXRpb24KbG9zcyBzY29yZSBpcyBub3Qgb2J2aW91cy4KCmBgYHtyIHBsb3Qta2ZvbGQtcmVzdWx0cywgbWVzc2FnZT1GQUxTRX0KZ2dwbG90KHJlc3VsdHMsIGFlcyhlcG9jaCwgdmFsdWUsIGNvbG9yID0gZGF0YSkpICsKICBnZW9tX3BvaW50KGFscGhhID0gMC41KSArIAogIGdlb21fc21vb3RoKCkgKwogIGZhY2V0X3dyYXAofiBtZXRyaWMsIG5jb2wgPSAxLCBzY2FsZXMgPSAiZnJlZV95IikKYGBgCgpCdXQgaWYgd2Ugem9vbSBpbiBvbiB0aGUgdmFsaWRhdGlvbiBsb3NzIHdlIGNhbiBzZWUgdGhlIHZhcmlhbmNlIHRoYXQgZXhpc3RzOgoKYGBge3IgcGxvdC1rZm9sZC12YWwtcmVzdWx0cywgbWVzc2FnZT1GQUxTRX0KcmVzdWx0cyAlPiUKICBmaWx0ZXIoZGF0YSA9PSAndmFsaWRhdGlvbicsIG1ldHJpYyA9PSAnbG9zcycpICU+JQogIGdncGxvdChhZXMoZXBvY2gsIHZhbHVlKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjUpICsKICBzdGF0X3N1bW1hcnkoZnVuLmRhdGEgPSAibWVhbl9jbF9ib290IiwgY29sb3VyID0gInJlZCIpICsKICBnZW9tX3Ntb290aCgpICsKICBzY2FsZV95X2xvZzEwKCkKYGBgCgpJZiB3ZSBwaWNrIHRoZSBlcG9jaCB3aXRoIHRoZSBsb3dlc3QgX19fYXZlcmFnZV9fXyB2YWxpZGF0aW9uIGxvc3MsIHdlIGNhbiBzZWUgCnRoYXQgb3VyIHZhbGlkYXRpb24gbG9zcyBpcyBhYm91dC4uLiAKCmBgYHtyLCBtZXNzYWdlPUZBTFNFfQojIHdoaWNoIGVwaWMgaGFzIGxvd2VzdCBhdmcgbG9zcwpiZXN0X2Vwb2NoIDwtIHJlc3VsdHMgJT4lCiAgZ3JvdXBfYnkoZXBvY2gpICU+JQogIGZpbHRlcihtZXRyaWMgPT0gJ2xvc3MnLCBkYXRhID09ICd2YWxpZGF0aW9uJykgJT4lCiAgc3VtbWFyaXNlKGF2Z19sb3NzID0gbWVhbih2YWx1ZSksIAogICAgICAgICAgICBzdGRfbG9zcyA9IHNkKHZhbHVlKSkgJT4lCiAgdG9wX24oLTEsIHd0ID0gYXZnX2xvc3MpCgpiZXN0X2Vwb2NoCmBgYAoKSWYgd2UgcmUtdHJhaW4gb3VyIG1vZGVsIGFuZCB1c2UgdGhlIGJlc3QgZXBvY2gsIHdlIHNob3VsZCBzZWUgc2ltaWxhciByZXN1bHRzIAp3aXRoaW4gcmVhc29uIHdoZW4gc2NvcmluZyBvbiBuZXcgZGF0YToKCmBgYHtyIHRyYWluLWV2YWx1YXRlfQpuZXR3b3JrIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEpICU+JQogIGNvbXBpbGUoCiAgICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMDEpLAogICAgbG9zcyA9ICJtc2xlIiwKICAgIG1ldHJpY3MgPSBjKCJtYWUiKQogICkKCmhpc3RvcnkgPC0gbmV0d29yayAlPiUgZml0KAogIHhfdHJhaW4sICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICB5X3RyYWluLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgZXBvY2hzID0gYmVzdF9lcG9jaCRlcG9jaCwKICBiYXRjaF9zaXplID0gMzIsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICBjYWxsYmFja3MgPSBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShmYWN0b3IgPSAwLjIsIHBhdGllbmNlID0gNSksCiAgdmVyYm9zZSA9IEZBTFNFCiAgKQpgYGAKCmBgYHtyfQpuZXR3b3JrICU+JSBldmFsdWF0ZSh4X3Rlc3QsIHlfdGVzdCwgdmVyYm9zZSA9IEZBTFNFKQpgYGAK