This module is designed to get you thinking about how to strategically approach training a deep learning (DL) model. Although finding optimal hyperparameters for a DL model includes a lot of artistry and patience, using a methodical approach can reduce overall model exploratory time.

The following steps provide a good mental model for tuning our model. Realize this approach does not guarantee you’ll find the optimal model; however, it should give you a higher probability of finding a near optimal one.

  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

We’ll demonstrate with one of the most famous benchmark data sets, MNIST. We’ll continue working with a multi-layer perceptron (MLP); however, realize that these steps also translate to other DL models (i.e. CNNs, RNNs, LSTMs).

Package Requirements

library(keras)       # for modeling
library(tidyverse)   # for wrangling & visualization
library(glue)        # for string literals

MNIST

keras has many built in data sets (or functions to automatically install data sets). Check out the available datasets with dataset_ + tab.

We’re going to use the MNIST data set which is the “hello world” for learning deep learning! ℹ️

mnist <- dataset_mnist()
str(mnist)
List of 2
 $ train:List of 2
  ..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
  ..$ y: int [1:60000(1d)] 5 0 4 1 9 2 1 3 1 4 ...
 $ test :List of 2
  ..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
  ..$ y: int [1:10000(1d)] 7 2 1 0 4 1 4 9 5 9 ...

Our training images (aka features) are stored as 3D arrays

  • 60,000 images consisting of a…
  • 28x28 matrix with…
  • values ranging from 0-255 representing gray scale pixel values.
# 60K images of 28x28 pixels
dim(mnist$train$x)
[1] 60000    28    28
# pixel values are gray scale ranging from 0-255
range(mnist$train$x)
[1]   0 255

Check out the first digit

digit <- mnist$train$x[1,,]
digit

Lets plot the first digit and compare to the above matrix

plot(as.raster(digit, max = 255))

Now lets check out the first 100 digits

par(mfrow = c(10, 10), mar = c(0,0,0,0))
for (i in 1:100) {
  plot(as.raster(mnist$train$x[i,,], max = 255))
}

Prepare Data

When we work with keras:

  • training and test sets need to be independent
  • features and labels (aka target, response) need to be independent
  • use %<-% for object unpacking (see ?zeallot::%<-%)
c(c(train_images, train_labels), c(test_images, test_labels)) %<-% mnist

# the above is the same as
# train_images <- mnist$train$x
# train_labels <- mnist$train$y
# test_images <- mnist$test$x
# test_labels <- mnist$test$y

Shape into proper tensor form

The shape of our data is dependent on the type of DL model we are training. MLPs require our data to be in a 2D tensor (aka matrix); however, our data are currently in a 3D tensor.

We can reshape our tensor from 3D to 2D. Much like a matrix can be flattened to a vector:

m <- matrix(1:9, ncol = 3)
m
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9
# flattened matrix
as.vector(m)
[1] 1 2 3 4 5 6 7 8 9

We can reshape a 3D array to a 2D array with array_reshape()

# reshape 3D tensor (aka array) to a 2D tensor (aka matrix)
train_images <- array_reshape(train_images, c(60000, 28 * 28))
test_images <- array_reshape(test_images, c(10000, 28 * 28))

# our training data is now a matrix with 60K observations and
# 784 features (28 pixels x 28 pixels = 784)
str(train_images)
 int [1:60000, 1:784] 0 0 0 0 0 0 0 0 0 0 ...

Since we are dealing with a multi-classification problem where the target ranges from 0-9, we’ll reformat with to_categorical().

Note: column 1 refers to the digit “0”, column 2 refers to the digit “1”, etc.

train_labels <- to_categorical(train_labels)
test_labels <- to_categorical(test_labels)

head(train_labels)
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    0    0    0    0    0    1    0    0    0     0
[2,]    1    0    0    0    0    0    0    0    0     0
[3,]    0    0    0    0    1    0    0    0    0     0
[4,]    0    1    0    0    0    0    0    0    0     0
[5,]    0    0    0    0    0    0    0    0    0     1
[6,]    0    0    1    0    0    0    0    0    0     0

Stabilize learning by data scaling

When applying DL models, 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.

When features have large or widely varying values, large gradient updates can be triggered that will prevent the network from converging

Tips:

  1. When all features have the same value range (i.e. images), we can standardize values between 0-1.

  2. When features varying in range from one another (i.e. age, height, longitude) normalize each feature to have mean of 0 and standard deviation of 1 (?scale())

# all our features (pixels) range from 0-255
range(train_images)
[1]   0 255
# standardize train and test features
train_images <- train_images / 255
test_images <- test_images / 255

Randomize data

Although I know that this data is not ordered, we should always get in the habit of randomizing our data so that our train and validation datasets are properly represented.

obs <- nrow(train_images)
set.seed(123)
randomize <- sample(seq_len(obs), size = obs, replace = FALSE)
train_images <- train_images[randomize, ]
train_labels <- train_labels[randomize, ]

We’re finally ready to train some DL models!!

Balance Batch Size & Default Learning Rate

The learning rate is the most important hyperparameter to get right but before I tuning it, I like to find a batch size that balances:

  • smoothness of the learning curve
  • speed of training

Tips: start with…

  • a default learning rate,
  • a single hidden layer with units set to around mean(n_features + n_response_classes),
  • the default batch size of 32,
  • then test ranges of batch sizes [16, 32, 64, 128, 256, 512].
# get number of features
n_feat <- ncol(train_images)

# 1. Define model architecture
model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = 'relu', input_shape = n_feat) %>%
  layer_dense(units = 10, activation = 'softmax')

# 2. Define how our model is going to learn
model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = "sgd",
  metrics = "accuracy"
)

summary(model)
Model: "sequential_15"
________________________________________________________________________________________
Layer (type)                           Output Shape                       Param #       
========================================================================================
dense_30 (Dense)                       (None, 512)                        401920        
________________________________________________________________________________________
dense_31 (Dense)                       (None, 10)                         5130          
========================================================================================
Total params: 407,050
Trainable params: 407,050
Non-trainable params: 0
________________________________________________________________________________________

Let’s train our model with the default batch size of 32.

# 3. Train our model
history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2
  )
history
Trained on 48,000 samples (batch_size=32, epochs=10)
Final epoch (plot to see history):
        loss: 0.1697
    accuracy: 0.9531
    val_loss: 0.178
val_accuracy: 0.9496 
plot(history)

Your turn! (3min)

Now retrain the same model but try with batch sizes of 16, 128, 512 while leaving everything else the same. How do the results (loss, accuracy, and compute time) compare?

# define model architecture
model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = 'relu', input_shape = n_feat) %>%
  layer_dense(units = 10, activation = 'softmax')

# define how the model is gonig to learn
model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = "sgd",
  metrics = "accuracy"
  )

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  ____ = ____
  )

Pick an Adaptive Learning Rate Optimizer

Once we’ve picked an adequate batch size, next we want to start tuning the learning rate. There are two main considerations:

  1. We usually always assess different learning rates ranging from [1e-1, 1e-7].
  2. Adaptive learning rate optimizers nearly always outperform regular SGD

Adaptive learning rates help to escape “saddle points”.

Tips: the primary adaptive learning rates include:

  • SGD with momentum ?optimizer_sgd().
  • RMSprop (?optimizer_rmsprop()) which adds an exponentially decaying average of squared gradients –> similar effect as momentum.
  • Adam (?optimizer_adam()) which is RMSprop + momentum
  • Each has their own default learning rates so tune relative to those values
  • See https://ruder.io/optimizing-gradient-descent/ for more details

Your turn! (5min)

Retrain our DL model using:

  • SGD with momentum (optimizer_sgd())
    • Try default learning rate (0.01) with momentum (0.5, 0.9, 0.99)
    • Try larger learning rates (0.1, 1) with momentum
  • RMSprop (optimizer_rmsprop())
    • What is the default learning rate? Train with default.
    • Try a larger learning rate. What happens?
  • Adam (optimizer_adam())
    • What is the default learning rate? Train with default.
    • Try a larger learning rate. What happens?
model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = 'relu', input_shape = n_feat) %>%
  layer_dense(units = 10, activation = 'softmax')

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = ____,
  metrics = "accuracy"
  )

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128
  )

Add Callbacks

When training a model, sometimes we want to:

  • automatically stop a model once performance has stopped improving
  • dynamically adjust values of certain parameters (i.e. learning rate)
  • log model information to use or visualize later on
  • continually save the model during training and save the model with the best performance

Keras provides a suite of tools called _callbacks that help us to monitor, control, and customize the training procedure.

Stop training at the right time

We often don’t know how many epochs we’ll need to reach a minimum loss. The early stopping callback allows us to automatically stop training after we experience no improvement in our loss after patience number of epochs.

Tip:

  • If you are going to use the model after training you always want to retain the “best” model, which is the model with the lowest loss. restore_best_weights will restore the weights for this “best” model even after you’ve passed it by n epochs.

  • Sometimes your model will stall on a low validation loss resulting in a tie. Even with early stopping the model will continue to train for all the epochs. min_delta allows you to state some small value that the loss must improve by otherwise it will stop.

model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = 'relu', input_shape = n_feat) %>%
  layer_dense(units = 10, activation = 'softmax')

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = callback_early_stopping(patience = 3, restore_best_weights = TRUE,
                                     min_delta = 0.0001)
  )
history
Trained on 48,000 samples (batch_size=128, epochs=14)
Final epoch (plot to see history):
        loss: 0.002039
    accuracy: 1
    val_loss: 0.06845
val_accuracy: 0.983 

Note how we did not train for all the epochs…only until our loss didn’t improve by 0.0001 for 3 consistent epochs.

plot(history)

Add a learning rate scheduler

Although adaptive learning rate optimizers adjust the velocity of weight updates depending on the loss surface, we can also incorporate additional ways to modify the learning rate “on the fly”.

Options:

  • Learning rate decay reduces the learning rate at each epoch. Note the decay argument in ?optimizer_xxx().

  • There has been some great research on cyclical learning rates (see https://arxiv.org/abs/1506.01186). You can incorporate custom learning rates such as this with callback_learning_rate_scheduler(). This is more advanced but definitely worth reading up on post-workshop.

  • A simpler, and very practical approach, is to reduce the learning rate after the model has stopped improving with callback_reduce_lr_on_plateau(). This approach allows the model to tell us when to reduce the learning rate.

Tip: When using decay or callback_reduce_lr_on_plateau(), you can usually increase the base learning rate so that you learn quickly early on and then the learning rate will reduce so you can take smaller steps as you approach the minimum loss.

model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = 'relu', input_shape = n_feat) %>%
  layer_dense(units = 10, activation = 'softmax')

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = list(
    callback_early_stopping(patience = 3, restore_best_weights = TRUE, min_delta = 0.0001),
    callback_reduce_lr_on_plateau(patience = 1, factor = 0.1)
    )
  )
history
Trained on 48,000 samples (batch_size=128, epochs=13)
Final epoch (plot to see history):
        loss: 0.00732
    accuracy: 0.9994
    val_loss: 0.06091
val_accuracy: 0.983
          lr: 0.0001 
plot(history)

plot(history$metrics$lr)

Explore Model Capacity

Now that we have found a pretty good learning rate and we have good control over our training procedure with callbacks, we can start to assess how the capacity of our model effects performance.

Recall that we control capacity two ways:

  • width: the number of units in a hidden layer
  • depth: the number of hidden layers

Your turn! (5min)

Explore different model capacities while all other parameters constant. Try increasing

  • Width: remember that the number of units is usually a power of 2 (i.e. 32, 64, 128, 256, 512, 1024).
  • Depth: try using 2 or 3 hidden layers
model <- keras_model_sequential() %>%
  ___________ %>%
  layer_dense(units = 10, activation = 'softmax')

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = list(
    callback_early_stopping(patience = 3, restore_best_weights = TRUE, min_delta = 0.0001),
    callback_reduce_lr_on_plateau(patience = 1, factor = 0.1)
    )
  )

