In this case study, our objective is to predict the sales price of a home. This is a regression problem since the goal is to predict any real number across some spectrum ($119,201, $168,594, $301,446, etc). To predict the sales price, we will use numeric and categorical features of the home.

As you proceed, you’ll work through the steps we discussed in the last module:

  1. Prepare data
  2. Balance batch size with a default learning rate
  3. Tune the adaptive learning rate optimizer
  4. Add callbacks to control training
  5. Explore model capacity
  6. Regularize overfitting
  7. Repeat steps 1-6
  8. Evaluate final model results

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

Step 0: Our Data

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

Understanding our data

This data has been partially cleaned up and has no missing data:

sum(is.na(ames))
[1] 0

But this tabular data is a combination of numeric and categorical data that we need to address.

str(ames)
Classes ‘tbl_df’, ‘tbl’ and 'data.frame':   2930 obs. of  81 variables:
 $ MS_SubClass       : Factor w/ 16 levels "One_Story_1946_and_Newer_All_Styles",..: 1 1 1 1 6 6 12 12 12 6 ...
 $ MS_Zoning         : Factor w/ 7 levels "Floating_Village_Residential",..: 3 2 3 3 3 3 3 3 3 3 ...
 $ Lot_Frontage      : num  141 80 81 93 74 78 41 43 39 60 ...
 $ Lot_Area          : int  31770 11622 14267 11160 13830 9978 4920 5005 5389 7500 ...
 $ Street            : Factor w/ 2 levels "Grvl","Pave": 2 2 2 2 2 2 2 2 2 2 ...
 $ Alley             : Factor w/ 3 levels "Gravel","No_Alley_Access",..: 2 2 2 2 2 2 2 2 2 2 ...
 $ Lot_Shape         : Factor w/ 4 levels "Regular","Slightly_Irregular",..: 2 1 2 1 2 2 1 2 2 1 ...
 $ Land_Contour      : Factor w/ 4 levels "Bnk","HLS","Low",..: 4 4 4 4 4 4 4 2 4 4 ...
 $ Utilities         : Factor w/ 3 levels "AllPub","NoSeWa",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ Lot_Config        : Factor w/ 5 levels "Corner","CulDSac",..: 1 5 1 1 5 5 5 5 5 5 ...
 $ Land_Slope        : Factor w/ 3 levels "Gtl","Mod","Sev": 1 1 1 1 1 1 1 1 1 1 ...
 $ Neighborhood      : Factor w/ 28 levels "North_Ames","College_Creek",..: 1 1 1 1 7 7 17 17 17 7 ...
 $ Condition_1       : Factor w/ 9 levels "Artery","Feedr",..: 3 2 3 3 3 3 3 3 3 3 ...
 $ Condition_2       : Factor w/ 8 levels "Artery","Feedr",..: 3 3 3 3 3 3 3 3 3 3 ...
 $ Bldg_Type         : Factor w/ 5 levels "OneFam","TwoFmCon",..: 1 1 1 1 1 1 5 5 5 1 ...
 $ House_Style       : Factor w/ 8 levels "One_and_Half_Fin",..: 3 3 3 3 8 8 3 3 3 8 ...
 $ Overall_Qual      : Factor w/ 10 levels "Very_Poor","Poor",..: 6 5 6 7 5 6 8 8 8 7 ...
 $ Overall_Cond      : Factor w/ 10 levels "Very_Poor","Poor",..: 5 6 6 5 5 6 5 5 5 5 ...
 $ Year_Built        : int  1960 1961 1958 1968 1997 1998 2001 1992 1995 1999 ...
 $ Year_Remod_Add    : int  1960 1961 1958 1968 1998 1998 2001 1992 1996 1999 ...
 $ Roof_Style        : Factor w/ 6 levels "Flat","Gable",..: 4 2 4 4 2 2 2 2 2 2 ...
 $ Roof_Matl         : Factor w/ 8 levels "ClyTile","CompShg",..: 2 2 2 2 2 2 2 2 2 2 ...
 $ Exterior_1st      : Factor w/ 16 levels "AsbShng","AsphShn",..: 4 14 15 4 14 14 6 7 6 14 ...
 $ Exterior_2nd      : Factor w/ 17 levels "AsbShng","AsphShn",..: 11 15 16 4 15 15 6 7 6 15 ...
 $ Mas_Vnr_Type      : Factor w/ 5 levels "BrkCmn","BrkFace",..: 5 4 2 4 4 2 4 4 4 4 ...
 $ Mas_Vnr_Area      : num  112 0 108 0 0 20 0 0 0 0 ...
 $ Exter_Qual        : Factor w/ 4 levels "Excellent","Fair",..: 4 4 4 3 4 4 3 3 3 4 ...
 $ Exter_Cond        : Factor w/ 5 levels "Excellent","Fair",..: 5 5 5 5 5 5 5 5 5 5 ...
 $ Foundation        : Factor w/ 6 levels "BrkTil","CBlock",..: 2 2 2 2 3 3 3 3 3 3 ...
 $ Bsmt_Qual         : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 3 6 3 3 3 6 ...
 $ Bsmt_Cond         : Factor w/ 6 levels "Excellent","Fair",..: 3 6 6 6 6 6 6 6 6 6 ...
 $ Bsmt_Exposure     : Factor w/ 5 levels "Av","Gd","Mn",..: 2 4 4 4 4 4 3 4 4 4 ...
 $ BsmtFin_Type_1    : Factor w/ 7 levels "ALQ","BLQ","GLQ",..: 2 6 1 1 3 3 3 1 3 7 ...
 $ BsmtFin_SF_1      : num  2 6 1 1 3 3 3 1 3 7 ...
 $ BsmtFin_Type_2    : Factor w/ 7 levels "ALQ","BLQ","GLQ",..: 7 4 7 7 7 7 7 7 7 7 ...
 $ BsmtFin_SF_2      : num  0 144 0 0 0 0 0 0 0 0 ...
 $ Bsmt_Unf_SF       : num  441 270 406 1045 137 ...
 $ Total_Bsmt_SF     : num  1080 882 1329 2110 928 ...
 $ Heating           : Factor w/ 6 levels "Floor","GasA",..: 2 2 2 2 2 2 2 2 2 2 ...
 $ Heating_QC        : Factor w/ 5 levels "Excellent","Fair",..: 2 5 5 1 3 1 1 1 1 3 ...
 $ Central_Air       : Factor w/ 2 levels "N","Y": 2 2 2 2 2 2 2 2 2 2 ...
 $ Electrical        : Factor w/ 6 levels "FuseA","FuseF",..: 5 5 5 5 5 5 5 5 5 5 ...
 $ First_Flr_SF      : int  1656 896 1329 2110 928 926 1338 1280 1616 1028 ...
 $ Second_Flr_SF     : int  0 0 0 0 701 678 0 0 0 776 ...
 $ Low_Qual_Fin_SF   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ Gr_Liv_Area       : int  1656 896 1329 2110 1629 1604 1338 1280 1616 1804 ...
 $ Bsmt_Full_Bath    : num  1 0 0 1 0 0 1 0 1 0 ...
 $ Bsmt_Half_Bath    : num  0 0 0 0 0 0 0 0 0 0 ...
 $ Full_Bath         : int  1 1 1 2 2 2 2 2 2 2 ...
 $ Half_Bath         : int  0 0 1 1 1 1 0 0 0 1 ...
 $ Bedroom_AbvGr     : int  3 2 3 3 3 3 2 2 2 3 ...
 $ Kitchen_AbvGr     : int  1 1 1 1 1 1 1 1 1 1 ...
 $ Kitchen_Qual      : Factor w/ 5 levels "Excellent","Fair",..: 5 5 3 1 5 3 3 3 3 3 ...
 $ TotRms_AbvGrd     : int  7 5 6 8 6 7 6 5 5 7 ...
 $ Functional        : Factor w/ 8 levels "Maj1","Maj2",..: 8 8 8 8 8 8 8 8 8 8 ...
 $ Fireplaces        : int  2 0 0 2 1 1 0 0 1 1 ...
 $ Fireplace_Qu      : Factor w/ 6 levels "Excellent","Fair",..: 3 4 4 6 6 3 4 4 6 6 ...
 $ Garage_Type       : Factor w/ 7 levels "Attchd","Basment",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ Garage_Finish     : Factor w/ 4 levels "Fin","No_Garage",..: 1 4 4 1 1 1 1 3 3 1 ...
 $ Garage_Cars       : num  2 1 1 2 2 2 2 2 2 2 ...
 $ Garage_Area       : num  528 730 312 522 482 470 582 506 608 442 ...
 $ Garage_Qual       : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 6 6 6 6 6 6 ...
 $ Garage_Cond       : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 6 6 6 6 6 6 ...
 $ Paved_Drive       : Factor w/ 3 levels "Dirt_Gravel",..: 2 3 3 3 3 3 3 3 3 3 ...
 $ Wood_Deck_SF      : int  210 140 393 0 212 360 0 0 237 140 ...
 $ Open_Porch_SF     : int  62 0 36 0 34 36 0 82 152 60 ...
 $ Enclosed_Porch    : int  0 0 0 0 0 0 170 0 0 0 ...
 $ Three_season_porch: int  0 0 0 0 0 0 0 0 0 0 ...
 $ Screen_Porch      : int  0 120 0 0 0 0 0 144 0 0 ...
 $ Pool_Area         : int  0 0 0 0 0 0 0 0 0 0 ...
 $ Pool_QC           : Factor w/ 5 levels "Excellent","Fair",..: 4 4 4 4 4 4 4 4 4 4 ...
 $ Fence             : Factor w/ 5 levels "Good_Privacy",..: 5 3 5 5 3 5 5 5 5 5 ...
 $ Misc_Feature      : Factor w/ 6 levels "Elev","Gar2",..: 3 3 2 3 3 3 3 3 3 3 ...
 $ Misc_Val          : int  0 0 12500 0 0 0 0 0 0 0 ...
 $ Mo_Sold           : int  5 6 6 4 3 6 4 1 3 6 ...
 $ Year_Sold         : int  2010 2010 2010 2010 2010 2010 2010 2010 2010 2010 ...
 $ Sale_Type         : Factor w/ 10 levels "COD","Con","ConLD",..: 10 10 10 10 10 10 10 10 10 10 ...
 $ Sale_Condition    : Factor w/ 6 levels "Abnorml","AdjLand",..: 5 5 5 5 5 5 5 5 5 5 ...
 $ Sale_Price        : int  215000 105000 172000 244000 189900 195500 213500 191500 236500 189000 ...
 $ Longitude         : num  -93.6 -93.6 -93.6 -93.6 -93.6 ...
 $ Latitude          : num  42.1 42.1 42.1 42.1 42.1 ...

