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:

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:

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:

  1. Assigning fold i to the validation set and the remaining folds to the training set,
  2. Training our model using validation_data to supply our validation set,
  3. Save our results for that iteration,
  4. 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