Smart experimenting

As we start to experiment more, it becomes harder to organize and compare our results. Let’s make it more efficient by:

  • Creating a function that allows us to dynamically change the number of layers and units.
  • Using callback_tensorboard() which allows us to save and visually compare results.

Tip: there is another approach to performing grid searches. See the extras notebook https://rstudio-conf-2020.github.io/dl-keras-tf/notebooks/imdb-grid-search.nb.html for details.

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 = 10, activation = "softmax")
  
  # compile model
  model %>% compile(
    loss = "categorical_crossentropy",
    optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
    metrics = "accuracy"
  )
  
  # train model and store results with callback_tensorboard()
  history <- model %>% fit(
    train_images, train_labels,
    validation_split = 0.2,
    batch_size = 128,
    epochs = 20,
    callback = list(
      callback_early_stopping(patience = 3, restore_best_weights = TRUE, min_delta = 0.0001),
      callback_reduce_lr_on_plateau(patience = 1, factor = 0.1),
      callback_tensorboard(log_dir = log_to)
      ),
    verbose = FALSE
    )
  
  return(history)
  }

Now we can create a grid for various model capacities. We include an ID for each model, which we will use to save our results within callback_tensorboard().

grid <- expand_grid(
  units = c(128, 256, 512, 1024),
  layers = c(1:3)
) %>%
  mutate(id = paste0("mlp_", layers, "_layers_", units, "_units"))
grid

Now we can loop through each model capacity combination and train our models. This will take a few minutes so this is a good time to go to the 🚽.

for (row in seq_len(nrow(grid))) {
  # get parameters
  units <- grid[[row, "units"]]
  layers <- grid[[row, "layers"]]
  file_path <- paste0("mnist/", 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)
}
1 hidden layer(s) with 128 neurons: 0.07463152 
2 hidden layer(s) with 128 neurons: 0.07082343 
3 hidden layer(s) with 128 neurons: 0.07171181 
1 hidden layer(s) with 256 neurons: 0.06331765 
2 hidden layer(s) with 256 neurons: 0.06400301 
3 hidden layer(s) with 256 neurons: 0.06468146 
1 hidden layer(s) with 512 neurons: 0.06147339 
2 hidden layer(s) with 512 neurons: 0.06152161 
3 hidden layer(s) with 512 neurons: 0.06155642 
1 hidden layer(s) with 1024 neurons: 0.06003707 
2 hidden layer(s) with 1024 neurons: 0.0598318 
3 hidden layer(s) with 1024 neurons: 0.05883318 

Our results suggest that larger width models tend to perform better but it is unclear if deeper models add much benefit. However, to get more clarity we can analyze the learning rates with tensorboard().

Note that callback_tensorboard() saved all the model runs in /mnist subdirectory.

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

Your turn! (3 min)

Retrain the model with:

  • 2 hidden layers
  • 1024 units in each hidden layer
  • early stopping after 3 epochs of no improvement
  • reduce learning rate after 1 epoch of no improvement
model <- keras_model_sequential() %>%
  ___________ %>%
  ___________

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = list(
    callback______(patience = _____, restore_best_weights = TRUE, min_delta = 0.0001),
    callback______(patience = _____, factor = 0.1)
    )
  )

Regularize Overfitting

Often, once we’ve found a model that minimizes the loss, there is still some overfitting that is occuring…sometimes a lot. So our next objective is to try flatten the validation loss learning curve and bring it as close to the training loss curve as possible.

Weight decay

A common way to mitigate overfitting is to put constraints on the complexity of a network by forcing its weights to take on small values, which makes the distribution of weight values more regular. This is called weight regularization and its done by adding to the loss function of the network a cost associated with having large weights.

If you a familiar with regularized regression ℹ️ (lasso, ridge, elastic nets) then weight regularization is essentially the same thing. ℹ️

\[Loss = MSE + \lambda \sum^p_{j=1} w^2_j\]

Tips:

  • Although you can use L1, L2 or a combination, L2 is by far the most common and is known as weight decay in the context of neural nets.
  • Optimal values vary but when tuning we typically start with factors of \(10^{-s}\) where s ranges between 1-4 (0.1, 0.01, …, 0.0001).
  • The larger the weight regularizer, the more epochs generally required to reach a minimum loss.
  • Weight decay can cause a noisier learning curve so its often beneficial to increase the patience parameter for early stopping if this is noticable.
model <- keras_model_sequential() %>%
  layer_dense(
    units = 512, activation = "relu", input_shape = n_feat,
    kernel_regularizer = regularizer_l2(l = 0.001)    # regularization parameter
    ) %>%
  layer_dense(
    units = 512, activation = "relu",
    kernel_regularizer = regularizer_l2(l = 0.001)    # regularization parameter
    ) %>%
  layer_dense(units = 10, activation = "softmax")

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = list(
    callback_early_stopping(patience = 3, restore_best_weights = TRUE, min_delta = 0.0001),
    callback_reduce_lr_on_plateau(patience = 1, factor = 0.1)
    )
  )
history
Trained on 48,000 samples (batch_size=128, epochs=16)
Final epoch (plot to see history):
        loss: 0.02869
    accuracy: 0.9907
    val_loss: 0.0621
val_accuracy: 0.9835
          lr: 0.000001 
plot(history)

Dropout

Dropout is one of the most effective and commonly used regularization techniques for neural networks. Dropout applied to a layer randomly drops out (sets to zero) a certain percentage of the output features of that layer. By randomly dropping some of a layer’s outputs we minimize the chance of fitting patterns to noise in the data, a common cause of overfitting.

Tip:

  • Dropout rates typically ranges between 0.2-0.5. Sometimes higher rates are necessary but note that you will get a warning when supplying rate > 0.5.
  • The higher the dropout rate, the slower the convergence so you may need to increase the number of epochs.
  • Its common to apply dropout after each hidden layer and with the same rate; however, this is not necessary.
model <- keras_model_sequential() %>%
  layer_dense(units = 512, activation = "relu", input_shape = n_feat) %>%
  layer_dropout(0.3) %>%                            # regularization parameter
  layer_dense(units = 512, activation = "relu") %>%
  layer_dropout(0.3) %>%                           # regularization parameter
  layer_dense(units = 10, activation = "softmax")

model %>% compile(
  loss = "categorical_crossentropy",
  optimizer = optimizer_sgd(lr = 0.1, momentum = 0.9),
  metrics = "accuracy"
)

history <- model %>% fit(
  train_images, train_labels,
  validation_split = 0.2,
  batch_size = 128,
  epochs = 20,
  callback = list(
    callback_early_stopping(patience = 3, restore_best_weights = TRUE, min_delta = 0.0001),
    callback_reduce_lr_on_plateau(patience = 1, factor = 0.1)
    )
  )
history
Trained on 48,000 samples (batch_size=128, epochs=14)
Final epoch (plot to see history):
        loss: 0.02807
    accuracy: 0.9909
    val_loss: 0.06096
val_accuracy: 0.9829
          lr: 0.00001 
plot(history)

Repeat

At this point, we have a pretty good model. However, often, iterating over these steps can improve model performance even further. For brevity, we’ll act as if we have found a sufficient solution.

Evaluate results

Once a final model is chosen, we can evaluate the model on our test set to provide us with a accurate expectation of our generalization error. Our goal is that our test error is very close to our validation error.

model %>% evaluate(test_images, test_labels, verbose = FALSE)
$loss
[1] 0.059514

$accuracy
[1] 0.9823

Confusion matrix

To understand our model’s performance across the different response classes, we can assess a confusion matrix ℹ️.

First, we need to predict our classes and also get the actual response values.

predictions <- model %>% predict_classes(test_images)
actual <- mnist$test$y

We can see the number of missed predictions in our test set

missed_predictions <- sum(predictions != actual)
missed_predictions
[1] 177

We can use caret::confusionMatrix() to get our confusion matrix. We can see which digits our model confuses the most by analyzing the confusion matrix.

  • 9s are often confused with 4s
  • 6s are often confused with 0s & 5s
  • etc.
caret::confusionMatrix(factor(predictions), factor(actual))
Confusion Matrix and Statistics

          Reference
Prediction    0    1    2    3    4    5    6    7    8    9
         0  971    0    3    0    2    2    6    1    4    1
         1    0 1126    0    0    0    0    2    1    0    2
         2    0    3 1014    3    0    0    0    7    1    0
         3    0    1    4  997    0   10    1    2    2    5
         4    0    0    1    0  966    0    6    1    1    9
         5    0    1    0    2    0  867    5    0    4    1
         6    2    2    2    0    5    5  935    0    2    2
         7    1    0    5    3    1    1    0 1009    3    4
         8    5    2    3    2    0    4    3    3  954    1
         9    1    0    0    3    8    3    0    4    3  984

Overall Statistics
                                          
               Accuracy : 0.9823          
                 95% CI : (0.9795, 0.9848)
    No Information Rate : 0.1135          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9803          
                                          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: 0 Class: 1 Class: 2 Class: 3 Class: 4 Class: 5 Class: 6
Sensitivity            0.9908   0.9921   0.9826   0.9871   0.9837   0.9720   0.9760
Specificity            0.9979   0.9994   0.9984   0.9972   0.9980   0.9986   0.9978
Pos Pred Value         0.9808   0.9956   0.9864   0.9755   0.9817   0.9852   0.9791
Neg Pred Value         0.9990   0.9990   0.9980   0.9986   0.9982   0.9973   0.9975
Prevalence             0.0980   0.1135   0.1032   0.1010   0.0982   0.0892   0.0958
Detection Rate         0.0971   0.1126   0.1014   0.0997   0.0966   0.0867   0.0935
Detection Prevalence   0.0990   0.1131   0.1028   0.1022   0.0984   0.0880   0.0955
Balanced Accuracy      0.9944   0.9958   0.9905   0.9922   0.9909   0.9853   0.9869
                     Class: 7 Class: 8 Class: 9
Sensitivity            0.9815   0.9795   0.9752
Specificity            0.9980   0.9975   0.9976
Pos Pred Value         0.9825   0.9765   0.9781
Neg Pred Value         0.9979   0.9978   0.9972
Prevalence             0.1028   0.0974   0.1009
Detection Rate         0.1009   0.0954   0.0984
Detection Prevalence   0.1027   0.0977   0.1006
Balanced Accuracy      0.9898   0.9885   0.9864

Visualize missed predictions

We can also visualize this with the following:

tibble(
  actual,
  predictions
  ) %>% 
  filter(actual != predictions) %>%
  count(actual, predictions) %>%
  mutate(perc = n / n() * 100) %>% 
  filter(n > 1) %>% 
  ggplot(aes(actual, predictions, size = n)) +
  geom_point(shape = 15, col = "#9F92C6") +
  scale_x_continuous("Actual Target", breaks = 0:9) +
  scale_y_continuous("Prediction", breaks = 0:9) +
  scale_size_area(breaks = c(2, 5, 10, 15), max_size = 5) +
  coord_fixed() +
  ggtitle(paste(missed_predictions, "mismatches")) +
  theme(panel.grid.minor = element_blank()) +
  labs(caption = 'Adapted from Rick Scavetta')

Visualize missed predictions

Lastly, lets check out those mispredicted digits.

missed <- which(predictions != actual)
plot_dim <- ceiling(sqrt(length(missed)))

par(mfrow = c(plot_dim, plot_dim), mar = c(0,0,0,0))
for (i in missed) {
  plot(as.raster(mnist$test$x[i,,], max = 255))
}

If we look at the predicted vs actual we can reason about why our model mispredicted some of the digits.

par(mfrow = c(4, 4), mar = c(0,0,2,0))