The numeric variables are on different scales. For example:

ames %>%
  select(Lot_Area, Lot_Frontage, Year_Built, Gr_Liv_Area, Garage_Cars, Mo_Sold) %>%
  gather(feature, value) %>%
  ggplot(aes(feature, value)) +
  geom_boxplot() +
  scale_y_log10(labels = scales::comma)

There are categorical features that could be ordered:

ames %>%
  select(matches("(Qual|Cond|QC|Qu)$")) %>%
  str()
Classes ‘tbl_df’, ‘tbl’ and 'data.frame':   2930 obs. of  12 variables:
 $ Overall_Qual: Factor w/ 10 levels "Very_Poor","Poor",..: 6 5 6 7 5 6 8 8 8 7 ...
 $ Overall_Cond: Factor w/ 10 levels "Very_Poor","Poor",..: 5 6 6 5 5 6 5 5 5 5 ...
 $ Exter_Qual  : Factor w/ 4 levels "Excellent","Fair",..: 4 4 4 3 4 4 3 3 3 4 ...
 $ Exter_Cond  : Factor w/ 5 levels "Excellent","Fair",..: 5 5 5 5 5 5 5 5 5 5 ...
 $ Bsmt_Qual   : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 3 6 3 3 3 6 ...
 $ Bsmt_Cond   : Factor w/ 6 levels "Excellent","Fair",..: 3 6 6 6 6 6 6 6 6 6 ...
 $ Heating_QC  : Factor w/ 5 levels "Excellent","Fair",..: 2 5 5 1 3 1 1 1 1 3 ...
 $ Kitchen_Qual: Factor w/ 5 levels "Excellent","Fair",..: 5 5 3 1 5 3 3 3 3 3 ...
 $ Fireplace_Qu: Factor w/ 6 levels "Excellent","Fair",..: 3 4 4 6 6 3 4 4 6 6 ...
 $ Garage_Qual : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 6 6 6 6 6 6 ...
 $ Garage_Cond : Factor w/ 6 levels "Excellent","Fair",..: 6 6 6 6 6 6 6 6 6 6 ...
 $ Pool_QC     : Factor w/ 5 levels "Excellent","Fair",..: 4 4 4 4 4 4 4 4 4 4 ...

And some of the categorical features have many levels:

ames %>%
  select_if(~ is.factor(.) & length(levels(.)) > 8) %>%
  glimpse()
Observations: 2,930
Variables: 8
$ MS_SubClass  <fct> One_Story_1946_and_Newer_All_Styles, One_Story_1946_and_Newer_All_Styles, O…
$ Neighborhood <fct> North_Ames, North_Ames, North_Ames, North_Ames, Gilbert, Gilbert, Stone_Bro…
$ Condition_1  <fct> Norm, Feedr, Norm, Norm, Norm, Norm, Norm, Norm, Norm, Norm, Norm, Norm, No…
$ Overall_Qual <fct> Above_Average, Average, Above_Average, Good, Average, Above_Average, Very_G…
$ Overall_Cond <fct> Average, Above_Average, Above_Average, Average, Average, Above_Average, Ave…
$ Exterior_1st <fct> BrkFace, VinylSd, Wd Sdng, BrkFace, VinylSd, VinylSd, CemntBd, HdBoard, Cem…
$ Exterior_2nd <fct> Plywood, VinylSd, Wd Sdng, BrkFace, VinylSd, VinylSd, CmentBd, HdBoard, Cme…
$ Sale_Type    <fct> WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , WD , …

Consequently, our first challenge is transforming this dataset into numeric tensors that our model can use.

Step 1: Prep the Data

Create train & test splits

One of the first things we want to do is create a train and test set as you probably noticed that we do not have a train and test set similar to how MNIST was already set up for us. We can use the rsample package to create our train and test datasets.

Note: This will randomly select the 70/30 split so we are randomizing our data with this process.

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

Vectorize and scaling

All inputs and response values in a neural network must be tensors of either floating-point or integer data. Moreover, our feature values should not be relatively large compared to the randomized initial weights and all our features should take values in roughly the same range.

Consequently, we need to vectorize our data into a format conducive to neural networks ℹ️. For this data set, we’ll transform our data by:

  1. removing any zero-variance (or near zero-variance) features
  2. condensing unique levels of categorical features to “other”
  3. ordinal encoding the quality features
  4. normalize numeric feature distributions
  5. standardizing numeric features to mean = 0, std dev = 1
  6. one-hot encoding remaining categorical features

Note: we’re using the recipes package (https://tidymodels.github.io/recipes)

blueprint <- recipe(Sale_Price ~ ., data = ames_train) %>%
  step_nzv(all_nominal()) %>%                                       # step #1
  step_other(all_nominal(), threshold = .01, other = "other") %>%   # step #2
  step_integer(matches("(Qual|Cond|QC|Qu)$")) %>%                   # step #3
  step_YeoJohnson(all_numeric(), -all_outcomes()) %>%               # step #4
  step_center(all_numeric(), -all_outcomes()) %>%                   # step #5
  step_scale(all_numeric(), -all_outcomes()) %>%                    # step #5
  step_dummy(all_nominal(), -all_outcomes(), one_hot = TRUE)        # step #6

blueprint
Data Recipe

Inputs:

Operations:

Sparse, unbalanced variable filter on all_nominal
Collapsing factor levels for all_nominal
Integer encoding for matches, (Qual|Cond|QC|Qu)$
Yeo-Johnson transformation on all_numeric, -, all_outcomes()
Centering for all_numeric, -, all_outcomes()
Scaling for all_numeric, -, all_outcomes()
Dummy variables from all_nominal, -, all_outcomes()

This next step computes any relavent information (mean and std deviation of numeric features, names of one-hot encoded features) on the training data so there is no information leakage from the test data.

prepare <- prep(blueprint, training = ames_train)
prepare
Data Recipe

Inputs:

Training data contained 2051 data points and no missing data.

Operations:

Sparse, unbalanced variable filter removed Street, Alley, Land_Contour, Utilities, ... [trained]
Collapsing factor levels for MS_SubClass, MS_Zoning, Lot_Shape, Lot_Config, ... [trained]
Integer encoding for Overall_Qual, Overall_Cond, Exter_Qual, Exter_Cond, Bsmt_Qual, ... [trained]
Yeo-Johnson transformation on Lot_Frontage, Lot_Area, Overall_Qual, ... [trained]
Centering for Lot_Frontage, Lot_Area, Overall_Qual, Overall_Cond, ... [trained]
Scaling for Lot_Frontage, Lot_Area, Overall_Qual, Overall_Cond, ... [trained]
Dummy variables from MS_SubClass, MS_Zoning, Lot_Shape, Lot_Config, Neighborhood, ... [trained]

We can now vectorize our training and test data. If you scroll through the data you will notice that all features are now numeric and are either 0/1 (one hot encoded features) or have mean 0 and generally range between -3 and 3.

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

Lastly, we need to create the final feature and response objects for train and test data. Since keras and tensorflow require our features & labels to be seperate objects we need to separate them. In doing so, our features need to be a 2D tensor which is why we apply as.matrix and our response needs to be a vector which is why we apply pull.

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

Step 2: Balance batch size with a default learning rate

To get started, let’s build a simple model with…

  • a single layer model with 128 units in the hidden layer. We have 188 features and 1 response node so a good starting point is mean(c(188, 1)) and then round up to the nearest value in the \(2^s\) range (i.e. 32, 64, 128, 256, 512).
  • a basic SGD optimizer
  • use a mean square logarithmic error (“msle”)
  • also track the mean absolute error metric (“mae”)
  • 20% validation split

Now, start with the default batch size of 32 and then compare with smaller values (i.e. 16) and larger values (i.e. 128). You’re looking to balance the progression of the loss learning curve and the training spead.

Comment: The default batch size of 32 performs pretty well in this case but you could’ve easily have choosen lower (8 or 16) or higher (64, 128) without negative impacts. We can see that the loss is still trending downward so we should have lots of room for improvement.

n_feat <- ncol(x_train)

model <- keras_model_sequential() %>% 
  layer_dense(units = 128, activation = "relu", input_shape = ncol(x_train)) %>%
  layer_dense(units = 1)

model %>% compile(
    optimizer = "sgd",
    loss = "msle",
    metrics = "mae"
  )

history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  validation_split = 0.2
)
history
Trained on 1,640 samples (batch_size=32, epochs=10)
Final epoch (plot to see history):
    loss: 34.61
     mae: 180,347
val_loss: 34.72
 val_mae: 180,139 
plot(history)

Step 3: Tune the adaptive learning rate optimizer

Now go head and start assessing different adaptive learning rates such as:

  • SGD+momentum
  • RMSprop
  • Adamp

Try a variety of learning rates. Recall that we typically start assessing rates on a logarithmic scale (i.e. 0.1, 0.01, …, 0.0001).

Comment: The default learning rates on the common adaptive learning rate optimizers show a slow progression down the loss curve so we can afford to use a larger learning rate. I found that the RMSprop tended to provide the best results at this point.

model <- keras_model_sequential() %>% 
  layer_dense(units = 128, activation = "relu", input_shape = ncol(x_train)) %>%
  layer_dense(units = 1)

model %>% compile(
    optimizer = optimizer_rmsprop(lr = 0.1),
    loss = "msle",
    metrics = "mae"
  )

history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  validation_split = 0.2
)
history
Trained on 1,640 samples (batch_size=32, epochs=10)
Final epoch (plot to see history):
    loss: 0.01915
     mae: 15,686
val_loss: 0.01508
 val_mae: 16,308 
plot(history)

Step 4: Add callbacks to control training

Add the following callbacks and see if your performance improves:

  • early stopping with patience = 3 and min_delta = 0.00001
  • learning rate reduction upon a plateau with patience = 1

Comment: Adding early stopping improves performance because we can increase the epochs but stop when necessary. Most of my optimal models were stopping at around 15 epochs at this point. Also, adding callback_reduce_lr_on_plateau() also improves. I plot the learning rates by epoch below and we can see that they reduce multiple times which allows our model to eek out a little more performance improvements.

model <- keras_model_sequential() %>% 
  layer_dense(units = 128, activation = "relu", input_shape = ncol(x_train)) %>%
  layer_dense(units = 1)

model %>% compile(
    optimizer = optimizer_rmsprop(lr = 0.1),
    loss = "msle",
    metrics = "mae"
  )

history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  epochs = 30,
  validation_split = 0.2,
  callbacks = list(
    callback_early_stopping(patience = 3, min_delta = 0.00001),
    callback_reduce_lr_on_plateau(patience = 1)
  )
)
history
Trained on 1,640 samples (batch_size=32, epochs=16)
Final epoch (plot to see history):
    loss: 0.01694
     mae: 14,681
val_loss: 0.01404
 val_mae: 15,922
      lr: 0.0001 
plot(history)

Plotting the learning rate shows that it reduced multiple times during training:

plot(history$metrics$lr)

Step 5: Explore model capacity

Now start to explore different widths and depths to your model.

  • Assess a single layer with 128, 256, 512, and 1024 nodes
  • Assess 1, 2, and 3 hidden layers

Comment: I follow the same approach we used in the previous module to assess combinations of different nodes and hidden layers. I used the tensorboard callback to save my model runs and analyze them.

train_model <- function(n_units, n_layers, log_to) {
  
  # Create a model with a single hidden input layer
  model <- keras_model_sequential() %>%
    layer_dense(units = n_units, activation = "relu", input_shape = n_feat)
  
  # Add additional hidden layers based on input
  if (n_layers > 1) {
    for (i in seq_along(n_layers - 1)) {
      model %>% layer_dense(units = n_units, activation = "relu")
    }
  }
  
  # Add final output layer
  model %>% layer_dense(units = 1)
  
  # compile model
  model %>% compile(
    optimizer = optimizer_rmsprop(lr = 0.1),
    loss = "msle",
    metrics = "mae"
  )
  
  # train model and store results with callback_tensorboard()
  history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  epochs = 30,
  validation_split = 0.2,
  callbacks = list(
    callback_early_stopping(patience = 3, min_delta = 0.00001),
    callback_reduce_lr_on_plateau(patience = 1),
    callback_tensorboard(log_dir = log_to)
  ),
  verbose = FALSE
  )
  
  return(history)
}
grid <- expand_grid(
  units = c(128, 256, 512, 1024),
  layers = c(1:3)
) %>%
  mutate(id = paste0("mlp_", layers, "_layers_", units, "_units"))
grid

The initial results don’t show any glaring trends. All our models have loss scores ranging from 0.0135-0.015.

for (row in seq_len(nrow(grid))) {
  # get parameters
  units <- grid[[row, "units"]]
  layers <- grid[[row, "layers"]]
  file_path <- paste0("ames/", grid[[row, "id"]])
  
  # provide status update
  cat(layers, "hidden layer(s) with", units, "neurons: ")
  
  # train model
  m <- train_model(n_units = units, n_layers = layers, log_to = file_path)
  min_loss <- min(m$metrics$val_loss, na.rm = TRUE)
  
  # update status with loss
  cat(min_loss, "\n", append = TRUE)
}

Looking at the tensorboard shows that, really, any of the models are decent choices as they all have relatively similar results, low variance which means they all are stable models, they all have minimal overfitting, and compute time is definitely not a problem.

tensorboard("ames")
TensorBoard 2.0.1 at http://127.0.0.1:7065/ (Press CTRL+C to quit)
Started TensorBoard at http://127.0.0.1:7065 

Comment: After a little more experimenting I found a funnel shaped approached tended to produce a little more improvement in performance:

model <- keras_model_sequential() %>% 
  layer_dense(units = 1024, activation = "relu", input_shape = n_feat) %>%
  layer_dense(units = 512, activation = "relu") %>%
  layer_dense(units = 256, activation = "relu") %>%
  layer_dense(units = 1)

model %>% compile(
    optimizer = optimizer_rmsprop(lr = 0.1),
    loss = "msle",
    metrics = "mae"
  )

history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  epochs = 30,
  validation_split = 0.2,
  callbacks = list(
    callback_early_stopping(patience = 3, min_delta = 0.00001),
    callback_reduce_lr_on_plateau(patience = 1)
  )
)
history
Trained on 1,640 samples (batch_size=32, epochs=15)
Final epoch (plot to see history):
    loss: 0.01102
     mae: 12,501