for (i in missed[1:16]) {
  plot(as.raster(mnist$test$x[i,,], max = 255)) 
  title(main = paste("Predicted:", predictions[i]))
}

Key takeaways

Follow these steps and guidelines when tuning your DL model:

  1. Prepare data
    • data needs to be shaped into the right tensor dimensions
    • data should be scaled so they don’t trigger exploding gradients
  2. Balance batch size with a default learning rate so that…
    • your learning curve is not too noisy
    • the training compute time is sufficient
  3. Tune the adaptive learning rate optimizer
    • compare SGD+momentum with RMSprop & Adam
    • assess learning rates on log scale between [1e-1, 1e-7]
  4. Add callbacks to control training
    • use early stopping for more efficient training
    • use an adaptive learning rate callback to have more control of the learning rate
  5. Explore model capacity
    • compare adding width vs depth (consider loss vs. training time)
    • be smart with your experiments (use tensorboard callback!)
  6. Regularize overfitting
    • weight decay controls magnitude of weights; start by assessing values btwn 0.1, 0.01, …, 0.0001
    • dropout minimizes happenstance patterns from noise; typical values range from 0.2-0.5.
  7. Iterate, iterate, iterate!

🏠

LS0tCnRpdGxlOiAiQSBTdGFydGVyIFJlY2lwZSIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB0cnVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9taW5pbWFsKCkpCmBgYAoKVGhpcyBtb2R1bGUgaXMgZGVzaWduZWQgdG8gZ2V0IHlvdSB0aGlua2luZyBhYm91dCBob3cgdG8gc3RyYXRlZ2ljYWxseSBhcHByb2FjaAp0cmFpbmluZyBhIGRlZXAgbGVhcm5pbmcgKERMKSBtb2RlbC4gQWx0aG91Z2ggZmluZGluZyBvcHRpbWFsIGh5cGVycGFyYW1ldGVycwpmb3IgYSBETCBtb2RlbCBpbmNsdWRlcyBhIGxvdCBvZiBhcnRpc3RyeSBhbmQgcGF0aWVuY2UsIHVzaW5nIGEgbWV0aG9kaWNhbAphcHByb2FjaCBjYW4gcmVkdWNlIG92ZXJhbGwgbW9kZWwgZXhwbG9yYXRvcnkgdGltZS4gCgpUaGUgZm9sbG93aW5nIHN0ZXBzIHByb3ZpZGUgYSBnb29kIG1lbnRhbCBtb2RlbCBmb3IgdHVuaW5nIG91ciBtb2RlbC4gUmVhbGl6ZQp0aGlzIGFwcHJvYWNoIGRvZXMgbm90IGd1YXJhbnRlZSB5b3UnbGwgZmluZCB0aGUgb3B0aW1hbCBtb2RlbDsgaG93ZXZlciwgaXQKc2hvdWxkIGdpdmUgeW91IGEgaGlnaGVyIHByb2JhYmlsaXR5IG9mIGZpbmRpbmcgYSBuZWFyIG9wdGltYWwgb25lLgoKMS4gUHJlcGFyZSBkYXRhCjIuIEJhbGFuY2UgYmF0Y2ggc2l6ZSB3aXRoIGEgZGVmYXVsdCBsZWFybmluZyByYXRlCjMuIFR1bmUgdGhlIGFkYXB0aXZlIGxlYXJuaW5nIHJhdGUgb3B0aW1pemVyCjQuIEFkZCBjYWxsYmFja3MgdG8gY29udHJvbCB0cmFpbmluZwo1LiBFeHBsb3JlIG1vZGVsIGNhcGFjaXR5CjYuIFJlZ3VsYXJpemUgb3ZlcmZpdHRpbmcKNy4gUmVwZWF0IHN0ZXBzIDEtNgo4LiBFdmFsdWF0ZSBmaW5hbCBtb2RlbCByZXN1bHRzCgpXZSdsbCBkZW1vbnN0cmF0ZSB3aXRoIG9uZSBvZiB0aGUgbW9zdCBmYW1vdXMgYmVuY2htYXJrIGRhdGEgc2V0cywgTU5JU1QuIFdlJ2xsCmNvbnRpbnVlIHdvcmtpbmcgd2l0aCBhIG11bHRpLWxheWVyIHBlcmNlcHRyb24gKE1MUCk7IGhvd2V2ZXIsIHJlYWxpemUgdGhhdAp0aGVzZSBzdGVwcyBhbHNvIHRyYW5zbGF0ZSB0byBvdGhlciBETCBtb2RlbHMgKGkuZS4gQ05OcywgUk5OcywgTFNUTXMpLgoKIyBQYWNrYWdlIFJlcXVpcmVtZW50cwoKYGBge3J9CmxpYnJhcnkoa2VyYXMpICAgICAgICMgZm9yIG1vZGVsaW5nCmxpYnJhcnkodGlkeXZlcnNlKSAgICMgZm9yIHdyYW5nbGluZyAmIHZpc3VhbGl6YXRpb24KbGlicmFyeShnbHVlKSAgICAgICAgIyBmb3Igc3RyaW5nIGxpdGVyYWxzCmBgYAoKIyBNTklTVAoKYGtlcmFzYCBoYXMgbWFueSBidWlsdCBpbiBkYXRhIHNldHMgKG9yIGZ1bmN0aW9ucyB0byBhdXRvbWF0aWNhbGx5IGluc3RhbGwgZGF0YQpzZXRzKS4gQ2hlY2sgb3V0IHRoZSBhdmFpbGFibGUgZGF0YXNldHMgd2l0aCBgZGF0YXNldF9gICsgdGFiLgoKV2UncmUgZ29pbmcgdG8gdXNlIHRoZSBfX01OSVNUX18gZGF0YSBzZXQgd2hpY2ggaXMgdGhlICJoZWxsbyB3b3JsZCIgZm9yIApsZWFybmluZyBkZWVwIGxlYXJuaW5nISBb4oS577iPXShodHRwOi8veWFubi5sZWN1bi5jb20vZXhkYi9tbmlzdC8pCgpgYGB7ciBkYXRhfQptbmlzdCA8LSBkYXRhc2V0X21uaXN0KCkKc3RyKG1uaXN0KQpgYGAKCk91ciB0cmFpbmluZyBpbWFnZXMgKGFrYSBmZWF0dXJlcykgYXJlIHN0b3JlZCBhcyAzRCBhcnJheXMKCiogNjAsMDAwIGltYWdlcyBjb25zaXN0aW5nIG9mIGEuLi4KKiAyOHgyOCBtYXRyaXggd2l0aC4uLgoqIHZhbHVlcyByYW5naW5nIGZyb20gMC0yNTUgcmVwcmVzZW50aW5nIGdyYXkgc2NhbGUgcGl4ZWwgdmFsdWVzLgoKYGBge3IgZmVhdHVyZXN9CiMgNjBLIGltYWdlcyBvZiAyOHgyOCBwaXhlbHMKZGltKG1uaXN0JHRyYWluJHgpCgojIHBpeGVsIHZhbHVlcyBhcmUgZ3JheSBzY2FsZSByYW5naW5nIGZyb20gMC0yNTUKcmFuZ2UobW5pc3QkdHJhaW4keCkKYGBgCgpDaGVjayBvdXQgdGhlIGZpcnN0IGRpZ2l0CgpgYGB7ciBmaXJzdC1kaWdpdH0KZGlnaXQgPC0gbW5pc3QkdHJhaW4keFsxLCxdCmRpZ2l0CmBgYAoKTGV0cyBwbG90IHRoZSBmaXJzdCBkaWdpdCBhbmQgY29tcGFyZSB0byB0aGUgYWJvdmUgbWF0cml4CgpgYGB7ciBwbG90LWZpcnN0LWRpZ2l0fQpwbG90KGFzLnJhc3RlcihkaWdpdCwgbWF4ID0gMjU1KSkKYGBgCgpOb3cgbGV0cyBjaGVjayBvdXQgdGhlIGZpcnN0IDEwMCBkaWdpdHMKCmBgYHtyIHBsb3QtZmlyc3QtMTAwLWRpZ2l0c30KcGFyKG1mcm93ID0gYygxMCwgMTApLCBtYXIgPSBjKDAsMCwwLDApKQpmb3IgKGkgaW4gMToxMDApIHsKICBwbG90KGFzLnJhc3RlcihtbmlzdCR0cmFpbiR4W2ksLF0sIG1heCA9IDI1NSkpCn0KYGBgCgojIFByZXBhcmUgRGF0YQoKV2hlbiB3ZSB3b3JrIHdpdGgga2VyYXM6CgoqIHRyYWluaW5nIGFuZCB0ZXN0IHNldHMgbmVlZCB0byBiZSBpbmRlcGVuZGVudAoqIGZlYXR1cmVzIGFuZCBsYWJlbHMgKGFrYSB0YXJnZXQsIHJlc3BvbnNlKSBuZWVkIHRvIGJlIGluZGVwZW5kZW50CiogdXNlIGAlPC0lYCBmb3IgX19fb2JqZWN0IHVucGFja2luZ19fXyAoc2VlIGA/emVhbGxvdDo6JTwtJWApCgpgYGB7ciBleHRyYWN0LXRyYWluLXRlc3R9CmMoYyh0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscyksIGModGVzdF9pbWFnZXMsIHRlc3RfbGFiZWxzKSkgJTwtJSBtbmlzdAoKIyB0aGUgYWJvdmUgaXMgdGhlIHNhbWUgYXMKIyB0cmFpbl9pbWFnZXMgPC0gbW5pc3QkdHJhaW4keAojIHRyYWluX2xhYmVscyA8LSBtbmlzdCR0cmFpbiR5CiMgdGVzdF9pbWFnZXMgPC0gbW5pc3QkdGVzdCR4CiMgdGVzdF9sYWJlbHMgPC0gbW5pc3QkdGVzdCR5CmBgYAoKIyMgU2hhcGUgaW50byBwcm9wZXIgdGVuc29yIGZvcm0KClRoZSBzaGFwZSBvZiBvdXIgZGF0YSBpcyBkZXBlbmRlbnQgb24gdGhlIHR5cGUgb2YgREwgbW9kZWwgd2UgYXJlIHRyYWluaW5nLiBNTFBzCnJlcXVpcmUgb3VyIGRhdGEgdG8gYmUgaW4gYSAyRCB0ZW5zb3IgKGFrYSBtYXRyaXgpOyBob3dldmVyLCBvdXIgZGF0YSBhcmUKY3VycmVudGx5IGluIGEgM0QgdGVuc29yLgoKV2UgY2FuIHJlc2hhcGUgb3VyIHRlbnNvciBmcm9tIDNEIHRvIDJELiBNdWNoIGxpa2UgYSBtYXRyaXggY2FuIGJlIGZsYXR0ZW5lZCB0bwphIHZlY3RvcjoKCmBgYHtyfQptIDwtIG1hdHJpeCgxOjksIG5jb2wgPSAzKQptCgojIGZsYXR0ZW5lZCBtYXRyaXgKYXMudmVjdG9yKG0pCmBgYAoKV2UgY2FuIHJlc2hhcGUgYSAzRCBhcnJheSB0byBhIDJEIGFycmF5IHdpdGggYGFycmF5X3Jlc2hhcGUoKWAKCiFbXShpbWFnZXMvcmVzaGFwZS5wbmcpCgpgYGB7ciByZXNoYXBlLXRvLTJELXRlbnNvcn0KIyByZXNoYXBlIDNEIHRlbnNvciAoYWthIGFycmF5KSB0byBhIDJEIHRlbnNvciAoYWthIG1hdHJpeCkKdHJhaW5faW1hZ2VzIDwtIGFycmF5X3Jlc2hhcGUodHJhaW5faW1hZ2VzLCBjKDYwMDAwLCAyOCAqIDI4KSkKdGVzdF9pbWFnZXMgPC0gYXJyYXlfcmVzaGFwZSh0ZXN0X2ltYWdlcywgYygxMDAwMCwgMjggKiAyOCkpCgojIG91ciB0cmFpbmluZyBkYXRhIGlzIG5vdyBhIG1hdHJpeCB3aXRoIDYwSyBvYnNlcnZhdGlvbnMgYW5kCiMgNzg0IGZlYXR1cmVzICgyOCBwaXhlbHMgeCAyOCBwaXhlbHMgPSA3ODQpCnN0cih0cmFpbl9pbWFnZXMpCmBgYAoKU2luY2Ugd2UgYXJlIGRlYWxpbmcgd2l0aCBhIG11bHRpLWNsYXNzaWZpY2F0aW9uIHByb2JsZW0gd2hlcmUgdGhlIHRhcmdldCByYW5nZXMKZnJvbSAwLTksIHdlJ2xsIHJlZm9ybWF0IHdpdGggYHRvX2NhdGVnb3JpY2FsKClgLiAKCl9fTm90ZV9fOiBjb2x1bW4gMSByZWZlcnMgdG8gdGhlIGRpZ2l0ICIwIiwgY29sdW1uIDIgcmVmZXJzIHRvIHRoZSBkaWdpdCAiMSIsIGV0Yy4KCmBgYHtyfQp0cmFpbl9sYWJlbHMgPC0gdG9fY2F0ZWdvcmljYWwodHJhaW5fbGFiZWxzKQp0ZXN0X2xhYmVscyA8LSB0b19jYXRlZ29yaWNhbCh0ZXN0X2xhYmVscykKCmhlYWQodHJhaW5fbGFiZWxzKQpgYGAKCiMjIFN0YWJpbGl6ZSBsZWFybmluZyBieSBkYXRhIHNjYWxpbmcKCldoZW4gYXBwbHlpbmcgREwgbW9kZWxzLCBvdXIgZmVhdHVyZSB2YWx1ZXMgc2hvdWxkIG5vdCBiZSByZWxhdGl2ZWx5IGxhcmdlCmNvbXBhcmVkIHRvIHRoZSByYW5kb21pemVkIGluaXRpYWwgd2VpZ2h0cyBfYW5kXyBhbGwgb3VyIGZlYXR1cmVzIHNob3VsZCB0YWtlCnZhbHVlcyBpbiByb3VnaGx5IHRoZSBzYW1lIHJhbmdlLiAKCj4gX19fV2hlbiBmZWF0dXJlcyBoYXZlIGxhcmdlIG9yIHdpZGVseSB2YXJ5aW5nIHZhbHVlcywgbGFyZ2UgZ3JhZGllbnQgdXBkYXRlcyBjYW4KYmUgdHJpZ2dlcmVkIHRoYXQgd2lsbCBwcmV2ZW50IHRoZSBuZXR3b3JrIGZyb20gY29udmVyZ2luZ19fXwoKX19UaXBzX186CgoxLiBXaGVuIGFsbCBmZWF0dXJlcyBoYXZlIHRoZSBzYW1lIHZhbHVlIHJhbmdlIChpLmUuIGltYWdlcyksIHdlIGNhbiBzdGFuZGFyZGl6ZQogICB2YWx1ZXMgYmV0d2VlbiAwLTEuCgoyLiBXaGVuIGZlYXR1cmVzIHZhcnlpbmcgaW4gcmFuZ2UgZnJvbSBvbmUgYW5vdGhlciAoaS5lLiBhZ2UsIGhlaWdodCwgbG9uZ2l0dWRlKQogICBub3JtYWxpemUgZWFjaCBmZWF0dXJlIHRvIGhhdmUgbWVhbiBvZiAwIGFuZCBzdGFuZGFyZCBkZXZpYXRpb24gb2YgMSAoP2BzY2FsZSgpYCkKCmBgYHtyfQojIGFsbCBvdXIgZmVhdHVyZXMgKHBpeGVscykgcmFuZ2UgZnJvbSAwLTI1NQpyYW5nZSh0cmFpbl9pbWFnZXMpCmBgYAoKYGBge3J9CiMgc3RhbmRhcmRpemUgdHJhaW4gYW5kIHRlc3QgZmVhdHVyZXMKdHJhaW5faW1hZ2VzIDwtIHRyYWluX2ltYWdlcyAvIDI1NQp0ZXN0X2ltYWdlcyA8LSB0ZXN0X2ltYWdlcyAvIDI1NQpgYGAKCiMjIFJhbmRvbWl6ZSBkYXRhCgpBbHRob3VnaCBJIGtub3cgdGhhdCB0aGlzIGRhdGEgaXMgbm90IG9yZGVyZWQsIHdlIHNob3VsZCBhbHdheXMgZ2V0IGluIHRoZSBoYWJpdApvZiByYW5kb21pemluZyBvdXIgZGF0YSBzbyB0aGF0IG91ciB0cmFpbiBhbmQgdmFsaWRhdGlvbiBkYXRhc2V0cyBhcmUgcHJvcGVybHkKcmVwcmVzZW50ZWQuCgpgYGB7cn0Kb2JzIDwtIG5yb3codHJhaW5faW1hZ2VzKQpzZXQuc2VlZCgxMjMpCnJhbmRvbWl6ZSA8LSBzYW1wbGUoc2VxX2xlbihvYnMpLCBzaXplID0gb2JzLCByZXBsYWNlID0gRkFMU0UpCnRyYWluX2ltYWdlcyA8LSB0cmFpbl9pbWFnZXNbcmFuZG9taXplLCBdCnRyYWluX2xhYmVscyA8LSB0cmFpbl9sYWJlbHNbcmFuZG9taXplLCBdCmBgYAoKV2UncmUgZmluYWxseSByZWFkeSB0byB0cmFpbiBzb21lIERMIG1vZGVscyEhCgojIEJhbGFuY2UgQmF0Y2ggU2l6ZSAmIERlZmF1bHQgTGVhcm5pbmcgUmF0ZQoKVGhlIGxlYXJuaW5nIHJhdGUgaXMgdGhlIG1vc3QgaW1wb3J0YW50IGh5cGVycGFyYW1ldGVyIHRvIGdldCByaWdodCBidXQgYmVmb3JlIEkKdHVuaW5nIGl0LCBJIGxpa2UgdG8gZmluZCBhIGJhdGNoIHNpemUgdGhhdCBiYWxhbmNlczoKCi0gc21vb3RobmVzcyBvZiB0aGUgbGVhcm5pbmcgY3VydmUgCi0gc3BlZWQgb2YgdHJhaW5pbmcKCl9fVGlwc19fOiBzdGFydCB3aXRoLi4uCgotIGEgZGVmYXVsdCBsZWFybmluZyByYXRlLAotIGEgc2luZ2xlIGhpZGRlbiBsYXllciB3aXRoIHVuaXRzIHNldCB0byBhcm91bmQgYG1lYW4obl9mZWF0dXJlcyArIG5fcmVzcG9uc2VfY2xhc3NlcylgLAotIHRoZSBkZWZhdWx0IGJhdGNoIHNpemUgb2YgMzIsCi0gdGhlbiB0ZXN0IHJhbmdlcyBvZiBiYXRjaCBzaXplcyBbMTYsIDMyLCA2NCwgMTI4LCAyNTYsIDUxMl0uCgpgYGB7cn0KIyBnZXQgbnVtYmVyIG9mIGZlYXR1cmVzCm5fZmVhdCA8LSBuY29sKHRyYWluX2ltYWdlcykKCiMgMS4gRGVmaW5lIG1vZGVsIGFyY2hpdGVjdHVyZQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAncmVsdScsIGlucHV0X3NoYXBlID0gbl9mZWF0KSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwLCBhY3RpdmF0aW9uID0gJ3NvZnRtYXgnKQoKIyAyLiBEZWZpbmUgaG93IG91ciBtb2RlbCBpcyBnb2luZyB0byBsZWFybgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSIsCiAgb3B0aW1pemVyID0gInNnZCIsCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQoKc3VtbWFyeShtb2RlbCkKYGBgCgpMZXQncyB0cmFpbiBvdXIgbW9kZWwgd2l0aCB0aGUgZGVmYXVsdCBiYXRjaCBzaXplIG9mIDMyLgoKYGBge3J9CiMgMy4gVHJhaW4gb3VyIG1vZGVsCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCiAgKQpgYGAKCmBgYHtyfQpoaXN0b3J5CmBgYAoKYGBge3J9CnBsb3QoaGlzdG9yeSkKYGBgCgoKIyMgWW91ciB0dXJuISAoM21pbikKCk5vdyByZXRyYWluIHRoZSBzYW1lIG1vZGVsIGJ1dCB0cnkgd2l0aCBiYXRjaCBzaXplcyBvZiAxNiwgMTI4LCA1MTIgd2hpbGUKbGVhdmluZyBldmVyeXRoaW5nIGVsc2UgdGhlIHNhbWUuIEhvdyBkbyB0aGUgcmVzdWx0cyAobG9zcywgYWNjdXJhY3ksIGFuZApjb21wdXRlIHRpbWUpIGNvbXBhcmU/CgpgYGB7cn0KIyBkZWZpbmUgbW9kZWwgYXJjaGl0ZWN0dXJlCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICdyZWx1JywgaW5wdXRfc2hhcGUgPSBuX2ZlYXQpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAnc29mdG1heCcpCgojIGRlZmluZSBob3cgdGhlIG1vZGVsIGlzIGdvbmlnIHRvIGxlYXJuCm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAiY2F0ZWdvcmljYWxfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSAic2dkIiwKICBtZXRyaWNzID0gImFjY3VyYWN5IgogICkKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIF9fX18gPSBfX19fCiAgKQpgYGAKCgojIFBpY2sgYW4gQWRhcHRpdmUgTGVhcm5pbmcgUmF0ZSBPcHRpbWl6ZXIKCk9uY2Ugd2UndmUgcGlja2VkIGFuIGFkZXF1YXRlIGJhdGNoIHNpemUsIG5leHQgd2Ugd2FudCB0byBzdGFydCB0dW5pbmcgdGhlCmxlYXJuaW5nIHJhdGUuIFRoZXJlIGFyZSB0d28gbWFpbiBjb25zaWRlcmF0aW9uczoKCjEuIFdlIHVzdWFsbHkgYWx3YXlzIGFzc2VzcyBkaWZmZXJlbnQgbGVhcm5pbmcgcmF0ZXMgcmFuZ2luZyBmcm9tIFsxZS0xLCAxZS03XS4KMi4gQWRhcHRpdmUgbGVhcm5pbmcgcmF0ZSBvcHRpbWl6ZXJzIG5lYXJseSBhbHdheXMgb3V0cGVyZm9ybSByZWd1bGFyIFNHRAoKPiBfX19BZGFwdGl2ZSBsZWFybmluZyByYXRlcyBoZWxwIHRvIGVzY2FwZSAic2FkZGxlIHBvaW50cyIuX19fCgohW10oaW1hZ2VzL2NvbnRvdXJzX2V2YWx1YXRpb25fb3B0aW1pemVycy5naWYpCiFbXShpbWFnZXMvc2FkZGxlX3BvaW50X2V2YWx1YXRpb25fb3B0aW1pemVycy5naWYpCgpfX1RpcHNfXzogdGhlIHByaW1hcnkgYWRhcHRpdmUgbGVhcm5pbmcgcmF0ZXMgaW5jbHVkZToKCi0gU0dEIHdpdGggbW9tZW50dW0gYD9vcHRpbWl6ZXJfc2dkKClgLgotIFJNU3Byb3AgKGA/b3B0aW1pemVyX3Jtc3Byb3AoKWApIHdoaWNoIGFkZHMgYW4gZXhwb25lbnRpYWxseSBkZWNheWluZyBhdmVyYWdlCiAgb2Ygc3F1YXJlZCBncmFkaWVudHMgLS0+IHNpbWlsYXIgZWZmZWN0IGFzIG1vbWVudHVtLgotIEFkYW0gKGA/b3B0aW1pemVyX2FkYW0oKWApIHdoaWNoIGlzIFJNU3Byb3AgKyBtb21lbnR1bQotIEVhY2ggaGFzIHRoZWlyIG93biBkZWZhdWx0IGxlYXJuaW5nIHJhdGVzIHNvIHR1bmUgcmVsYXRpdmUgdG8gdGhvc2UgdmFsdWVzCi0gU2VlIGh0dHBzOi8vcnVkZXIuaW8vb3B0aW1pemluZy1ncmFkaWVudC1kZXNjZW50LyBmb3IgbW9yZSBkZXRhaWxzCgojIyBZb3VyIHR1cm4hICg1bWluKQoKUmV0cmFpbiBvdXIgREwgbW9kZWwgdXNpbmc6CgotIFNHRCB3aXRoIG1vbWVudHVtIChgb3B0aW1pemVyX3NnZCgpYCkKICAtIFRyeSBkZWZhdWx0IGxlYXJuaW5nIHJhdGUgKDAuMDEpIHdpdGggbW9tZW50dW0gKDAuNSwgMC45LCAwLjk5KQogIC0gVHJ5IGxhcmdlciBsZWFybmluZyByYXRlcyAoMC4xLCAxKSB3aXRoIG1vbWVudHVtCi0gUk1TcHJvcCAoYG9wdGltaXplcl9ybXNwcm9wKClgKQogIC0gV2hhdCBpcyB0aGUgZGVmYXVsdCBsZWFybmluZyByYXRlPyBUcmFpbiB3aXRoIGRlZmF1bHQuCiAgLSBUcnkgYSBsYXJnZXIgbGVhcm5pbmcgcmF0ZS4gV2hhdCBoYXBwZW5zPwotIEFkYW0gKGBvcHRpbWl6ZXJfYWRhbSgpYCkKICAtIFdoYXQgaXMgdGhlIGRlZmF1bHQgbGVhcm5pbmcgcmF0ZT8gVHJhaW4gd2l0aCBkZWZhdWx0LgogIC0gVHJ5IGEgbGFyZ2VyIGxlYXJuaW5nIHJhdGUuIFdoYXQgaGFwcGVucz8KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAncmVsdScsIGlucHV0X3NoYXBlID0gbl9mZWF0KSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwLCBhY3RpdmF0aW9uID0gJ3NvZnRtYXgnKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLAogIG9wdGltaXplciA9IF9fX18sCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKICApCgpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgdHJhaW5faW1hZ2VzLCB0cmFpbl9sYWJlbHMsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICBiYXRjaF9zaXplID0gMTI4CiAgKQpgYGAKCiMgQWRkIENhbGxiYWNrcwoKV2hlbiB0cmFpbmluZyBhIG1vZGVsLCBzb21ldGltZXMgd2Ugd2FudCB0bzoKCi0gYXV0b21hdGljYWxseSBzdG9wIGEgbW9kZWwgb25jZSBwZXJmb3JtYW5jZSBoYXMgc3RvcHBlZCBpbXByb3ZpbmcKLSBkeW5hbWljYWxseSBhZGp1c3QgdmFsdWVzIG9mIGNlcnRhaW4gcGFyYW1ldGVycyAoaS5lLiBsZWFybmluZyByYXRlKQotIGxvZyBtb2RlbCBpbmZvcm1hdGlvbiB0byB1c2Ugb3IgdmlzdWFsaXplIGxhdGVyIG9uCi0gY29udGludWFsbHkgc2F2ZSB0aGUgbW9kZWwgZHVyaW5nIHRyYWluaW5nIGFuZCBzYXZlIHRoZSBtb2RlbCB3aXRoIHRoZSBiZXN0CiAgcGVyZm9ybWFuY2UKICAKS2VyYXMgcHJvdmlkZXMgYSBzdWl0ZSBvZiB0b29scyBjYWxsZWQgX19fY2FsbGJhY2tzX18gdGhhdCBoZWxwIHVzIHRvIG1vbml0b3IsCmNvbnRyb2wsIGFuZCBjdXN0b21pemUgdGhlIHRyYWluaW5nIHByb2NlZHVyZS4KCiMjIFN0b3AgdHJhaW5pbmcgYXQgdGhlIHJpZ2h0IHRpbWUKCldlIG9mdGVuIGRvbid0IGtub3cgaG93IG1hbnkgZXBvY2hzIHdlJ2xsIG5lZWQgdG8gcmVhY2ggYSBtaW5pbXVtIGxvc3MuIFRoZQplYXJseSBzdG9wcGluZyBjYWxsYmFjayBhbGxvd3MgdXMgdG8gYXV0b21hdGljYWxseSBzdG9wIHRyYWluaW5nIGFmdGVyIHdlCmV4cGVyaWVuY2Ugbm8gaW1wcm92ZW1lbnQgaW4gb3VyIGxvc3MgYWZ0ZXIgYHBhdGllbmNlYCBudW1iZXIgb2YgZXBvY2hzLgoKX19UaXBfXzoKCiogSWYgeW91IGFyZSBnb2luZyB0byB1c2UgdGhlIG1vZGVsIGFmdGVyIHRyYWluaW5nIHlvdSBhbHdheXMgd2FudCB0byByZXRhaW4gdGhlCiAgImJlc3QiIG1vZGVsLCB3aGljaCBpcyB0aGUgbW9kZWwgd2l0aCB0aGUgbG93ZXN0IGxvc3MuIGByZXN0b3JlX2Jlc3Rfd2VpZ2h0c2AKICB3aWxsIHJlc3RvcmUgdGhlIHdlaWdodHMgZm9yIHRoaXMgImJlc3QiIG1vZGVsIGV2ZW4gYWZ0ZXIgeW91J3ZlIHBhc3NlZCBpdCBieQogIG4gZXBvY2hzLgogIAoqIFNvbWV0aW1lcyB5b3VyIG1vZGVsIHdpbGwgc3RhbGwgb24gYSBsb3cgdmFsaWRhdGlvbiBsb3NzIHJlc3VsdGluZyBpbiBhIHRpZS4KICBFdmVuIHdpdGggZWFybHkgc3RvcHBpbmcgdGhlIG1vZGVsIHdpbGwgY29udGludWUgdG8gdHJhaW4gZm9yIGFsbCB0aGUgZXBvY2hzLgogIGBtaW5fZGVsdGFgIGFsbG93cyB5b3UgdG8gc3RhdGUgc29tZSBzbWFsbCB2YWx1ZSB0aGF0IHRoZSBsb3NzIG11c3QgaW1wcm92ZSBieQogIG90aGVyd2lzZSBpdCB3aWxsIHN0b3AuCgpgYGB7cn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gJ3JlbHUnLCBpbnB1dF9zaGFwZSA9IG5fZmVhdCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMCwgYWN0aXZhdGlvbiA9ICdzb2Z0bWF4JykKCm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAiY2F0ZWdvcmljYWxfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfc2dkKGxyID0gMC4xLCBtb21lbnR1bSA9IDAuOSksCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHRyYWluX2ltYWdlcywgdHJhaW5fbGFiZWxzLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgYmF0Y2hfc2l6ZSA9IDEyOCwKICBlcG9jaHMgPSAyMCwKICBjYWxsYmFjayA9IGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgcmVzdG9yZV9iZXN0X3dlaWdodHMgPSBUUlVFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX2RlbHRhID0gMC4wMDAxKQogICkKYGBgCgpgYGB7cn0KaGlzdG9yeQpgYGAKCk5vdGUgaG93IHdlIGRpZCBub3QgdHJhaW4gZm9yIGFsbCB0aGUgZXBvY2hzLi4ub25seSB1bnRpbCBvdXIgbG9zcyBkaWRuJ3QKaW1wcm92ZSBieSAwLjAwMDEgZm9yIDMgY29uc2lzdGVudCBlcG9jaHMuCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKCiMjIEFkZCBhIGxlYXJuaW5nIHJhdGUgc2NoZWR1bGVyCgpBbHRob3VnaCBhZGFwdGl2ZSBsZWFybmluZyByYXRlIG9wdGltaXplcnMgYWRqdXN0IHRoZSB2ZWxvY2l0eSBvZiB3ZWlnaHQgdXBkYXRlcwpkZXBlbmRpbmcgb24gdGhlIGxvc3Mgc3VyZmFjZSwgd2UgY2FuIGFsc28gaW5jb3Jwb3JhdGUgYWRkaXRpb25hbCB3YXlzIHRvIG1vZGlmeQp0aGUgbGVhcm5pbmcgcmF0ZSAib24gdGhlIGZseSIuCgpfX09wdGlvbnNfXzoKCi0gTGVhcm5pbmcgcmF0ZSBkZWNheSByZWR1Y2VzIHRoZSBsZWFybmluZyByYXRlIGF0IGVhY2ggZXBvY2guIE5vdGUgdGhlIGBkZWNheWAKICBhcmd1bWVudCBpbiBgP29wdGltaXplcl94eHgoKWAuCiAgCi0gVGhlcmUgaGFzIGJlZW4gc29tZSBncmVhdCByZXNlYXJjaCBvbiBjeWNsaWNhbCBsZWFybmluZyByYXRlcyAoc2VlIAogIGh0dHBzOi8vYXJ4aXYub3JnL2Ficy8xNTA2LjAxMTg2KS4gWW91IGNhbiBpbmNvcnBvcmF0ZSBjdXN0b20gbGVhcm5pbmcgcmF0ZXMKICBzdWNoIGFzIHRoaXMgd2l0aCBgY2FsbGJhY2tfbGVhcm5pbmdfcmF0ZV9zY2hlZHVsZXIoKWAuIFRoaXMgaXMgbW9yZSBhZHZhbmNlZAogIGJ1dCBkZWZpbml0ZWx5IHdvcnRoIHJlYWRpbmcgdXAgb24gcG9zdC13b3Jrc2hvcC4KCi0gQSBzaW1wbGVyLCBhbmQgdmVyeSBwcmFjdGljYWwgYXBwcm9hY2gsIGlzIHRvIHJlZHVjZSB0aGUgbGVhcm5pbmcgcmF0ZSBhZnRlcgogIHRoZSBtb2RlbCBoYXMgc3RvcHBlZCBpbXByb3Zpbmcgd2l0aCBgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUoKWAuIFRoaXMKICBhcHByb2FjaCBhbGxvd3MgdGhlIG1vZGVsIHRvIHRlbGwgdXMgd2hlbiB0byByZWR1Y2UgdGhlIGxlYXJuaW5nIHJhdGUuCgpfX1RpcF9fOiBXaGVuIHVzaW5nIGBkZWNheWAgb3IgYGNhbGxiYWNrX3JlZHVjZV9scl9vbl9wbGF0ZWF1KClgLCB5b3UgY2FuCnVzdWFsbHkgaW5jcmVhc2UgdGhlIGJhc2UgbGVhcm5pbmcgcmF0ZSBzbyB0aGF0IHlvdSBsZWFybiBxdWlja2x5IGVhcmx5IG9uIGFuZAp0aGVuIHRoZSBsZWFybmluZyByYXRlIHdpbGwgcmVkdWNlIHNvIHlvdSBjYW4gdGFrZSBzbWFsbGVyIHN0ZXBzIGFzIHlvdSBhcHByb2FjaAp0aGUgbWluaW11bSBsb3NzLgoKYGBge3J9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICdyZWx1JywgaW5wdXRfc2hhcGUgPSBuX2ZlYXQpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAnc29mdG1heCcpCgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSIsCiAgb3B0aW1pemVyID0gb3B0aW1pemVyX3NnZChsciA9IDAuMSwgbW9tZW50dW0gPSAwLjkpLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIGJhdGNoX3NpemUgPSAxMjgsCiAgZXBvY2hzID0gMjAsCiAgY2FsbGJhY2sgPSBsaXN0KAogICAgY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAzLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUsIG1pbl9kZWx0YSA9IDAuMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEsIGZhY3RvciA9IDAuMSkKICAgICkKICApCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKCmBgYHtyfQpwbG90KGhpc3RvcnkkbWV0cmljcyRscikKYGBgCgoKCiMgRXhwbG9yZSBNb2RlbCBDYXBhY2l0eQoKTm93IHRoYXQgd2UgaGF2ZSBmb3VuZCBhIHByZXR0eSBnb29kIGxlYXJuaW5nIHJhdGUgYW5kIHdlIGhhdmUgZ29vZCBjb250cm9sIG92ZXIKb3VyIHRyYWluaW5nIHByb2NlZHVyZSB3aXRoIGNhbGxiYWNrcywgd2UgY2FuIHN0YXJ0IHRvIGFzc2VzcyBob3cgdGhlIGNhcGFjaXR5Cm9mIG91ciBtb2RlbCBlZmZlY3RzIHBlcmZvcm1hbmNlLgoKUmVjYWxsIHRoYXQgd2UgY29udHJvbCBjYXBhY2l0eSB0d28gd2F5czoKCi0gX193aWR0aF9fOiB0aGUgbnVtYmVyIG9mIHVuaXRzIGluIGEgaGlkZGVuIGxheWVyCi0gX19kZXB0aF9fOiB0aGUgbnVtYmVyIG9mIGhpZGRlbiBsYXllcnMKCgojIyBZb3VyIHR1cm4hICg1bWluKQoKRXhwbG9yZSBkaWZmZXJlbnQgbW9kZWwgY2FwYWNpdGllcyB3aGlsZSBhbGwgb3RoZXIgcGFyYW1ldGVycyBjb25zdGFudC4gVHJ5CmluY3JlYXNpbmcKCiogV2lkdGg6IHJlbWVtYmVyIHRoYXQgdGhlIG51bWJlciBvZiB1bml0cyBpcyB1c3VhbGx5IGEgcG93ZXIgb2YgMiAoaS5lLiAzMiwgNjQsCiAgMTI4LCAyNTYsIDUxMiwgMTAyNCkuCiogRGVwdGg6IHRyeSB1c2luZyAyIG9yIDMgaGlkZGVuIGxheWVycwoKYGBge3J9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBfX19fX19fX19fXyAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEwLCBhY3RpdmF0aW9uID0gJ3NvZnRtYXgnKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLAogIG9wdGltaXplciA9IG9wdGltaXplcl9zZ2QobHIgPSAwLjEsIG1vbWVudHVtID0gMC45KSwKICBtZXRyaWNzID0gImFjY3VyYWN5IgopCgpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgdHJhaW5faW1hZ2VzLCB0cmFpbl9sYWJlbHMsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICBiYXRjaF9zaXplID0gMTI4LAogIGVwb2NocyA9IDIwLAogIGNhbGxiYWNrID0gbGlzdCgKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMywgcmVzdG9yZV9iZXN0X3dlaWdodHMgPSBUUlVFLCBtaW5fZGVsdGEgPSAwLjAwMDEpLAogICAgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUocGF0aWVuY2UgPSAxLCBmYWN0b3IgPSAwLjEpCiAgICApCiAgKQpgYGAKCgojIyBTbWFydCBleHBlcmltZW50aW5nCgpBcyB3ZSBzdGFydCB0byBleHBlcmltZW50IG1vcmUsIGl0IGJlY29tZXMgaGFyZGVyIHRvIG9yZ2FuaXplIGFuZCBjb21wYXJlIG91cgpyZXN1bHRzLiBMZXQncyBtYWtlIGl0IG1vcmUgZWZmaWNpZW50IGJ5OiAKCi0gQ3JlYXRpbmcgYSBmdW5jdGlvbiB0aGF0IGFsbG93cyB1cyB0byBkeW5hbWljYWxseSBjaGFuZ2UgdGhlIG51bWJlciBvZiBsYXllcnMKICBhbmQgdW5pdHMuCi0gVXNpbmcgYGNhbGxiYWNrX3RlbnNvcmJvYXJkKClgIHdoaWNoIGFsbG93cyB1cyB0byBzYXZlIGFuZCB2aXN1YWxseSBjb21wYXJlCiAgcmVzdWx0cy4KCl9fVGlwX186IHRoZXJlIGlzIGFub3RoZXIgYXBwcm9hY2ggdG8gcGVyZm9ybWluZyBncmlkIHNlYXJjaGVzLiBTZWUgdGhlIGV4dHJhcwpub3RlYm9vayBodHRwczovL3JzdHVkaW8tY29uZi0yMDIwLmdpdGh1Yi5pby9kbC1rZXJhcy10Zi9ub3RlYm9va3MvaW1kYi1ncmlkLXNlYXJjaC5uYi5odG1sCmZvciBkZXRhaWxzLgoKYGBge3J9CnRyYWluX21vZGVsIDwtIGZ1bmN0aW9uKG5fdW5pdHMsIG5fbGF5ZXJzLCBsb2dfdG8pIHsKICAKICAjIENyZWF0ZSBhIG1vZGVsIHdpdGggYSBzaW5nbGUgaGlkZGVuIGlucHV0IGxheWVyCiAgbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSBuX3VuaXRzLCBhY3RpdmF0aW9uID0gInJlbHUiLCBpbnB1dF9zaGFwZSA9IG5fZmVhdCkKICAKICAjIEFkZCBhZGRpdGlvbmFsIGhpZGRlbiBsYXllcnMgYmFzZWQgb24gaW5wdXQKICBpZiAobl9sYXllcnMgPiAxKSB7CiAgICBmb3IgKGkgaW4gc2VxX2Fsb25nKG5fbGF5ZXJzIC0gMSkpIHsKICAgICAgbW9kZWwgJT4lIGxheWVyX2RlbnNlKHVuaXRzID0gbl91bml0cywgYWN0aXZhdGlvbiA9ICJyZWx1IikKICAgIH0KICB9CiAgCiAgIyBBZGQgZmluYWwgb3V0cHV0IGxheWVyCiAgbW9kZWwgJT4lIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAic29mdG1heCIpCiAgCiAgIyBjb21waWxlIG1vZGVsCiAgbW9kZWwgJT4lIGNvbXBpbGUoCiAgICBsb3NzID0gImNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSIsCiAgICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfc2dkKGxyID0gMC4xLCBtb21lbnR1bSA9IDAuOSksCiAgICBtZXRyaWNzID0gImFjY3VyYWN5IgogICkKICAKICAjIHRyYWluIG1vZGVsIGFuZCBzdG9yZSByZXN1bHRzIHdpdGggY2FsbGJhY2tfdGVuc29yYm9hcmQoKQogIGhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICAgIHRyYWluX2ltYWdlcywgdHJhaW5fbGFiZWxzLAogICAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICAgIGJhdGNoX3NpemUgPSAxMjgsCiAgICBlcG9jaHMgPSAyMCwKICAgIGNhbGxiYWNrID0gbGlzdCgKICAgICAgY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAzLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUsIG1pbl9kZWx0YSA9IDAuMDAwMSksCiAgICAgIGNhbGxiYWNrX3JlZHVjZV9scl9vbl9wbGF0ZWF1KHBhdGllbmNlID0gMSwgZmFjdG9yID0gMC4xKSwKICAgICAgY2FsbGJhY2tfdGVuc29yYm9hcmQobG9nX2RpciA9IGxvZ190bykKICAgICAgKSwKICAgIHZlcmJvc2UgPSBGQUxTRQogICAgKQogIAogIHJldHVybihoaXN0b3J5KQogIH0KYGBgCgpOb3cgd2UgY2FuIGNyZWF0ZSBhIGdyaWQgZm9yIHZhcmlvdXMgbW9kZWwgY2FwYWNpdGllcy4gV2UgaW5jbHVkZSBhbiBJRCBmb3IgZWFjaAptb2RlbCwgd2hpY2ggd2Ugd2lsbCB1c2UgdG8gc2F2ZSBvdXIgcmVzdWx0cyB3aXRoaW4gYGNhbGxiYWNrX3RlbnNvcmJvYXJkKClgLgoKYGBge3J9CmdyaWQgPC0gZXhwYW5kX2dyaWQoCiAgdW5pdHMgPSBjKDEyOCwgMjU2LCA1MTIsIDEwMjQpLAogIGxheWVycyA9IGMoMTozKQopICU+JQogIG11dGF0ZShpZCA9IHBhc3RlMCgibWxwXyIsIGxheWVycywgIl9sYXllcnNfIiwgdW5pdHMsICJfdW5pdHMiKSkKZ3JpZApgYGAKCk5vdyB3ZSBjYW4gbG9vcCB0aHJvdWdoIGVhY2ggbW9kZWwgY2FwYWNpdHkgY29tYmluYXRpb24gYW5kIHRyYWluIG91ciBtb2RlbHMuClRoaXMgd2lsbCB0YWtlIGEgZmV3IG1pbnV0ZXMgc28gdGhpcyBpcyBhIGdvb2QgdGltZSB0byBnbyB0byB0aGUg8J+avS4KCmBgYHtyfQpmb3IgKHJvdyBpbiBzZXFfbGVuKG5yb3coZ3JpZCkpKSB7CiAgIyBnZXQgcGFyYW1ldGVycwogIHVuaXRzIDwtIGdyaWRbW3JvdywgInVuaXRzIl1dCiAgbGF5ZXJzIDwtIGdyaWRbW3JvdywgImxheWVycyJdXQogIGZpbGVfcGF0aCA8LSBwYXN0ZTAoIm1uaXN0LyIsIGdyaWRbW3JvdywgImlkIl1dKQogIAogICMgcHJvdmlkZSBzdGF0dXMgdXBkYXRlCiAgY2F0KGxheWVycywgImhpZGRlbiBsYXllcihzKSB3aXRoIiwgdW5pdHMsICJuZXVyb25zOiAiKQogIAogICMgdHJhaW4gbW9kZWwKICBtIDwtIHRyYWluX21vZGVsKG5fdW5pdHMgPSB1bml0cywgbl9sYXllcnMgPSBsYXllcnMsIGxvZ190byA9IGZpbGVfcGF0aCkKICBtaW5fbG9zcyA8LSBtaW4obSRtZXRyaWNzJHZhbF9sb3NzLCBuYS5ybSA9IFRSVUUpCiAgCiAgIyB1cGRhdGUgc3RhdHVzIHdpdGggbG9zcwogIGNhdChtaW5fbG9zcywgIlxuIiwgYXBwZW5kID0gVFJVRSkKfQpgYGAKCk91ciByZXN1bHRzIHN1Z2dlc3QgdGhhdCBsYXJnZXIgd2lkdGggbW9kZWxzIHRlbmQgdG8gcGVyZm9ybSBiZXR0ZXIgYnV0IGl0IGlzCnVuY2xlYXIgaWYgZGVlcGVyIG1vZGVscyBhZGQgbXVjaCBiZW5lZml0LiBIb3dldmVyLCB0byBnZXQgbW9yZSBjbGFyaXR5IHdlIGNhbgphbmFseXplIHRoZSBsZWFybmluZyByYXRlcyB3aXRoIGB0ZW5zb3Jib2FyZCgpYC4KCk5vdGUgdGhhdCBgY2FsbGJhY2tfdGVuc29yYm9hcmQoKWAgc2F2ZWQgYWxsIHRoZSBtb2RlbCBydW5zIGluIGAvbW5pc3RgCnN1YmRpcmVjdG9yeS4KCmBgYHtyfQp0ZW5zb3Jib2FyZCgibW5pc3QiKQpgYGAKCiMjIFlvdXIgdHVybiEgKDMgbWluKQoKUmV0cmFpbiB0aGUgbW9kZWwgd2l0aDoKCi0gMiBoaWRkZW4gbGF5ZXJzCi0gMTAyNCB1bml0cyBpbiBlYWNoIGhpZGRlbiBsYXllcgotIGVhcmx5IHN0b3BwaW5nIGFmdGVyIDMgZXBvY2hzIG9mIG5vIGltcHJvdmVtZW50Ci0gcmVkdWNlIGxlYXJuaW5nIHJhdGUgYWZ0ZXIgMSBlcG9jaCBvZiBubyBpbXByb3ZlbWVudAoKCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgX19fX19fX19fX18gJT4lCiAgX19fX19fX19fX18KCm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAiY2F0ZWdvcmljYWxfY3Jvc3NlbnRyb3B5IiwKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfc2dkKGxyID0gMC4xLCBtb21lbnR1bSA9IDAuOSksCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKKQoKaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHRyYWluX2ltYWdlcywgdHJhaW5fbGFiZWxzLAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgYmF0Y2hfc2l6ZSA9IDEyOCwKICBlcG9jaHMgPSAyMCwKICBjYWxsYmFjayA9IGxpc3QoCiAgICBjYWxsYmFja19fX19fXyhwYXRpZW5jZSA9IF9fX19fLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUsIG1pbl9kZWx0YSA9IDAuMDAwMSksCiAgICBjYWxsYmFja19fX19fXyhwYXRpZW5jZSA9IF9fX19fLCBmYWN0b3IgPSAwLjEpCiAgICApCiAgKQpgYGAKCiMgUmVndWxhcml6ZSBPdmVyZml0dGluZwoKT2Z0ZW4sIG9uY2Ugd2UndmUgZm91bmQgYSBtb2RlbCB0aGF0IG1pbmltaXplcyB0aGUgbG9zcywgdGhlcmUgaXMgc3RpbGwgc29tZQpvdmVyZml0dGluZyB0aGF0IGlzIG9jY3VyaW5nLi4uc29tZXRpbWVzIGEgbG90LiBTbyBvdXIgbmV4dCBvYmplY3RpdmUgaXMgdG8gdHJ5CmZsYXR0ZW4gdGhlIHZhbGlkYXRpb24gbG9zcyBsZWFybmluZyBjdXJ2ZSBhbmQgYnJpbmcgaXQgYXMgY2xvc2UgdG8gdGhlIHRyYWluaW5nCmxvc3MgY3VydmUgYXMgcG9zc2libGUuCgojIyBXZWlnaHQgZGVjYXkKCkEgY29tbW9uIHdheSB0byBtaXRpZ2F0ZSBvdmVyZml0dGluZyBpcyB0byBwdXQgY29uc3RyYWludHMgb24gdGhlIGNvbXBsZXhpdHkgb2YKYSBuZXR3b3JrIGJ5IGZvcmNpbmcgaXRzIHdlaWdodHMgdG8gdGFrZSBvbiBzbWFsbCB2YWx1ZXMsIHdoaWNoIG1ha2VzIHRoZQpkaXN0cmlidXRpb24gb2Ygd2VpZ2h0IHZhbHVlcyBtb3JlIHJlZ3VsYXIuIFRoaXMgaXMgY2FsbGVkIF93ZWlnaHQgcmVndWxhcml6YXRpb25fCmFuZCBpdHMgZG9uZSBieSBhZGRpbmcgdG8gdGhlIGxvc3MgZnVuY3Rpb24gb2YgdGhlIG5ldHdvcmsgYSBjb3N0IGFzc29jaWF0ZWQKd2l0aCBoYXZpbmcgbGFyZ2Ugd2VpZ2h0cy4KCklmIHlvdSBhIGZhbWlsaWFyIHdpdGggcmVndWxhcml6ZWQgcmVncmVzc2lvbiBb4oS577iPXShodHRwOi8vYml0Lmx5L2hvbWxyLXJlZ3VsYXJpemUpCihsYXNzbywgcmlkZ2UsIGVsYXN0aWMgbmV0cykgdGhlbiB3ZWlnaHQgcmVndWxhcml6YXRpb24gaXMgZXNzZW50aWFsbHkgdGhlIHNhbWUKdGhpbmcuIFvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtMDIjMjMpCgokJExvc3MgPSBNU0UgKyBcbGFtYmRhIFxzdW1ecF97aj0xfSB3XjJfaiQkCgohW10oaW1hZ2VzL3dlaWdodC1kZWNheS5wbmcpCgpfX1RpcHNfXzoKCi0gQWx0aG91Z2ggeW91IGNhbiB1c2UgTDEsIEwyIG9yIGEgY29tYmluYXRpb24sIEwyIGlzIGJ5IGZhciB0aGUgbW9zdCBjb21tb24gYW5kCiAgaXMga25vd24gYXMgX19fd2VpZ2h0IGRlY2F5X19fIGluIHRoZSBjb250ZXh0IG9mIG5ldXJhbCBuZXRzLgotIE9wdGltYWwgdmFsdWVzIHZhcnkgYnV0IHdoZW4gdHVuaW5nIHdlIHR5cGljYWxseSBzdGFydCB3aXRoIGZhY3RvcnMgb2YgJDEwXnstc30kCiAgd2hlcmUgcyByYW5nZXMgYmV0d2VlbiAxLTQgKDAuMSwgMC4wMSwgLi4uLCAwLjAwMDEpLgotIFRoZSBsYXJnZXIgdGhlIHdlaWdodCByZWd1bGFyaXplciwgdGhlIG1vcmUgZXBvY2hzIGdlbmVyYWxseSByZXF1aXJlZCB0byByZWFjaAogIGEgbWluaW11bSBsb3NzLgotIFdlaWdodCBkZWNheSBjYW4gY2F1c2UgYSBub2lzaWVyIGxlYXJuaW5nIGN1cnZlIHNvIGl0cyBvZnRlbiBiZW5lZmljaWFsIHRvCiAgaW5jcmVhc2UgdGhlIGBwYXRpZW5jZWAgcGFyYW1ldGVyIGZvciBlYXJseSBzdG9wcGluZyBpZiB0aGlzIGlzIG5vdGljYWJsZS4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZGVuc2UoCiAgICB1bml0cyA9IDUxMiwgYWN0aXZhdGlvbiA9ICJyZWx1IiwgaW5wdXRfc2hhcGUgPSBuX2ZlYXQsCiAgICBrZXJuZWxfcmVndWxhcml6ZXIgPSByZWd1bGFyaXplcl9sMihsID0gMC4wMDEpICAgICMgcmVndWxhcml6YXRpb24gcGFyYW1ldGVyCiAgICApICU+JQogIGxheWVyX2RlbnNlKAogICAgdW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIsCiAgICBrZXJuZWxfcmVndWxhcml6ZXIgPSByZWd1bGFyaXplcl9sMihsID0gMC4wMDEpICAgICMgcmVndWxhcml6YXRpb24gcGFyYW1ldGVyCiAgICApICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAic29mdG1heCIpCgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSIsCiAgb3B0aW1pemVyID0gb3B0aW1pemVyX3NnZChsciA9IDAuMSwgbW9tZW50dW0gPSAwLjkpLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIGJhdGNoX3NpemUgPSAxMjgsCiAgZXBvY2hzID0gMjAsCiAgY2FsbGJhY2sgPSBsaXN0KAogICAgY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAzLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUsIG1pbl9kZWx0YSA9IDAuMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEsIGZhY3RvciA9IDAuMSkKICAgICkKICApCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKCiMjIERyb3BvdXQKCl9fX0Ryb3BvdXRfX18gaXMgb25lIG9mIHRoZSBtb3N0IGVmZmVjdGl2ZSBhbmQgY29tbW9ubHkgdXNlZCByZWd1bGFyaXphdGlvbgp0ZWNobmlxdWVzIGZvciBuZXVyYWwgbmV0d29ya3MuIERyb3BvdXQgYXBwbGllZCB0byBhIGxheWVyIHJhbmRvbWx5IGRyb3BzIG91dAooc2V0cyB0byB6ZXJvKSBhIGNlcnRhaW4gcGVyY2VudGFnZSBvZiB0aGUgb3V0cHV0IGZlYXR1cmVzIG9mIHRoYXQgbGF5ZXIuIEJ5CnJhbmRvbWx5IGRyb3BwaW5nIHNvbWUgb2YgYSBsYXllcidzIG91dHB1dHMgd2UgbWluaW1pemUgdGhlIGNoYW5jZSBvZiBmaXR0aW5nCnBhdHRlcm5zIHRvIG5vaXNlIGluIHRoZSBkYXRhLCBhIGNvbW1vbiBjYXVzZSBvZiBvdmVyZml0dGluZy4KCiFbXShpbWFnZXMvZHJvcG91dC5wbmcpCgpfX1RpcF9fOgoKLSBEcm9wb3V0IHJhdGVzIHR5cGljYWxseSByYW5nZXMgYmV0d2VlbiAwLjItMC41LiBTb21ldGltZXMgaGlnaGVyIHJhdGVzIGFyZQogIG5lY2Vzc2FyeSBidXQgbm90ZSB0aGF0IHlvdSB3aWxsIGdldCBhIHdhcm5pbmcgd2hlbiBzdXBwbHlpbmcgcmF0ZSA+IDAuNS4KLSBUaGUgaGlnaGVyIHRoZSBkcm9wb3V0IHJhdGUsIHRoZSBzbG93ZXIgdGhlIGNvbnZlcmdlbmNlIHNvIHlvdSBtYXkgbmVlZCB0bwogIGluY3JlYXNlIHRoZSBudW1iZXIgb2YgZXBvY2hzLgotIEl0cyBjb21tb24gdG8gYXBwbHkgZHJvcG91dCBhZnRlciBlYWNoIGhpZGRlbiBsYXllciBhbmQgd2l0aCB0aGUgc2FtZSByYXRlOwogIGhvd2V2ZXIsIHRoaXMgaXMgbm90IG5lY2Vzc2FyeS4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIsIGlucHV0X3NoYXBlID0gbl9mZWF0KSAlPiUKICBsYXllcl9kcm9wb3V0KDAuMykgJT4lICAgICAgICAgICAgICAgICAgICAgICAgICAgICMgcmVndWxhcml6YXRpb24gcGFyYW1ldGVyCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA1MTIsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2Ryb3BvdXQoMC4zKSAlPiUgICAgICAgICAgICAgICAgICAgICAgICAgICAjIHJlZ3VsYXJpemF0aW9uIHBhcmFtZXRlcgogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAic29mdG1heCIpCgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImNhdGVnb3JpY2FsX2Nyb3NzZW50cm9weSIsCiAgb3B0aW1pemVyID0gb3B0aW1pemVyX3NnZChsciA9IDAuMSwgbW9tZW50dW0gPSAwLjkpLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIGJhdGNoX3NpemUgPSAxMjgsCiAgZXBvY2hzID0gMjAsCiAgY2FsbGJhY2sgPSBsaXN0KAogICAgY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAzLCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUsIG1pbl9kZWx0YSA9IDAuMDAwMSksCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDEsIGZhY3RvciA9IDAuMSkKICAgICkKICApCmBgYAoKYGBge3J9Cmhpc3RvcnkKYGBgCgpgYGB7cn0KcGxvdChoaXN0b3J5KQpgYGAKCiMgUmVwZWF0CgpBdCB0aGlzIHBvaW50LCB3ZSBoYXZlIGEgcHJldHR5IGdvb2QgbW9kZWwuIEhvd2V2ZXIsIG9mdGVuLCBpdGVyYXRpbmcgb3ZlciB0aGVzZQpzdGVwcyBjYW4gaW1wcm92ZSBtb2RlbCBwZXJmb3JtYW5jZSBldmVuIGZ1cnRoZXIuIEZvciBicmV2aXR5LCB3ZSdsbCBhY3QgYXMgaWYKd2UgaGF2ZSBmb3VuZCBhIHN1ZmZpY2llbnQgc29sdXRpb24uCgojIEV2YWx1YXRlIHJlc3VsdHMKCk9uY2UgYSBmaW5hbCBtb2RlbCBpcyBjaG9zZW4sIHdlIGNhbiBldmFsdWF0ZSB0aGUgbW9kZWwgb24gb3VyIHRlc3Qgc2V0IHRvCnByb3ZpZGUgdXMgd2l0aCBhIGFjY3VyYXRlIGV4cGVjdGF0aW9uIG9mIG91ciBnZW5lcmFsaXphdGlvbiBlcnJvci4gT3VyIGdvYWwgaXMKdGhhdCBvdXIgdGVzdCBlcnJvciBpcyB2ZXJ5IGNsb3NlIHRvIG91ciB2YWxpZGF0aW9uIGVycm9yLgoKYGBge3J9Cm1vZGVsICU+JSBldmFsdWF0ZSh0ZXN0X2ltYWdlcywgdGVzdF9sYWJlbHMsIHZlcmJvc2UgPSBGQUxTRSkKYGBgCgojIyBDb25mdXNpb24gbWF0cml4CgpUbyB1bmRlcnN0YW5kIG91ciBtb2RlbCdzIHBlcmZvcm1hbmNlIGFjcm9zcyB0aGUgZGlmZmVyZW50IHJlc3BvbnNlIGNsYXNzZXMsIHdlCmNhbiBhc3Nlc3MgYSBjb25mdXNpb24gbWF0cml4IFvihLnvuI9dKGh0dHBzOi8vYnJhZGxleWJvZWhta2UuZ2l0aHViLmlvL0hPTUwvcHJvY2Vzcy5odG1sI2NsYXNzaWZpY2F0aW9uLW1vZGVscykuCgpGaXJzdCwgd2UgbmVlZCB0byBwcmVkaWN0IG91ciBjbGFzc2VzIGFuZCBhbHNvIGdldCB0aGUgYWN0dWFsIHJlc3BvbnNlIHZhbHVlcy4KCmBgYHtyfQpwcmVkaWN0aW9ucyA8LSBtb2RlbCAlPiUgcHJlZGljdF9jbGFzc2VzKHRlc3RfaW1hZ2VzKQphY3R1YWwgPC0gbW5pc3QkdGVzdCR5CmBgYAoKV2UgY2FuIHNlZSB0aGUgbnVtYmVyIG9mIG1pc3NlZCBwcmVkaWN0aW9ucyBpbiBvdXIgdGVzdCBzZXQKCmBgYHtyfQptaXNzZWRfcHJlZGljdGlvbnMgPC0gc3VtKHByZWRpY3Rpb25zICE9IGFjdHVhbCkKbWlzc2VkX3ByZWRpY3Rpb25zCmBgYAoKV2UgY2FuIHVzZSBgY2FyZXQ6OmNvbmZ1c2lvbk1hdHJpeCgpYCB0byBnZXQgb3VyIGNvbmZ1c2lvbiBtYXRyaXguIFdlIGNhbiBzZWUKd2hpY2ggZGlnaXRzIG91ciBtb2RlbCBjb25mdXNlcyB0aGUgbW9zdCBieSBhbmFseXppbmcgdGhlIGNvbmZ1c2lvbiBtYXRyaXguCgoqIDlzIGFyZSBvZnRlbiBjb25mdXNlZCB3aXRoIDRzCiogNnMgYXJlIG9mdGVuIGNvbmZ1c2VkIHdpdGggMHMgJiA1cwoqIGV0Yy4KCmBgYHtyIGNvbmZ1c2lvbi1tYXRyaXh9CmNhcmV0Ojpjb25mdXNpb25NYXRyaXgoZmFjdG9yKHByZWRpY3Rpb25zKSwgZmFjdG9yKGFjdHVhbCkpCmBgYAoKIyMgVmlzdWFsaXplIG1pc3NlZCBwcmVkaWN0aW9ucwoKV2UgY2FuIGFsc28gdmlzdWFsaXplIHRoaXMgd2l0aCB0aGUgZm9sbG93aW5nOgoKYGBge3IgdmlzdWFsLWNvbmZ1c2lvbi1tYXRyaXh9CnRpYmJsZSgKICBhY3R1YWwsCiAgcHJlZGljdGlvbnMKICApICU+JSAKICBmaWx0ZXIoYWN0dWFsICE9IHByZWRpY3Rpb25zKSAlPiUKICBjb3VudChhY3R1YWwsIHByZWRpY3Rpb25zKSAlPiUKICBtdXRhdGUocGVyYyA9IG4gLyBuKCkgKiAxMDApICU+JSAKICBmaWx0ZXIobiA+IDEpICU+JSAKICBnZ3Bsb3QoYWVzKGFjdHVhbCwgcHJlZGljdGlvbnMsIHNpemUgPSBuKSkgKwogIGdlb21fcG9pbnQoc2hhcGUgPSAxNSwgY29sID0gIiM5RjkyQzYiKSArCiAgc2NhbGVfeF9jb250aW51b3VzKCJBY3R1YWwgVGFyZ2V0IiwgYnJlYWtzID0gMDo5KSArCiAgc2NhbGVfeV9jb250aW51b3VzKCJQcmVkaWN0aW9uIiwgYnJlYWtzID0gMDo5KSArCiAgc2NhbGVfc2l6ZV9hcmVhKGJyZWFrcyA9IGMoMiwgNSwgMTAsIDE1KSwgbWF4X3NpemUgPSA1KSArCiAgY29vcmRfZml4ZWQoKSArCiAgZ2d0aXRsZShwYXN0ZShtaXNzZWRfcHJlZGljdGlvbnMsICJtaXNtYXRjaGVzIikpICsKICB0aGVtZShwYW5lbC5ncmlkLm1pbm9yID0gZWxlbWVudF9ibGFuaygpKSArCiAgbGFicyhjYXB0aW9uID0gJ0FkYXB0ZWQgZnJvbSBSaWNrIFNjYXZldHRhJykKYGBgCgojIFZpc3VhbGl6ZSBtaXNzZWQgcHJlZGljdGlvbnMKCkxhc3RseSwgbGV0cyBjaGVjayBvdXQgdGhvc2UgbWlzcHJlZGljdGVkIGRpZ2l0cy4KCmBgYHtyIG1pcy1wcmVkaWN0ZWQtZGlnaXRzfQptaXNzZWQgPC0gd2hpY2gocHJlZGljdGlvbnMgIT0gYWN0dWFsKQpwbG90X2RpbSA8LSBjZWlsaW5nKHNxcnQobGVuZ3RoKG1pc3NlZCkpKQoKcGFyKG1mcm93ID0gYyhwbG90X2RpbSwgcGxvdF9kaW0pLCBtYXIgPSBjKDAsMCwwLDApKQpmb3IgKGkgaW4gbWlzc2VkKSB7CiAgcGxvdChhcy5yYXN0ZXIobW5pc3QkdGVzdCR4W2ksLF0sIG1heCA9IDI1NSkpCn0KYGBgCgpJZiB3ZSBsb29rIGF0IHRoZSBwcmVkaWN0ZWQgdnMgYWN0dWFsIHdlIGNhbiByZWFzb24gYWJvdXQgd2h5IG91ciBtb2RlbCAKbWlzcHJlZGljdGVkIHNvbWUgb2YgdGhlIGRpZ2l0cy4KCmBgYHtyfQpwYXIobWZyb3cgPSBjKDQsIDQpLCBtYXIgPSBjKDAsMCwyLDApKQoKZm9yIChpIGluIG1pc3NlZFsxOjE2XSkgewogIHBsb3QoYXMucmFzdGVyKG1uaXN0JHRlc3QkeFtpLCxdLCBtYXggPSAyNTUpKSAKICB0aXRsZShtYWluID0gcGFzdGUoIlByZWRpY3RlZDoiLCBwcmVkaWN0aW9uc1tpXSkpCn0KYGBgCgojIEtleSB0YWtlYXdheXMKCkZvbGxvdyB0aGVzZSBzdGVwcyBhbmQgZ3VpZGVsaW5lcyB3aGVuIHR1bmluZyB5b3VyIERMIG1vZGVsOgoKMS4gUHJlcGFyZSBkYXRhCiAgIC0gZGF0YSBuZWVkcyB0byBiZSBzaGFwZWQgaW50byB0aGUgcmlnaHQgdGVuc29yIGRpbWVuc2lvbnMKICAgLSBkYXRhIHNob3VsZCBiZSBzY2FsZWQgc28gdGhleSBkb24ndCB0cmlnZ2VyIGV4cGxvZGluZyBncmFkaWVudHMKMi4gQmFsYW5jZSBiYXRjaCBzaXplIHdpdGggYSBkZWZhdWx0IGxlYXJuaW5nIHJhdGUgc28gdGhhdC4uLgogICAtIHlvdXIgbGVhcm5pbmcgY3VydmUgaXMgbm90IHRvbyBub2lzeQogICAtIHRoZSB0cmFpbmluZyBjb21wdXRlIHRpbWUgaXMgc3VmZmljaWVudAozLiBUdW5lIHRoZSBhZGFwdGl2ZSBsZWFybmluZyByYXRlIG9wdGltaXplcgogICAtIGNvbXBhcmUgU0dEK21vbWVudHVtIHdpdGggUk1TcHJvcCAmIEFkYW0KICAgLSBhc3Nlc3MgbGVhcm5pbmcgcmF0ZXMgb24gbG9nIHNjYWxlIGJldHdlZW4gWzFlLTEsIDFlLTddCjQuIEFkZCBjYWxsYmFja3MgdG8gY29udHJvbCB0cmFpbmluZwogICAtIHVzZSBlYXJseSBzdG9wcGluZyBmb3IgbW9yZSBlZmZpY2llbnQgdHJhaW5pbmcKICAgLSB1c2UgYW4gYWRhcHRpdmUgbGVhcm5pbmcgcmF0ZSBjYWxsYmFjayB0byBoYXZlIG1vcmUgY29udHJvbCBvZiB0aGUgbGVhcm5pbmcKICAgICByYXRlCjUuIEV4cGxvcmUgbW9kZWwgY2FwYWNpdHkKICAgLSBjb21wYXJlIGFkZGluZyB3aWR0aCB2cyBkZXB0aCAoY29uc2lkZXIgbG9zcyB2cy4gdHJhaW5pbmcgdGltZSkKICAgLSBiZSBzbWFydCB3aXRoIHlvdXIgZXhwZXJpbWVudHMgKHVzZSB0ZW5zb3Jib2FyZCBjYWxsYmFjayEpCjYuIFJlZ3VsYXJpemUgb3ZlcmZpdHRpbmcKICAgLSB3ZWlnaHQgZGVjYXkgY29udHJvbHMgbWFnbml0dWRlIG9mIHdlaWdodHM7IHN0YXJ0IGJ5IGFzc2Vzc2luZyB2YWx1ZXMgYnR3bgogICAgIDAuMSwgMC4wMSwg4oCmLCAwLjAwMDEKICAgLSBkcm9wb3V0IG1pbmltaXplcyBoYXBwZW5zdGFuY2UgcGF0dGVybnMgZnJvbSBub2lzZTsgdHlwaWNhbCB2YWx1ZXMgcmFuZ2UKICAgICBmcm9tIDAuMi0wLjUuCjcuIEl0ZXJhdGUsIGl0ZXJhdGUsIGl0ZXJhdGUhCgpb8J+PoF0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8tY29uZi0yMDIwL2RsLWtlcmFzLXRmKQ==