val_loss: 0.01345
 val_mae: 14,026
      lr: 0.00000001 
plot(history) + scale_y_log10()

Step 6: Regularize overfitting

If your model is overfitting, try to add…

  • weight decay (i.e. kernel_regularizer = regularizer_l2(l = xxx)). Remember, we typically start by assessing values on logarithmic scale [0.1, 0.00001].
  • dropout (layer_dropout()) between each layer. Remember, dropout rates typically range from 20-50%.

Comment: Pretty much any weight regularizer hurt model performance. In this case, since our validation loss has minimal overfitting we can probably disregard any additional regularization.

model <- keras_model_sequential() %>% 
  layer_dense(units = 1024, activation = "relu", input_shape = n_feat) %>%
  layer_dropout(0.2) %>%
  layer_dense(units = 512, activation = "relu") %>%
  layer_dropout(0.2) %>%
  layer_dense(units = 256, activation = "relu") %>%
  layer_dropout(0.2) %>%
  layer_dense(units = 1)

model %>% compile(
    optimizer = optimizer_rmsprop(lr = 0.1),
    loss = "msle",
    metrics = "mae"
  )

history <- model %>% fit(
  x_train,
  y_train,
  batch_size = 32,
  epochs = 30,
  validation_split = 0.2,
  callbacks = list(
    callback_early_stopping(patience = 3, min_delta = 0.00001),
    callback_reduce_lr_on_plateau(patience = 1)
  )
)
history
Trained on 1,640 samples (batch_size=32, epochs=14)
Final epoch (plot to see history):
    loss: 0.03473
     mae: 25,796
val_loss: 0.01615
 val_mae: 15,914
      lr: 0.00001 

Step 7: Repeat steps 1-6

As this point we could repeat the process and…

  1. Prepare data
    • try to find additional data to add
    • try new feature engineering approaches
  2. Balance batch size with a default learning rate
    • reassess batch size
  3. Tune the adaptive learning rate optimizer
    • fine tune our learning rate
    • see if the current optimizer still outperforms others
  4. Add callbacks to control training
    • maybe assess more sophisticated learning rate schedulers (i.e. cyclical learning rates)
  5. Explore model capacity
    • after some tweaks we may want to reassess model capacity combinations
  6. Regularize overfitting

🏠

LS0tCnRpdGxlOiAiTWluaS1wcm9qZWN0OiBBbWVzIC0tIFJlZ3Jlc3Npb24gdG8gcHJlZGljdCBBbWVzLCBJQSBIb21lIFNhbGVzIFByaWNlcyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB0cnVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9taW5pbWFsKCkpCmBgYAoKSW4gdGhpcyBjYXNlIHN0dWR5LCBvdXIgb2JqZWN0aXZlIGlzIHRvIHByZWRpY3QgdGhlIHNhbGVzIHByaWNlIG9mIGEgaG9tZS4gVGhpcyAKaXMgYSBfcmVncmVzc2lvbl8gcHJvYmxlbSBzaW5jZSB0aGUgZ29hbCBpcyB0byBwcmVkaWN0IGFueSByZWFsIG51bWJlciBhY3Jvc3MKc29tZSBzcGVjdHJ1bSAoXCQxMTksMjAxLCBcJDE2OCw1OTQsIFwkMzAxLDQ0NiwgZXRjKS4gVG8gcHJlZGljdCB0aGUgc2FsZXMgCnByaWNlLCB3ZSB3aWxsIHVzZSBudW1lcmljIGFuZCBjYXRlZ29yaWNhbCBmZWF0dXJlcyBvZiB0aGUgaG9tZS4KCkFzIHlvdSBwcm9jZWVkLCB5b3UnbGwgd29yayB0aHJvdWdoIHRoZSBzdGVwcyB3ZSBkaXNjdXNzZWQgaW4gdGhlIGxhc3QgbW9kdWxlOgoKMS4gUHJlcGFyZSBkYXRhCjIuIEJhbGFuY2UgYmF0Y2ggc2l6ZSB3aXRoIGEgZGVmYXVsdCBsZWFybmluZyByYXRlCjMuIFR1bmUgdGhlIGFkYXB0aXZlIGxlYXJuaW5nIHJhdGUgb3B0aW1pemVyCjQuIEFkZCBjYWxsYmFja3MgdG8gY29udHJvbCB0cmFpbmluZwo1LiBFeHBsb3JlIG1vZGVsIGNhcGFjaXR5CjYuIFJlZ3VsYXJpemUgb3ZlcmZpdHRpbmcKNy4gUmVwZWF0IHN0ZXBzIDEtNgo4LiBFdmFsdWF0ZSBmaW5hbCBtb2RlbCByZXN1bHRzCgojIFBhY2thZ2UgUmVxdWlyZW1lbnRzCgpgYGB7ciBsb2FkLXBrZ3N9CmxpYnJhcnkoa2VyYXMpICAgICAjIGZvciBkZWVwIGxlYXJuaW5nCmxpYnJhcnkodGVzdHRoYXQpICAjIHVuaXQgdGVzdGluZwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBmb3IgZHBseXIsIGdncGxvdDIsIGV0Yy4KbGlicmFyeShyc2FtcGxlKSAgICMgZm9yIGRhdGEgc3BsaXR0aW5nCmxpYnJhcnkocmVjaXBlcykgICAjIGZvciBmZWF0dXJlIGVuZ2luZWVyaW5nCmBgYAoKIyBTdGVwIDA6IE91ciBEYXRhCgojIyBUaGUgQW1lcyBob3VzaW5nIGRhdGFzZXQKCkZvciB0aGlzIGNhc2Ugc3R1ZHkgd2Ugd2lsbCB1c2UgdGhlIFtBbWVzIGhvdXNpbmcgZGF0YXNldF0oaHR0cDovL2pzZS5hbXN0YXQub3JnL3YxOW4zL2RlY29jay5wZGYpIApwcm92aWRlZCBieSB0aGUgX19BbWVzSG91c2luZ19fIHBhY2thZ2UuCgpgYGB7ciBnZXQtZGF0YX0KYW1lcyA8LSBBbWVzSG91c2luZzo6bWFrZV9hbWVzKCkKZGltKGFtZXMpCmBgYAoKIyMgVW5kZXJzdGFuZGluZyBvdXIgZGF0YQoKVGhpcyBkYXRhIGhhcyBiZWVuIHBhcnRpYWxseSBjbGVhbmVkIHVwIGFuZCBoYXMgbm8gbWlzc2luZyBkYXRhOgoKYGBge3J9CnN1bShpcy5uYShhbWVzKSkKYGBgCgpCdXQgdGhpcyB0YWJ1bGFyIGRhdGEgaXMgYSBjb21iaW5hdGlvbiBvZiBudW1lcmljIGFuZCBjYXRlZ29yaWNhbCBkYXRhIHRoYXQgd2UKbmVlZCB0byBhZGRyZXNzLgoKYGBge3IgYW1lcy1zdHJ1Y3R1cmV9CnN0cihhbWVzKQpgYGAKClRoZSBudW1lcmljIHZhcmlhYmxlcyBhcmUgb24gZGlmZmVyZW50IHNjYWxlcy4gRm9yIGV4YW1wbGU6CgpgYGB7ciBudW1lcmljLXJhbmdlc30KYW1lcyAlPiUKICBzZWxlY3QoTG90X0FyZWEsIExvdF9Gcm9udGFnZSwgWWVhcl9CdWlsdCwgR3JfTGl2X0FyZWEsIEdhcmFnZV9DYXJzLCBNb19Tb2xkKSAlPiUKICBnYXRoZXIoZmVhdHVyZSwgdmFsdWUpICU+JQogIGdncGxvdChhZXMoZmVhdHVyZSwgdmFsdWUpKSArCiAgZ2VvbV9ib3hwbG90KCkgKwogIHNjYWxlX3lfbG9nMTAobGFiZWxzID0gc2NhbGVzOjpjb21tYSkKYGBgCgpUaGVyZSBhcmUgY2F0ZWdvcmljYWwgZmVhdHVyZXMgdGhhdCBjb3VsZCBiZSBvcmRlcmVkOgoKYGBge3IgbnVtZXJpYy1jYXRlZ29yaWVzfQphbWVzICU+JQogIHNlbGVjdChtYXRjaGVzKCIoUXVhbHxDb25kfFFDfFF1KSQiKSkgJT4lCiAgc3RyKCkKYGBgCgpBbmQgc29tZSBvZiB0aGUgY2F0ZWdvcmljYWwgZmVhdHVyZXMgaGF2ZSBtYW55IGxldmVsczoKCmBgYHtyfQphbWVzICU+JQogIHNlbGVjdF9pZih+IGlzLmZhY3RvciguKSAmIGxlbmd0aChsZXZlbHMoLikpID4gOCkgJT4lCiAgZ2xpbXBzZSgpCmBgYAoKQ29uc2VxdWVudGx5LCBvdXIgZmlyc3QgY2hhbGxlbmdlIGlzIHRyYW5zZm9ybWluZyB0aGlzIGRhdGFzZXQgaW50byBudW1lcmljCnRlbnNvcnMgdGhhdCBvdXIgbW9kZWwgY2FuIHVzZS4KCiMgU3RlcCAxOiBQcmVwIHRoZSBEYXRhCgojIyBDcmVhdGUgdHJhaW4gJiB0ZXN0IHNwbGl0cwoKT25lIG9mIHRoZSBmaXJzdCB0aGluZ3Mgd2Ugd2FudCB0byBkbyBpcyBjcmVhdGUgYSB0cmFpbiBhbmQgdGVzdCBzZXQgYXMgeW91CnByb2JhYmx5IG5vdGljZWQgdGhhdCB3ZSBkbyBub3QgaGF2ZSBhIHRyYWluIGFuZCB0ZXN0IHNldCBzaW1pbGFyIHRvIGhvdyBNTklTVCAKd2FzIGFscmVhZHkgc2V0IHVwIGZvciB1cy4gV2UgY2FuIHVzZSB0aGUgX19yc2FtcGxlX18gcGFja2FnZSB0byBjcmVhdGUgb3VyCnRyYWluIGFuZCB0ZXN0IGRhdGFzZXRzLgoKX19Ob3RlX186IFRoaXMgd2lsbCByYW5kb21seSBzZWxlY3QgdGhlIDcwLzMwIHNwbGl0IHNvIHdlIGFyZSByYW5kb21pemluZyBvdXIKZGF0YSB3aXRoIHRoaXMgcHJvY2Vzcy4KCmBgYHtyfQpzZXQuc2VlZCgxMjMpCmFtZXNfc3BsaXQgPC0gaW5pdGlhbF9zcGxpdChhbWVzLCBwcm9wID0gMC43KQphbWVzX3RyYWluIDwtIGFuYWx5c2lzKGFtZXNfc3BsaXQpCmFtZXNfdGVzdCA8LSBhc3Nlc3NtZW50KGFtZXNfc3BsaXQpCgpkaW0oYW1lc190cmFpbikKZGltKGFtZXNfdGVzdCkKYGBgCgojIyBWZWN0b3JpemUgYW5kIHNjYWxpbmcKCkFsbCBpbnB1dHMgYW5kIHJlc3BvbnNlIHZhbHVlcyBpbiBhIG5ldXJhbCBuZXR3b3JrIG11c3QgYmUgdGVuc29ycyBvZiBlaXRoZXIgCmZsb2F0aW5nLXBvaW50IG9yIGludGVnZXIgZGF0YS4gTW9yZW92ZXIsIG91ciBmZWF0dXJlIHZhbHVlcyBzaG91bGQgbm90IGJlCnJlbGF0aXZlbHkgbGFyZ2UgY29tcGFyZWQgdG8gdGhlIHJhbmRvbWl6ZWQgaW5pdGlhbCB3ZWlnaHRzIF9hbmRfIGFsbCBvdXIgCmZlYXR1cmVzIHNob3VsZCB0YWtlIHZhbHVlcyBpbiByb3VnaGx5IHRoZSBzYW1lIHJhbmdlLgoKQ29uc2VxdWVudGx5LCB3ZSBuZWVkIHRvIF9fX3ZlY3Rvcml6ZV9fXyBvdXIgZGF0YSBpbnRvIGEgZm9ybWF0IGNvbmR1Y2l2ZSB0byBuZXVyYWwgCm5ldHdvcmtzIFvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtMDIjMykuIEZvciB0aGlzIGRhdGEgc2V0LCB3ZSdsbCB0cmFuc2Zvcm0gb3VyCmRhdGEgYnk6CgoxLiByZW1vdmluZyBhbnkgemVyby12YXJpYW5jZSAob3IgbmVhciB6ZXJvLXZhcmlhbmNlKSBmZWF0dXJlcwoyLiBjb25kZW5zaW5nIHVuaXF1ZSBsZXZlbHMgb2YgY2F0ZWdvcmljYWwgZmVhdHVyZXMgdG8gIm90aGVyIgozLiBvcmRpbmFsIGVuY29kaW5nIHRoZSBxdWFsaXR5IGZlYXR1cmVzCjQuIG5vcm1hbGl6ZSBudW1lcmljIGZlYXR1cmUgZGlzdHJpYnV0aW9ucwo1LiBzdGFuZGFyZGl6aW5nIG51bWVyaWMgZmVhdHVyZXMgdG8gbWVhbiA9IDAsIHN0ZCBkZXYgPSAxCjYuIG9uZS1ob3QgZW5jb2RpbmcgcmVtYWluaW5nIGNhdGVnb3JpY2FsIGZlYXR1cmVzCgpfX05vdGVfXzogd2UncmUgdXNpbmcgdGhlIHJlY2lwZXMgcGFja2FnZSAoaHR0cHM6Ly90aWR5bW9kZWxzLmdpdGh1Yi5pby9yZWNpcGVzKQoKYGBge3J9CmJsdWVwcmludCA8LSByZWNpcGUoU2FsZV9QcmljZSB+IC4sIGRhdGEgPSBhbWVzX3RyYWluKSAlPiUKICBzdGVwX256dihhbGxfbm9taW5hbCgpKSAlPiUgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHN0ZXAgIzEKICBzdGVwX290aGVyKGFsbF9ub21pbmFsKCksIHRocmVzaG9sZCA9IC4wMSwgb3RoZXIgPSAib3RoZXIiKSAlPiUgICAjIHN0ZXAgIzIKICBzdGVwX2ludGVnZXIobWF0Y2hlcygiKFF1YWx8Q29uZHxRQ3xRdSkkIikpICU+JSAgICAgICAgICAgICAgICAgICAjIHN0ZXAgIzMKICBzdGVwX1llb0pvaG5zb24oYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUgICAgICAgICAgICAgICAjIHN0ZXAgIzQKICBzdGVwX2NlbnRlcihhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpICU+JSAgICAgICAgICAgICAgICAgICAjIHN0ZXAgIzUKICBzdGVwX3NjYWxlKGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lICAgICAgICAgICAgICAgICAgICAjIHN0ZXAgIzUKICBzdGVwX2R1bW15KGFsbF9ub21pbmFsKCksIC1hbGxfb3V0Y29tZXMoKSwgb25lX2hvdCA9IFRSVUUpICAgICAgICAjIHN0ZXAgIzYKCmJsdWVwcmludApgYGAKClRoaXMgbmV4dCBzdGVwIGNvbXB1dGVzIGFueSByZWxhdmVudCBpbmZvcm1hdGlvbiAobWVhbiBhbmQgc3RkIGRldmlhdGlvbiBvZgpudW1lcmljIGZlYXR1cmVzLCBuYW1lcyBvZiBvbmUtaG90IGVuY29kZWQgZmVhdHVyZXMpIG9uIHRoZSB0cmFpbmluZyBkYXRhIHNvCnRoZXJlIGlzIG5vIGluZm9ybWF0aW9uIGxlYWthZ2UgZnJvbSB0aGUgdGVzdCBkYXRhLgoKYGBge3J9CnByZXBhcmUgPC0gcHJlcChibHVlcHJpbnQsIHRyYWluaW5nID0gYW1lc190cmFpbikKcHJlcGFyZQpgYGAKCldlIGNhbiBub3cgdmVjdG9yaXplIG91ciB0cmFpbmluZyBhbmQgdGVzdCBkYXRhLiBJZiB5b3Ugc2Nyb2xsIHRocm91Z2ggdGhlIGRhdGEKeW91IHdpbGwgbm90aWNlIHRoYXQgYWxsIGZlYXR1cmVzIGFyZSBub3cgbnVtZXJpYyBhbmQgYXJlIGVpdGhlciAwLzEgKG9uZSBob3QKZW5jb2RlZCBmZWF0dXJlcykgb3IgaGF2ZSBtZWFuIDAgYW5kIGdlbmVyYWxseSByYW5nZSBiZXR3ZWVuIC0zIGFuZCAzLgoKYGBge3J9CmJha2VkX3RyYWluIDwtIGJha2UocHJlcGFyZSwgbmV3X2RhdGEgPSBhbWVzX3RyYWluKQpiYWtlZF90ZXN0IDwtIGJha2UocHJlcGFyZSwgbmV3X2RhdGEgPSBhbWVzX3Rlc3QpCgojIHVuaXQgdGVzdGluZyB0byBlbnN1cmUgYWxsIGNvbHVtbnMgYXJlIG51bWVyaWMKZXhwZWN0X2VxdWFsKG1hcF9sZ2woYmFrZWRfdHJhaW4sIH4gIWlzLm51bWVyaWMoLikpICU+JSBzdW0oKSwgMCkKZXhwZWN0X2VxdWFsKG1hcF9sZ2woYmFrZWRfdGVzdCwgfiAhaXMubnVtZXJpYyguKSkgJT4lIHN1bSgpLCAwKQoKYmFrZWRfdHJhaW4KYGBgCgpMYXN0bHksIHdlIG5lZWQgdG8gY3JlYXRlIHRoZSBmaW5hbCBmZWF0dXJlIGFuZCByZXNwb25zZSBvYmplY3RzIGZvciB0cmFpbiBhbmQgCnRlc3QgZGF0YS4gU2luY2UgX19rZXJhc19fIGFuZCBfX3RlbnNvcmZsb3dfXyByZXF1aXJlIG91ciBmZWF0dXJlcyAmIGxhYmVscyB0byBiZSAKc2VwZXJhdGUgb2JqZWN0cyB3ZSBuZWVkIHRvIHNlcGFyYXRlIHRoZW0uIEluIGRvaW5nIHNvLCBvdXIgZmVhdHVyZXMgbmVlZCB0byBiZSAKYSAyRCB0ZW5zb3Igd2hpY2ggaXMgd2h5IHdlIGFwcGx5IGBhcy5tYXRyaXhgIGFuZCBvdXIgcmVzcG9uc2UgbmVlZHMgdG8gYmUgYSAKdmVjdG9yIHdoaWNoIGlzIHdoeSB3ZSBhcHBseSBgcHVsbGAuCgpgYGB7cn0KeF90cmFpbiA8LSBzZWxlY3QoYmFrZWRfdHJhaW4sIC1TYWxlX1ByaWNlKSAlPiUgYXMubWF0cml4KCkKeV90cmFpbiA8LSBiYWtlZF90cmFpbiAlPiUgcHVsbChTYWxlX1ByaWNlKQoKeF90ZXN0IDwtIHNlbGVjdChiYWtlZF90ZXN0LCAtU2FsZV9QcmljZSkgJT4lIGFzLm1hdHJpeCgpCnlfdGVzdCA8LSBiYWtlZF90ZXN0ICU+JSBwdWxsKFNhbGVfUHJpY2UpCgojIHVuaXQgdGVzdGluZyB0byB4ICYgeSB0ZW5zb3JzIGhhdmUgc2FtZSBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zCmV4cGVjdF9lcXVhbChucm93KHhfdHJhaW4pLCBsZW5ndGgoeV90cmFpbikpCmV4cGVjdF9lcXVhbChucm93KHhfdGVzdCksIGxlbmd0aCh5X3Rlc3QpKQpgYGAKCk91ciBmaW5hbCBmZWF0dXJlIHNldCBub3cgaGFzIDE4OCBpbnB1dCB2YXJpYWJsZXM6CgpgYGB7cn0KZGltKHhfdHJhaW4pCmRpbSh4X3Rlc3QpCmBgYAoKIyBTdGVwIDI6IEJhbGFuY2UgYmF0Y2ggc2l6ZSB3aXRoIGEgZGVmYXVsdCBsZWFybmluZyByYXRlCgpUbyBnZXQgc3RhcnRlZCwgbGV0J3MgYnVpbGQgYSBzaW1wbGUgbW9kZWwgd2l0aC4uLgoKLSBhIHNpbmdsZSBsYXllciBtb2RlbCB3aXRoIDEyOCB1bml0cyBpbiB0aGUgaGlkZGVuIGxheWVyLiBXZSBoYXZlIDE4OCBmZWF0dXJlcwogIGFuZCAxIHJlc3BvbnNlIG5vZGUgc28gYSBnb29kIHN0YXJ0aW5nIHBvaW50IGlzIG1lYW4oYygxODgsIDEpKSBhbmQgdGhlbiByb3VuZAogIHVwIHRvIHRoZSBuZWFyZXN0IHZhbHVlIGluIHRoZSAkMl5zJCByYW5nZSAoaS5lLiAzMiwgNjQsIDEyOCwgMjU2LCA1MTIpLgotIGEgYmFzaWMgU0dEIG9wdGltaXplcgotIHVzZSBhIG1lYW4gc3F1YXJlIGxvZ2FyaXRobWljIGVycm9yICgibXNsZSIpCi0gYWxzbyB0cmFjayB0aGUgbWVhbiBhYnNvbHV0ZSBlcnJvciBtZXRyaWMgKCJtYWUiKQotIDIwJSB2YWxpZGF0aW9uIHNwbGl0CgpOb3csIHN0YXJ0IHdpdGggdGhlIGRlZmF1bHQgYmF0Y2ggc2l6ZSBvZiAzMiBhbmQgdGhlbiBjb21wYXJlIHdpdGggc21hbGxlcgp2YWx1ZXMgKGkuZS4gMTYpIGFuZCBsYXJnZXIgdmFsdWVzIChpLmUuIDEyOCkuIFlvdSdyZSBsb29raW5nIHRvIGJhbGFuY2UgdGhlCnByb2dyZXNzaW9uIG9mIHRoZSBsb3NzIGxlYXJuaW5nIGN1cnZlIGFuZCB0aGUgdHJhaW5pbmcgc3BlYWQuCgpfX0NvbW1lbnRfXzogVGhlIGRlZmF1bHQgYmF0Y2ggc2l6ZSBvZiAzMiBwZXJmb3JtcyBwcmV0dHkgd2VsbCBpbiB0aGlzIGNhc2UgYnV0CnlvdSBjb3VsZCd2ZSBlYXNpbHkgaGF2ZSBjaG9vc2VuIGxvd2VyICg4IG9yIDE2KSBvciBoaWdoZXIgKDY0LCAxMjgpIHdpdGhvdXQKbmVnYXRpdmUgaW1wYWN0cy4gV2UgY2FuIHNlZSB0aGF0IHRoZSBsb3NzIGlzIHN0aWxsIHRyZW5kaW5nIGRvd253YXJkIHNvIHdlCnNob3VsZCBoYXZlIGxvdHMgb2Ygcm9vbSBmb3IgaW1wcm92ZW1lbnQuCgpgYGB7cn0Kbl9mZWF0IDwtIG5jb2woeF90cmFpbikKCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMjgsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgICBvcHRpbWl6ZXIgPSAic2dkIiwKICAgIGxvc3MgPSAibXNsZSIsCiAgICBtZXRyaWNzID0gIm1hZSIKICApCgpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgeF90cmFpbiwKICB5X3RyYWluLAogIGJhdGNoX3NpemUgPSAzMiwKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCikKYGBgCgpgYGB7cn0KaGlzdG9yeQpgYGAKCmBgYHtyfQpwbG90KGhpc3RvcnkpCmBgYAoKIyBTdGVwIDM6IFR1bmUgdGhlIGFkYXB0aXZlIGxlYXJuaW5nIHJhdGUgb3B0aW1pemVyCgpOb3cgZ28gaGVhZCBhbmQgc3RhcnQgYXNzZXNzaW5nIGRpZmZlcmVudCBhZGFwdGl2ZSBsZWFybmluZyByYXRlcyBzdWNoIGFzOgoKLSBTR0QrbW9tZW50dW0KLSBSTVNwcm9wCi0gQWRhbXAKClRyeSBhIHZhcmlldHkgb2YgbGVhcm5pbmcgcmF0ZXMuIFJlY2FsbCB0aGF0IHdlIHR5cGljYWxseSBzdGFydCBhc3Nlc3NpbmcgcmF0ZXMKb24gYSBsb2dhcml0aG1pYyBzY2FsZSAoaS5lLiAwLjEsIDAuMDEsIC4uLiwgMC4wMDAxKS4KCl9fQ29tbWVudF9fOiBUaGUgZGVmYXVsdCBsZWFybmluZyByYXRlcyBvbiB0aGUgY29tbW9uIGFkYXB0aXZlIGxlYXJuaW5nIHJhdGUKb3B0aW1pemVycyBzaG93IGEgc2xvdyBwcm9ncmVzc2lvbiBkb3duIHRoZSBsb3NzIGN1cnZlIHNvIHdlIGNhbiBhZmZvcmQgdG8gdXNlCmEgbGFyZ2VyIGxlYXJuaW5nIHJhdGUuIEkgZm91bmQgdGhhdCB0aGUgUk1TcHJvcCB0ZW5kZWQgdG8gcHJvdmlkZSB0aGUgYmVzdApyZXN1bHRzIGF0IHRoaXMgcG9pbnQuCgpgYGB7cl19Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMjgsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbmNvbCh4X3RyYWluKSkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcChsciA9IDAuMSksCiAgICBsb3NzID0gIm1zbGUiLAogICAgbWV0cmljcyA9ICJtYWUiCiAgKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sCiAgeV90cmFpbiwKICBiYXRjaF9zaXplID0gMzIsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMgopCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKCiMgU3RlcCA0OiBBZGQgY2FsbGJhY2tzIHRvIGNvbnRyb2wgdHJhaW5pbmcKCkFkZCB0aGUgZm9sbG93aW5nIGNhbGxiYWNrcyBhbmQgc2VlIGlmIHlvdXIgcGVyZm9ybWFuY2UgaW1wcm92ZXM6CgotIGVhcmx5IHN0b3BwaW5nIHdpdGggYHBhdGllbmNlID0gM2AgYW5kIGBtaW5fZGVsdGEgPSAwLjAwMDAxYAotIGxlYXJuaW5nIHJhdGUgcmVkdWN0aW9uIHVwb24gYSBwbGF0ZWF1IHdpdGggYHBhdGllbmNlID0gMWAKCl9fQ29tbWVudF9fOiBBZGRpbmcgZWFybHkgc3RvcHBpbmcgaW1wcm92ZXMgcGVyZm9ybWFuY2UgYmVjYXVzZSB3ZSBjYW4gaW5jcmVhc2UKdGhlIGVwb2NocyBidXQgc3RvcCB3aGVuIG5lY2Vzc2FyeS4gTW9zdCBvZiBteSBvcHRpbWFsIG1vZGVscyB3ZXJlIHN0b3BwaW5nIGF0CmFyb3VuZCAxNSBlcG9jaHMgYXQgdGhpcyBwb2ludC4gQWxzbywgYWRkaW5nIGBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdSgpYAphbHNvIGltcHJvdmVzLiBJIHBsb3QgdGhlIGxlYXJuaW5nIHJhdGVzIGJ5IGVwb2NoIGJlbG93IGFuZCB3ZSBjYW4gc2VlIHRoYXQKdGhleSByZWR1Y2UgbXVsdGlwbGUgdGltZXMgd2hpY2ggYWxsb3dzIG91ciBtb2RlbCB0byBlZWsgb3V0IGEgbGl0dGxlIG1vcmUKcGVyZm9ybWFuY2UgaW1wcm92ZW1lbnRzLgoKYGBge3JdfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gMTI4LCBhY3RpdmF0aW9uID0gInJlbHUiLCBpbnB1dF9zaGFwZSA9IG5jb2woeF90cmFpbikpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSkKCm1vZGVsICU+JSBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjEpLAogICAgbG9zcyA9ICJtc2xlIiwKICAgIG1ldHJpY3MgPSAibWFlIgogICkKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLAogIHlfdHJhaW4sCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIGVwb2NocyA9IDMwLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgbWluX2RlbHRhID0gMC4wMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEpCiAgKQopCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKClBsb3R0aW5nIHRoZSBsZWFybmluZyByYXRlIHNob3dzIHRoYXQgaXQgcmVkdWNlZCBtdWx0aXBsZSB0aW1lcyBkdXJpbmcgdHJhaW5pbmc6CgpgYGB7cn0KcGxvdChoaXN0b3J5JG1ldHJpY3MkbHIpCmBgYAoKCiMgU3RlcCA1OiBFeHBsb3JlIG1vZGVsIGNhcGFjaXR5CgpOb3cgc3RhcnQgdG8gZXhwbG9yZSBkaWZmZXJlbnQgd2lkdGhzIGFuZCBkZXB0aHMgdG8geW91ciBtb2RlbC4KCi0gQXNzZXNzIGEgc2luZ2xlIGxheWVyIHdpdGggMTI4LCAyNTYsIDUxMiwgYW5kIDEwMjQgbm9kZXMKLSBBc3Nlc3MgMSwgMiwgYW5kIDMgaGlkZGVuIGxheWVycwoKX19Db21tZW50X186IEkgZm9sbG93IHRoZSBzYW1lIGFwcHJvYWNoIHdlIHVzZWQgaW4gdGhlIHByZXZpb3VzIG1vZHVsZSB0byAKYXNzZXNzIGNvbWJpbmF0aW9ucyBvZiBkaWZmZXJlbnQgbm9kZXMgYW5kIGhpZGRlbiBsYXllcnMuIEkgdXNlZCB0aGUgdGVuc29yYm9hcmQKY2FsbGJhY2sgdG8gc2F2ZSBteSBtb2RlbCBydW5zIGFuZCBhbmFseXplIHRoZW0uCgpgYGB7cn0KdHJhaW5fbW9kZWwgPC0gZnVuY3Rpb24obl91bml0cywgbl9sYXllcnMsIGxvZ190bykgewogIAogICMgQ3JlYXRlIGEgbW9kZWwgd2l0aCBhIHNpbmdsZSBoaWRkZW4gaW5wdXQgbGF5ZXIKICBtb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IG5fdW5pdHMsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbl9mZWF0KQogIAogICMgQWRkIGFkZGl0aW9uYWwgaGlkZGVuIGxheWVycyBiYXNlZCBvbiBpbnB1dAogIGlmIChuX2xheWVycyA+IDEpIHsKICAgIGZvciAoaSBpbiBzZXFfYWxvbmcobl9sYXllcnMgLSAxKSkgewogICAgICBtb2RlbCAlPiUgbGF5ZXJfZGVuc2UodW5pdHMgPSBuX3VuaXRzLCBhY3RpdmF0aW9uID0gInJlbHUiKQogICAgfQogIH0KICAKICAjIEFkZCBmaW5hbCBvdXRwdXQgbGF5ZXIKICBtb2RlbCAlPiUgbGF5ZXJfZGVuc2UodW5pdHMgPSAxKQogIAogICMgY29tcGlsZSBtb2RlbAogIG1vZGVsICU+JSBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjEpLAogICAgbG9zcyA9ICJtc2xlIiwKICAgIG1ldHJpY3MgPSAibWFlIgogICkKICAKICAjIHRyYWluIG1vZGVsIGFuZCBzdG9yZSByZXN1bHRzIHdpdGggY2FsbGJhY2tfdGVuc29yYm9hcmQoKQogIGhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLAogIHlfdHJhaW4sCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIGVwb2NocyA9IDMwLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgbWluX2RlbHRhID0gMC4wMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEpLAogICAgY2FsbGJhY2tfdGVuc29yYm9hcmQobG9nX2RpciA9IGxvZ190bykKICApLAogIHZlcmJvc2UgPSBGQUxTRQogICkKICAKICByZXR1cm4oaGlzdG9yeSkKfQpgYGAKCmBgYHtyfQpncmlkIDwtIGV4cGFuZF9ncmlkKAogIHVuaXRzID0gYygxMjgsIDI1NiwgNTEyLCAxMDI0KSwKICBsYXllcnMgPSBjKDE6MykKKSAlPiUKICBtdXRhdGUoaWQgPSBwYXN0ZTAoIm1scF8iLCBsYXllcnMsICJfbGF5ZXJzXyIsIHVuaXRzLCAiX3VuaXRzIikpCmdyaWQKYGBgCgpUaGUgaW5pdGlhbCByZXN1bHRzIGRvbid0IHNob3cgYW55IGdsYXJpbmcgdHJlbmRzLiBBbGwgb3VyIG1vZGVscyBoYXZlIGxvc3MKc2NvcmVzIHJhbmdpbmcgZnJvbSAwLjAxMzUtMC4wMTUuCgpgYGB7cn0KZm9yIChyb3cgaW4gc2VxX2xlbihucm93KGdyaWQpKSkgewogICMgZ2V0IHBhcmFtZXRlcnMKICB1bml0cyA8LSBncmlkW1tyb3csICJ1bml0cyJdXQogIGxheWVycyA8LSBncmlkW1tyb3csICJsYXllcnMiXV0KICBmaWxlX3BhdGggPC0gcGFzdGUwKCJhbWVzLyIsIGdyaWRbW3JvdywgImlkIl1dKQogIAogICMgcHJvdmlkZSBzdGF0dXMgdXBkYXRlCiAgY2F0KGxheWVycywgImhpZGRlbiBsYXllcihzKSB3aXRoIiwgdW5pdHMsICJuZXVyb25zOiAiKQogIAogICMgdHJhaW4gbW9kZWwKICBtIDwtIHRyYWluX21vZGVsKG5fdW5pdHMgPSB1bml0cywgbl9sYXllcnMgPSBsYXllcnMsIGxvZ190byA9IGZpbGVfcGF0aCkKICBtaW5fbG9zcyA8LSBtaW4obSRtZXRyaWNzJHZhbF9sb3NzLCBuYS5ybSA9IFRSVUUpCiAgCiAgIyB1cGRhdGUgc3RhdHVzIHdpdGggbG9zcwogIGNhdChtaW5fbG9zcywgIlxuIiwgYXBwZW5kID0gVFJVRSkKfQpgYGAKCkxvb2tpbmcgYXQgdGhlIHRlbnNvcmJvYXJkIHNob3dzIHRoYXQsIHJlYWxseSwgYW55IG9mIHRoZSBtb2RlbHMgYXJlIGRlY2VudApjaG9pY2VzIGFzIHRoZXkgYWxsIGhhdmUgcmVsYXRpdmVseSBzaW1pbGFyIHJlc3VsdHMsIGxvdyB2YXJpYW5jZSB3aGljaCBtZWFucwp0aGV5IGFsbCBhcmUgc3RhYmxlIG1vZGVscywgdGhleSBhbGwgaGF2ZSBtaW5pbWFsIG92ZXJmaXR0aW5nLCBhbmQgY29tcHV0ZSB0aW1lCmlzIGRlZmluaXRlbHkgbm90IGEgcHJvYmxlbS4KCmBgYHtyfQp0ZW5zb3Jib2FyZCgiYW1lcyIpCmBgYAoKX19Db21tZW50X186IEFmdGVyIGEgbGl0dGxlIG1vcmUgZXhwZXJpbWVudGluZyBJIGZvdW5kIGEgZnVubmVsIHNoYXBlZCBhcHByb2FjaGVkIHRlbmRlZCB0bwpwcm9kdWNlIGEgbGl0dGxlIG1vcmUgaW1wcm92ZW1lbnQgaW4gcGVyZm9ybWFuY2U6CgpgYGB7cn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbl9mZWF0KSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAyNTYsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSkKCm1vZGVsICU+JSBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjEpLAogICAgbG9zcyA9ICJtc2xlIiwKICAgIG1ldHJpY3MgPSAibWFlIgogICkKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLAogIHlfdHJhaW4sCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIGVwb2NocyA9IDMwLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgbWluX2RlbHRhID0gMC4wMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEpCiAgKQopCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KSArIHNjYWxlX3lfbG9nMTAoKQpgYGAKCiMgU3RlcCA2OiBSZWd1bGFyaXplIG92ZXJmaXR0aW5nCgpJZiB5b3VyIG1vZGVsIGlzIG92ZXJmaXR0aW5nLCB0cnkgdG8gYWRkLi4uCgotIHdlaWdodCBkZWNheSAoaS5lLiBga2VybmVsX3JlZ3VsYXJpemVyID0gcmVndWxhcml6ZXJfbDIobCA9IHh4eClgKS4gUmVtZW1iZXIsCiAgd2UgdHlwaWNhbGx5IHN0YXJ0IGJ5IGFzc2Vzc2luZyB2YWx1ZXMgb24gbG9nYXJpdGhtaWMgc2NhbGUgWzAuMSwgMC4wMDAwMV0uCi0gZHJvcG91dCAoYGxheWVyX2Ryb3BvdXQoKWApIGJldHdlZW4gZWFjaCBsYXllci4gUmVtZW1iZXIsIGRyb3BvdXQgcmF0ZXMKICB0eXBpY2FsbHkgcmFuZ2UgZnJvbSAyMC01MCUuCiAgCl9fQ29tbWVudF9fOiBQcmV0dHkgbXVjaCBhbnkgd2VpZ2h0IHJlZ3VsYXJpemVyIGh1cnQgbW9kZWwgcGVyZm9ybWFuY2UuIEluIHRoaXMKY2FzZSwgc2luY2Ugb3VyIHZhbGlkYXRpb24gbG9zcyBoYXMgbWluaW1hbCBvdmVyZml0dGluZyB3ZSBjYW4gcHJvYmFibHkgZGlzcmVnYXJkCmFueSBhZGRpdGlvbmFsIHJlZ3VsYXJpemF0aW9uLgogIApgYGB7cn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwMjQsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbl9mZWF0KSAlPiUKICBsYXllcl9kcm9wb3V0KDAuMikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2Ryb3BvdXQoMC4yKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDI1NiwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfZHJvcG91dCgwLjIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSkKCm1vZGVsICU+JSBjb21waWxlKAogICAgb3B0aW1pemVyID0gb3B0aW1pemVyX3Jtc3Byb3AobHIgPSAwLjEpLAogICAgbG9zcyA9ICJtc2xlIiwKICAgIG1ldHJpY3MgPSAibWFlIgogICkKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLAogIHlfdHJhaW4sCiAgYmF0Y2hfc2l6ZSA9IDMyLAogIGVwb2NocyA9IDMwLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgbWluX2RlbHRhID0gMC4wMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEpCiAgKQopCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgojIFN0ZXAgNzogUmVwZWF0IHN0ZXBzIDEtNgoKQXMgdGhpcyBwb2ludCB3ZSBjb3VsZCByZXBlYXQgdGhlIHByb2Nlc3MgYW5kLi4uCgoxLiBQcmVwYXJlIGRhdGEKICAgLSB0cnkgdG8gZmluZCBhZGRpdGlvbmFsIGRhdGEgdG8gYWRkCiAgIC0gdHJ5IG5ldyBmZWF0dXJlIGVuZ2luZWVyaW5nIGFwcHJvYWNoZXMKMi4gQmFsYW5jZSBiYXRjaCBzaXplIHdpdGggYSBkZWZhdWx0IGxlYXJuaW5nIHJhdGUKICAgLSByZWFzc2VzcyBiYXRjaCBzaXplCjMuIFR1bmUgdGhlIGFkYXB0aXZlIGxlYXJuaW5nIHJhdGUgb3B0aW1pemVyCiAgIC0gZmluZSB0dW5lIG91ciBsZWFybmluZyByYXRlCiAgIC0gc2VlIGlmIHRoZSBjdXJyZW50IG9wdGltaXplciBzdGlsbCBvdXRwZXJmb3JtcyBvdGhlcnMKNC4gQWRkIGNhbGxiYWNrcyB0byBjb250cm9sIHRyYWluaW5nCiAgIC0gbWF5YmUgYXNzZXNzIG1vcmUgc29waGlzdGljYXRlZCBsZWFybmluZyByYXRlIHNjaGVkdWxlcnMgKGkuZS4gY3ljbGljYWwKICAgICBsZWFybmluZyByYXRlcykKNS4gRXhwbG9yZSBtb2RlbCBjYXBhY2l0eQogICAtIGFmdGVyIHNvbWUgdHdlYWtzIHdlIG1heSB3YW50IHRvIHJlYXNzZXNzIG1vZGVsIGNhcGFjaXR5IGNvbWJpbmF0aW9ucwo2LiBSZWd1bGFyaXplIG92ZXJmaXR0aW5nCgpb8J+PoF0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8tY29uZi0yMDIwL2RsLWtlcmFzLXRmKQ==